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