Time domain#

This notebook builds upon the serial chain system showcased in the serial chain notebook. If you are not already familiar with the concepts described in that notebook, please go through it before this one.

import numpy as np
import Karana.Dynamics as kd
import Karana.Dynamics.SOADyn_types as kdt
from Karana.KUtils.Sim import SimDS
import Karana.Models as kmdl
import Karana.Integrators as ki
import Karana.KUtils as ku
import Karana.Core as kc
import atexit
import Karana.Math as km
from Karana.Math.Kquantities import ureg
from pathlib import Path
import runpy
from tempfile import NamedTemporaryFile

Create the same serial chain simulation and populate the multibody using a DataStruct (DS) file just as before.

sim_ds_path = Path("..") / "resources" / "2_link_pendulum" / "2_link_pendulum_no_gravity.h5"
sim = SimDS.fromFile(sim_ds_path).toSim()
sim.mb.resetData()
sim.mb.getScene().viewAroundFrame(
    sim.mb.virtualRoot(), np.array([0.0, 5.0, 0.0]), np.zeros(3), np.array([0.0, 0.0, 1.0])
)
sim.mb.getScene().update()
[WebUI] Web server is running on port 35157
You may be able to connect in your browser at:
	http://newton:35157
[WebUI] Karana Viewer is running on port 29534
Open this URL to connect:
	http://newton:29534

State propagator#

In order to move the simulation forward in time, we need to solve the equations of motion and numerically integrate the state in time. We need a StatePropagator instance to do this. The Sim class creates one for us with a default integrator. Here, we will utilize the Python API to change that integrator to RK4.

sim.sp.setIntegrator(ki.IntegratorType.RK4)

Set initial position#

As before, we’ll use the Python API to set the joint values to 0.5 and 1 radians. We will also set the velocity of the second pendulum to 1 rad/s. Then, we need to transfer the state from the bodies to the Integrator by calling sim.sp.setState.

# Set the first body's hinge to 0.5 radians
bd1 = sim.mb.getBody("bd1")
bd1.parentHinge().subhinge(0).setQ(0.5)

# Set the second body's hinge to 1.0 radians
bd2 = sim.mb.getBody("bd2")
bd2.parentHinge().subhinge(0).setQ(1.0)

# Assign a 1 radian/s rate to the second body's hinge
bd2.parentHinge().subhinge(0).setU(1.0)
True

Now we will initialize the Integrator state with these values

sim.sp.ensureHealthy()

# first call hardReset() to resize the integrator's internal state to
# match the coordinates in the multibody model
sim.sp.hardReset()

Now use assembleState() to look up the current angle and rate coordinates for the bodies in the multibody model and pack them into a state vector to use to set the integrator’s state

x = sim.sp.assembleState()

Use this state vector to initialize the integrator’s state via the state propagator.

sim.sp.setState(x)

Register a Timed Event#

To stop the simulation and print information every 0.1 seconds, we create a timed event and register it with the state propagator. See timed events for more information.

sp = sim.sp


def fn(t):
    """Print out the time and state.

    Parameters
    ----------
    t : float
        The current time.
    """
    print(f"t = {float(sp.getTime()) / 1e9}s; x = {np.round(sp.getIntegrator().getX(), 6)}")


# the time period at which the above function should be invoked
h = np.timedelta64(100, "ms")

# create the 'show_state' timed event inst
te = kd.TimedEvent("show_state", h, fn, False)
te.period = h

# register the timed event
sim.sp.registerTimedEvent(te)
del te

Add plots#

Use the addPlot helper method on the sim to add some plots to the GUI.

import platform

if platform.system() != "Darwin":
    # In the Jupyter notebook, the live plotting does not work on macOS due to multiprocessing issues.
    # This works just fine from a regular Python script if guarded by if __name__ == "__main__"
    sim.addPlots(
        [
            ku.SinglePlotData(
                "Pendulum Angles",
                sim.sp.getVars().time,
                [
                    kc.VarVec(
                        "p1_angle", bd1.parentHinge().subhinge(0).getVars().Q, "pendulum 1 angle"
                    ),
                    kc.VarVec(
                        "p2_angle", bd2.parentHinge().subhinge(0).getVars().Q, "pendulum 2 angle"
                    ),
                ],
            ),
            ku.SinglePlotData(
                "Kinetic Energy",
                sim.sp.getVars().time,
                [
                    (
                        "Kinetic Energy",
                        lambda: np.array([kd.Algorithms.evalKineticEnergy(sim.mb)]),
                        1,
                    )
                ],
            ),
        ],
        0.1,
        title="Time domain plots",
    )
Dash app started at http://newton:41771

Run the simulation#

Now, let’s run the simulation. advanceTo will advance the simulation by the specified time interval, while invoking the registered timed event at its specified period. You can view the evolving state of the system in the gui’s 3D graphics as well.

sim.sp.advanceTo(3)
t = 0.1s; x = [0.502012 1.097472 0.040353 0.949576]
t = 0.2s; x = [0.508088 1.189946 0.081222 0.900079]
t = 0.3s; x = [0.518266 1.277523 0.122369 0.851642]
t = 0.4s; x = [0.532565 1.36031  0.163611 0.804272]
t = 0.5s; x = [0.550986 1.438411 0.204798 0.757902]
t = 0.6s; x = [0.573518 1.511921 0.245789 0.712428]
t = 0.7s; x = [0.600133 1.580923 0.286447 0.667733]
t = 0.8s; x = [0.630791 1.64549  0.326632 0.623705]
t = 0.9s; x = [0.665438 1.705682 0.366199 0.580238]
t = 1.0s; x = [0.704006 1.761553 0.405002 0.537243]
t = 1.1s; x = [0.746408 1.813144 0.442893 0.494644]
t = 1.2s; x = [0.792549 1.860493 0.479729 0.452381]
t = 1.3s; x = [0.842315 1.90363  0.515376 0.410408]
t = 1.4s; x = [0.89558  1.942583 0.549709 0.368686]
t = 1.5s; x = [0.952209 1.977375 0.58262  0.327187]
t = 1.6s; x = [1.012054 2.008027 0.614017 0.285887]
t = 1.7s; x = [1.074959 2.034558 0.643823 0.244763]
t = 1.8s; x = [1.140763 2.056985 0.67198  0.203795]
t = 1.9s; x = [1.209299 2.075321 0.698447 0.162961]
t = 2.0s; x = [1.280396 2.089581 0.723199 0.122238]
t = 2.1s; x = [1.353881 2.099772 0.746221 0.081601]
t = 2.2s; x = [1.429582 2.105903 0.767515 0.041022]
t = 2.3s; x = [1.507327e+00 2.107977e+00 7.870900e-01 4.730000e-04]
t = 2.4s; x = [ 1.586943  2.105997  0.804965 -0.040076]
t = 2.5s; x = [ 1.668264  2.099961  0.821169 -0.080653]
t = 2.6s; x = [ 1.751123  2.089865  0.835737 -0.121289]
t = 2.7s; x = [ 1.835358  2.075701  0.848716 -0.16201 ]
t = 2.8s; x = [ 1.920814  2.057459  0.860159 -0.202841]
t = 2.9s; x = [ 2.007341  2.035128  0.870133 -0.243805]
t = 3.0s; x = [ 2.094795  2.008693  0.878717 -0.284925]
<SpStatusEnum.REACHED_END_TIME: 1>

Invoke the advanceBy method again to advance the simulation further in time.

sim.sp.advanceBy(7.0)
t = 3.1s; x = [ 2.183041  1.978137  0.886005 -0.326221]
t = 3.2s; x = [ 2.271956  1.943442  0.892106 -0.367715]
t = 3.3s; x = [ 2.361427  1.904587  0.897149 -0.409431]
t = 3.4s; x = [ 2.451356  1.861547  0.90128  -0.451398]
t = 3.5s; x = [ 2.541658  1.814297  0.904664 -0.493654]
t = 3.6s; x = [ 2.63227   1.762806  0.907485 -0.536244]
t = 3.7s; x = [ 2.723143  1.707035  0.909945 -0.57923 ]
t = 3.8s; x = [ 2.814254  1.646944  0.912259 -0.622685]
t = 3.9s; x = [ 2.905598  1.58248   0.914655 -0.666699]
t = 4.0s; x = [ 2.997195  1.513582  0.917369 -0.711376]
t = 4.1s; x = [ 3.08909   1.440179  0.92064  -0.756831]
t = 4.2s; x = [ 3.18135   1.362186  0.924703 -0.803179]
t = 4.3s; x = [ 3.274064  1.27951   0.929782 -0.850525]
t = 4.4s; x = [ 3.367346  1.192046  0.936075 -0.898936]
t = 4.5s; x = [ 3.461325  1.099687  0.943741 -0.948409]
t = 4.6s; x = [ 3.556144  1.002332  0.95287  -0.998815]
t = 4.7s; x = [ 3.651948  0.899903  0.963453 -1.049822]
t = 4.8s; x = [ 3.748878  0.792368  0.975333 -1.100802]
t = 4.9s; x = [ 3.847046  0.679778  0.988152 -1.15071 ]
t = 5.0s; x = [ 3.946519  0.562314  1.001301 -1.197978]
t = 5.1s; x = [ 4.047288  0.440341  1.013893 -1.240474]
t = 5.2s; x = [ 4.149242  0.314464  1.024809 -1.2756  ]
t = 5.3s; x = [ 4.252154  0.185559  1.032846 -1.300618]
t = 5.4s; x = [ 4.35568   0.054757  1.036966 -1.313198]
t = 5.5s; x = [ 4.459396 -0.076621  1.036584 -1.312038]
t = 5.6s; x = [ 4.562848 -0.207194  1.031756 -1.297261]
t = 5.7s; x = [ 4.665621 -0.335668  1.023159 -1.270379]
t = 5.8s; x = [ 4.76739  -0.460948  1.011879 -1.233833]
t = 5.9s; x = [ 4.867947 -0.582204  0.999122 -1.190371]
t = 6.0s; x = [ 4.967202 -0.698876  0.985974 -1.142531]
t = 6.1s; x = [ 5.065158 -0.810633  0.973276 -1.092351]
t = 6.2s; x = [ 5.161891 -0.917318  0.961594 -1.041307]
t = 6.3s; x = [ 5.257521 -1.018898  0.951247 -0.990365]
t = 6.4s; x = [ 5.352189 -1.115414  0.942364 -0.940099]
t = 6.5s; x = [ 5.446043 -1.20695   0.934935 -0.890798]
t = 6.6s; x = [ 5.539222 -1.29361   0.928856 -0.842566]
t = 6.7s; x = [ 5.631853 -1.375499  0.923961 -0.795392]
t = 6.8s; x = [ 5.724046 -1.452721  0.920046 -0.749201]
t = 6.9s; x = [ 5.815888 -1.525368  0.916884 -0.703884]
t = 7.0s; x = [ 5.907441 -1.593523  0.91424  -0.659324]
t = 7.1s; x = [ 5.998745 -1.657255  0.911874 -0.61541 ]
t = 7.2s; x = [ 6.089817 -1.716623  0.909551 -0.572039]
t = 7.3s; x = [ 6.180649 -1.771678  0.907046 -0.529125]
t = 7.4s; x = [ 6.271213 -1.822461  0.904144 -0.486594]
t = 7.5s; x = [ 6.361458 -1.869007  0.900648 -0.444389]
t = 7.6s; x = [ 6.451316 -1.911348  0.896377 -0.402466]
t = 7.7s; x = [ 6.540702 -1.949509  0.891167 -0.360789]
t = 7.8s; x = [ 6.629514 -1.983513  0.884877 -0.31933 ]
t = 7.9s; x = [ 6.717637 -2.013381  0.87738  -0.278064]
t = 8.0s; x = [ 6.804946 -2.039132  0.86857  -0.236972]
t = 8.1s; x = [ 6.891304 -2.060781  0.858357 -0.196031]
t = 8.2s; x = [ 6.976568 -2.078342  0.846662 -0.15522 ]
t = 8.3s; x = [ 7.060585 -2.091828  0.833422 -0.114515]
t = 8.4s; x = [ 7.143199 -2.101248  0.818584 -0.073891]
t = 8.5s; x = [ 7.224247 -2.106608  0.802104 -0.03332 ]
t = 8.6s; x = [ 7.303564e+00 -2.107913e+00  7.839480e-01  7.226000e-03]
t = 8.7s; x = [ 7.38098  -2.105163  0.764088  0.047778]
t = 8.8s; x = [ 7.456324 -2.098356  0.742506  0.088365]
t = 8.9s; x = [ 7.529424 -2.087488  0.719196  0.129015]
t = 9.0s; x = [ 7.600106 -2.07255   0.694157  0.169754]
t = 9.1s; x = [ 7.668198 -2.053533  0.667406  0.210609]
t = 9.2s; x = [ 7.733531 -2.030424  0.638971  0.251601]
t = 9.3s; x = [ 7.795938 -2.003207  0.608896  0.292753]
t = 9.4s; x = [ 7.855257 -1.971867  0.577241  0.334085]
t = 9.5s; x = [ 7.911336 -1.936384  0.544086  0.375619]
t = 9.6s; x = [ 7.964028 -1.896736  0.509526  0.41738 ]
t = 9.7s; x = [ 8.013198 -1.852899  0.473672  0.459399]
t = 9.8s; x = [ 8.058723 -1.804846  0.436651  0.501714]
t = 9.9s; x = [ 8.100494 -1.752545  0.398598  0.544375]
t = 10.0s; x = [ 8.138413 -1.695957  0.359658  0.587443]
<SpStatusEnum.REACHED_END_TIME: 1>

Python script#

Let’s use the simulation we already have to create a standalone Python script. This will dump the Multibody and StatePropagator information to a file.

tf = NamedTemporaryFile(suffix=".py")
sim.toDS().toFile(tf.name)
[warning] Model auto_time_display (TimeDisplay) does not have a "toDS" method, so it cannot be added to the StatePropagatorDS.
[warning] Model data_plotter (DataPlotter) does not have a "toDS" method, so it cannot be added to the StatePropagatorDS.
1 file reformatted

The following will display the generated file within the Jupyter notebook

from IPython.display import Markdown
from pathlib import Path

Markdown(f"```python\n{Path(tf.name).read_text()}\n```")
# This is an auto-generated file for the Sim
# This file was generated on 2026-06-30 09:56:03.205060
import Karana.Math as km
from Karana.Math.Kquantities import ureg
import Karana.Dynamics as kd
import Karana.Scene as ks
import Karana.Integrators as ki
from Karana.KUtils.Sim import Sim
import numpy as np
import Karana.Models as kmdl
import atexit


def populateMultibodyAndStatePropagator(mb: kd.Multibody, sp: kd.StatePropagator):
    """Populate and configure the provided Multibody and StatePropagator.

    Parameters
    ----------
    mb : kd.Multibody
        The Multibody to populate with physical bodies.
    sp : kd.StatePropagator
        The StatePropagator to populate and configure.
    """
    # This creates, adds, and connects physical bodies via hinges,
    # also referred to as joints, to the provided Multibody instance.
    # Often, the input `Multibody' instance is empty and contains just a
    # virtual root body.
    #
    # The code below consists of repeated blocks - one per body. Each block
    # defines the mass properties for the body, the parent hinge type and
    # location on the body and its parent body, and the mesh geometries attached.
    # ----------------------------------------------------------------------------------------------
    # Look up the unique virtual root body for the multibody system that
    # will serve as an anchor for the new physical bodies.
    vroot = mb.virtualRoot()

    # ----------------------------------------------------------------------------------------------
    # Create 'bd1' body
    # ----------------------------------------------------------------------------------------------

    # ----------------------------------------------------------------------------------------------
    # Start defining meshes for 'bd1' body"

    # Create one or more meshes attached to the 'bd1' body. Each mesh
    # consists of the shape and color/texture definition for its visual appearance.
    # The shapes can be parameterized shapes (e.g., spheres, boxes, etc., or be
    # defined in external files such as '.glb', '.obj', etc. formats.

    # ----------------------------------------------------------------------------------------------
    # Define the 'bd1_body' mesh parameters for 'bd1' body
    # Define the part shape (geometry)
    box = ks.BoxGeometry(0.05000000074505806, 0.05000000074505806, 1.0)

    # Define the part color/texture material
    physical_material_info = ks.PhysicalMaterialInfo()
    physical_material_info.color = ks.Color.fromRGBA(
        0.6980392336845398, 0.13333334028720856, 0.13333334028720856, 1.0
    )
    physical_material_info.roughness = 1.0
    physical_material = ks.PhysicalMaterial(physical_material_info)
    bd1_body = ks.ScenePartSpec(
        name="bd1_body",
        transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
        ),
        geometry=box,
        material=physical_material,
        scale=np.array([1.0, 1.0, 1.0]),
        layers=419430400,
    )

    # ----------------------------------------------------------------------------------------------
    # Define the 'bd1_pivot' mesh parameters for 'bd1' body
    # Define the part shape (geometry)
    cylinder = ks.CylinderGeometry(0.10000000149011612, 0.10000000149011612)

    # Define the part color/texture material
    physical_material_1_info = ks.PhysicalMaterialInfo()
    physical_material_1_info.color = ks.Color.fromRGBA(
        0.6980392336845398, 0.13333334028720856, 0.13333334028720856, 1.0
    )
    physical_material_1_info.roughness = 1.0
    physical_material_1 = ks.PhysicalMaterial(physical_material_1_info)
    bd1_pivot = ks.ScenePartSpec(
        name="bd1_pivot",
        transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, -0.5]) * ureg.meter,
        ),
        geometry=cylinder,
        material=physical_material_1,
        scale=np.array([1.0, 1.0, 1.0]),
        layers=419430400,
    )

    # End defining meshes for 'bd1' body
    # ----------------------------------------------------------------------------------------------

    # Assemble overall parameters needed to define the 'bd1' body
    # including mass and kinematics properties.
    bd1_params = kd.PhysicalBodyParams(
        # 'bd1' mass properties
        spI=km.SpatialInertia(
            2.0 * ureg.kilogram,
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
            np.array([[3.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]])
            * ureg.kilogram
            * ureg.meter**2,
        ),
        # Define the location and orientation of the 'bd1' parent hinge
        # relative to the 'bd1' body frame
        body_to_joint_transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([-0.0, -0.0, 0.5]) * ureg.meter,
        ),
        # Define the location and orientation of the 'bd1' parent hinge
        # relative to the parent body's frame
        inb_to_joint_transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
        ),
        # define the 'bd1' parent hinge type and parameters
        hinge_params=kd.PhysicalHingeParams(
            hinge_type=kd.HingeType.REVOLUTE,
            subhinge_params=[
                kd.PinSubhingeParams(
                    unit_axis=np.array([0.0, 1.0, 0.0]),
                    prescribed=False,
                ),
            ],
        ),
        # Add 'bd1' scene parts defined above
        scene_part_specs=[bd1_body, bd1_pivot],
    )

    # Create the 'bd1' body
    bd1 = kd.PhysicalBody("bd1", mb)

    # Connect the 'bd1' body to its parent via a hinge
    kd.PhysicalHinge(vroot, bd1, kd.HingeType.REVOLUTE)

    # Set the parameters for the 'bd1' body
    bd1.setParams(bd1_params)

    # ----------------------------------------------------------------------------------------------
    # Create 'bd2' body
    # ----------------------------------------------------------------------------------------------

    # ----------------------------------------------------------------------------------------------
    # Start defining meshes for 'bd2' body"

    # Create one or more meshes attached to the 'bd2' body. Each mesh
    # consists of the shape and color/texture definition for its visual appearance.
    # The shapes can be parameterized shapes (e.g., spheres, boxes, etc., or be
    # defined in external files such as '.glb', '.obj', etc. formats.

    # ----------------------------------------------------------------------------------------------
    # Define the 'bd2_body' mesh parameters for 'bd2' body
    # Define the part shape (geometry)
    box_1 = ks.BoxGeometry(0.05000000074505806, 0.05000000074505806, 1.0)

    # Define the part color/texture material
    physical_material_2_info = ks.PhysicalMaterialInfo()
    physical_material_2_info.color = ks.Color.fromRGBA(
        0.6980392336845398, 0.13333334028720856, 0.13333334028720856, 1.0
    )
    physical_material_2_info.roughness = 1.0
    physical_material_2 = ks.PhysicalMaterial(physical_material_2_info)
    bd2_body = ks.ScenePartSpec(
        name="bd2_body",
        transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, 0.5]) * ureg.meter,
        ),
        geometry=box_1,
        material=physical_material_2,
        scale=np.array([1.0, 1.0, 1.0]),
        layers=419430400,
    )

    # ----------------------------------------------------------------------------------------------
    # Define the 'bd2_end' mesh parameters for 'bd2' body
    # Define the part shape (geometry)
    cylinder_1 = ks.CylinderGeometry(0.10000000149011612, 0.10000000149011612)

    # Define the part color/texture material
    physical_material_3_info = ks.PhysicalMaterialInfo()
    physical_material_3_info.color = ks.Color.fromRGBA(1.0, 0.843137264251709, 0.0, 1.0)
    physical_material_3_info.roughness = 1.0
    physical_material_3 = ks.PhysicalMaterial(physical_material_3_info)
    bd2_end = ks.ScenePartSpec(
        name="bd2_end",
        transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
        ),
        geometry=cylinder_1,
        material=physical_material_3,
        scale=np.array([2.0, 2.0, 2.0]),
        layers=419430400,
    )

    # End defining meshes for 'bd2' body
    # ----------------------------------------------------------------------------------------------

    # Assemble overall parameters needed to define the 'bd2' body
    # including mass and kinematics properties.
    bd2_params = kd.PhysicalBodyParams(
        # 'bd2' mass properties
        spI=km.SpatialInertia(
            2.0 * ureg.kilogram,
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
            np.array([[3.0, 0.0, 0.0], [0.0, 2.0, 0.0], [0.0, 0.0, 1.0]])
            * ureg.kilogram
            * ureg.meter**2,
        ),
        # Define the location and orientation of the 'bd2' parent hinge
        # relative to the 'bd2' body frame
        body_to_joint_transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([-0.0, -0.0, 1.0]) * ureg.meter,
        ),
        # Define the location and orientation of the 'bd2' parent hinge
        # relative to the parent body's frame
        inb_to_joint_transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, -0.5]) * ureg.meter,
        ),
        # define the 'bd2' parent hinge type and parameters
        hinge_params=kd.PhysicalHingeParams(
            hinge_type=kd.HingeType.REVOLUTE,
            subhinge_params=[
                kd.PinSubhingeParams(
                    unit_axis=np.array([0.0, 1.0, 0.0]),
                    prescribed=False,
                ),
            ],
        ),
        # Add 'bd2' scene parts defined above
        scene_part_specs=[bd2_body, bd2_end],
    )

    # Create the 'bd2' body
    bd2 = kd.PhysicalBody("bd2", mb)

    # Connect the 'bd2' body to its parent via a hinge
    kd.PhysicalHinge(bd1, bd2, kd.HingeType.REVOLUTE)

    # Set the parameters for the 'bd2' body
    bd2.setParams(bd2_params)

    # ----------------------------------------------------------------------------------------------
    # Start defining parts attached to the virtual root
    # ----------------------------------------------------------------------------------------------
    # Define the 'obstacle_part' mesh parameters for virtual root
    # Define the part shape (geometry)
    box_2 = ks.BoxGeometry(0.25, 0.25, 0.25)

    # Define the part color/texture material
    physical_material_4_info = ks.PhysicalMaterialInfo()
    physical_material_4_info.color = ks.Color.fromRGBA(
        0.49803921580314636, 1.0, 0.8313725590705872, 1.0
    )
    physical_material_4_info.roughness = 1.0
    physical_material_4 = ks.PhysicalMaterial(physical_material_4_info)
    obstacle_part = ks.ScenePartSpec(
        name="obstacle_part",
        transform=km.HomTran(
            km.UnitQuaternion(np.array([0.0, 0.0, 0.0, 1.0])),
            np.array([0.0, 0.0, 0.0]) * ureg.meter,
        ),
        geometry=box_2,
        material=physical_material_4,
        scale=np.array([1.0, 1.0, 1.0]),
        layers=419430400,
    )

    vroot.addScenePartSpec(obstacle_part)

    # End defining parts attached to the virtual root
    # ----------------------------------------------------------------------------------------------

    # ----------------------------------------------------------------------------------------------
    # Finished physical body additions
    # ----------------------------------------------------------------------------------------------

    # Finalize the multibody for the added bodies
    mb.ensureHealthy()

    # ----------------------------------------------------------------------------------------------
    # Populate and configure the StatePropagator. This will involve
    # defining the type of solver to use for the multibody equations of
    # motion, adding an integrator to use for state propagation,
    # addition of KModels for interacting with the multibody dynamics,
    # and timed events for execution during time advancement.

    # ----------------------------------------------------------------------------------------------
    # Set the StatePropagator's solver type. 'FORWARD_DYNAMICS' is the
    # typical choice for setting up the propagator to solve the
    # equations of motion for time domain simulations
    sp.setSolverType(kd.MMSolverType.FORWARD_DYNAMICS)

    # Set up the integrator
    sp.setIntegrator(ki.IntegratorType.RK4)

    # ----------------------------------------------------------------------------------------------
    # Set the StatePropagator options

    # Call with True to make an extra dynamics derivative call at the
    # end of a simulation step to update the acceleration values.
    sp.setUpdateStateDerivativesHopEnd(False)

    # Set a limit on the maximum integration time step size.
    # A value of 0.0 means there is no maximum limit.
    sp.setMaxStepSize(0.0)

    # ----------------------------------------------------------------------------------------------
    # Add KModels. KModels are parameterized models for
    # component actuator, sensors, environmental, etc. that interact
    # with the dynamics.
    # ----------------------------------------------------------------------------------------------

    # ----------------------------------------------------------------------------------------------
    # Add the update_proxy_scene KModel
    multibody_scene = mb.getScene()
    if multibody_scene is not None:
        update_proxy_scene = kmdl.UpdateProxyScene("update_proxy_scene", sp, multibody_scene)

    # ----------------------------------------------------------------------------------------------
    # Add the gil_release KModel
    gil_release = kmdl.GilRelease("gil_release", sp)

    # ----------------------------------------------------------------------------------------------
    # Add the sync_real_time KModel
    sync_real_time = kmdl.SyncRealTime("sync_real_time", sp, 1.0)

    # Finalize the StatePropagator for the updated multibody system, the
    # KModels and the integrator
    sp.hardReset()
    sp.ensureHealthy()

    # End setting up StatePropagator and KModels
    # ----------------------------------------------------------------------------------------------


if __name__ == "__main__":
    # Create a simulation
    sim = Sim()

    # Populate the Multibody and StatePropagator for the Sim
    populateMultibodyAndStatePropagator(sim.mb, sim.sp)

    # Code to cleanup upon exit
    def cleanup():
        global sim
        del sim

    atexit.register(cleanup)

Above, you’ll see the multibody code similar to the serial chain notebook, as well as code to create the StatePropagator.

del sim
globals().update(runpy.run_path(tf.name, run_name="__main__"))

Let’s bring up the GUI again just to show we captured everything

# Create the GUI
sim.setupGui()

# Update the camera view
sim.mb.resetData()
sim.mb.getScene().viewAroundFrame(
    sim.mb.virtualRoot(), np.array([0.0, 5.0, 0.0]), np.zeros(3), np.array([0.0, 0.0, 1.0])
)

# Redraw the scene
sim.mb.getScene().update()
[WebUI] Web server is running on port 39963
You may be able to connect in your browser at:
	http://newton:39963
[WebUI] Karana Viewer is running on port 29534
Open this URL to connect:
	http://newton:29534

Cleanup#

def cleanup():
    """Clean up the simulation."""
    global bd1, bd2, sim, sp
    del bd1, bd2, sim, sp


atexit.register(cleanup)
<function __main__.cleanup()>