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
parentandlocal_transform(a constant[x, y, z, w, qx, qy, qz]offset). Never calladd_state/add_trajectoryfor 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 viaadd_state/add_trajectoryas 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
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 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 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
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
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
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.