simview 6.2.2__tar.gz → 6.2.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. {simview-6.2.2/simview.egg-info → simview-6.2.3}/PKG-INFO +3 -3
  2. {simview-6.2.2 → simview-6.2.3}/README.md +2 -2
  3. {simview-6.2.2 → simview-6.2.3}/pyproject.toml +1 -1
  4. {simview-6.2.2 → simview-6.2.3}/simview/__main__.py +6 -5
  5. {simview-6.2.2 → simview-6.2.3}/simview/columnar.py +5 -4
  6. {simview-6.2.2 → simview-6.2.3}/simview/diff.py +10 -7
  7. {simview-6.2.2 → simview-6.2.3}/simview/info.py +6 -7
  8. {simview-6.2.2 → simview-6.2.3}/simview/launcher.py +14 -14
  9. {simview-6.2.2 → simview-6.2.3}/simview/live.py +3 -0
  10. {simview-6.2.2 → simview-6.2.3}/simview/merge.py +18 -8
  11. {simview-6.2.2 → simview-6.2.3}/simview/model.py +72 -35
  12. {simview-6.2.2 → simview-6.2.3}/simview/remote.py +1 -1
  13. {simview-6.2.2 → simview-6.2.3}/simview/render.py +6 -4
  14. {simview-6.2.2 → simview-6.2.3}/simview/scene.py +114 -112
  15. {simview-6.2.2 → simview-6.2.3}/simview/server.py +35 -22
  16. {simview-6.2.2 → simview-6.2.3}/simview/state.py +30 -24
  17. {simview-6.2.2 → simview-6.2.3}/simview/static/css/controls.css +84 -107
  18. {simview-6.2.2 → simview-6.2.3}/simview/static/js/SimView.js +42 -70
  19. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/AnimationController.js +8 -15
  20. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/BatchManager.js +6 -8
  21. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/InteractionController.js +3 -10
  22. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/Scene.js +6 -6
  23. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/StateStore.js +6 -11
  24. {simview-6.2.2 → simview-6.2.3}/simview/static/js/components/WindowedField.js +2 -0
  25. {simview-6.2.2 → simview-6.2.3}/simview/static/js/config.js +2 -1
  26. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/Body.js +12 -12
  27. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/StaticObject.js +2 -8
  28. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/Terrain.js +11 -20
  29. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/colormap.js +9 -16
  30. simview-6.2.3/simview/static/js/objects/similarity.js +25 -0
  31. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/utils.js +15 -45
  32. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/BodyStateWindow.js +20 -31
  33. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/Controls.js +27 -32
  34. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/ErrorMetrics.js +4 -3
  35. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/Legend.js +7 -5
  36. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/PlaybackControls.js +4 -3
  37. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/ScalarPlotter.js +13 -8
  38. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/TerrainProfile.js +1 -2
  39. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/batchVisibility.js +1 -0
  40. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/blobCodec.js +5 -5
  41. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/blobWindow.js +9 -2
  42. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/bodyTransforms.js +3 -4
  43. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/cameraRange.js +6 -3
  44. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/csv.js +3 -2
  45. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/errorMath.js +2 -5
  46. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/keyboard.js +3 -2
  47. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/terrainSample.js +3 -7
  48. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/uplot.js +4 -5
  49. {simview-6.2.2/simview/static/lib → simview-6.2.3/simview/static/js/vendor}/js-colormaps.js +25 -3
  50. {simview-6.2.2 → simview-6.2.3}/simview/templates/index.html +16 -12
  51. {simview-6.2.2 → simview-6.2.3}/simview/terrain.py +13 -13
  52. {simview-6.2.2 → simview-6.2.3}/simview/view.py +2 -1
  53. {simview-6.2.2 → simview-6.2.3/simview.egg-info}/PKG-INFO +3 -3
  54. {simview-6.2.2 → simview-6.2.3}/simview.egg-info/SOURCES.txt +3 -3
  55. {simview-6.2.2 → simview-6.2.3}/tests/test_cli.py +2 -2
  56. {simview-6.2.2 → simview-6.2.3}/tests/test_columnar_states.py +4 -3
  57. {simview-6.2.2 → simview-6.2.3}/tests/test_diff.py +1 -1
  58. {simview-6.2.2 → simview-6.2.3}/tests/test_info.py +1 -1
  59. {simview-6.2.2 → simview-6.2.3}/tests/test_launcher.py +1 -1
  60. {simview-6.2.2 → simview-6.2.3}/tests/test_roundtrip.py +1 -1
  61. {simview-6.2.2 → simview-6.2.3}/tests/test_scene.py +4 -4
  62. {simview-6.2.2 → simview-6.2.3}/tests/test_server.py +3 -3
  63. {simview-6.2.2 → simview-6.2.3}/tests/test_terrain_query.py +1 -1
  64. {simview-6.2.2 → simview-6.2.3}/tests/test_utils.py +1 -1
  65. simview-6.2.2/simview/static/js/objects/similarity.js +0 -38
  66. {simview-6.2.2 → simview-6.2.3}/LICENSE +0 -0
  67. {simview-6.2.2 → simview-6.2.3}/MANIFEST.in +0 -0
  68. {simview-6.2.2 → simview-6.2.3}/setup.cfg +0 -0
  69. {simview-6.2.2 → simview-6.2.3}/simview/__init__.py +0 -0
  70. {simview-6.2.2 → simview-6.2.3}/simview/py.typed +0 -0
  71. {simview-6.2.2 → simview-6.2.3}/simview/static/js/main.js +0 -0
  72. {simview-6.2.2 → simview-6.2.3}/simview/static/js/objects/Polyline.js +0 -0
  73. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/AnalysisPanel.js +0 -0
  74. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/BatchLegend.js +0 -0
  75. {simview-6.2.2 → simview-6.2.3}/simview/static/js/ui/chartControls.js +0 -0
  76. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/batchPresets.js +0 -0
  77. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/episodes.js +0 -0
  78. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/html.js +0 -0
  79. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/interpolate.js +0 -0
  80. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/liveFollow.js +0 -0
  81. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/scalarSeries.js +0 -0
  82. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/viewState.js +0 -0
  83. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/viewerDefaults.js +0 -0
  84. {simview-6.2.2 → simview-6.2.3}/simview/static/js/utils/visibleRanges.js +0 -0
  85. {simview-6.2.2 → simview-6.2.3}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  86. {simview-6.2.2 → simview-6.2.3}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  87. {simview-6.2.2 → simview-6.2.3}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  88. {simview-6.2.2 → simview-6.2.3}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  89. {simview-6.2.2/simview/static/lib → simview-6.2.3/simview/static/lib/uPlot-1.6.32}/uPlot.esm.js +0 -0
  90. {simview-6.2.2/simview/static/lib → simview-6.2.3/simview/static/lib/uPlot-1.6.32}/uPlot.min.css +0 -0
  91. {simview-6.2.2 → simview-6.2.3}/simview/static/textures/contacts/red-cross0.png +0 -0
  92. {simview-6.2.2 → simview-6.2.3}/simview/static/textures/points/ball1.png +0 -0
  93. {simview-6.2.2 → simview-6.2.3}/simview/utils.py +0 -0
  94. {simview-6.2.2 → simview-6.2.3}/simview.egg-info/dependency_links.txt +0 -0
  95. {simview-6.2.2 → simview-6.2.3}/simview.egg-info/entry_points.txt +0 -0
  96. {simview-6.2.2 → simview-6.2.3}/simview.egg-info/requires.txt +0 -0
  97. {simview-6.2.2 → simview-6.2.3}/simview.egg-info/top_level.txt +0 -0
  98. {simview-6.2.2 → simview-6.2.3}/tests/test_batch_selection.py +0 -0
  99. {simview-6.2.2 → simview-6.2.3}/tests/test_columnar_ondisk.py +0 -0
  100. {simview-6.2.2 → simview-6.2.3}/tests/test_contacts.py +0 -0
  101. {simview-6.2.2 → simview-6.2.3}/tests/test_embed_defaults.py +0 -0
  102. {simview-6.2.2 → simview-6.2.3}/tests/test_episodes.py +0 -0
  103. {simview-6.2.2 → simview-6.2.3}/tests/test_live.py +0 -0
  104. {simview-6.2.2 → simview-6.2.3}/tests/test_merge.py +0 -0
  105. {simview-6.2.2 → simview-6.2.3}/tests/test_pointcloud_features.py +0 -0
  106. {simview-6.2.2 → simview-6.2.3}/tests/test_remote.py +0 -0
  107. {simview-6.2.2 → simview-6.2.3}/tests/test_show.py +0 -0
  108. {simview-6.2.2 → simview-6.2.3}/tests/test_terrain.py +0 -0
  109. {simview-6.2.2 → simview-6.2.3}/tests/test_trajectory.py +0 -0
  110. {simview-6.2.2 → simview-6.2.3}/tests/test_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simview
3
- Version: 6.2.2
3
+ Version: 6.2.3
4
4
  Summary: THREE.js-based simulation visualization tool
5
5
  Author-email: Jan Vlk <vlkjan6@fel.cvut.cz>, Ales Kucera <kuceral4@fel.cvut.cz>, David Korcak <david.korcak@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -130,8 +130,8 @@ source .venv/bin/activate
130
130
 
131
131
  SimView is distributed under the [BSD 3-Clause License](LICENSE). The web interface
132
132
  vendors [uPlot](https://github.com/leeoniya/uPlot) and [three.js](https://github.com/mrdoob/three.js)
133
- (both MIT licensed) under
134
- `simview/static/lib/` so the viewer works fully offline — see the
133
+ (both MIT licensed) under `simview/static/lib/`, and the matplotlib colour-map tables
134
+ (`js-colormaps.js`) under `simview/static/js/vendor/`, so the viewer works fully offline — see the
135
135
  [documentation](https://vlk-jan.github.io/simview/dev/architecture/#vendored-third-party-libraries)
136
136
  for details. All third-party libraries used by SimView are permissively licensed
137
137
  (MIT/BSD), so there are no licensing restrictions on commercial use.
@@ -99,8 +99,8 @@ source .venv/bin/activate
99
99
 
100
100
  SimView is distributed under the [BSD 3-Clause License](LICENSE). The web interface
101
101
  vendors [uPlot](https://github.com/leeoniya/uPlot) and [three.js](https://github.com/mrdoob/three.js)
102
- (both MIT licensed) under
103
- `simview/static/lib/` so the viewer works fully offline — see the
102
+ (both MIT licensed) under `simview/static/lib/`, and the matplotlib colour-map tables
103
+ (`js-colormaps.js`) under `simview/static/js/vendor/`, so the viewer works fully offline — see the
104
104
  [documentation](https://vlk-jan.github.io/simview/dev/architecture/#vendored-third-party-libraries)
105
105
  for details. All third-party libraries used by SimView are permissively licensed
106
106
  (MIT/BSD), so there are no licensing restrictions on commercial use.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simview"
7
- version = "6.2.2"
7
+ version = "6.2.3"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  license = "BSD-3-Clause"
@@ -1,3 +1,5 @@
1
+ """The `simview` command-line interface."""
2
+
1
3
  import argparse
2
4
  import gzip
3
5
  import json
@@ -436,8 +438,8 @@ def build_parser() -> argparse.ArgumentParser:
436
438
  metavar="PATH",
437
439
  help=(
438
440
  "Merge the given input files and write the result to PATH instead of "
439
- "launching the viewer. Requires at least 2 input files. Gzips the "
440
- "output if PATH ends in .gz."
441
+ "launching the viewer. Requires at least 2 input files, or 1 with a "
442
+ "'#<batches>' selection. Gzips the output if PATH ends in .gz."
441
443
  ),
442
444
  )
443
445
  _add_remote_flags(view)
@@ -654,9 +656,8 @@ def main():
654
656
  _configure_logging()
655
657
  argv = sys.argv[1:]
656
658
 
657
- # Checked first, and unconditionally, to match every other flag/subcommand
658
- # combination -- mirrors the old flat parser, where --version short-circuited
659
- # before any other validation ran.
659
+ # Checked before parsing so --version works alongside any other
660
+ # arguments, without tripping their validation.
660
661
  if "--version" in argv:
661
662
  print(_package_version())
662
663
  return
@@ -2,7 +2,7 @@
2
2
  covering a whole trajectory, instead of thousands of small per-frame objects.
3
3
 
4
4
  This is both a **wire** format (served by `simview/server.py`, consumed by
5
- `static/js/components/StateStore.js`) and, since it became first-class, an
5
+ `static/js/components/StateStore.js`) and an
6
6
  **on-disk** format (`SimulationScene.save(columnar=True)`). The two are the
7
7
  same document; only how a blob is referenced differs:
8
8
 
@@ -15,7 +15,7 @@ That symmetry is the whole point: a columnar file needs no repacking at all
15
15
  to be served, and `columnar.rewrite_blobs` (shared by `server.py`) does the
16
16
  rewriting for free.
17
17
 
18
- Shape (see README.md "JSON Format Specification" for the authoritative spec)::
18
+ Shape (see docs/dev/json-format.md for the authoritative spec)::
19
19
 
20
20
  {"version": 4,
21
21
  "times": [t0, t1, ...], # length T
@@ -171,7 +171,8 @@ def _decode_state_field_rows(value, width: int, batch_size: int):
171
171
  Mirrors the shapes SimViewBodyState.to_json()/add_trajectory produce: a
172
172
  ``__b64__`` blob is always batch_size rows of `width` floats; a plain list
173
173
  is either already nested (one row per batch) or, for a single-batch scene,
174
- a flat list of `width` floats (see README "Authoring whole trajectories").
174
+ a flat list of `width` floats (see docs/dev/json-format.md "Authoring whole
175
+ trajectories").
175
176
  """
176
177
  assert np is not None
177
178
  if isinstance(value, str):
@@ -221,7 +222,7 @@ def columnarize_states(states_data: list, model_data: dict | None, register_blob
221
222
  Strict by design: this trades a bit of coverage (an inconsistent scene just
222
223
  doesn't get the win) for never risking a subtly wrong repack. The one
223
224
  deliberate exception is `contacts`, which may legitimately come and go per
224
- frame (see README) without disqualifying the rest of the scene.
225
+ frame (see docs/dev/json-format.md) without disqualifying the rest of the scene.
225
226
  """
226
227
  if np is None or not states_data:
227
228
  return None
@@ -3,9 +3,9 @@
3
3
  ground truth vs. prediction, baseline vs. post-adaptation) actually track
4
4
  each other.
5
5
 
6
- Deliberately dependency-free (stdlib only: json, base64, struct, math via
7
- its imports) so it works on a base install without the `authoring` extra
8
- (torch/numpy) -- see CLAUDE.md. Shares its blob-decoding and
6
+ Deliberately dependency-free (stdlib only) so it works on a base install
7
+ without the `authoring` extra (torch/numpy) -- see "Lazy imports" in
8
+ docs/dev/architecture.md. Shares its blob-decoding and
9
9
  body-resolution helpers with `simview/terrain.py`/`simview/info.py` via
10
10
  `simview.columnar`/`simview.utils`, which are stdlib-only on this read path.
11
11
 
@@ -36,8 +36,8 @@ _MAX_SERIES_ROWS = 10
36
36
  def _build_body_meta(model_data: dict, state_names: list) -> dict[str, dict]:
37
37
  """`name -> {"parent", "localTransform"}` for every body, from the model's
38
38
  `bodies` section. Bodies that appear in the states but not in the model are
39
- added as parentless roots, so a hand-edited or third-party file still
40
- diffs exactly as it did before parent resolution existed."""
39
+ added as parentless roots, so a hand-edited or third-party file whose
40
+ model omits them still diffs."""
41
41
  meta: dict[str, dict] = {}
42
42
  for entry in model_data.get("bodies") or []:
43
43
  name = entry.get("name")
@@ -54,8 +54,8 @@ def _build_body_meta(model_data: dict, state_names: list) -> dict[str, dict]:
54
54
 
55
55
  def _topo_sort_bodies(meta: dict[str, dict]) -> list[str]:
56
56
  """Order body names so every parent precedes its children. Independent
57
- stdlib port of `static/js/utils/bodyTransforms.js`'s `topoSortBodies` (see
58
- module docstring on why these aren't shared); each body has at most one
57
+ stdlib port of `static/js/utils/bodyTransforms.js`'s `topoSortBodies` (the
58
+ viewer's JS can't be shared with the CLI); each body has at most one
59
59
  parent, so a DFS post-order walk suffices."""
60
60
  order: list[str] = []
61
61
  status: dict[str, str] = {}
@@ -374,6 +374,9 @@ def compute_trajectory_diff(
374
374
 
375
375
 
376
376
  def format_diff_text(result: dict) -> str:
377
+ """Render `compute_trajectory_diff`'s output as a skimmable text report,
378
+ capping each body's per-frame table (use `--json`/`--csv` for the full
379
+ series)."""
377
380
  lines = [
378
381
  f"Batches {result['batch_a']} vs {result['batch_b']} every={result['every']}"
379
382
  ]
@@ -3,11 +3,10 @@ agents inspecting simview-generated data files.
3
3
 
4
4
  Deliberately dependency-free (stdlib only: json, gzip via
5
5
  `simview.utils.read_maybe_gzipped_bytes`, base64) so it works on a base
6
- install without the `authoring` extra (torch/numpy) -- see
7
- CLAUDE.md. Does not import `simview.model`/`simview.server`: the former
8
- pulls in torch at module scope, the latter fastapi/uvicorn just to
9
- reach a numpy-gated helper -- both wrong layering for a lightweight
10
- inspection tool. Shares its blob-decoding/body-label helpers with
6
+ install without the `authoring` extra (torch/numpy) -- see "Lazy imports" in
7
+ docs/dev/architecture.md. Does not import `simview.model`/`simview.server`:
8
+ the former pulls in torch at module scope, the latter fastapi/uvicorn --
9
+ both wrong layering for a lightweight inspection tool. Shares its blob-decoding/body-label helpers with
11
10
  `simview/terrain.py`/`simview/diff.py` via `simview.columnar`/`simview.utils`,
12
11
  which are stdlib-only on this read path.
13
12
  """
@@ -241,8 +240,8 @@ def _summarize_states(states, model: dict | None, warnings: list[str]) -> dict:
241
240
 
242
241
  # Per-body walk, mirroring columnar.py's columnarize_states structural
243
242
  # checks (field-set consistency, first-appearance-at-frame-0, no
244
- # dropping out) but never decoding/shaping blob values -- see module
245
- # docstring.
243
+ # dropping out) without numpy: blobs are decoded only to count their
244
+ # floats, never shaped into arrays.
246
245
  body_order: list = []
247
246
  body_first_frame: dict = {}
248
247
  body_name_value: dict = {}
@@ -1,3 +1,5 @@
1
+ """Blocking launch of the viewer for a scene object or a scene file."""
2
+
1
3
  import gc
2
4
  import logging
3
5
  from pathlib import Path
@@ -11,15 +13,19 @@ logger = logging.getLogger("simview.launcher")
11
13
 
12
14
 
13
15
  class SimViewLauncher:
16
+ """Serve a scene in the foreground until interrupted, then free it.
17
+
18
+ The blocking counterpart to `SimulationScene.show()`'s background viewer.
19
+ """
20
+
14
21
  def __init__(
15
22
  self,
16
23
  source: SimulationScene | str | Path,
17
24
  ) -> None:
18
- """
19
- Initializes the visualizer.
20
- - If source is a SimulationScene: it is handed to the server in memory (no
21
- temporary file), and its internal data is cleared after the server stops.
22
- - If source is a path: it is used directly.
25
+ """Validate `source`, a `SimulationScene` or a path to a scene file.
26
+
27
+ A scene is handed to the server in memory (no temporary file) and its
28
+ internal data is cleared once the server stops; a path is served as is.
23
29
  """
24
30
  self._scene: SimulationScene | None = None
25
31
  self._sim_file_path: Path | None = None
@@ -47,9 +53,7 @@ class SimViewLauncher:
47
53
  )
48
54
 
49
55
  def launch(self, host: str = "127.0.0.1", preferred_port: int = 5420) -> None:
50
- """
51
- Launches the SimViewServer, then clears the in-memory scene (if any).
52
- """
56
+ """Run the server until it stops, then clear the in-memory scene (if any)."""
53
57
  try:
54
58
  if self._scene is not None:
55
59
  # Serialize the scene once and hand it to the server directly,
@@ -62,9 +66,7 @@ class SimViewLauncher:
62
66
  server = SimViewServer(data=data)
63
67
  server.run(host=host, port=find_free_port(host, preferred_port))
64
68
  else:
65
- # __init__ guarantees exactly one of self._scene / self._sim_file_path
66
- # is set (it raises otherwise), so if self._scene is None here,
67
- # self._sim_file_path must have been set.
69
+ # __init__ sets exactly one of _scene / _sim_file_path.
68
70
  assert self._sim_file_path is not None
69
71
  SimViewServer.start(
70
72
  sim_path=self._sim_file_path,
@@ -80,9 +82,7 @@ class SimViewLauncher:
80
82
  self.cleanup()
81
83
 
82
84
  def cleanup(self) -> None:
83
- """
84
- Clears the in-memory scene's data. Safe to call multiple times.
85
- """
85
+ """Clear the in-memory scene's data. Safe to call multiple times."""
86
86
  if self._scene is not None:
87
87
  logger.info(
88
88
  "SimViewLauncher: Clearing source SimulationScene internal data..."
@@ -1,3 +1,5 @@
1
+ """Stream a running simulation to the browser viewer over WebSocket."""
2
+
1
3
  import asyncio
2
4
  import logging
3
5
  import queue
@@ -89,6 +91,7 @@ class LiveViewer:
89
91
 
90
92
  @property
91
93
  def url(self) -> str:
94
+ """Base URL of the live viewer."""
92
95
  return self._threaded.url
93
96
 
94
97
  def push_state(self, time, body_states, scalar_values=None) -> None:
@@ -313,11 +313,14 @@ def _validate_doc(doc: dict, label: str) -> None:
313
313
  def _expand_batched(
314
314
  values: list | str, is_singleton: bool, batch_size: int, field: str, label: str
315
315
  ) -> list | str:
316
- # b64 blobs pass through untouched: _decode_per_batch resolves their row
317
- # layout by length (one shared row for a deduplicated singleton terrain,
318
- # or batch_size rows for per-batch / legacy broadcast-singleton data) and
319
- # replicates the shared case itself. Only plain lists (where the row count
320
- # is directly observable) may need broadcasting below.
316
+ """Broadcast a plain-list terrain field to `batch_size` rows.
317
+
318
+ ``__b64__`` blobs pass through untouched: `_decode_per_batch` resolves
319
+ their row layout by length (one shared row for a deduplicated singleton
320
+ terrain, or `batch_size` rows for per-batch / legacy broadcast-singleton
321
+ data) and replicates the shared case itself. Only plain lists, where the
322
+ row count is directly observable, may need broadcasting here.
323
+ """
321
324
  if is_blob(values):
322
325
  return values
323
326
 
@@ -633,8 +636,8 @@ def _merge_terrain(
633
636
  )
634
637
 
635
638
  def _bound(name: str, key: str, pick) -> float | None:
636
- # A null bound means "unknown" (json-format.md), and one unknown
637
- # makes the merged range unknown too.
639
+ # A null bound means "unknown", and one unknown makes the merged
640
+ # range unknown too.
638
641
  values = [t["properties"][name].get(key) for t in terrains]
639
642
  return None if any(v is None for v in values) else pick(values)
640
643
 
@@ -684,6 +687,13 @@ def _merge_states(
684
687
  labels: list[str],
685
688
  selections: list[list[int] | None],
686
689
  ) -> list[dict]:
690
+ """Build the merged per-frame states on the first file's timeline.
691
+
692
+ Every other file contributes its nearest-in-time frame (zero-order hold).
693
+ Per-body fields and scalars are concatenated in file order, keeping only
694
+ each file's selected batches; rigidly attached bodies are skipped, since
695
+ they never appear in states.
696
+ """
687
697
  ref_times = [s["time"] for s in states_list[0]]
688
698
  times_by_file = [[s["time"] for s in states] for states in states_list]
689
699
  total_batches = sum(_output_sizes(batch_sizes, selections))
@@ -926,7 +936,7 @@ def merge_simulation_files(
926
936
  if dropped:
927
937
  logger.warning(
928
938
  "Dropping episode boundaries from %s: they index their own "
929
- "timeline, and the merged scene uses '%s'’s timeline.",
939
+ "timeline, and the merged scene uses '%s's timeline.",
930
940
  ", ".join(f"'{label}'" for label in dropped),
931
941
  labels[0],
932
942
  )
@@ -13,7 +13,7 @@ logger = logging.getLogger("simview.model")
13
13
 
14
14
 
15
15
  def _encode_blob(array) -> str:
16
- """Encode a numpy array as an inline little-endian float32 `__b64__` blob."""
16
+ """Encode a numpy array as an inline little-endian float32 ``__b64__`` blob."""
17
17
  return inline_blob(array.astype("<f4").tobytes())
18
18
 
19
19
 
@@ -72,6 +72,8 @@ def _validated_visible_ranges(owner: str, ranges: Any) -> list[list[float]] | No
72
72
 
73
73
 
74
74
  class BodyShapeType(StrEnum):
75
+ """Shape kind of a body or static object (the wire `shape.type`)."""
76
+
75
77
  POINTCLOUD = "pointcloud"
76
78
  MESH = "mesh"
77
79
  BOX = "box"
@@ -80,6 +82,8 @@ class BodyShapeType(StrEnum):
80
82
 
81
83
 
82
84
  class OptionalBodyStateAttribute(StrEnum):
85
+ """Optional per-body state fields beyond `bodyTransform` (wire keys)."""
86
+
83
87
  CONTACTS = "contacts"
84
88
  VELOCITY = "velocity"
85
89
  ANGULAR_VELOCITY = "angularVelocity"
@@ -100,7 +104,7 @@ class TerrainProperty:
100
104
  """One arbitrary named per-cell scalar field over the terrain grid (e.g.
101
105
  friction, stiffness, or any other user-defined property), stored the same
102
106
  way as `SimViewTerrain.height_data` -- a plain nested list, or an opaque
103
- `__b64__`-prefixed blob string for compactness."""
107
+ ``__b64__``-prefixed blob string for compactness."""
104
108
 
105
109
  data: list[list[float]] | str
106
110
  # Value range used by the viewer to normalize the color map (analogous to
@@ -118,6 +122,9 @@ class TerrainProperty:
118
122
 
119
123
  @dataclass
120
124
  class SimViewTerrain:
125
+ """Terrain heightmap (plus optional normals, named properties and
126
+ embeddings) over a regular x/y grid, shared or per batch."""
127
+
121
128
  extent_x: float
122
129
  extent_y: float
123
130
  shape_x: int
@@ -128,10 +135,9 @@ class SimViewTerrain:
128
135
  max_y: float
129
136
  min_z: float
130
137
  max_z: float
131
- # These are plain nested lists when constructed directly, but `create()` (and
132
- # deserialization from JSON) may instead store an opaque `__b64__`-prefixed
133
- # base64 blob string for compactness; `to_json`/`from_dict` pass them through
134
- # as-is either way.
138
+ # `create()` always stores an opaque ``__b64__``-prefixed base64 blob
139
+ # string; hand-built instances may use plain nested lists. `to_json`/
140
+ # `from_dict` pass either through as-is.
135
141
  height_data: list[list[float]] | str
136
142
  # None when the terrain was authored with `compute_normals=False`: the
137
143
  # viewer then shades from normals it computes itself, and the file is
@@ -140,9 +146,7 @@ class SimViewTerrain:
140
146
  is_singleton: bool
141
147
  # Arbitrary named per-cell scalar fields (e.g. "friction", "stiffness", or
142
148
  # any other user-defined property), keyed by name -- see `TerrainProperty`.
143
- # Adding a new property never requires touching this class or the viewer:
144
- # supply it by name in `create()`'s `properties` dict and it becomes
145
- # selectable as a terrain color mode automatically.
149
+ # Any name passed in `create()`'s `properties` becomes a viewer color mode.
146
150
  properties: dict[str, TerrainProperty] = field(default_factory=dict)
147
151
  # Per-cell K-wide feature vector (e.g. a reduced-dim PCA projection of a
148
152
  # learned backbone's features), stored the same way as `normals` (width
@@ -185,7 +189,7 @@ class SimViewTerrain:
185
189
  """Reconstruct a SimViewTerrain from the dict produced by `to_json`.
186
190
 
187
191
  `heightData`/`normals`/each property's `data` are kept in whatever
188
- form they were serialized in (plain nested lists or a `__b64__` blob
192
+ form they were serialized in (plain nested lists or a ``__b64__`` blob
189
193
  string) -- decode with `simview.columnar.blob_floats` if you need the
190
194
  flat float values back out.
191
195
  """
@@ -222,7 +226,7 @@ class SimViewTerrain:
222
226
 
223
227
  @staticmethod
224
228
  def create(
225
- heightmap: torch.Tensor, # ! remember the x,y indexing is assumed to follow torch's "xy" convention, so increasing column index is increasing x coordinate
229
+ heightmap: torch.Tensor,
226
230
  normals: torch.Tensor | None,
227
231
  x_lim: tuple[float, float],
228
232
  y_lim: tuple[float, float],
@@ -231,6 +235,17 @@ class SimViewTerrain:
231
235
  property_bounds: dict[str, tuple[float, float]] | None = None,
232
236
  embedding_map: torch.Tensor | None = None,
233
237
  ) -> "SimViewTerrain":
238
+ """Build a terrain from batched tensors, encoding each field as a blob.
239
+
240
+ `heightmap` is `(B, Dy, Dx)` with torch's "xy" indexing: the column
241
+ index increases with x. `normals` is `(B, 3, Dy, Dx)` or None,
242
+ `properties` maps names to `(B, Dy, Dx)` maps, and `embedding_map` is
243
+ `(B, K, Dy, Dx)`. `x_lim`/`y_lim` are the `(min, max)` world extents.
244
+ `property_bounds` optionally pins a property's color-scale range;
245
+ otherwise the map's own data range is used. Every grid must match
246
+ the heightmap's. Batch broadcasting is the caller's job (see
247
+ `SimulationScene.create_terrain`).
248
+ """
234
249
  if heightmap.ndim != 3:
235
250
  raise ValueError(
236
251
  f"Heightmap must include a batch dimension (ndim=3); got ndim={heightmap.ndim}."
@@ -333,6 +348,9 @@ class SimViewTerrain:
333
348
 
334
349
  @dataclass
335
350
  class SimViewBody:
351
+ """A dynamic body: its shape, look, optional parent attachment and the
352
+ optional state attributes its states carry."""
353
+
336
354
  name: str
337
355
  shape: dict
338
356
  available_attributes: list[OptionalBodyStateAttribute] | None = None
@@ -382,6 +400,7 @@ class SimViewBody:
382
400
  def set_available_attributes(
383
401
  self, available_attributes: list[str | OptionalBodyStateAttribute]
384
402
  ) -> None:
403
+ """Set the optional attributes this body's states carry (once only)."""
385
404
  if self.available_attributes is not None:
386
405
  raise ValueError("Available attributes already set")
387
406
  self.available_attributes = [
@@ -393,7 +412,8 @@ class SimViewBody:
393
412
 
394
413
  @staticmethod
395
414
  def _create_shape_dict(body_type: BodyShapeType, **kwargs) -> dict:
396
- """Helper to create the shape dictionary, converting tensors."""
415
+ """Build a shape dict, encoding multi-element tensors as blobs and
416
+ validating required primitive dimensions."""
397
417
  shape_dict: dict[str, Any] = {"type": body_type.value}
398
418
  for key, value in kwargs.items():
399
419
  if isinstance(value, torch.Tensor):
@@ -430,6 +450,10 @@ class SimViewBody:
430
450
  visible_ranges: list | None = None,
431
451
  **kwargs,
432
452
  ) -> "SimViewBody":
453
+ """Build a body of `body_type`; `**kwargs` are its shape parameters
454
+ (e.g. `hx`/`hy`/`hz` for a box, `radius` for a sphere, `radius`/
455
+ `height` for a cylinder, `vertices`/`faces` for a mesh). The other
456
+ parameters are as on `SimulationScene.create_body`."""
433
457
  shape_dict = SimViewBody._create_shape_dict(body_type, **kwargs)
434
458
  if local_transform is not None and hasattr(local_transform, "tolist"):
435
459
  local_transform = local_transform.tolist()
@@ -453,6 +477,7 @@ class SimViewBody:
453
477
  def create_box(
454
478
  name: str, hx: float, hy: float, hz: float, **kwargs
455
479
  ) -> "SimViewBody":
480
+ """Build a box body with half-extents `hx`/`hy`/`hz`."""
456
481
  return SimViewBody.create(
457
482
  name, BodyShapeType.BOX, hx=hx, hy=hy, hz=hz, **kwargs
458
483
  )
@@ -465,13 +490,15 @@ class SimViewBody:
465
490
  embedding: torch.Tensor | None = None,
466
491
  **kwargs,
467
492
  ) -> "SimViewBody":
468
- """`color` (N, 3) in [0, 1] is an optional static per-point RGB color,
493
+ """Build a point-cloud body from `(N, 3)` `points`.
494
+
495
+ `color` (N, 3) in [0, 1] is an optional static per-point RGB color,
469
496
  used by the viewer for vertex-colored rendering. `embedding` (N, K) is
470
497
  an optional per-point K-wide feature vector (e.g. a reduced-dim PCA
471
498
  projection of a learned backbone's features) enabling the viewer's
472
499
  click-to-similarity color mode: cosine similarity to a clicked point,
473
- computed entirely in-browser from this data. Both round-trip through
474
- the existing generic `__b64__` blob mechanism -- no new wire format."""
500
+ computed entirely in-browser from this data. Both are stored as
501
+ ``__b64__`` blobs."""
475
502
  if points.ndim != 2 or points.shape[1] != 3:
476
503
  raise ValueError(
477
504
  f"points must have shape (N, 3); got {tuple(points.shape)}."
@@ -504,6 +531,7 @@ class SimViewBody:
504
531
  def create_mesh(
505
532
  name: str, vertices: torch.Tensor, faces: torch.Tensor, **kwargs
506
533
  ) -> "SimViewBody":
534
+ """Build a mesh body from `vertices` `(V, 3)` and `faces` `(F, 3)`."""
507
535
  return SimViewBody.create(
508
536
  name, BodyShapeType.MESH, vertices=vertices, faces=faces, **kwargs
509
537
  )
@@ -554,6 +582,9 @@ class SimViewBody:
554
582
 
555
583
  @dataclass
556
584
  class SimViewStaticObject:
585
+ """A static (never-moving) object: one shape shared by every batch
586
+ (singleton) or one shape per batch."""
587
+
557
588
  name: str
558
589
  is_singleton: bool
559
590
  shape: dict | None = None # Used if is_singleton is True
@@ -578,9 +609,9 @@ class SimViewStaticObject:
578
609
  visible_ranges: list | None = None,
579
610
  **kwargs,
580
611
  ) -> "SimViewStaticObject":
581
- shape_dict = SimViewBody._create_shape_dict(
582
- shape_type, **kwargs
583
- ) # Reuse helper
612
+ """Build a static object shared by every batch; `**kwargs` are the
613
+ shape parameters, as on `SimViewBody.create`."""
614
+ shape_dict = SimViewBody._create_shape_dict(shape_type, **kwargs)
584
615
  return SimViewStaticObject(
585
616
  name=name,
586
617
  is_singleton=True,
@@ -595,16 +626,12 @@ class SimViewStaticObject:
595
626
  shapes_kwargs: list[dict[str, Any]],
596
627
  visible_ranges: list | None = None,
597
628
  ) -> "SimViewStaticObject":
598
- """
599
- Creates a batched static object where all instances share the same shape type.
600
-
601
- Args:
602
- name: The name of the static object group.
603
- shape_type: The BodyShapeType common to all instances in the batch.
604
- shapes_kwargs: A list of dictionaries, where each dictionary contains the
605
- keyword arguments for creating the shape of one instance
606
- in the batch (e.g., [{'hx': 0.1, 'hy': 0.1, 'hz': 0.1}, {'hx': 0.2, ...}]).
607
- The length of this list must match the batch size.
629
+ """Build a static object with one shape per batch, all of `shape_type`.
630
+
631
+ `name` names the group. `shapes_kwargs` holds one dict of shape
632
+ keyword arguments per batch instance (e.g. `[{"hx": 0.1, "hy": 0.1,
633
+ "hz": 0.1}, {"hx": 0.2, ...}]`); its length must match the model's
634
+ batch size.
608
635
  """
609
636
  shapes_list = []
610
637
  if not shapes_kwargs:
@@ -616,9 +643,7 @@ class SimViewStaticObject:
616
643
  raise ValueError(
617
644
  "Do not include 'type' in shapes_kwargs; use the shape_type argument."
618
645
  )
619
- shapes_list.append(
620
- SimViewBody._create_shape_dict(shape_type, **kwargs)
621
- ) # Reuse helper
646
+ shapes_list.append(SimViewBody._create_shape_dict(shape_type, **kwargs))
622
647
  return SimViewStaticObject(
623
648
  name=name,
624
649
  is_singleton=False,
@@ -658,7 +683,7 @@ class SimViewPolyline:
658
683
  """A world-space polyline: a fixed `points` list (a route), and/or a
659
684
  per-frame `frames` blob (a replanned local path) of shape
660
685
  `(T, B, max_vertices, 3)` float32, trailing NaN rows padding each frame
661
- up to `max_vertices`. Model-level by design (no new states field), so it
686
+ up to `max_vertices`. Model-level by design (not a states field), so it
662
687
  rides the generic blob machinery; not supported in live mode."""
663
688
 
664
689
  name: str
@@ -723,6 +748,7 @@ class SimViewPolyline:
723
748
 
724
749
  @classmethod
725
750
  def from_dict(cls, d: dict) -> "SimViewPolyline":
751
+ """Reconstruct a SimViewPolyline from the dict produced by `to_json`."""
726
752
  try:
727
753
  name = d["name"]
728
754
  except KeyError as e:
@@ -774,6 +800,7 @@ class SimViewEpisode:
774
800
 
775
801
  @classmethod
776
802
  def from_dict(cls, d: dict) -> "SimViewEpisode":
803
+ """Reconstruct a SimViewEpisode from the dict produced by `to_json`."""
777
804
  try:
778
805
  start_index = d["startIndex"]
779
806
  except (KeyError, TypeError) as e:
@@ -815,6 +842,9 @@ def _validate_parent_ref(name: str, parent: str | None, known_bodies: dict) -> N
815
842
 
816
843
  @dataclass
817
844
  class SimViewModel:
845
+ """The static part of a scene: batch layout, terrain, bodies, static
846
+ objects, polylines and metadata (the wire `model` section)."""
847
+
818
848
  batch_size: int
819
849
  scalar_names: list[str]
820
850
  # None means "not known"; the viewer then infers it from consecutive
@@ -859,12 +889,15 @@ class SimViewModel:
859
889
  _validate_episodes(self.episodes)
860
890
 
861
891
  def add_body(self, body: SimViewBody) -> None:
892
+ """Add `body`; its name must be new and its parent already added."""
862
893
  if body.name in self.bodies:
863
894
  raise ValueError(f"Dynamic body {body.name} already exists")
864
895
  _validate_parent_ref(body.name, body.parent, self.bodies)
865
896
  self.bodies[body.name] = body
866
897
 
867
898
  def add_static_object(self, static_object: SimViewStaticObject) -> None:
899
+ """Add `static_object`; its name must be new and a batched one needs
900
+ one shape per batch."""
868
901
  if static_object.name in self.static_objects:
869
902
  raise ValueError(f"Static object {static_object.name} already exists")
870
903
  if not static_object.is_singleton:
@@ -880,6 +913,7 @@ class SimViewModel:
880
913
  self.static_objects[static_object.name] = static_object
881
914
 
882
915
  def add_polyline(self, polyline: SimViewPolyline) -> None:
916
+ """Add `polyline`; its name must be new."""
883
917
  if polyline.name in self.polylines:
884
918
  raise ValueError(f"Polyline {polyline.name} already exists")
885
919
  self.polylines[polyline.name] = polyline
@@ -914,9 +948,11 @@ class SimViewModel:
914
948
  def from_dict(cls, d: dict) -> "SimViewModel":
915
949
  """Reconstruct a SimViewModel from the dict produced by `to_json`.
916
950
 
917
- Centralizes parsing of the wire format: terrain, bodies and static
918
- objects are all rebuilt via their own `from_dict`, keyed by name so
919
- `add_body`/`add_static_object`'s uniqueness checks stay meaningful.
951
+ Centralizes parsing of the wire format: each part is rebuilt via its
952
+ own `from_dict`. Bodies are checked for duplicate names and parent
953
+ references inline; static objects and polylines go through
954
+ `add_static_object`/`add_polyline`, so they're validated exactly as
955
+ when authoring.
920
956
  """
921
957
  try:
922
958
  batch_size = d["simBatches"]
@@ -962,4 +998,5 @@ class SimViewModel:
962
998
 
963
999
  @property
964
1000
  def is_complete(self) -> bool:
1001
+ """True once a terrain is set (required to save or show)."""
965
1002
  return self.terrain is not None
@@ -121,7 +121,7 @@ def is_remote_spec(spec: str) -> bool:
121
121
 
122
122
 
123
123
  def cache_dir() -> Path:
124
- """Root of simview's on-disk cache, honouring XDG_CACHE_HOME."""
124
+ """Root of simview's on-disk cache, honoring XDG_CACHE_HOME."""
125
125
  base = os.environ.get("XDG_CACHE_HOME")
126
126
  root = Path(base) if base else Path.home() / ".cache"
127
127
  return root / CACHE_DIR