Serial chain#

This is an introductory example of creating a serial chain system along with starting up the GUI for introspection.

import numpy as np
import Karana.Dynamics as kd
import Karana.Dynamics.SOADyn_types as kdt
from Karana.KUtils.Sim import SimDS
import atexit
import Karana.Math as km
from Karana.Math.Kquantities import ureg
from pathlib import Path
import runpy
from tempfile import NamedTemporaryFile

Create a simulation and populate the multibody using a DataStruct (DS) model file.

sim_ds_path = Path("..") / "resources" / "2_link_pendulum" / "2_link_pendulum_no_gravity.h5"
sim = SimDS.fromFile(sim_ds_path).toSim()

# reset internal coordinates and state to zero values
sim.mb.resetData()

# position the 3D viewing pose
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 44553
You may be able to connect in your browser at:
	http://newton:44553
[WebUI] Karana Viewer is running on port 29534
Open this URL to connect:
	http://newton:29534

A Karana Viewer gui should have started up. You can use the printed URL to open it up in a browser tab as well to explore the created model. The gui includes panes to view the model in 3D graphics, a pane to see the bodies and their connection topology, a pane with a tree view of the bodies. You can use the mouse to select objects from any of these panes, and more information and options for the selected object will be displayed in the Info pane in the bottom right.

You can also examine the model structure from the command line as follows.

sim.mb.displayModel()
LEGEND: <body number> <body name> <parent body> <hinge type> [prescribed subhinges] <U dofs/offset> [flex dofs/offset]

      Body  Parent                 Hinge    Dofs          
      ____  ______                 _____    ____          
   1. bd1   multibody_MBVROOT_    REVOLUTE  1/0           
   2. bd2   bd1                   REVOLUTE  1/1           
                                            ____          
                                            2             
sim.mb.dumpTree()
 Nodes
 ---
LEGEND: [<hinge type[/dofs]> <prescribed subhinges>] <body name>[/num embedded bodies > 0] <flex dofs > 0>
|-multibody_MBVROOT_
   |-[REVOLUTE] bd1
      |-[REVOLUTE] bd2

Set joint values#

Now use the Python API to set the joint values to 0.5 and 1 radians.

# 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
sim.mb.getBody("bd2").parentHinge().subhinge(0).setQ(1.0)

# Update the scene to show the new positions in the GUI
sim.mb.getScene().update()

Inspection#

A graph visualization of the all the Frames in the simulation thus far is shown below.

Lets examine the structure of the multibody system that has been created.

Let’s use the API to get the location of ‘bd2’ with respect to the inertial frame by making use of the Frames layer.

f_to_f = sim.mb.virtualRoot().frameToFrame(bd1)
print(f_to_f.relTransform().getTranslation())
[-0.2397127693021015 0.0 -0.4387912809451864] meter

If we change bd1’s subhinge value, then the location of bd2 will change.

bd1.parentHinge().subhinge(0).setQ(0.0)
print(f_to_f.relTransform().getTranslation())
[-0.0 -0.0 -0.5] meter

Now, let’s use the API to get the system’s center of mass.

f_to_f = sim.mb.virtualRoot().frameToFrame(sim.mb.cmFrame())
print(f_to_f.relTransform().getTranslation())
[-0.42073549240394825 0.0 -1.0201511529340699] meter

If we change the mass properties of one of the bodies, then this should change the cm location.

si_orig = bd1.getSpatialInertia()
bd1.parentHinge().subhinge(0).setQ(0.0)
sim.mb.getBody("bd2").setSpatialInertia(
    km.SpatialInertia(10.0, si_orig.bodyToCm(), si_orig.inertia())
)
print(f_to_f.relTransform().getTranslation())
[-0.7012258206732471 0.0 -1.3669185882234498] meter

Cloning the system#

There are many ways to clone the 2-link pendulum. Here, we will use a DataStruct to do so. Let’s clone the system and add it onto the serial chain.

# Create a DataStruct of the current system
sg_ds = sim.mb.toDS()

# Now, let's rename the bodies to clearly differential them from the old system
sg_ds.renameBodies("pendulum_2_")

# Finally, let's add these cloned bodies to the multibody
new_sg = sg_ds.toSubGraph(sim.mb, sim.mb.getBody("bd2"))

# We'll again reset and redraw the scene. The GUI will now reflect these changes.
sim.mb.ensureHealthy()
sim.mb.resetData()
sim.mb.getScene().update()

Let’s show the new multibody tree.

sim.mb.dumpTree()
LEGEND: [<hinge type[/dofs]> <prescribed subhinges>] <body name>[/num embedded bodies > 0] <flex dofs > 0>
|-multibody_MBVROOT_
   |-[REVOLUTE] bd1
      |-[REVOLUTE] bd2
         |-[REVOLUTE] pendulum_2_bd1
            |-[REVOLUTE] pendulum_2_bd2

Python script#

Let’s use the simulation we already have to create a standalone Python script that can be used to regenerate the system we have created. This will dump the simulation information into the file.

tf = NamedTemporaryFile(suffix=".py")
sim.mb.toDS().toFile(tf.name)
1 file reformatted

This call will list the generated file within the Jupyter notebook. Note that the file contains repeated segments defining the kinematics and mass properties for each body and its parent hinge, along with directives to create the body and hinge objects themselves. The parameters in the file can be changed, more bodies can be added following the pattern to create a new simulation instance.

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 Multibody
# This file was generated on 2026-06-30 09:36:45.112844
import Karana.Math as km
import numpy as np
import atexit
from Karana.Math.Kquantities import ureg
from Karana.KUtils.Sim import Sim
import Karana.Scene as ks
import Karana.Dynamics as kd


def populateMbody(mb: kd.Multibody):
    """Create, add, and connect physical bodies via hinges.

    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 method 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.

    Parameters
    ----------
    mb : kd.Multibody
        The Multibody to populate with physical bodies.
    """
    # ----------------------------------------------------------------------------------------------
    # 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,
    )

    # ----------------------------------------------------------------------------------------------
    # Define the 'obstacle_part' mesh parameters for 'bd2' body
    # 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,
    )

    # 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(
            10.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, obstacle_part],
    )

    # 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)

    # ----------------------------------------------------------------------------------------------
    # Create 'pendulum_2_bd1' body
    # ----------------------------------------------------------------------------------------------

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

    # Create one or more meshes attached to the 'pendulum_2_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 'pendulum_2_bd1' body
    # Define the part shape (geometry)
    box = ks.BoxGeometry(0.05000000074505806, 0.05000000074505806, 1.0)

    bd1_body_1 = 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 'pendulum_2_bd1' body
    # Define the part shape (geometry)
    cylinder = ks.CylinderGeometry(0.10000000149011612, 0.10000000149011612)

    bd1_pivot_1 = 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 'pendulum_2_bd1' body
    # ----------------------------------------------------------------------------------------------

    # Assemble overall parameters needed to define the 'pendulum_2_bd1' body
    # including mass and kinematics properties.
    pendulum_2_bd1_params = kd.PhysicalBodyParams(
        # 'pendulum_2_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 'pendulum_2_bd1' parent hinge
        # relative to the 'pendulum_2_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 'pendulum_2_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 'pendulum_2_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 'pendulum_2_bd1' scene parts defined above
        scene_part_specs=[bd1_body_1, bd1_pivot_1],
    )

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

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

    # Set the parameters for the 'pendulum_2_bd1' body
    pendulum_2_bd1.setParams(pendulum_2_bd1_params)

    # ----------------------------------------------------------------------------------------------
    # Create 'pendulum_2_bd2' body
    # ----------------------------------------------------------------------------------------------

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

    # Create one or more meshes attached to the 'pendulum_2_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 'pendulum_2_bd2' body
    # Define the part shape (geometry)
    box_1 = ks.BoxGeometry(0.05000000074505806, 0.05000000074505806, 1.0)

    bd2_body_1 = 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 'pendulum_2_bd2' body
    # Define the part shape (geometry)
    cylinder_1 = ks.CylinderGeometry(0.10000000149011612, 0.10000000149011612)

    bd2_end_1 = 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 'pendulum_2_bd2' body
    # ----------------------------------------------------------------------------------------------

    # Assemble overall parameters needed to define the 'pendulum_2_bd2' body
    # including mass and kinematics properties.
    pendulum_2_bd2_params = kd.PhysicalBodyParams(
        # 'pendulum_2_bd2' mass properties
        spI=km.SpatialInertia(
            10.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 'pendulum_2_bd2' parent hinge
        # relative to the 'pendulum_2_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 'pendulum_2_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 'pendulum_2_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 'pendulum_2_bd2' scene parts defined above
        scene_part_specs=[bd2_body_1, bd2_end_1],
    )

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

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

    # Set the parameters for the 'pendulum_2_bd2' body
    pendulum_2_bd2.setParams(pendulum_2_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)

    obstacle_part_1 = 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_1)

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

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

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


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

    # Populate the Multibody
    populateMbody(sim.mb)

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

    atexit.register(cleanup)

Above, you’ll see values populate for each body such as inb_to_joint_transform. The image below gives a visual for what all of these values mean.

At this time, we will delete our current sim and create a new one from this generated file.

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

Let check the structure of the new model from the command line.

sim.mb.displayModel()
LEGEND: <body number> <body name> <parent body> <hinge type> [prescribed subhinges] <U dofs/offset> [flex dofs/offset]

      Body             Parent                 Hinge    Dofs          
      ____             ______                 _____    ____          
   1. bd1              multibody_MBVROOT_    REVOLUTE  1/0           
   2. bd2              bd1                   REVOLUTE  1/1           
   3. pendulum_2_bd1   bd2                   REVOLUTE  1/2           
   4. pendulum_2_bd2   pendulum_2_bd1        REVOLUTE  1/3           
                                                       ____          
                                                       4             
sim.mb.dumpTree()
 Nodes
 ---
LEGEND: [<hinge type[/dofs]> <prescribed subhinges>] <body name>[/num embedded bodies > 0] <flex dofs > 0>
|-multibody_MBVROOT_
   |-[REVOLUTE] bd1
      |-[REVOLUTE] bd2
         |-[REVOLUTE] pendulum_2_bd1
            |-[REVOLUTE] pendulum_2_bd2

Let’s bring up the GUI again just to visually explore the new model.

# 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 42999
You may be able to connect in your browser at:
	http://newton:42999
[WebUI] Karana Viewer is running on port 29534
Open this URL to connect:
	http://newton:29534

You can now make changes to the generated Python file to experiment and customize the model and explore more of the kdFlex API.

Cleanup#

def cleanup():
    """Clean up the simulation."""
    global bd1, sim, f_to_f, new_sg, sg_ds
    del bd1, f_to_f, sim, new_sg, sg_ds


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