Skip to content

Scenes & Viewer Handles

simview.scene is the main authoring API: SimulationScene builds a model incrementally (terrain, bodies, static objects) and accumulates states, then saves/loads JSON or launches a viewer. SimulationScene.show() returns a ViewerHandle, the non-blocking background server.

simview.scene

SimulationScene

SimulationScene(
    batch_size: int,
    scalar_names: list[str],
    dt: float | None,
    collapse: bool = False,
    terrain: SimViewTerrain | None = None,
    bodies: dict[str, SimViewBody] | None = None,
    static_objects: dict[str, SimViewStaticObject]
    | None = None,
    batch_names: list[str] | None = None,
    metadata: dict[str, Any] | None = None,
    episodes: list[SimViewEpisode] | None = None,
    viewer_defaults: dict[str, Any] | None = None,
)

Authoring container for a scene: a SimViewModel plus its time-series states, with helpers to build, save, load and show it.

Create an empty scene around a new SimViewModel.

batch_size, scalar_names, dt (None to let the viewer infer it), collapse, terrain, bodies, static_objects and batch_names initialize the model fields of the same name.

metadata is free-form, JSON-serializable run provenance (e.g. engine name, checkpoint path, git commit, CLI args) with no meaning to the viewer -- it's carried through to simview info and the browser so a saved scene stays self-describing. Can also be set/updated later via self.model.metadata.

episodes marks the frames an episodic (e.g. RL) recording resets at. Usually easier to build up as you go with mark_episode() than to pass here up front.

viewer_defaults declares the viewer's initial UI state (hidden point clouds, collapsed panels, open/closed GUI folders, ...); see docs/dev/json-format.md for the supported keys.

add_state

add_state(
    time: float,
    body_states: list[SimViewBodyState],
    scalar_values: dict[str, Tensor | ndarray | list]
    | None = None,
) -> None

Append one state (a snapshot at time) to the scene.

Every body in body_states must exist in the model, must not be rigidly attached, and must carry one row per batch. When the model has scalar_names, scalar_values must map exactly those names to one value per batch; otherwise it is ignored with a warning.

add_trajectory

add_trajectory(
    times: Tensor | ndarray | Sequence[float],
    trajectories: list[BodyTrajectory],
    scalar_values: dict[str, Tensor | ndarray | list]
    | None = None,
    binary: bool = True,
) -> None

Append an entire time-series in one call.

times is a length-T sequence of snapshot times (seconds). trajectories holds one BodyTrajectory per body; each body must already exist in the model. For a scene with scalar_names, scalar_values maps each name to a (T, B) (or (T,) when B == 1) series.

Equivalent to looping add_state over T frames, but converts each body's pose/vector tensors once (vectorized) instead of per frame, which is dramatically faster for long trajectories. With binary=True the numeric per-body fields (bodyTransform and any provided vectors) are packed as float32 __b64__ blobs, shrinking the output file and the parse cost; the viewer and merge_simulation_files decode these transparently. Set binary=False to emit plain JSON lists. A body's contacts (if provided on its BodyTrajectory) are ragged and always emitted as plain JSON per frame, using the same encoding as SimViewBodyState / add_state.

create_body

create_body(
    body_name: str,
    shape_type: BodyShapeType,
    available_attributes: list[
        OptionalBodyStateAttribute | str
    ]
    | None = None,
    parent: str | None = None,
    local_transform: LocalTransformLike | None = None,
    color: Sequence[float] | None = None,
    opacity: float | None = None,
    visible: bool = True,
    visible_ranges: list | None = None,
    **kwargs,
) -> None

Add a dynamic body named body_name of shape_type to the model.

**kwargs are the shape parameters (see SimViewBody.create), and available_attributes optionally pre-declares the optional state attributes (save/show reconcile them from the states anyway).

color (RGB in [0, 1]) and opacity (in (0, 1]) style the mesh/primitive representation (box/sphere/cylinder/mesh); a point cloud's per-point color is its shape's own color. visible=False hides the body initially. visible_ranges is an optional list of [t_from, t_to] pairs (seconds on the states timeline); the viewer hides the body outside them.

parent/local_transform attach this body to another body already in the model, instead of it moving in world space:

  • Rigid attachment (e.g. a wheel bolted to a chassis): pass both parent and local_transform (a constant [x, y, z, w, qx, qy, qz] offset). Never call add_state/add_trajectory for this body afterwards -- its world pose is derived by the viewer every frame from the parent's current pose plus this fixed offset.
  • Articulated attachment (e.g. an arm joint): pass only parent. Keep supplying this body's pose every frame via add_state/ add_trajectory as usual -- it's just interpreted as local to the parent's current-frame pose instead of world space.

create_pointcloud

create_pointcloud(
    body_name: str,
    points: Tensor,
    color: Tensor | None = None,
    embedding: Tensor | None = None,
    color_layers: dict[str, Tensor] | None = None,
    **kwargs,
) -> None

Add a point-cloud body named body_name to the model.

points holds the (N, 3) point positions. color is an optional (N, 3) per-point RGB in [0, 1] for static vertex-colored rendering. embedding is an optional (N, K) per-point feature vector (e.g. a reduced-dim PCA projection) enabling the viewer's click-to-similarity color mode. color_layers maps names to extra (N, 3) RGB colorings selectable as "Point Color Mode" choices. Other **kwargs go to SimViewBody.create.

create_polyline

create_polyline(
    name: str,
    points: Tensor | ndarray | None = None,
    *,
    frames: Tensor | ndarray | None = None,
    color: Sequence[float] = (1.0, 1.0, 1.0),
    width: float = 2.0,
    dashed: bool = False,
    visible_ranges: list | None = None,
) -> None

Add a world-space polyline named name to the model.

points (N, 3) draws a fixed line (a planned route). frames (T, B, max_vertices, 3) -- or (T, max_vertices, 3) when batch_size == 1 -- draws a different line every state (a replanned local path); pad shorter frames with trailing NaN rows. T must equal the number of states by the time the scene is saved or shown. color is RGB in [0, 1], dashed draws a dashed line, and width is in pixels (the viewer currently draws 1 px); visible_ranges as in create_body.

create_terrain

create_terrain(
    heightmap: Tensor,
    normals: Tensor | None = None,
    x_lim: tuple[float, float] | None = None,
    y_lim: tuple[float, float] | None = None,
    grid_res: float | None = None,
    properties: dict[str, Tensor] | None = None,
    property_bounds: dict[str, tuple[float, float]]
    | None = None,
    embedding_map: Tensor | None = None,
    compute_normals: bool = True,
) -> None

Add terrain to the simulation model.

heightmap is a 2D or 3D (batched) tensor of terrain heights. normals is a 3D or 4D tensor of terrain normals; if None they are computed from the heightmap gradients, unless compute_normals=False, which stores no normals at all (three floats per cell less on disk and over the wire). The viewer then shades the surface from normals it computes itself, and draws the "normals" arrows from those too.

x_lim/y_lim are the (min, max) coordinates of the X/Y axes. grid_res is the spacing between adjacent grid nodes; if x_lim and y_lim are omitted they are inferred so the nodes sit grid_res apart, centered at 0 (a W-wide grid spans (W - 1) * grid_res).

properties holds optional named per-cell scalar maps (2D or 3D, like heightmap), e.g. {"friction": friction_map}; each becomes a terrain color mode in the viewer. property_bounds optionally gives an explicit (min, max) color-scale range per property name, e.g. {"friction": (0.0, 1.0)}. Each name must also appear in properties; names left out use that map's own data range. Use it to keep one scale comparable across scenes -- cells outside the range saturate at the end colors rather than being hidden.

embedding_map is an optional per-cell K-wide feature map (3D channels-first (K, Dy, Dx) or 4D (B, K, Dy, Dx), like normals) enabling the viewer's click-to-similarity "features" color mode.

Each field's batch dim must be 1 (shared) or batch_size.

from_dict classmethod

from_dict(d: dict) -> SimulationScene

Reconstruct a SimulationScene from the document save writes (the parsed {"model": ..., "states": ...} dict).

Binary __b64__-encoded fields inside states (e.g. from add_trajectory(binary=True)) are left as-is, matching the on-disk wire format, so a subsequent save() reproduces the same bytes for those fields without a decode/re-encode round trip.

Accepts either states layout (see simview.columnar). A columnar document is expanded back into the per-frame list states is in memory, so every authoring API keeps working unchanged; the expansion is exact, so re-saving columnar reproduces the same blobs.

load classmethod

load(path: str | Path) -> SimulationScene

Load a SimulationScene previously written by save.

Transparently reads gzip-compressed files (detected by magic bytes, regardless of extension) as well as plain JSON. Enables round-tripping from Python: SimulationScene.load(p).save(p2).

mark_episode

mark_episode(
    label: str | None = None, start_index: int | None = None
) -> SimViewEpisode

Mark the start of an episode in this scene's timeline.

Call it right before adding the first frame of a new episode (a reset), which is what the default start_index -- the index the next added frame will land at -- means::

for episode in range(num_episodes):
    scene.mark_episode(label=f"episode {episode}")
    for t in range(episode_length):
        scene.add_state(...)

Pass start_index explicitly to annotate an already-recorded scene. Episode starts must be strictly increasing; a repeated or out-of-order index raises ValueError. Returns the created SimViewEpisode.

reconcile_available_attributes

reconcile_available_attributes() -> None

Set each body's available_attributes to the optional attributes its states actually carry (an attribute may be absent from earlier frames but present later). The viewer only builds contact markers and vector arrows for declared attributes, so save() and show() both call this before serializing the model. Unknown per-body keys (e.g. from a hand-edited file) are ignored with a warning.

save

save(
    filepath: str | Path,
    compress: bool = False,
    columnar: bool | None = None,
) -> None

Write the scene (model and states) to a JSON file at filepath.

A filepath ending in .gz is gzip-compressed regardless of compress. compress=True gzip-compresses the output (useful for large simulations, which can reach 100+ MB as plain JSON) and appends .gz if filepath lacks it, so the extension reflects the contents.

columnar picks the states layout (see simview.columnar). None (the default) writes the columnar layout when the states allow it and silently falls back to the legacy per-frame array when they don't. True requires it, raising ValueError rather than falling back -- useful in a pipeline that depends on the smaller output. False always writes the legacy array, streamed frame by frame.

The columnar layout stores one whole-trajectory binary blob per body per field instead of thousands of small per-frame ones, which makes both the file and the viewer's load of it dramatically cheaper. Both layouts are read transparently by load, merge_simulation_files, the viewer and the CLI tools.

save_static

save_static(directory: str | Path) -> None

Write a static bundle (model.json, states.json, blob/<id>) that the viewer's static mode (#data=<base url>, see simview.view_hash) can load from any plain static file server.

Uses the same columnar layout and fallback as save.

show

show(
    host: str = "127.0.0.1",
    preferred_port: int = 5420,
    open_browser: bool = False,
    view: str | None = None,
    height: int = 600,
) -> ViewerHandle

Serve a snapshot of this scene on a background thread and return immediately, instead of blocking like SimViewLauncher/SimViewServer.run.

Intended for Jupyter notebooks and scripts that want to keep running (or keep the cell interactive) while the viewer is up: the returned ViewerHandle renders inline via _repr_html_ when it's a cell's result, and its stop() (or exiting it as a context manager) shuts the server down. The scene itself is left untouched -- unlike SimViewLauncher, show doesn't clear self.states/self.model, so the same scene can still be save()d or shown again afterwards.

Multiple concurrent show() calls (on the same or different scenes) are fine -- each gets its own server thread and port (via find_free_port).

view is a view-link fragment (see simview.view_hash) the inline iframe opens with, e.g. a fixed camera or ui=False; height is the iframe height in pixels. host/preferred_port pick where to bind, and open_browser=True also opens the URL in the default browser.

simview.server.ViewerHandle

ViewerHandle(
    app,
    host: str = "127.0.0.1",
    preferred_port: int = 5420,
    thread_name: str = "simview-server",
    log_level: str = "info",
    view: str | None = None,
    iframe_height: int = 600,
)

A SimViewServer's uvicorn app running on a background daemon thread.

Returned by SimulationScene.show, and the shared non-blocking plumbing behind LiveViewer and simview render. __init__ blocks until the socket is actually bound (or raises if startup fails/times out); stop() (also called on context-manager exit) shuts it down. _repr_html_ lets Jupyter render the viewer inline in an iframe just by evaluating the handle in a cell.

url property

url: str

Base URL of the running viewer.

stop

stop() -> None

Signal the server to exit and wait for its thread to finish.

Idempotent -- safe to call multiple times.

url_for

url_for(view: str) -> str

url with a view-link fragment (see simview.view_hash) appended.