Advanced Guide#

This area contains detailed technical discussion related to kdFlex architecture and design.

Multibody root frames#

The frames layer (Frames Layer section) forms a foundational layer for the kdFlex software that the multibody model definition builds upon. The instance provides a root frame that serves as the unique anchor for the rest of the frames in the system.

As described in {ref}{multibodytop_sec section), the multibody model consists of a collection of physical body connected by is a virtual root body that is automatically created by the instance. This section focuses on intermediate frames between the root frame and the virtual root body including:

  • Newtoninan frame: The Newtonian frame is the frame that is used as the inertial frame when carrying out dynamics computations. For this purpose, the Newtonian frame is regarded as a non-accelerating and a non-rotating frame. kdFlex allows any frame upstream of the virtual root body be designated as a Newtonian frame, with the understanding that all rotational velocities and linear accelerations upstream of this frame will not be factored into the dynamics computations.

  • Multibody root frame: The multibody frame is downstream of the Newtonian frame and is typically the parent of the virtual root body. It’s typical use is to serve as a local frame that allows for the use of reasonable values for the generalized coordinates for the multibody system.

When the frames are distinct, the frame container root frame is upstream of the Newtonian frame, which is upstream of the multibody root frame, which upstream of the virtual root body, which in turn is upstream of all the body and nodes defined in the multibody system.

We now present some illustrative examples to illustrate the choice of these frames when creating the multibody model.

Root frame = Newtonian frame = Multibody root frame#

This is the simplest scenario where the virtual root body is directly connected to the root frame, which also serves as the Newtonian and the multibody root frame. A good use case for this are unit tests for multibody systems as well as for robotics types scenarios. The following illustrates an example of the frame hierarchy for a robotics simulation.

digraph multi_node_cutjoint {
    // Enable ranking across cluster boundaries
    newrank=true;

    // Draw the original parent-to-child direction from top to bottom.
    title="AAAA";
    graph [bgcolor="transparent", rankdir=TB, nodesep=0.45, ranksep=0.55,
          label="Frames structure for robotics simulations.\nSame root, Newtonian and multibody frames"];
    node [shape=circle, style=filled, fontname="Helvetica"];
    edge [color="#4a4a4a"];

   // frame container root
    subgraph cluster_fc {
        label="frame container";
        color="#38761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        root [label="root", fillcolor="#f4cccc"];
     }

    subgraph cluster_mb {
        label="Multibody";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";
    vroot [fillcolor="#4fe2f3", shape="box"];

    // vehicle frames
    subgraph cluster_vehicle1 {
        label="Robot A";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        link1 [label="linkA1", fillcolor="#a1eefa", shape="box"];
        link2 [label="linkA2", fillcolor="#a1eefa", shape="box"];
        link3 [label="linkA3", fillcolor="#a1eefa", shape="box"];
    }
    subgraph cluster_vehicle2 {
        label="Robot B";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        linkB1 [label="linkB1", fillcolor="#a1ee22", shape="box"];
        linkB2 [label="linkB2", fillcolor="#a1ee22", shape="box"];
        linkB3 [label="linkB3", fillcolor="#a1ee22", shape="box"];
    }
    }

    root -> vroot [label="Usually identity transform"]; 
    vroot -> link1;
    link1 -> link2;
    link2 -> link3;
    
    vroot -> linkB1;
    linkB1 -> linkB2;
    linkB2 -> linkB3;
 }

Root frame != Newtonian frame = Multibody root frame (non-rotating)#

In this scenario, while the Newtonian frame and multibody frames are the same, they are distinct from the root frame. The basic ground vehicle simulation scenario described below illustrates this situation. The simulation involves simulating the dynamics of a ground vehicle at a specific site. The terrain surface DEM has a local frame, which is attached to the root frame, and serves as the Newtonian and multibody frames for the simulation. As defined, the multibody frame does not move.

digraph multi_node_cutjoint {
    // Enable ranking across cluster boundaries
    newrank=true;

    // Draw the original parent-to-child direction from top to bottom.
    title="AAAA";
    graph [bgcolor="transparent", rankdir=TB, nodesep=0.45, ranksep=0.55,
          label="Frames structure for a basic surface vehicle simulations\nwith no ephemerides.\nSame Newtonian and multibody frames"];
    node [shape=circle, style=filled, fontname="Helvetica"];
    edge [color="#4a4a4a"];

   // frame container root
    subgraph cluster_fc {
        label="frame container";
        color="#38761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        root [label="root", fillcolor="#f4cccc"];
     }

    // surface frames
    subgraph cluster_surface {
        label="Surface\n(from geotiff, gis)";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        dem [fillcolor="#d9ead3", shape="box"];
        rock [fillcolor="#d9ead3", shape="box"];
        site [fillcolor="#d9ead3", shape="box"];
    }
    
    surface_text [label="Used as both Newtonian\n& multibody root frames\nwhen planet rotational\nvelocity can be ignored)", shape=plaintext];
    // Force them on the same rank
    { rank=same; dem; surface_text; }

    subgraph cluster_mb {
        label="Multibody";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";
    vroot [fillcolor="#4fe2f3", shape="box"];

    // vehicle frames
    subgraph cluster_vehicle1 {
        label="Vehicle A";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisA [fillcolor="#a1eefa", shape="box"];
        engine [fillcolor="#a1eefa", shape="box"];
        camera [fillcolor="#a1eefa", shape="box"];
    }
    subgraph cluster_vehicle2 {
        label="Vehicle B";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisB [fillcolor="#a1ee22", shape="box"];
        wheel1 [fillcolor="#a1ee22", shape="box"];
        wheel2 [fillcolor="#a1ee22", shape="box"];
    }
    }

    root -> dem [label="Usually identity transform"]; 
    dem -> rock;
    dem -> site;

    site -> vroot [label="Usually identity transform"]; 
    vroot -> chassisA;
    chassisA -> engine;
    chassisA -> camera;
    
    vroot -> chassisB;
    chassisB -> wheel1;
    chassisB -> wheel2;
}

Root frame != Newtonian frame = Multibody root frame (rotating)#

This scenario is a variant of the ground vehicle simulation described above, but where the sun direction and its motion with time is included in the simulation since it changes the lighting and shadows. To meet this need, the simulation includes ephemerides for planetary bodies via the Spice toolkit. The terrain surface DEM (possibly obtained from a geotiff file) is attached to the surface of a planetary body such as the rotating Earth. The planet center attached rotating frame is referred to as the planet centered rotating (PCR) frame. Since the PCR frame is a rotating frame - it is not strictly speaking an inertial frame. As a consequence, the surface terrain frame is not an inertial frame either. However, the non-inertial contributions from the PCR rotation are small, and can times be neglected. Continuing to choose the surface terrain frame as the Newtonian and multibody root frames omits these non-inertial contributions to the system dynamics. The ephemerides is only used to update the locations of the celestial bodies as needed by the lighting models. Note that the PCR frame could also have been used for the Newtonian and multibody root frames without changing the dynamics, but with the downside that the coordinates needed to define the location of the ground vehicle would be inconveniently large to include the distance from the center of the planetary body.

The frame hierarchy for this is shown below.

digraph multi_node_cutjoint {
    // Enable ranking across cluster boundaries
    newrank=true;

    // Draw the original parent-to-child direction from top to bottom.
    title="AAAA";
    graph [bgcolor="transparent", rankdir=TB, nodesep=0.45, ranksep=0.55,
          label="Frames structure for surface vehicle simulations\n where planet rotation can be ignored.\nSame Newtonian and multibody frames."];
    node [shape=circle, style=filled, fontname="Helvetica"];
    edge [color="#4a4a4a"];

   // frame container root
    subgraph cluster_fc {
        label="frame container";
        color="#38761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        root [label="root", fillcolor="#f4cccc"];
     }

    // spice frames
    subgraph cluster_spice {
        label="Spice\n(ephemerides)";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        SSBC [fillcolor="#cfe2f3", shape="box"];
        Sun [fillcolor="#cfe2f3", shape="box"];
        EarthPCI [fillcolor="#cfe2f3", shape="box"];
        EarthPCR [fillcolor="#cfe2f3", shape="box"]; 
    }
     spice_text [label="Used to position Sun and\n other planetary bodies", shape=plaintext];
    // Force them on the same rank
    { rank=same; SSBC; spice_text; }

    // surface frames
    subgraph cluster_surface {
        label="Surface\n(from geotiff, gis)";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        dem [fillcolor="#d9ead3", shape="box"];
        rock [fillcolor="#d9ead3", shape="box"];
        site [fillcolor="#d9ead3", shape="box"];
    }
    
    surface_text [label="Used as both Newtonian\n& multibody root frames\nwhen planet rotational\nvelocity can be ignored)", shape=plaintext];
    // Force them on the same rank
    { rank=same; dem; surface_text; }

    subgraph cluster_mb {
        label="Multibody";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";
    vroot [fillcolor="#4fe2f3", shape="box"];

    // vehicle frames
    subgraph cluster_vehicle1 {
        label="Vehicle A";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisA [fillcolor="#a1eefa", shape="box"];
        engine [fillcolor="#a1eefa", shape="box"];
        camera [fillcolor="#a1eefa", shape="box"];
    }
    subgraph cluster_vehicle2 {
        label="Vehicle B";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisB [fillcolor="#a1ee22", shape="box"];
        wheel1 [fillcolor="#a1ee22", shape="box"];
        wheel2 [fillcolor="#a1ee22", shape="box"];
    }
    }

    root -> SSBC;
    SSBC -> Sun;
    SSBC -> EarthPCI;
    SSBC -> EarthPCR;

    EarthPCR -> dem [label="Transform to local\n surface (eg. ENU) frame"]; 
    dem -> rock;
    dem -> site;

    site -> vroot [label="usually identity transform\n(except for non-inertial dynamics)"]; 
    vroot -> chassisA;
    chassisA -> engine;
    chassisA -> camera;
    
    vroot -> chassisB;
    chassisB -> wheel1;
    chassisB -> wheel2;
}

Root frame != Newtonian frame != Multibody root frame#

There are times where the inertial effects cannot be ignored. This would be case if the previous scenario involved an orbiter or lander vehicle. To obtain a suitable frame to use as inertial frame for dynamics computation, the normal practice is to define a planet centered inertial (PCI) frame that is a non-rotating frame co-located with the PCR frame. In this scenario, the PCI frame is used as the the Newtonian frame, and the surface terrain frame as the multibody frame. This is the most general case and illustrated in the figure below.

digraph multi_node_cutjoint {
    // Enable ranking across cluster boundaries
    newrank=true;

    // Draw the original parent-to-child direction from top to bottom.
    title="AAAA";
    graph [bgcolor="transparent", rankdir=TB, nodesep=0.45, ranksep=0.55, label="Frames structure for orbiter, lander simulations"];
    node [shape=circle, style=filled, fontname="Helvetica"];
    edge [color="#4a4a4a"];

   // frame container root
    subgraph cluster_fc {
        label="frame container";
        color="#38761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        root [label="root", fillcolor="#f4cccc"];
     }

    // spice frames
    subgraph cluster_spice {
        label="Spice\n(ephemerides)";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        SSBC [fillcolor="#cfe2f3", shape="box"];
        Sun [fillcolor="#cfe2f3", shape="box"];
        EarthPCI [fillcolor="#cfe2f3", shape="box"];
        EarthPCR [fillcolor="#cfe2f3", shape="box"]; 
    }
     spice_text [label="A PCI frame is usually\nused as Newtonian\nframe for dynamics", shape=plaintext];
    // Force them on the same rank
    { rank=same; SSBC; spice_text; }

    // surface frames
    subgraph cluster_surface {
        label="Surface\n(from geotiff, gis)";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        dem [fillcolor="#d9ead3", shape="box"];
        rock [fillcolor="#d9ead3", shape="box"];
        site [fillcolor="#d9ead3", shape="box"];
    }
    
    surface_text [label="Used as intermediate\n multibody root\nframe for smaller\n Q coordinates values", shape=plaintext];
    // Force them on the same rank
    { rank=same; dem; surface_text; }

    subgraph cluster_mb {
        label="Multibody";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";
    vroot [fillcolor="#4fe2f3", shape="box"];

    // vehicle frames
    subgraph cluster_vehicle1 {
        label="Vehicle A";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisA [fillcolor="#a1eefa", shape="box"];
        engine [fillcolor="#a1eefa", shape="box"];
        camera [fillcolor="#a1eefa", shape="box"];
    }
    subgraph cluster_vehicle2 {
        label="Vehicle B";
        color="#98761d";
        fontcolor="#274e13";
        style="dashed,rounded";

        chassisB [fillcolor="#a1ee22", shape="box"];
        wheel1 [fillcolor="#a1ee22", shape="box"];
        wheel2 [fillcolor="#a1ee22", shape="box"];
    }
    }

    root -> SSBC;
    SSBC -> Sun;
    SSBC -> EarthPCI;
    SSBC -> EarthPCR;

    EarthPCR -> dem [label="Transform to local\n surface (eg. ENU) frame"]; 
    dem -> rock;
    dem -> site;

    site -> vroot [label="usually identity transform\n(except for non-inertial dynamics)"]; 
    vroot -> chassisA;
    chassisA -> engine;
    chassisA -> camera;
    
    vroot -> chassisB;
    chassisB -> wheel1;
        chassisB -> wheel2;
}

Partitioned integrators#

Karana.Integrators.createIntegrator() takes num_q_states immediately after num_states. The state layout is [Q, U]: the first num_q_states entries are Q and the remaining entries are U. Ordinary integrators store this count but do not use the partition; pass zero when it is unspecified. Every integrator requires num_q_states <= num_states. The Karana.Integrators.Integrator.nQ() getter returns the stored count. Karana.Integrators.Integrator.hardReset() takes both dimensions, while a soft reset preserves them.

Karana.Integrators.PartitionedIntegrator is abstract and requires nonempty Q and U blocks for a nonempty system. An empty system uses both counts set to zero and advances time without evaluating derivatives.

Karana.Integrators.ExplicitVerletIntegrator uses explicit kick–drift–kick steps. Each advanceTo call takes one step over the requested interval. Second-order accuracy requires separable equations Qdot = f(U) and Udot = g(t, Q). Q and U may have different sizes. Coordinate-dependent Q rates and velocity-dependent forces fall outside this contract; the integrator cannot check those dependencies from the callback.

For a unit harmonic oscillator:

import Karana.Core as kc
import Karana.Integrators as ki
import numpy as np

def oscillator(time: object, state: np.ndarray, derivative: np.ndarray) -> bool:
    """Write [U, -Q] into derivative for state=[Q, U]; time is unused; return True."""
    derivative[0] = state[1]
    derivative[1] = -state[0]
    return True

integrator = ki.createIntegrator(ki.IntegratorType.EXPLICIT_VERLET, 2, 1, oscillator)
try:
    integrator.softReset(0.0, np.array([1.0, 0.0]))
    for step in range(1, 101):
        integrator.advanceTo(step / 100)
    print(integrator.getX())
finally:
    kc.discard(integrator)

Manual construction calls must supply the new argument, and dependent C++ libraries must be rebuilt. StatePropagator integration also requires its factory and hard-reset calls to supply the Q count of the actual packed layout.