Skip to content

Architecture Overview

SimView has two halves that only talk to each other over HTTP and WebSocket: a Python backend (FastAPI/uvicorn) that owns a scene's data — a static model plus time-ordered states — and a vanilla-JS/THREE.js frontend that renders it in the browser. Neither side needs to know the other's internals beyond the JSON wire format they agree on.

Python (authoring or file-on-disk)           Browser
┌───────────────────────────┐   HTTP GET     ┌───────────────────────────┐
│ SimulationScene / JSON    │  ───────────▶  │ SimView.js (main.js)      │
│  - model (static)         │   /model       │  - loadData/fetchBlobs    │
│  - states (per-frame)     │   /states,     │  - StateStore, Scene,     │
│                           │   /blob/...    │    AnimationController    │
│ SimViewServer (FastAPI)   │  ◀──────────── │  - Controls, panels       │
│  - columnar (see below)   │   WebSocket    │                           │
│  - LiveViewer push_state  │   (live only)  │                           │
└───────────────────────────┘                └───────────────────────────┘

Python backend (simview/)

  • model.py — static scene definition: SimViewModel, SimViewBody (shape + optional parent/localTransform for rigid attachment), SimViewStaticObject, SimViewTerrain, SimViewPolyline, SimViewEpisode. Data/validation plus the tensor-to-blob encoding of geometry and terrain grids, so it imports torch (authoring-only, hence lazily imported — see below).
  • state.py — per-frame dynamic data: SimViewBodyState (one frame) and BodyTrajectory (a whole (T, B, ...) trajectory in one call, authoring-only, needs torch/numpy). Both can binary-encode numeric fields (__b64__ + little-endian float32) instead of plain JSON lists — see JSON Format Specification.
  • scene.py — SimulationScene, the main authoring API: builds a model incrementally (create_terrain, create_body, create_polyline, ...), accumulates states via add_state/add_trajectory (plus mark_episode), and can save()/load() JSON (optionally gzipped), save_static() a static bundle for plain file hosting, or show() a non-blocking viewer (returns a ViewerHandle, usable in Jupyter via _repr_html_).
  • columnar.py — the columnar ("v4") states layout: one binary blob per body per field covering the whole trajectory, instead of thousands of small per-frame values. Both a wire format and (via SimulationScene.save) an on-disk one — the same document either way, with inline __b64__ blobs on disk and /blob/ URLs over HTTP. columnarize_states (the writer/repacker) needs numpy; expand_columnar_states (the inverse) is stdlib-only so every reader, including the base install and the stdlib-only CLI tools, can consume a columnar file. write_static_bundle (behind save_static) writes the same document as model.json/states.json plus raw blob/<id> files, for the viewer's static (#data=) mode.
  • server.py — SimViewServer: FastAPI app serving templates/index.html and static/, plus /model, /states and /blob/{token}/{id} (range-request capable). A columnar file is served as-is (only its inline blobs are rewritten into URLs); a legacy per-frame file is repacked into the columnar payload at load time for much cheaper playback of long recordings, falling back to serving the per-frame array if the frames aren't uniform enough to columnarize. Also handles WebSocket live-push (see live.py) and batch-rename persistence. ViewerHandle runs a server on a background thread (shared by scene.show(), LiveViewer and simview render).
  • live.py — LiveViewer: starts the server immediately (on a background thread via ViewerHandle) and streams push_state calls to connected browser tabs over WebSocket as a simulation runs, instead of saving-then-viewing after the fact.
  • launcher.py — SimViewLauncher: blocking launch used by the CLI / save+view workflows (as opposed to live.py's streaming launch or scene.show()'s non-blocking one).
  • merge.py — merge_simulation_files: combines multiple scene JSON files (e.g. a real-world recording and a simulated rerun) that share the same bodies/terrain into one scene with extra batches, resampling every file but the first onto the first file's timeline by nearest timestamp. Each file may contribute only a subset of its own batches (selections=, the CLI's file.json#1,3), so a ground truth shared by several files isn't merged once per file.
  • diff.py — per-frame position/orientation divergence between two batches, backing simview diff.
  • terrain.py — bilinear-interpolated point/area terrain queries, backing simview terrain.
  • info.py — structural summary (body/terrain/state breakdown, consistency warnings), backing simview info.
  • view.py — view_hash, the stdlib-only builder for the viewer's view-link URL fragment (mirrors static/js/utils/viewState.js; a test keeps the flag order in sync).
  • render.py — headless PNG screenshots via a real (headless) browser driving a real SimViewServer instance, backing simview render.
  • remote.py — scp-style host:path inputs: fetches a scene over SSH (gzipped on the wire) into a local cache, re-fetching only when the remote file changes.
  • utils.py — small shared helpers (e.g. free-port lookup, gzip-transparent file reads).
  • __main__.py — CLI entry point (simview script): view file(s), simview info/terrain/diff/render, simview clear cache cleanup, simview static-viewer (copy the viewer page for static hosting), --save-merged.

Lazy imports

simview/__init__.py only imports authoring symbols (SimulationScene, SimViewBody, etc.) on first attribute access, via a module-level __getattr__ that looks each name up in a _LAZY_EXPORTS table (SimulationScene, ViewerHandle, LiveViewer, the model.py classes and enums, SimViewBodyState, BodyTrajectory). Only __version__, CACHE_DIR and the stdlib-only view_hash are imported eagerly. This keeps import simview torch-free for viewing-only installs — a viewing-only install can import simview and use simview.server.SimViewServer/CLI features without ever needing torch/numpy installed, and only pays that import cost (and dependency requirement) the moment an authoring symbol like SimulationScene is actually touched.

Frontend (simview/static/js/)

Vanilla JS ES modules (no bundler/framework), loaded via templates/index.html's importmap. Entry point main.js → SimView.js (SimView class), which owns startup (loadData/fetchBlobs/initFromModel) and wires everything else together. config.js holds the shared constants (theme colors, UI_DEFAULT_CONFIG, update rates, camera/renderer settings).

  • components/ — Scene (THREE.js scene/camera/renderer + OrbitControls), StateStore (decoded trajectory data + playback lookups: ColumnarStateStore for a loaded scene, LegacyStateStore for per-frame/live data), AnimationController (playback loop, speed, interpolation), BatchManager (per-batch color/focus/visibility), InteractionController (mouse picking, terrain probe, click-to-similarity), WindowedField (range-fetched per-body vector fields).
  • objects/ — THREE.js object wrappers: Body, StaticObject, Polyline (model.polylines), Terrain (heightfield mesh + per-property/click-to-similarity "features" color modes), plus shared helpers in utils.js. colormap.js/similarity.js factor the colormap resolver and cosine-similarity math out of utils.js into small, dependency-light modules used by both Body's click-to-similarity point coloring and Terrain's "features" mode.
  • ui/ — DOM-based UI panels: Controls (main options panel), PlaybackControls, BodyStateWindow, Legend/BatchLegend, and ScalarPlotter/ErrorMetrics/ TerrainProfile (all three behind AnalysisPanel's tab switcher, all three plotted with vendored uPlot via the shared utils/uplot.js chart helper and the DOM-free chartControls.js pieces).
  • utils/ — pure logic factored out for unit testing without a DOM/THREE.js: blobCodec.js (decode the server's columnar float32 blobs — must stay in sync with STATE_FIELD_WIDTHS/columnarize_states in columnar.py), bodyTransforms.js (resolve parent-relative poses, topoSortBodies), interpolate.js, errorMath.js, csv.js, viewState.js (encode/decode the shareable view-link URL hash and the data=/ui= startup keys), viewerDefaults.js (apply the model's viewerDefaults), keyboard.js (ignore shortcuts while typing in a form field), liveFollow.js (should new live frames auto-scroll playback), terrainSample.js (bilinear terrain layer sampling for the Analysis panel's Terrain tab, plus hasBodyTrajectory gating whether that tab shows up for a given body), episodes.js (episode segments, navigation and per-episode aggregates), batchVisibility.js (which batches get built and drawn), blobWindow.js (window arithmetic for range-fetching a long trajectory's field blobs, used by components/WindowedField.js), scalarSeries.js (growing the scalar plots' series as frames arrive), visibleRanges.js (visibleRanges time-window checks), html.js (escapeHtml for untrusted scene text), batchPresets.js, cameraRange.js. The one exception to "pure logic" is uplot.js, which owns the uPlot chart skeleton the three Analysis-panel plots share.

Vendored third-party libraries

uPlot (MIT), three.js (MIT, plus the OrbitControls and lil-gui addons it ships) and js-colormaps.js (the matplotlib color-map tables, trimmed to the ones the viewer offers) are vendored locally rather than loaded from a CDN, so the viewer works fully offline. All third-party libraries used by SimView are permissively licensed (MIT/BSD), so there are no licensing restrictions on commercial use. Don't add new CDN-loaded dependencies without a reason to break that pattern.

simview/static/lib/ is served with an immutable Cache-Control header, so every library there lives in a version-stamped directory (lib/three-0.174.0/, lib/uPlot-1.6.32/); upgrading one must change its directory name. js-colormaps.js is edited locally (trimmed), so it lives in simview/static/js/vendor/ with the normal short cache instead.

Wire format

Python and JavaScript agree on the model/states JSON shape, binary field encoding, parent-relative bodies, and the columnar layout independently — there is no shared schema file. See JSON Format Specification for the full contract; read it before changing anything that touches serialization on either side (model.py/state.py/columnar.py/server.py in Python, blobCodec.js/ bodyTransforms.js/components/StateStore.js in JS).

Project structure

simview/
├── simview/                # Python package
│   ├── __init__.py          # public API, lazy authoring imports
│   ├── model.py             # SimViewModel, SimViewBody, SimViewStaticObject, SimViewTerrain, ...
│   ├── state.py             # SimViewBodyState, BodyTrajectory
│   ├── scene.py             # SimulationScene
│   ├── columnar.py          # columnar ("v4") states layout (wire + on disk)
│   ├── server.py            # SimViewServer (FastAPI app, columnar repack), ViewerHandle
│   ├── live.py               # LiveViewer
│   ├── launcher.py           # SimViewLauncher
│   ├── merge.py               # merge_simulation_files
│   ├── remote.py              # host:path inputs over SSH, local cache
│   ├── diff.py                 # simview diff backend
│   ├── terrain.py              # simview terrain backend
│   ├── info.py                  # simview info backend
│   ├── view.py                    # view_hash: view-link fragments
│   ├── render.py                 # simview render backend (Playwright)
│   ├── utils.py                   # shared helpers
│   ├── __main__.py                 # simview CLI entry point
│   ├── templates/                   # index.html
│   └── static/
│       ├── css/
│       ├── js/
│       │   ├── main.js               # entry point
│       │   ├── SimView.js             # top-level SimView class
│       │   ├── config.js              # shared constants, UI defaults
│       │   ├── components/
│       │   ├── objects/
│       │   ├── ui/
│       │   ├── utils/
│       │   └── vendor/                # js-colormaps (locally trimmed)
│       ├── lib/                       # vendored three.js, uPlot (version-stamped)
│       └── textures/
├── tests/                    # pytest suite (+ tests/js/ vitest, tests/e2e/ Playwright)
├── demo/                      # demo scene (deployed by .github/workflows/demo.yml)
├── example.py                 # authoring example (see Quick Start)
├── example_live.py             # LiveViewer example
├── mkdocs.yml                    # this documentation site
├── docs/                          # documentation source
├── package.json                    # JS test deps (vitest, Playwright); see vitest/playwright configs
└── pyproject.toml                  # package metadata, ruff/pyright config, uv dependency-groups