No XML required

Build the world in Python.

A scene is a validated Python object. References returned by geometry and joint methods make articulation explicit, inspectable, and refactorable.

A LavenderSim articulated arm, peg, and socket
Geometry becomes a control problem only after Python adds sensing, actions, reset distribution, and reward. PegInsertion uses named sites and contact wrench sensing to turn an articulated scene into a precision task.

The scene lifecycle

construct Scenevalidate referencescompile native statereset and step

Build the description once, compile it into a runtime, then reset its mutable state many times. Task randomization belongs in reset logic; topology belongs in the scene builder.

A minimal articulated scene

python
from lavendersim import PD, Scene
from lavendersim.runtime import CodeSceneEnv

scene = Scene("Two-link robot")
base = scene.box("base", size=(.3, .2, .3), position=(0, .1, 0), mass=0)
link = scene.capsule("link", radius=.05, length=.8,
                     position=(0, .55, 0), mass=1)

joint = scene.revolute(
    "shoulder", base, link,
    anchor=(0, .2, 0), axis=(0, 0, 1),
    limits=(-1.5, 1.5), pd=PD(30, 2, 20),
)
tip = scene.site("tip", body=link, position=(0, .4, 0))

with CodeSceneEnv(scene, control_dt=1/60) as sim:
    observation, _ = sim.reset(seed=1)
    observation, *_ = sim.step({"shoulder": .4})
Name things for telemetry, not appearance. Stable names such as arm.elbow and tool.tip make observations, overlays, checkpoints, and debug logs understandable.

Bodies, joints, and sites

PrimitiveUse it forKey choices
Box, sphere, capsule, cylinderRigid collision and visible robot partsSize, pose, mass, material, contact group
Revolute / prismatic jointOne scalar degree of freedomAnchor, axis, limits, damping
Free bodyBalls, payloads, floating robotsInitial pose and inertial properties
Named siteEnd effectors, goals, sensors, framesBody-relative position and rotation
HeightfieldTerrain and maze floorsGrid, horizontal size, height scale, friction

A site has no collision geometry. It is a coordinate frame attached to a body, useful for world pose, velocity, force/torque sensing, targets, and visualization.

Sensors and sampling effects

Attach IMUs, tactile patches, cameras, rangefinder fans, and site force/torque sensors. A shared SensorConfig adds seeded noise, low-pass filtering, sample intervals, and delay.

python
from lavendersim import SensorConfig

scan = scene.site("scan", body=link, position=(0, .25, .1))
scene.rangefinder(
    "lidar", site=scan, fan=(9, 3.14), max_distance=4,
    sensor=SensorConfig(noise=.005, cutoff_hz=20, delay=.016),
)
scene.imu("body.imu", link, sensor=SensorConfig(noise=.002))
SensorTypical policy use
IMUBody orientation, angular velocity, acceleration
Joint telemetryProprioceptive position and velocity
Range fanLocal terrain or obstacle distance
Tactile patchDistributed grasp contact
Site force/torqueInsertion and interaction wrench
RGB cameraVisualization and future vision policies

Actuators and tendons

Joints accept position, velocity, effort, and impedance commands. Named actuators add transmission and motor dynamics. Fixed tendons linearly couple scalar joints—useful for jaws, antagonistic mechanisms, and spring linkages.

python
tendon = scene.fixed_tendon(
    "paired_jaws", {left_joint: 1, right_joint: -1},
    stiffness=20, damping=1,
)
scene.actuator(
    "jaw.motor", target=tendon, mode="position",
    gear=.12, control_range=(-1, 1), force_range=(-40, 40),
    activation_tau=.025,
)

gear maps control to target effort, force_range clamps output, and activation_tau prevents an unrealistically instantaneous motor response. Armature and friction loss can model additional drive-side inertia and resistance.

Heightfields and contacts

scene.heightfield() accepts a rectangular numeric grid and compiles it into native static collision cells. Contacts expose static, dynamic, torsional, and rolling friction, restitution, radius, and compliance.

python
heights = np.zeros((24, 32), dtype=np.float32)
heights[:, 12:14] = .18
terrain = scene.heightfield(
    "terrain", heights, size=(6, 4), height_scale=1,
    friction=(.8, .6, .02),
)

Turn a scene into an RL task

The task wrapper decides what the policy can see and what good behavior means. Keep reward components named in info; debugging a single scalar reward is unnecessarily hard.

python
class MyTask(LavenderEnv):
    def reset(self, seed=None):
        self.rng = seeded_rng(seed)
        self._randomize_goal(self.rng)
        self.sim.reset(seed=seed)
        return self._observation(), {"goal": self.goal.copy()}

    def step(self, action):
        self.sim.step(self._decode_action(action))
        obs = self._observation()
        progress = self.previous_error - self._error()
        reward = 2.0 * progress - 0.001 * float(action @ action)
        return obs, reward, self._failed(), self._timed_out(), {
            "progress": progress, "success": self._succeeded(),
        }

Render a reproducible documentation frame

Camera sensors belong to the scene and render authoritative native state. The documentation images in this site use this exact route.

python
camera = scene.camera_sensor(
    "docs.camera", position=(3, 2, 4),
    rotation=look_rotation((3, 2, 4), target=(0, .5, 0)),
    width=160, height=120, fov_y=50,
)
rgb = sim.render_camera(camera.name)

See docs-site/scripts/render_scenes.py for complete deterministic camera placement and PNG generation.

Branch simulation state

python
state = sim.snapshot()
result_a = sim.step(action_a)
sim.restore(state)
result_b = sim.step(action_b)

portable = state.to_bytes()
sim.restore(portable)

Snapshots include body and joint state, controls, actuator activation, sensor buffers, simulation time, RNG state, and a scene fingerprint. Use them for counterfactual controllers, recovery data, and reproducible bug reports.

Before you train

  • Reset twice with the same seed and compare observations.
  • Check every observation and reward with np.isfinite.
  • Step zero, minimum, maximum, and small random actions.
  • Confirm the horizon, success path, and failure path independently.
  • Inspect named reward components and contact counts.
  • Run 32 short instances before a long PPO job.