simview 6.2.2__tar.gz → 6.3.0__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.3.0}/PKG-INFO +3 -3
  2. {simview-6.2.2 → simview-6.3.0}/README.md +2 -2
  3. {simview-6.2.2 → simview-6.3.0}/pyproject.toml +1 -1
  4. {simview-6.2.2 → simview-6.3.0}/simview/__main__.py +21 -6
  5. {simview-6.2.2 → simview-6.3.0}/simview/columnar.py +5 -4
  6. {simview-6.2.2 → simview-6.3.0}/simview/diff.py +10 -7
  7. {simview-6.2.2 → simview-6.3.0}/simview/info.py +17 -12
  8. {simview-6.2.2 → simview-6.3.0}/simview/launcher.py +14 -14
  9. {simview-6.2.2 → simview-6.3.0}/simview/live.py +3 -0
  10. {simview-6.2.2 → simview-6.3.0}/simview/merge.py +18 -8
  11. {simview-6.2.2 → simview-6.3.0}/simview/model.py +72 -35
  12. {simview-6.2.2 → simview-6.3.0}/simview/remote.py +1 -1
  13. {simview-6.2.2 → simview-6.3.0}/simview/render.py +6 -4
  14. {simview-6.2.2 → simview-6.3.0}/simview/scene.py +114 -112
  15. {simview-6.2.2 → simview-6.3.0}/simview/server.py +216 -34
  16. {simview-6.2.2 → simview-6.3.0}/simview/state.py +30 -24
  17. {simview-6.2.2 → simview-6.3.0}/simview/static/css/controls.css +84 -107
  18. {simview-6.2.2 → simview-6.3.0}/simview/static/js/SimView.js +66 -74
  19. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/AnimationController.js +8 -15
  20. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/BatchManager.js +6 -8
  21. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/InteractionController.js +3 -10
  22. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/Scene.js +6 -6
  23. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/StateStore.js +6 -11
  24. {simview-6.2.2 → simview-6.3.0}/simview/static/js/components/WindowedField.js +2 -0
  25. {simview-6.2.2 → simview-6.3.0}/simview/static/js/config.js +2 -1
  26. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/Body.js +12 -12
  27. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/StaticObject.js +2 -8
  28. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/Terrain.js +11 -20
  29. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/colormap.js +9 -16
  30. simview-6.3.0/simview/static/js/objects/similarity.js +25 -0
  31. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/utils.js +15 -45
  32. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/BodyStateWindow.js +20 -31
  33. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/Controls.js +27 -32
  34. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/ErrorMetrics.js +4 -3
  35. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/Legend.js +7 -5
  36. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/PlaybackControls.js +4 -3
  37. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/ScalarPlotter.js +13 -8
  38. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/TerrainProfile.js +14 -5
  39. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/batchVisibility.js +1 -0
  40. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/blobCodec.js +5 -5
  41. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/blobWindow.js +9 -2
  42. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/bodyTransforms.js +3 -4
  43. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/cameraRange.js +6 -3
  44. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/csv.js +3 -2
  45. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/errorMath.js +2 -5
  46. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/keyboard.js +3 -2
  47. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/terrainSample.js +3 -7
  48. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/uplot.js +4 -5
  49. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/viewState.js +21 -0
  50. {simview-6.2.2/simview/static/lib → simview-6.3.0/simview/static/js/vendor}/js-colormaps.js +25 -3
  51. {simview-6.2.2 → simview-6.3.0}/simview/templates/index.html +16 -12
  52. {simview-6.2.2 → simview-6.3.0}/simview/terrain.py +13 -13
  53. {simview-6.2.2 → simview-6.3.0}/simview/view.py +9 -6
  54. {simview-6.2.2 → simview-6.3.0/simview.egg-info}/PKG-INFO +3 -3
  55. {simview-6.2.2 → simview-6.3.0}/simview.egg-info/SOURCES.txt +3 -3
  56. {simview-6.2.2 → simview-6.3.0}/tests/test_cli.py +6 -2
  57. {simview-6.2.2 → simview-6.3.0}/tests/test_columnar_states.py +4 -3
  58. {simview-6.2.2 → simview-6.3.0}/tests/test_diff.py +1 -1
  59. {simview-6.2.2 → simview-6.3.0}/tests/test_info.py +1 -1
  60. {simview-6.2.2 → simview-6.3.0}/tests/test_launcher.py +1 -1
  61. {simview-6.2.2 → simview-6.3.0}/tests/test_live.py +61 -1
  62. {simview-6.2.2 → simview-6.3.0}/tests/test_roundtrip.py +1 -1
  63. {simview-6.2.2 → simview-6.3.0}/tests/test_scene.py +4 -4
  64. {simview-6.2.2 → simview-6.3.0}/tests/test_server.py +46 -4
  65. {simview-6.2.2 → simview-6.3.0}/tests/test_show.py +20 -0
  66. {simview-6.2.2 → simview-6.3.0}/tests/test_terrain_query.py +1 -1
  67. {simview-6.2.2 → simview-6.3.0}/tests/test_utils.py +1 -1
  68. simview-6.2.2/simview/static/js/objects/similarity.js +0 -38
  69. {simview-6.2.2 → simview-6.3.0}/LICENSE +0 -0
  70. {simview-6.2.2 → simview-6.3.0}/MANIFEST.in +0 -0
  71. {simview-6.2.2 → simview-6.3.0}/setup.cfg +0 -0
  72. {simview-6.2.2 → simview-6.3.0}/simview/__init__.py +0 -0
  73. {simview-6.2.2 → simview-6.3.0}/simview/py.typed +0 -0
  74. {simview-6.2.2 → simview-6.3.0}/simview/static/js/main.js +0 -0
  75. {simview-6.2.2 → simview-6.3.0}/simview/static/js/objects/Polyline.js +0 -0
  76. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/AnalysisPanel.js +0 -0
  77. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/BatchLegend.js +0 -0
  78. {simview-6.2.2 → simview-6.3.0}/simview/static/js/ui/chartControls.js +0 -0
  79. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/batchPresets.js +0 -0
  80. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/episodes.js +0 -0
  81. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/html.js +0 -0
  82. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/interpolate.js +0 -0
  83. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/liveFollow.js +0 -0
  84. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/scalarSeries.js +0 -0
  85. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/viewerDefaults.js +0 -0
  86. {simview-6.2.2 → simview-6.3.0}/simview/static/js/utils/visibleRanges.js +0 -0
  87. {simview-6.2.2 → simview-6.3.0}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  88. {simview-6.2.2 → simview-6.3.0}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  89. {simview-6.2.2 → simview-6.3.0}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  90. {simview-6.2.2 → simview-6.3.0}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  91. {simview-6.2.2/simview/static/lib → simview-6.3.0/simview/static/lib/uPlot-1.6.32}/uPlot.esm.js +0 -0
  92. {simview-6.2.2/simview/static/lib → simview-6.3.0/simview/static/lib/uPlot-1.6.32}/uPlot.min.css +0 -0
  93. {simview-6.2.2 → simview-6.3.0}/simview/static/textures/contacts/red-cross0.png +0 -0
  94. {simview-6.2.2 → simview-6.3.0}/simview/static/textures/points/ball1.png +0 -0
  95. {simview-6.2.2 → simview-6.3.0}/simview/utils.py +0 -0
  96. {simview-6.2.2 → simview-6.3.0}/simview.egg-info/dependency_links.txt +0 -0
  97. {simview-6.2.2 → simview-6.3.0}/simview.egg-info/entry_points.txt +0 -0
  98. {simview-6.2.2 → simview-6.3.0}/simview.egg-info/requires.txt +0 -0
  99. {simview-6.2.2 → simview-6.3.0}/simview.egg-info/top_level.txt +0 -0
  100. {simview-6.2.2 → simview-6.3.0}/tests/test_batch_selection.py +0 -0
  101. {simview-6.2.2 → simview-6.3.0}/tests/test_columnar_ondisk.py +0 -0
  102. {simview-6.2.2 → simview-6.3.0}/tests/test_contacts.py +0 -0
  103. {simview-6.2.2 → simview-6.3.0}/tests/test_embed_defaults.py +0 -0
  104. {simview-6.2.2 → simview-6.3.0}/tests/test_episodes.py +0 -0
  105. {simview-6.2.2 → simview-6.3.0}/tests/test_merge.py +0 -0
  106. {simview-6.2.2 → simview-6.3.0}/tests/test_pointcloud_features.py +0 -0
  107. {simview-6.2.2 → simview-6.3.0}/tests/test_remote.py +0 -0
  108. {simview-6.2.2 → simview-6.3.0}/tests/test_terrain.py +0 -0
  109. {simview-6.2.2 → simview-6.3.0}/tests/test_trajectory.py +0 -0
  110. {simview-6.2.2 → simview-6.3.0}/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.3.0
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.3.0"
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
@@ -295,9 +297,18 @@ def save_merged(
295
297
  logger.error("Error: --save-merged requires at least 2 input files to merge.")
296
298
  sys.exit(1)
297
299
 
300
+ from simview.columnar import columnarize_states, inline_blob
298
301
  from simview.merge import merge_simulation_files
299
302
 
300
303
  merged = merge_simulation_files(paths, selections)
304
+ # Pack the merged states the same way SimulationScene.save does: merge
305
+ # decodes every blob to plain lists, so dumping it directly would write
306
+ # the fat legacy per-frame layout. Falls back silently when numpy is
307
+ # unavailable or the states are too irregular (columnarize_states
308
+ # returns None).
309
+ columnar_states = columnarize_states(merged["states"], merged["model"], inline_blob)
310
+ if columnar_states is not None:
311
+ merged["states"] = columnar_states
301
312
  payload = json.dumps(merged).encode("utf-8")
302
313
  if out_path.suffix == ".gz":
303
314
  payload = gzip.compress(payload, compresslevel=1)
@@ -370,7 +381,12 @@ def _add_host_port_flags(sp: argparse.ArgumentParser) -> None:
370
381
  "--host",
371
382
  type=str,
372
383
  default="127.0.0.1",
373
- help="Host/interface for the server to bind to (default: 127.0.0.1).",
384
+ help=(
385
+ "Host/interface for the server to bind to (default: 127.0.0.1). "
386
+ "Note: the server has no authentication, so binding to a "
387
+ "non-loopback address exposes the scene data to everyone who can "
388
+ "reach it."
389
+ ),
374
390
  )
375
391
  sp.add_argument(
376
392
  "--port",
@@ -436,8 +452,8 @@ def build_parser() -> argparse.ArgumentParser:
436
452
  metavar="PATH",
437
453
  help=(
438
454
  "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."
455
+ "launching the viewer. Requires at least 2 input files, or 1 with a "
456
+ "'#<batches>' selection. Gzips the output if PATH ends in .gz."
441
457
  ),
442
458
  )
443
459
  _add_remote_flags(view)
@@ -654,9 +670,8 @@ def main():
654
670
  _configure_logging()
655
671
  argv = sys.argv[1:]
656
672
 
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.
673
+ # Checked before parsing so --version works alongside any other
674
+ # arguments, without tripping their validation.
660
675
  if "--version" in argv:
661
676
  print(_package_version())
662
677
  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
  ]
@@ -1,17 +1,16 @@
1
1
  """Structural summary of a scene JSON file, for `simview info` and for coding
2
2
  agents inspecting simview-generated data files.
3
3
 
4
- Deliberately dependency-free (stdlib only: json, gzip via
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
4
+ Deliberately dependency-free (stdlib only: json, gzip, base64) so it works on a base
5
+ install without the `authoring` extra (torch/numpy) -- see "Lazy imports" in
6
+ docs/dev/architecture.md. Does not import `simview.model`/`simview.server`:
7
+ the former pulls in torch at module scope, the latter fastapi/uvicorn --
8
+ both wrong layering for a lightweight inspection tool. Shares its blob-decoding/body-label helpers with
11
9
  `simview/terrain.py`/`simview/diff.py` via `simview.columnar`/`simview.utils`,
12
10
  which are stdlib-only on this read path.
13
11
  """
14
12
 
13
+ import gzip
15
14
  import json
16
15
  from pathlib import Path
17
16
  from typing import Any
@@ -24,7 +23,7 @@ from simview.columnar import (
24
23
  is_blob,
25
24
  is_columnar,
26
25
  )
27
- from simview.utils import body_label, cap, human_bytes, read_maybe_gzipped_bytes
26
+ from simview.utils import body_label, cap, human_bytes
28
27
 
29
28
  _MAX_DETAIL_ITEMS = 20
30
29
  _LARGE_FILE_BYTES = 100_000_000
@@ -219,9 +218,12 @@ def _summarize_states(states, model: dict | None, warnings: list[str]) -> dict:
219
218
 
220
219
  scalars_summary = {}
221
220
  for name in scalar_names:
222
- missing = [i for i, s in enumerate(states) if name not in s]
221
+ # One pass per scalar: collect missing-frame indices and check
222
+ # lengths in the same walk.
223
+ missing = []
223
224
  for i, s in enumerate(states):
224
225
  if name not in s:
226
+ missing.append(i)
225
227
  continue
226
228
  value = s[name]
227
229
  length = len(value) if isinstance(value, list) else 1
@@ -241,8 +243,8 @@ def _summarize_states(states, model: dict | None, warnings: list[str]) -> dict:
241
243
 
242
244
  # Per-body walk, mirroring columnar.py's columnarize_states structural
243
245
  # checks (field-set consistency, first-appearance-at-frame-0, no
244
- # dropping out) but never decoding/shaping blob values -- see module
245
- # docstring.
246
+ # dropping out) without numpy: blobs are decoded only to count their
247
+ # floats, never shaped into arrays.
246
248
  body_order: list = []
247
249
  body_first_frame: dict = {}
248
250
  body_name_value: dict = {}
@@ -396,7 +398,10 @@ def summarize_scene(path: str | Path) -> dict:
396
398
  path = Path(path)
397
399
  raw = path.read_bytes()
398
400
  gzipped = raw[:2] == b"\x1f\x8b"
399
- data = json.loads(read_maybe_gzipped_bytes(path))
401
+ # Decompress from the bytes already in hand rather than calling
402
+ # read_maybe_gzipped_bytes(path), which would read the whole file a
403
+ # second time -- scene files are often hundreds of MB.
404
+ data = json.loads(gzip.decompress(raw) if gzipped else raw)
400
405
  if not isinstance(data, dict):
401
406
  raise ValueError(
402
407
  "scene file must contain a JSON object with 'model'/'states' keys"
@@ -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
  )