Python API

World

An Elodin simulation begins with a World object. The World object is the root of the simulation hierarchy and provides methods for composing and running the simulation. The World object also provides helper methods for displaying entities and graphs in the editor.

class elodin.World

The Elodin simulation world.

  • __init__() -> elodin.World

    Create a new world object.

  • spawn(archetypes, name) -> elodin.EntityId

    Spawn a new entity with the given archetypes and name.

    • archetypes : one or many Archetypes,
    • name : optional name of the entity
  • insert(id, archetypes) -> None

    Insert archetypes into an existing entity.

  • insert_asset(asset) -> handle reference

    Insert a 3D asset into the world.

  • shape(mesh, material) -> elodin.Shape

    Create a shape as an Elodin Shape Archetype.

    • mesh: the mesh of the shape,
    • material: the material of the shape
  • glb(url) -> elodin.Scene

    Load a GLB asset as an Elodin Scene Archetype.

    • url: the URL or filepath of the GLB asset
  • run(system, sim_time_step, run_time_step, default_playback_speed, max_ticks, optimize, is_canceled, pre_step, post_step, db_path, interactive, start_timestamp) -> None

    Run the simulation.

    • system : elodin.System, the systems to run, can be supplied as a list of systems delineated by pipes.
    • sim_time_step : float, optional, the amount of simulated time between each tick, defaults to 1 / 120.0.
    • run_time_step : float | None, optional, the amount of real time between each tick. By default it is None and runs at max speed. For real-time playback set to same value as sim_time_step.
    • default_playback_speed : float, optional, the default playback speed of the Elodin client when running this simulation, defaults to 1.0 (real-time).
    • max_ticks : integer, optional, the maximum number of ticks to run the simulation for before stopping.
    • optimize : bool, optional flag to enable runtime optimizations for the simulation code, defaults to False. If optimizations are enabled, the simulation will start slower but run faster.
    • is_canceled : Callable[[], bool], optional, a polling function checked during the simulation loop. If it returns True, the simulation exits gracefully. Useful for integrating with external control systems (e.g., a GUI stop button, or a watchdog timeout).
    • pre_step : Callable[[int, StepContext], None], optional, a callback function called before each simulation tick. Receives the tick number and a elodin.StepContext for direct database access. Useful for injecting external data (e.g., from hardware-in-the-loop systems) before the physics step runs.
    • post_step : Callable[[int, StepContext], None], optional, a callback function called after each simulation tick. Receives the tick number and a elodin.StepContext for direct database access. Useful for reading simulation results and sending data to external systems (e.g., SITL flight controllers).
    • db_path : string, optional, the path to the database directory. If not provided, a temporary database is created.
    • interactive : bool, optional, controls simulation behavior after reaching max_ticks, defaults to True. When True, the simulation pauses but remains running for continued interaction in the Elodin editor. When False, the simulation terminates completely after reaching max_ticks.
    • start_timestamp : int, optional, the starting timestamp for the simulation in microseconds. If None (default), uses the current system time (epoch-based). Set to 0 for zero-based timing where the simulation starts at t=0.
    • log_level : str, optional, log level for the embedded Elodin-DB instance (error, warn, info, debug, trace). Defaults to info unless RUST_LOG is set.

class elodin.EntityId

Integer reference identifier for entities in Elodin.

class elodin.StepContext

Context object passed to pre_step and post_step callbacks, providing direct database read/write access. This enables Software-In-The-Loop (SITL) and Hardware-In-The-Loop (HITL) workflows where external systems need to exchange data with the simulation at each tick.

StepContext provides direct database access within the same process, avoiding the overhead of a separate TCP connection. This is essential for high-frequency lockstep synchronization with external flight controllers or sensor systems.

Properties

  • tick -> int

    The current simulation tick count (0-indexed).

  • timestamp -> int

    The current simulation timestamp in microseconds since epoch. This value is calculated as start_timestamp + (tick * sim_time_step).

Methods

  • read_component(pair_name) -> numpy.ndarray

    Read the latest component data from the database.

    • pair_name : string, the full component name in "entity.component" format (e.g., "drone.accel", "drone.world_pos")

    Returns a NumPy array containing the component data. The array dtype matches the component schema and is always 1D; reshape if needed.

    Raises RuntimeError if the component doesn't exist or has no data.

  • write_component(pair_name, data, timestamp=None) -> None

    Write component data to the database.

    • pair_name : string, the full component name in "entity.component" format (e.g., "drone.motor_command")
    • data : numpy.ndarray, the component data to write
    • timestamp : int, optional, the timestamp (microseconds since epoch) to write at. If None, uses the current simulation timestamp.

    Raises RuntimeError if the component doesn't exist, or ValueError if the data size doesn't match the component schema.

    Timestamps must be monotonically increasing per component. Writing with a timestamp less than the last write will raise a TimeTravel error.

  • component_batch_operation(reads=[], writes=None, write_timestamps=None) -> dict[str, numpy.ndarray]

    Perform multiple component reads and writes in a single database operation. This is more efficient than calling read_component/write_component multiple times, as it only acquires the database lock once for all operations.

    • reads : list[str], list of component names to read (e.g., ["drone.accel", "drone.gyro"])
    • writes : dict[str, numpy.ndarray], optional, dict mapping component names to numpy arrays to write
    • write_timestamps : dict[str, int], optional, dict mapping component names to timestamps (microseconds since epoch). Components not in this dict use the current simulation timestamp.

    Returns a dict mapping read component names to their numpy array values.

    Use batch operations when reading or writing multiple components in a single callback for better performance at high tick rates.

  • truncate() -> None

    Truncate all component data and message logs in the database, resetting the tick counter to 0. This clears all stored time-series data while preserving component schemas and metadata.

    Use this to control the freshness of the database and ensure reliable data from a known tick. Common use case: clearing warmup data before starting the actual simulation run.

    After truncate(), any subsequent write_component() calls in the same callback will write at the start timestamp (tick 0), preventing TimeTravel errors on the next tick.

  • stop_recipes() -> None

    Gracefully terminate all s10-managed recipes (external processes).

    This signals all processes managed by s10 (registered via world.recipe()) to shut down gracefully. The processes receive SIGTERM and have approximately 2 seconds to clean up before being force-killed.

    Use this to ensure clean shutdown of external processes (like Betaflight SITL) before the simulation exits, preventing memory corruption or resource leaks.

    This is a no-op if no recipes were registered or if running with --no-s10.

    Call stop_recipes() before the simulation exits to allow external processes time to clean up. You may want to add a brief delay (e.g., time.sleep(0.5)) after calling this method to ensure the processes have finished shutting down.

Example: SITL Integration

This example demonstrates a typical Software-In-The-Loop workflow where a flight controller receives sensor data and returns motor commands:

import elodin as el
import numpy as np
import time

# External flight controller interface (e.g., Betaflight SITL)
flight_controller = FlightControllerBridge()
MAX_TICKS = 10000

def sitl_post_step(tick: int, ctx: el.StepContext):
    """Post-step callback for lockstep SITL synchronization."""

    # Read sensor data from the physics simulation using batch operation
    sensor_data = ctx.component_batch_operation(
        reads=["drone.accel", "drone.gyro", "drone.world_pos"]
    )

    accel = sensor_data["drone.accel"]
    gyro = sensor_data["drone.gyro"]
    position = sensor_data["drone.world_pos"]

    # Send sensor data to flight controller, receive motor commands
    motors = flight_controller.step(
        accel=accel,
        gyro=gyro,
        timestamp=ctx.timestamp
    )

    # Write motor commands back to the simulation
    ctx.write_component("drone.motor_command", motors)

    # Print status every 1000 ticks
    if tick % 1000 == 0:
        print(f"Tick {tick}: motors={motors}, pos={position}")

    # Graceful shutdown before simulation ends
    if tick >= MAX_TICKS - 1:
        ctx.stop_recipes()  # Signal s10 processes to terminate
        time.sleep(0.5)     # Allow time for graceful shutdown

# Run simulation with SITL callback
world.run(
    system,
    sim_time_step=1/1000.0,  # 1kHz for flight controller
    max_ticks=MAX_TICKS,
    post_step=sitl_post_step,
    db_path="sitl_data",
    interactive=False,  # Headless mode
)

For writing data at custom timestamps (e.g., logging historical sensor readings):

def log_with_timestamps(tick: int, ctx: el.StepContext):
    # Write current data at simulation timestamp (default)
    ctx.write_component("drone.motor_command", motors)

    # Write historical data at a specific timestamp
    historical_ts = ctx.timestamp - 100_000  # 100ms ago (microseconds)
    ctx.write_component("drone.sensor_log", sensor_data, timestamp=historical_ts)

    # Batch write with per-component timestamps
    ctx.component_batch_operation(
        writes={
            "drone.data_a": data_a,
            "drone.data_b": data_b,
        },
        write_timestamps={
            "drone.data_b": custom_timestamp,  # Custom timestamp for data_b
            # data_a uses current simulation timestamp
        }
    )

class elodin.Panel

A configuration object for creating a panel view in the Elodin Client UI.

  • Panel.viewport(track_entity, track_rotation, fov, active, pos, looking_at, show_grid, hdr, name) -> elodin.Panel

    Create a viewport panel.

    • track_entity : elodin.EntityId, optional, the entity to track.
    • track_rotation : boolean, whether to track the rotation of the entity, defaults to True.
    • fov : float, the field of view of the camera, defaults to 45.0.
    • active : boolean, whether the panel is active, defaults to False.
    • pos : list, optional, the position of the camera.
    • looking_at : list, optional, the point the camera is looking at.
    • show_grid : boolean, whether to show the grid, defaults to False.
    • hdr : boolean, whether to use HDR rendering, defaults to False.
    • name : string, optional, the name of the panel.
  • Panel.graph(*entities, name) -> elodin.Panel

    Create a graph panel.

    • *entities : Sequence of elodin.GraphEntity objects to include in the graph.
    • name : string, optional, the name of the panel.
  • Panel.vsplit(*panels, active) -> elodin.Panel

    Create a vertical split panel.

    • *panels : Sequence of elodin.Panel objects to vertically split across.
    • active : boolean, whether the panel is active, defaults to False.
  • Panel.hsplit(*panels, active) -> elodin.Panel

    Create a horizontal split panel.

    • *panels : Sequence of elodin.Panel objects to horizontally split across.
    • active : boolean, whether the panel is active, defaults to False.

class elodin.GraphEntity

A configuration object for creating a graph entity in the Elodin Client UI.

  • __init__(entity_id, *components) -> elodin.GraphEntity

    Create a graph entity.

    • entity_id : elodin.EntityId, the entity to graph.
    • *components : Sequence of elodin.ShapeIndexer indexes of components to graph.

class elodin.Mesh

A built in class for creating basic 3D meshes.

  • Mesh.cuboid(x: float, y: float, z: float) -> elodin.Mesh

    Create a cuboid mesh with dimensions x, y, and z.

  • Mesh.sphere(radius: float) -> elodin.Mesh

    Create a sphere mesh with radius radius.

class elodin.Material

A built in class for creating basic 3D materials.

  • Material.color(r: float, g: float, b: float) -> elodin.Material

    Create a material with RGB color values.

class elodin.Shape

Shape describes a basic entity for rendering 3D assets in Elodin.

  • __init__(mesh, material) -> elodin.Shape

    Create a shape archetype initialized to the provided mesh and material.

    • mesh : handle reference returned from World.insert_asset() using the elodin.Mesh class.
    • material : handle reference returned from World.insert_asset() using the elodin.Material class.

class elodin.Scene

Scene describes a complex scene entity loaded from a glb file.

  • __init__(glb) -> elodin.Scene

    Create a scene from a loaded file.

    • glb : handle reference returned from World.insert_asset() using the elodin.Glb class.

Example

This example creates a simple simulation with a spinning cuboid body:

import elodin as el
import jax.numpy as jnp

@el.map
def spin(f: el.Force, inertia: el.Inertia) -> el.Force:
    return f + el.Force(torque=(inertia.mass() * jnp.array([0.0, 1.0, 0.0])))

w = el.World()

mesh = w.insert_asset(el.Mesh.cuboid(0.1, 0.8, 0.3))
material = w.insert_asset(el.Material.color(25.3, 18.4, 1.0))

cuboid_id = w.spawn([el.Body(), el.Shape(mesh, material)], name="cuboid")

camera = el.Panel.viewport(pos=[0.0, -5.0, 0.0], hdr=True, name="camera")
graph = el.Panel.graph(
    el.GraphEntity(cuboid_id, *el.Component.index(el.WorldPos)[:4]), name="graph"
)

w.spawn(el.Panel.vsplit(camera, graph), name="main_view")

sys = el.six_dof(sys=spin)
sim = w.run(sys, sim_time_step=1.0 / 120.0)



6 Degrees of Freedom Model

Elodin has a built-in 6 Degrees of Freedom (6DoF) system implementation for simulating rigid bodies, such as flight vehicles. You can review the implementation here. Using the associated elodin.Body archetype and prebuilt components, we can create a 6DoF system that aligns closely with this familiar model from Simulink.

function elodin.six_dof

  • six_dof(time_step, sys, integrator) -> elodin.System

    Create a system that models the 6DoF dynamics of a rigid body in 3D space. The provided set of systems can be integrated as effectors using the provided integrator and simulated in a world with a given time_step.

    • time_step : float, The time step used when integrating a body's acceleration into its velocity and position. Defaults to the sim_time_step provided in World.run(...) if unset
    • sys : one or more elodin.System instances used as effectors
    • integrator : elodin.Integrator, default is Integrator.Rk4

class elodin.Integrator

  • elodin.Integrator.Rk4 -> elodin.Integrator

    Runge-Kutta 4th Order (RK4) Integrator: Elodin provides a built-in implementation for a 4th order Runge-Kutta integrator. The RK4 integrator is a numerical method used to solve ordinary differential equations. You can review the implementation here.

  • elodin.Integrator.SemiImplicit -> elodin.Integrator

    Semi-Implicit Integrator: Elodin provides a built-in implementation for a semi-implicit Euler integrator. The semi-implicit integrator is a numerical method used to solve ordinary differential equations. You can review the implementation here.

class elodin.Body

Body is an archetype that represents the state of a rigid body with six degrees of freedom. It provides all of the spatial information necessary for the elodin.six_dof system

Example

A simple example of a 6DoF system that models gravity acting on a rigid body in 3D space.

import elodin as el
import jax.numpy as jnp

SIM_TIME_STEP = 1.0 / 120.0

@el.map
def gravity(f: el.Force, inertia: el.Inertia) -> el.Force:
    return f + el.Force(linear=(inertia.mass() * jnp.array([0.0, -9.81, 0.0])))

w = el.World()
w.spawn(el.Body(), name="example")
sys = el.six_dof(sys=gravity, integrator=el.Integrator.Rk4)
sim = w.run(sys, SIM_TIME_STEP)

You should never need to use the six_dof time_step parameter unless you need to simulate a sensor at a specific frequency different from the world simulation. This is an advanced feature and should be used with caution, and likely a symptom of needing to move your testing into your flight software & communicate with the simulation over Impel.

# lower frequency time step
SIX_DOF_TIME_STEP = 1.0 / 60.0
sys = el.six_dof(time_step=SIX_DOF_TIME_STEP, sys=gravity)
sim = w.run(sys, SIM_TIME_STEP)



Components

Components are containers of data that is associated with an entity. See ECS Data Model for more context on entities and components.

To define a new component, add elodin.Component as metadata to a base class using typing.Annotated. The base class can be jax.Array or some other container of array data. This is an example of a component that annotates jax.Array:

import elodin as el

Wind = typing.Annotated[
    jax.Array,
    el.Component(
        "wind",
        el.ComponentType(el.PrimitiveType.F64, (3,)),
        metadata={"element_names": "x,y,z"},
    ),
]

class elodin.Component

A container of component metadata.

  • __init__(name, type = None, asset = False, metadata = {}) -> elodin.Component

    Create a new component with:

    • Unique name (e.g. "world_pos, "inertia").
    • Component type information (via elodin.ComponentType). This is optional if the base class already provides component type information as part of __metadata__, which is the case for elodin.Quaternion, elodin.Edge, and all spatial vector algebra classes.
    • Flag indicating whether the component is an asset (e.g. a mesh, texture, etc.).
    • Other metadata that is optional (e.g. description, units, labels, etc.).
    import elodin as el
    
    el.Component(
        "wind",
        el.ComponentType(el.PrimitiveType.F64, (3,)),
        metadata={"element_names": "x,y,z"},
    ),
    

    The above example defines a "wind" component that is a 3D vector of float64 values. The "element_names" entry is an example of optional metadata. It specifies the labels for each element of the vector that are displayed in the component inspector.

  • Component.name(component) -> string

    The unique name of the component.

  • Component.index(component) -> elodin.ShapeIndexer

    A shape indexer that can be used to access the component data.

class elodin.ComponentType

ComponentType describes the shape and data type of a component. The shape is a tuple of integers that specifies the size of each dimension (e.g. () for scalars, (3,) for 3D vectors). The data type is an elodin.PrimitiveType.

  • __init__(dtype, shape) -> elodin.ComponentType

    Create a component type from a data type and shape.

class elodin.Edge

An edge is a relationship between two entities. See elodin.GraphQuery for information on how to use edges in graph queries.

  • __init__(left, right) -> elodin.Edge

    Create an edge between two entities given their unique ids.



Archetypes

An archetype is a combination of components with a unique name. To define a new archetype, create a subclass of elodin.Archetype with the desired components as fields. Here is an example of an archetype for a kalman filter:

To automatically generate __init__(), you can use the @dataclass decorator.

import elodin as el
from dataclasses import dataclass

@dataclass
class KalmanFilter(el.Archetype):
    p: P
    att_est: AttEst
    ang_vel_est: AngVelEst
    bias_est: BiasEst

The archetype can then be used to attach components to entities:

world.insert(
    satellite,
    KalmanFilter(
        p=np.identity(6),
        att_est=el.Quaternion.identity(),
        ang_vel_est=np.zeros(3),
        bias_est=np.zeros(3),
    ),
)



Systems

Systems are the building blocks of simulation; they are functions that operate on a set of input components and produce a set of output components. Elodin provides decorators that allow for systems to be easily defined from functions.

@elodin.system

This is a lower-level primitive; for many cases @elodin.map – a wrapper around @elodin.system – is easier to use.

This is a lower-level API for defining a system. A function decorated with @elodin.system accepts special parameter types (such as elodin.Query and elodin.GraphQuery) that specify what data the system needs access to. It returns an elodin.Query containing one or more components. Some examples of @elodin.system are:

import elodin as el

@el.system
def gravity(
    graph: el.GraphQuery[GravityEdge],
    query: el.Query[el.WorldPos, el.Inertia],
) -> el.Query[el.Force]: ...

@el.system
def apply_wind(
    w: el.Query[Wind], q: el.Query[el.Force, el.WorldVel]
) -> el.Query[el.Force]: ...

@elodin.map

Graph queries cannot be used with @elodin.map. Use @elodin.system instead.

This is a higher-level API for defining a system that reduces the boilerplate of @elodin.system by unpacking the input and output queries into individual components, and wrapping the body of the function in a query.map(ret_type, ...) call. It is useful for systems with simple data flow patterns. Some examples of @elodin.map are:

import elodin as el

@el.map
def gravity(f: el.Force, inertia: el.Inertia) -> el.Force: ...

@el.map
def gyro_omega(vel: el.WorldVel) -> GyroOmega: ...

The following systems are equivalent as the @elodin.map definition effectively desugars to the @elodin.system one:

import elodin as el

@el.map
def gravity(f: el.Force, inertia: el.Inertia) -> el.Force:
    return f + el.SpatialForce(linear=inertia.mass() * jnp.array([0.0, -9.81, 0.0]))

@el.system
def gravity(query: el.Query[el.Force, el.Inertia]) -> el.Query[el.Force]:
    return query.map(
        el.Force,
        lambda f, inertia: f + el.SpatialForce(linear=inertia.mass() * jnp.array([0.0, -9.81, 0.0])),
    )

@elodin.map_seq

@elodin.map_seq is similar to @elodin.map. In fact they will produce the same results numerically but their performance differs.

@elodin.map_seq maps over items sequentially, so it does lose some parallelism, but it also preserves jax.lax.cond()'s behavior, which only evaluates one of its consequents. @elodin.map translates all jax.lax.cond()s to jax.lax.select()s, which always evaluates both of its consequents. So in cases where one of your consequents is both expensive and seldom, @elodin.map_seq can be faster than @elodin.map.

import elodin as el

@el.map_seq
def gravity(f: el.Force, inertia: el.Inertia) -> el.Force: ...

@el.map_seq
def gyro_omega(vel: el.WorldVel) -> GyroOmega: ...

The following systems are equivalent as the @elodin.map_seq definition effectively desugars to the @elodin.system one:

import elodin as el

@el.map_seq
def gravity(f: el.Force, inertia: el.Inertia) -> el.Force:
    return f + el.SpatialForce(linear=inertia.mass() * jnp.array([0.0, -9.81, 0.0]))

@el.system
def gravity(query: el.Query[el.Force, el.Inertia]) -> el.Query[el.Force]:
    return query.map_seq(
        el.Force,
        lambda f, inertia: f + el.SpatialForce(linear=inertia.mass() * jnp.array([0.0, -9.81, 0.0])),
    )

class elodin.Query

Query is the primary mechanism for accessing data in Elodin. It is a view into the world state that is filtered by the components specified in the query. Only entities that have been spawned with all of the query's components will be selected for processing. For example, the query Query[WorldPos, Inertia] would only select entities that have both a WorldPos and an Inertia component (typically via the Body archetype).

  • map(ret_type, map_fn) -> elodin.Query

    Apply a function map_fn to the query's components and return a new query with the specified ret_type return type. map_fn should be a function that takes the query's components as arguments and returns a single value of type ret_type.

    import elodin as el
    
    @el.system
    def gravity(query: el.Query[el.Force, el.Inertia]) -> el.Query[el.Force]:
        return query.map(
            el.Force,
            lambda f, inertia: f + el.SpatialForce(linear=inertia.mass() * jnp.array([0.0, -9.81, 0.0])),
        )
    

    In this example, ret_type is el.Force and map_fn is a lambda function with the signature (el.Force, el.Inertia) -> el.Force.

    To return multiple components as output, ret_type must be a tuple:

    import elodin as el
    
    @el.system
    def multi_out_sys(query: el.Query[A]) -> el.Query[C, D]:
        return query.map((C, D), lambda a: ...)
    

class elodin.GraphQuery

GraphQuery is a special type of query for operating on edges in an entity graph. Edges represent relationships between entities and are fundamental for modeling physics systems such as gravity.

A GraphQuery requires exactly one type argument, which must be an annotated elodin.Edge component. For example, GraphQuery[GravityEdge] is a valid graph query if GravityEdge is a component with Edge as the base class:

GravityEdge = typing.Annotated[elodin.Edge, elodin.Component("gravity_edge")]
  • edge_fold(left_query, right_query, return_type, init_value, fold_fn) -> elodin.Query

    For each edge, query the left and right entity components using left_query and right_query, respectively. Then, apply the fold_fn function to those input components to compute the return_type output component(s).

    The return_type component(s) must belong to the left entity of the edge.

    A single left entity may have edges to multiple right entities, but it can only hold a single value for each return_type component. So, the fold_fn computations for each entity's edges must be accumulated into a single final value. To carry the intermediate results, fold_fn takes an "accumulator" value as the first argument. Its output is set as the accumulator value for the next iteration. init_value is the initial value of the accumulator.

    edge_fold makes no guarantees about the order in which edges are processed. For associative operators like +, the order the elements are combined in is not important, but for non-associative operators like -, the order will affect the final result.

    See the Three-Body Orbit Tutorial for a practical example of using edge_fold to compute gravitational forces between entities.



Primitives

class elodin.PrimitiveType

  • elodin.PrimitiveType.F64 -> elodin.PrimitiveType

    A constant representing the 64-bit floating point data type.

  • elodin.PrimitiveType.U64 -> elodin.PrimitiveType

    A constant representing the 64-bit unsigned integer data type.

class elodin.Quaternion

Unit quaternions are used to represent spatial orientations and rotations of bodies in 3D space.



Spatial Vector Algebra

Elodin uses Featherstone’s spatial vector algebra notation for rigid-body dynamics as it is a compact way of representing the state of a rigid body with six degrees of freedom. You can read a short into here or in Rigid Body Dynamics Algorithms (Featherstone - 2008).

class elodin.SpatialTransform

A spatial transform is a 7D vector that represents a rigid body transformation in 3D space.

class elodin.SpatialMotion

A spatial motion is a 6D vector that represents either the velocity or acceleration of a rigid body in 3D space.

  • __init__(angular, linear) -> elodin.SpatialMotion

    Create a spatial motion from an angular and a linear vector. Both arguments are optional and default to zero vectors.

    • angular : jax.Array with shape (3), default is [0, 0, 0]
    • linear : jax.Array with shape (3), default is [0, 0, 0]
  • linear() -> jax.Array

    Get the linear part of the spatial motion as a vector with shape (3,).

  • angular() -> jax.Array

    Get the angular part of the spatial motion as a vector with shape (3,).

  • __add__(other) -> elodin.SpatialMotion

    Add two spatial motions.

class elodin.SpatialForce

A spatial force is a 6D vector that represents the linear force and torque applied to a rigid body in 3D space.

  • __init__(arr, torque, force) -> elodin.SpatialForce

    Create a spatial force from either arr or torque and force. If no arguments are provided, the spatial force is initialized to zero torque and force.

    • arr : jax.Array with shape (6)
    • torque : jax.Array with shape (3), default is [0, 0, 0]
    • force : jax.Array with shape (3), default is [0, 0, 0]
  • force() -> jax.Array

    Get the linear force part of the spatial force as a vector with shape (3,).

  • torque() -> jax.Array

    Get the torque part of the spatial force as a vector with shape (3,).

  • __add__(other) -> elodin.SpatialForce

    Add two spatial forces.

class elodin.SpatialInertia

A spatial inertia is a 7D vector that represents the mass, moment of inertia, and momentum of a rigid body in 3D space. The moment of inertia is represented in its diagonalized form of $[I_1, I_2, I_3]$.

  • __init__(mass, inertia) -> elodin.SpatialInertia

    Create a spatial tensor inertia from a scalar mass and an optional inertia tensor diagonal with shape (3,). If the inertia tensor is not provided, it is set to the same value as the mass along all axes.

  • mass() -> jax.Array

    Get the scalar mass of the spatial inertia.

  • inertia_diag() -> jax.Array

    Get the inertia tensor diagonal of the spatial inertia with shape (3,).



Schematic Syntax for 3D Objects

When visualizing entities in the Elodin editor, you can define 3D objects using KDL schematic syntax. The object_3d declaration connects a visual representation to an entity's world_pos component.

Basic Shapes

Create basic geometric shapes with customizable dimensions and colors:

object_3d ball.world_pos {
    sphere radius=0.2 {
        color 25 50 255  // RGB values (0-255), optional alpha
    }
}

object_3d box.world_pos {
    box x=1.0 y=1.0 z=1.0 {
        color 255 128 0
    }
}

object_3d ground.world_pos {
    plane width=20 depth=20 {
        color 32 128 32
    }
}

object_3d cylinder.world_pos {
    cylinder radius=0.5 height=2.0 {
        color 100 100 200
    }
}

Ellipsoids with Dynamic Scaling

Create ellipsoids that can dynamically change size based on component values:

object_3d satellite.world_pos {
    ellipsoid scale="(1, 1, 2)" {
        color 200 200 0
    }
}

The scale parameter accepts an EQL expression for dynamic sizing.

GLB Models

Load external 3D models from GLB files with optional transformations:

object_3d aircraft.world_pos {
    glb path="f22.glb" scale=0.01 translate="(0, 0, 1.5)" rotate="(0, 90, 0)"
}

GLB Parameters:

  • path (required): Path or URL to the GLB file
  • scale (optional): Uniform scale multiplier (default: 1.0)
  • translate (optional): Translation offset as "(x, y, z)" tuple in meters (default: "(0, 0, 0)")
  • rotate (optional): Rotation as "(x, y, z)" Euler angles in degrees (default: "(0, 0, 0)")

Example use cases:

Scale down a large model:

object_3d rocket.world_pos {
    glb path="rocket.glb" scale=0.1
}

Offset model origin to center of mass:

object_3d drone.world_pos {
    glb path="drone.glb" translate="(0, 0, -0.05)"
}

Rotate model to match simulation frame:

object_3d vehicle.world_pos {
    glb path="car.glb" rotate="(0, 0, 180)"
}

Combine all transformations:

object_3d jet.world_pos {
    glb path="jet.glb" scale=0.01 translate="(0, 0, 0.5)" rotate="(0, 90, 0)"
}

Notes:

  • Transformations are applied in order: scale → rotate → translate
  • Rotations use XYZ Euler order
  • Coordinates are in the simulation's coordinate frame
  • The parent entity's world_pos determines the base position, and these transformations are applied as offsets



EQL Viewport Formulas

When defining viewport positions in schematics, you can use EQL (Elodin Query Language) formulas to apply rotations and translations to entity positions. These formulas are chainable and operate on elodin.WorldPos (SpatialTransform) data.

Rotation Formulas

Rotation formulas modify the orientation quaternion of a spatial transform. All angles are specified in degrees.

Body-Frame Rotations

Body-frame rotations apply relative to the entity's local coordinate system (the axes rotate with the entity):

  • rotate_x(angle) - Rotate about the body X axis (roll)
  • rotate_y(angle) - Rotate about the body Y axis (pitch)
  • rotate_z(angle) - Rotate about the body Z axis (yaw)
  • rotate(x, y, z) - Apply combined XYZ Euler rotations in order

Example - FPV camera rotated 90° right:

viewport name=FPVCamera pos="aircraft.world_pos.rotate_z(-90)" show_grid=#true

World-Frame Rotations

World-frame rotations apply relative to the world coordinate system (independent of entity orientation):

  • rotate_world_x(angle) - Rotate about the world X axis
  • rotate_world_y(angle) - Rotate about the world Y axis
  • rotate_world_z(angle) - Rotate about the world Z axis
  • rotate_world(x, y, z) - Apply combined XYZ Euler rotations in world frame

Example - Camera tilted down 15° in world space:

viewport name=TopView pos="satellite.world_pos.rotate_world_y(-15)" look_at="earth.world_pos"

Translation Formulas

Translation formulas modify the position component of a spatial transform.

Body-Frame Translations

Body-frame translations move the camera relative to the entity's local axes (the offset direction rotates with the entity):

  • translate_x(distance) - Translate along body X axis (forward/back)
  • translate_y(distance) - Translate along body Y axis (left/right)
  • translate_z(distance) - Translate along body Z axis (up/down)
  • translate(x, y, z) - Apply combined XYZ translation

Example - Camera 2m behind and 1m above in body frame:

viewport name=ChaseCamera pos="car.world_pos.translate_x(-2.0).translate_z(1.0)"

World-Frame Translations

World-frame translations move the camera in world coordinates (the offset stays fixed regardless of entity orientation):

  • translate_world_x(distance) - Translate along world X axis (East in ENU)
  • translate_world_y(distance) - Translate along world Y axis (North in ENU)
  • translate_world_z(distance) - Translate along world Z axis (Up in ENU)
  • translate_world(x, y, z) - Apply combined XYZ translation

Example - Chase camera at fixed world offset:

viewport name=Viewport pos="drone.world_pos.translate_world(-5, -5, 3)" look_at="drone.world_pos"

Chaining Formulas

All rotation and translation formulas can be chained together to create complex camera behaviors:

// Rotate 90° right, then move 2m left in new orientation, then offset up 5m in world space
viewport pos="jet.world_pos.rotate_z(-90).translate_y(-2.0).translate_world_z(5.0)"

// Multiple rotations
viewport pos="satellite.world_pos.rotate_x(45).rotate_z(90)"

// Body-frame position then world-frame adjustment
viewport pos="aircraft.world_pos.translate(1, 0, 0.5).translate_world(0, 0, 10)"

Usage Notes

  • Body-frame functions (rotate, translate) are ideal for:

    • FPV cameras that move with the vehicle
    • Wing-mounted or cockpit cameras
    • Camera positions defined relative to vehicle geometry
  • World-frame functions (rotate_world, translate_world) are ideal for:

    • Chase cameras that maintain fixed world offset
    • Orbital cameras
    • External tracking cameras
  • Rotations are applied before translations when chained

  • The old + operator for viewport positions is equivalent to translate_world()