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 + optionalparent/localTransformfor 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) andBodyTrajectory(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, ...), accumulatesstatesviaadd_state/add_trajectory(plusmark_episode), and cansave()/load()JSON (optionally gzipped),save_static()a static bundle for plain file hosting, orshow()a non-blocking viewer (returns aViewerHandle, 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 (viaSimulationScene.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(behindsave_static) writes the same document asmodel.json/states.jsonplus rawblob/<id>files, for the viewer's static (#data=) mode.server.py—SimViewServer: FastAPI app servingtemplates/index.htmlandstatic/, plus/model,/statesand/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 (seelive.py) and batch-rename persistence.ViewerHandleruns a server on a background thread (shared byscene.show(),LiveViewerandsimview render).live.py—LiveViewer: starts the server immediately (on a background thread viaViewerHandle) and streamspush_statecalls 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 tolive.py's streaming launch orscene.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'sfile.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, backingsimview diff.terrain.py— bilinear-interpolated point/area terrain queries, backingsimview terrain.info.py— structural summary (body/terrain/state breakdown, consistency warnings), backingsimview info.view.py—view_hash, the stdlib-only builder for the viewer's view-link URL fragment (mirrorsstatic/js/utils/viewState.js; a test keeps the flag order in sync).render.py— headless PNG screenshots via a real (headless) browser driving a realSimViewServerinstance, backingsimview render.remote.py— scp-stylehost:pathinputs: 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 (simviewscript): view file(s),simview info/terrain/diff/render,simview clearcache 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:ColumnarStateStorefor a loaded scene,LegacyStateStorefor 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 inutils.js.colormap.js/similarity.jsfactor the colormap resolver and cosine-similarity math out ofutils.jsinto small, dependency-light modules used by bothBody's click-to-similarity point coloring andTerrain's "features" mode.ui/— DOM-based UI panels:Controls(main options panel),PlaybackControls,BodyStateWindow,Legend/BatchLegend, andScalarPlotter/ErrorMetrics/TerrainProfile(all three behindAnalysisPanel's tab switcher, all three plotted with vendored uPlot via the sharedutils/uplot.jschart helper and the DOM-freechartControls.jspieces).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 withSTATE_FIELD_WIDTHS/columnarize_statesincolumnar.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 thedata=/ui=startup keys),viewerDefaults.js(apply the model'sviewerDefaults),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, plushasBodyTrajectorygating 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 bycomponents/WindowedField.js),scalarSeries.js(growing the scalar plots' series as frames arrive),visibleRanges.js(visibleRangestime-window checks),html.js(escapeHtmlfor untrusted scene text),batchPresets.js,cameraRange.js. The one exception to "pure logic" isuplot.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