Parallel vehicle propagation#

This notebook runs two independent vehicle SubTrees in parallel via Karana.Models.ParallelVehicles. ParallelVehicles registers on a global state propagator and advances a list of vehicle state propagator instances during the global propagator’s hop.

This example uses small serial-chain vehicles so the setup is easy to see. Each vehicle has its own SubTree, its own state propagator, its own integrator, and a recurring pre-hop event that sleeps for 0.1 seconds. The sleep events make the wall-clock difference between parallel and serial execution visible.

import time
import atexit

import numpy as np
import Karana.Core as kc
import Karana.Dynamics as kd
import Karana.Integrators as ki
import Karana.Models as kmdl
from Karana.KUtils.Sim import Sim

Build one simulation with two vehicle propagators#

The helper below creates one simple serial-chain vehicle, creates a vehicle state propagator for that vehicle’s SubTree, and registers a recurring pre-hop sleep event. It returns the vehicle state propagator.

The notebook uses one Karana.KUtils.Sim.Sim for the entire example.

bc = kc.BaseContainer.singleton()


def createVehicleStatePropagator(
    sim: Sim,
    prefix: str,
    integrator: ki.IntegratorType,
    sleep_time: float = 0.1,
) -> kd.StatePropagator:
    """Create one vehicle and its state propagator.

    Parameters
    ----------
    sim : Sim
        Simulation that owns the global multibody and global state propagator.
    prefix : str
        Prefix used to name the vehicle bodies, subtree, and timed event.
    integrator : ki.IntegratorType
        Integrator type to use for the vehicle state propagator.
    sleepTime : float
        Wall-clock sleep duration, in seconds, for each recurring pre-hop event.

    Returns
    -------
    kd.StatePropagator
        State propagator for the vehicle subtree.
    """
    # Create the bodies for the vehicle
    kd.PhysicalBody.addSerialChain(
        f"{prefix}_bd",
        3,
        sim.mb.virtualRoot(),
        kd.PhysicalBodyParams(
            hinge_params=kd.PhysicalHingeParams(
                kd.HingeType.REVOLUTE,
                subhinge_params=[kd.PinSubhingeParams(unit_axis=np.array([1.0, 0.0, 0.0]))],
            )
        ),
    )

    sim.mb.ensureHealthy()
    sim.mb.resetData()

    # Create the SubTree for the vehicle
    subtree = kd.SubTree(
        prefix,
        sim.mb,
        sim.mb.virtualRoot(),
        [sim.mb.getBody(f"{prefix}_bd_0")],
    )

    # Create the StatePropagator for the vehicle
    sp = kd.StatePropagator(subtree)
    bc.at_exit_fns.insertBefore(
        f"_discard_sp_{sim.sp.id()}",
        f"_discard_vsp_{sp.id()}",
        lambda: kc.discard(sp),
    )
    sp.setIntegrator(integrator)
    sp.ensureHealthy()
    sp.setTime(0.0)
    sp.hardReset()
    sp.setState(sp.assembleState())
    sp.setMaxHopSize(1.0)

    # Register the preHop sleep event
    def sleepPreHop(t, _) -> None:
        """Sleep before a vehicle hop."""
        time.sleep(sleep_time)

    sp.fns.pre_hop_fns["sleep"] = sleepPreHop

    return sp
sim = Sim()

sp1 = createVehicleStatePropagator(sim, "v1", ki.IntegratorType.RK4)
sp2 = createVehicleStatePropagator(sim, "v2", ki.IntegratorType.CVODE)

Register ParallelVehicles#

Size the shared Karana.Core.ThreadPool with ThreadPool.setThreadPoolSize, register ParallelVehicles on the global state propagator, and set the global propagator to Karana.Integrators.IntegratorType.NO_OP. The global state propagator coordinates the vehicle state propagators; the vehicle state propagators do the integration.

# Size the thread pool to be at least the number of vehicles + 1
kc.ThreadPool.singleton().setThreadPoolSize(3)

# Create the ParallelVehicles KModel on the global StatePropagator (sim.sp)
parallel_vehicles = kmdl.ParallelVehicles("parallel_vehicles", sim.sp, [sp1, sp2])
del sp1, sp2

# Set the global state propagator to be NO_OP
sim.sp.setIntegrator(ki.IntegratorType.NO_OP)
sim.sp.ensureHealthy()
sim.sp.hardReset()
sim.sp.setState(sim.sp.assembleState())

Run in parallel mode#

First run the simulation for 5 seconds with. This lets the vehicles run in parallel.

tic = time.perf_counter()
sim.sp.advanceBy(5.0)
parallel_elapsed = time.perf_counter() - tic

parallel_result = {"mode": "parallel", "elapsed_s": parallel_elapsed}
parallel_result
{'mode': 'parallel', 'elapsed_s': 0.5023632100783288}

Run the same simulation in serial mode#

Now run the same Sim for another 5 seconds with ParallelVehicles.setRunInSerial set to True. This runs the same simulation, but executes the vehicles in serial.

parallel_vehicles.setRunInSerial(True)

tic = time.perf_counter()
sim.sp.advanceBy(5.0)
serial_elapsed = time.perf_counter() - tic

serial_result = {"mode": "serial", "elapsed_s": serial_elapsed}
serial_result
{'mode': 'serial', 'elapsed_s': 1.0018136049620807}

Compare results#

Both timing measurements cover a 5 second simulation interval. The first interval advances from 0 to 5 seconds in parallel mode, and the second advances from 5 to 10 seconds in serial mode. Because each vehicle has a recurring 0.1 second pre-hop sleep event, the parallel run should take less wall-clock time.

speedup = serial_result["elapsed_s"] / parallel_result["elapsed_s"]

for result in (parallel_result, serial_result):
    print(f"{result['mode']}: elapsed={result['elapsed_s']:.3f} s, ")

print(f"parallel speedup over serial: {speedup:.2f}x")
parallel: elapsed=0.502 s, 
serial: elapsed=1.002 s, 
parallel speedup over serial: 1.99x

Cleanup#

def cleanup():
    """Clean up the simulation."""
    global sim, parallel_vehicles
    del sim, parallel_vehicles


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