simview 6.2.1__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.1/simview.egg-info → simview-6.2.3}/PKG-INFO +3 -3
  2. {simview-6.2.1 → simview-6.2.3}/README.md +2 -2
  3. {simview-6.2.1 → simview-6.2.3}/pyproject.toml +1 -1
  4. {simview-6.2.1 → simview-6.2.3}/simview/__main__.py +6 -5
  5. {simview-6.2.1 → simview-6.2.3}/simview/columnar.py +36 -20
  6. {simview-6.2.1 → simview-6.2.3}/simview/diff.py +10 -7
  7. {simview-6.2.1 → simview-6.2.3}/simview/info.py +6 -7
  8. {simview-6.2.1 → simview-6.2.3}/simview/launcher.py +14 -14
  9. {simview-6.2.1 → simview-6.2.3}/simview/live.py +3 -0
  10. {simview-6.2.1 → simview-6.2.3}/simview/merge.py +26 -24
  11. {simview-6.2.1 → simview-6.2.3}/simview/model.py +72 -35
  12. {simview-6.2.1 → simview-6.2.3}/simview/remote.py +1 -1
  13. {simview-6.2.1 → simview-6.2.3}/simview/render.py +6 -4
  14. {simview-6.2.1 → simview-6.2.3}/simview/scene.py +114 -112
  15. {simview-6.2.1 → simview-6.2.3}/simview/server.py +54 -60
  16. {simview-6.2.1 → simview-6.2.3}/simview/state.py +30 -24
  17. {simview-6.2.1 → simview-6.2.3}/simview/static/css/controls.css +84 -108
  18. {simview-6.2.1 → simview-6.2.3}/simview/static/js/SimView.js +57 -83
  19. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/AnimationController.js +8 -31
  20. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/BatchManager.js +6 -8
  21. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/InteractionController.js +3 -10
  22. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/Scene.js +6 -6
  23. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/StateStore.js +13 -27
  24. {simview-6.2.1 → simview-6.2.3}/simview/static/js/components/WindowedField.js +2 -0
  25. {simview-6.2.1 → simview-6.2.3}/simview/static/js/config.js +2 -5
  26. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/Body.js +15 -21
  27. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/StaticObject.js +2 -8
  28. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/Terrain.js +11 -20
  29. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/colormap.js +12 -18
  30. simview-6.2.3/simview/static/js/objects/similarity.js +25 -0
  31. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/utils.js +19 -60
  32. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/AnalysisPanel.js +0 -12
  33. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/BatchLegend.js +1 -3
  34. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/BodyStateWindow.js +41 -71
  35. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/Controls.js +65 -126
  36. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/ErrorMetrics.js +45 -61
  37. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/Legend.js +11 -12
  38. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/PlaybackControls.js +11 -10
  39. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/ScalarPlotter.js +18 -12
  40. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/TerrainProfile.js +7 -8
  41. {simview-6.2.1 → simview-6.2.3}/simview/static/js/ui/chartControls.js +25 -12
  42. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/batchVisibility.js +1 -0
  43. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/blobCodec.js +5 -5
  44. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/blobWindow.js +9 -2
  45. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/bodyTransforms.js +3 -4
  46. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/cameraRange.js +6 -3
  47. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/csv.js +3 -2
  48. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/episodes.js +3 -3
  49. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/errorMath.js +2 -5
  50. simview-6.2.3/simview/static/js/utils/html.js +5 -0
  51. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/keyboard.js +3 -2
  52. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/scalarSeries.js +2 -2
  53. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/terrainSample.js +9 -15
  54. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/uplot.js +4 -5
  55. {simview-6.2.1/simview/static/lib → simview-6.2.3/simview/static/js/vendor}/js-colormaps.js +28 -33
  56. {simview-6.2.1 → simview-6.2.3}/simview/templates/index.html +16 -12
  57. {simview-6.2.1 → simview-6.2.3}/simview/terrain.py +13 -13
  58. {simview-6.2.1 → simview-6.2.3}/simview/view.py +2 -1
  59. {simview-6.2.1 → simview-6.2.3/simview.egg-info}/PKG-INFO +3 -3
  60. {simview-6.2.1 → simview-6.2.3}/simview.egg-info/SOURCES.txt +4 -3
  61. {simview-6.2.1 → simview-6.2.3}/tests/test_cli.py +2 -2
  62. {simview-6.2.1 → simview-6.2.3}/tests/test_columnar_states.py +4 -3
  63. {simview-6.2.1 → simview-6.2.3}/tests/test_diff.py +1 -1
  64. {simview-6.2.1 → simview-6.2.3}/tests/test_info.py +1 -1
  65. {simview-6.2.1 → simview-6.2.3}/tests/test_launcher.py +1 -1
  66. {simview-6.2.1 → simview-6.2.3}/tests/test_roundtrip.py +1 -1
  67. {simview-6.2.1 → simview-6.2.3}/tests/test_scene.py +4 -4
  68. {simview-6.2.1 → simview-6.2.3}/tests/test_server.py +3 -3
  69. {simview-6.2.1 → simview-6.2.3}/tests/test_terrain_query.py +1 -1
  70. {simview-6.2.1 → simview-6.2.3}/tests/test_utils.py +1 -1
  71. simview-6.2.1/simview/static/js/objects/similarity.js +0 -38
  72. {simview-6.2.1 → simview-6.2.3}/LICENSE +0 -0
  73. {simview-6.2.1 → simview-6.2.3}/MANIFEST.in +0 -0
  74. {simview-6.2.1 → simview-6.2.3}/setup.cfg +0 -0
  75. {simview-6.2.1 → simview-6.2.3}/simview/__init__.py +0 -0
  76. {simview-6.2.1 → simview-6.2.3}/simview/py.typed +0 -0
  77. {simview-6.2.1 → simview-6.2.3}/simview/static/js/main.js +0 -0
  78. {simview-6.2.1 → simview-6.2.3}/simview/static/js/objects/Polyline.js +0 -0
  79. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/batchPresets.js +0 -0
  80. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/interpolate.js +0 -0
  81. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/liveFollow.js +0 -0
  82. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/viewState.js +0 -0
  83. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/viewerDefaults.js +0 -0
  84. {simview-6.2.1 → simview-6.2.3}/simview/static/js/utils/visibleRanges.js +0 -0
  85. {simview-6.2.1 → simview-6.2.3}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  86. {simview-6.2.1 → simview-6.2.3}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  87. {simview-6.2.1 → simview-6.2.3}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  88. {simview-6.2.1 → simview-6.2.3}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  89. {simview-6.2.1/simview/static/lib → simview-6.2.3/simview/static/lib/uPlot-1.6.32}/uPlot.esm.js +0 -0
  90. {simview-6.2.1/simview/static/lib → simview-6.2.3/simview/static/lib/uPlot-1.6.32}/uPlot.min.css +0 -0
  91. {simview-6.2.1 → simview-6.2.3}/simview/static/textures/contacts/red-cross0.png +0 -0
  92. {simview-6.2.1 → simview-6.2.3}/simview/static/textures/points/ball1.png +0 -0
  93. {simview-6.2.1 → simview-6.2.3}/simview/utils.py +0 -0
  94. {simview-6.2.1 → simview-6.2.3}/simview.egg-info/dependency_links.txt +0 -0
  95. {simview-6.2.1 → simview-6.2.3}/simview.egg-info/entry_points.txt +0 -0
  96. {simview-6.2.1 → simview-6.2.3}/simview.egg-info/requires.txt +0 -0
  97. {simview-6.2.1 → simview-6.2.3}/simview.egg-info/top_level.txt +0 -0
  98. {simview-6.2.1 → simview-6.2.3}/tests/test_batch_selection.py +0 -0
  99. {simview-6.2.1 → simview-6.2.3}/tests/test_columnar_ondisk.py +0 -0
  100. {simview-6.2.1 → simview-6.2.3}/tests/test_contacts.py +0 -0
  101. {simview-6.2.1 → simview-6.2.3}/tests/test_embed_defaults.py +0 -0
  102. {simview-6.2.1 → simview-6.2.3}/tests/test_episodes.py +0 -0
  103. {simview-6.2.1 → simview-6.2.3}/tests/test_live.py +0 -0
  104. {simview-6.2.1 → simview-6.2.3}/tests/test_merge.py +0 -0
  105. {simview-6.2.1 → simview-6.2.3}/tests/test_pointcloud_features.py +0 -0
  106. {simview-6.2.1 → simview-6.2.3}/tests/test_remote.py +0 -0
  107. {simview-6.2.1 → simview-6.2.3}/tests/test_show.py +0 -0
  108. {simview-6.2.1 → simview-6.2.3}/tests/test_terrain.py +0 -0
  109. {simview-6.2.1 → simview-6.2.3}/tests/test_trajectory.py +0 -0
  110. {simview-6.2.1 → 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.1
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.1"
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
 
@@ -12,10 +12,10 @@ same document; only how a blob is referenced differs:
12
12
  URL the browser fetches in parallel as a `Float32Array`.
13
13
 
14
14
  That symmetry is the whole point: a columnar file needs no repacking at all
15
- to be served, and `server.py`'s existing `extract_blobs` walker does the
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
@@ -122,6 +122,29 @@ def encode_floats(flat) -> str:
122
122
  return inline_blob(struct.pack(f"<{len(flat)}f", *flat))
123
123
 
124
124
 
125
+ def batch_rows(flat: list[float], per_row: int, batch_size: int) -> list | None:
126
+ """Split a decoded blob into `batch_size` rows of `per_row` floats: either
127
+ one row shared by every batch (replicated) or one row per batch. None if
128
+ `flat` is neither length -- callers raise their own error."""
129
+ if len(flat) == per_row:
130
+ return [flat] * batch_size
131
+ if len(flat) == batch_size * per_row:
132
+ return [flat[i : i + per_row] for i in range(0, len(flat), per_row)]
133
+ return None
134
+
135
+
136
+ def rewrite_blobs(obj: Any, register_blob) -> None:
137
+ """Replace, in place, every inline ``__b64__`` blob string nested anywhere
138
+ in `obj` with `register_blob(raw_bytes)`."""
139
+ if not isinstance(obj, (dict, list)):
140
+ return
141
+ for k, v in obj.items() if isinstance(obj, dict) else enumerate(obj):
142
+ if is_blob(v):
143
+ obj[k] = register_blob(blob_bytes(v))
144
+ else:
145
+ rewrite_blobs(v, register_blob)
146
+
147
+
125
148
  def decode_transform_row(value: Any, batch_size: int, batch_idx: int) -> list[float]:
126
149
  """Decode one state's `bodyTransform` field value for a single batch into
127
150
  a flat 7-element `[x, y, z, w, qx, qy, qz]` row, without numpy.
@@ -134,8 +157,6 @@ def decode_transform_row(value: Any, batch_size: int, batch_idx: int) -> list[fl
134
157
  if len(flat) == batch_size * width:
135
158
  start = batch_idx * width
136
159
  return flat[start : start + width]
137
- if len(flat) == width and batch_size == 1:
138
- return flat
139
160
  raise ValueError(
140
161
  f"bodyTransform has {len(flat)} floats; expected {width} "
141
162
  f"(batch_size=1, flat) or {batch_size * width} "
@@ -150,7 +171,8 @@ def _decode_state_field_rows(value, width: int, batch_size: int):
150
171
  Mirrors the shapes SimViewBodyState.to_json()/add_trajectory produce: a
151
172
  ``__b64__`` blob is always batch_size rows of `width` floats; a plain list
152
173
  is either already nested (one row per batch) or, for a single-batch scene,
153
- 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").
154
176
  """
155
177
  assert np is not None
156
178
  if isinstance(value, str):
@@ -200,7 +222,7 @@ def columnarize_states(states_data: list, model_data: dict | None, register_blob
200
222
  Strict by design: this trades a bit of coverage (an inconsistent scene just
201
223
  doesn't get the win) for never risking a subtly wrong repack. The one
202
224
  deliberate exception is `contacts`, which may legitimately come and go per
203
- 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.
204
226
  """
205
227
  if np is None or not states_data:
206
228
  return None
@@ -451,23 +473,17 @@ def write_static_bundle(data: dict, directory) -> None:
451
473
  (out / "blob").mkdir(parents=True, exist_ok=True)
452
474
  count = 0
453
475
 
454
- def externalize(obj):
476
+ def write_blob(raw: bytes) -> str:
455
477
  nonlocal count
456
- if not isinstance(obj, (dict, list)):
457
- return
458
- for k, v in obj.items() if isinstance(obj, dict) else enumerate(obj):
459
- if is_blob(v):
460
- (out / "blob" / str(count)).write_bytes(blob_bytes(v))
461
- obj[k] = f"/blob/{count}"
462
- count += 1
463
- else:
464
- externalize(v)
465
-
466
- externalize(data["model"])
478
+ (out / "blob" / str(count)).write_bytes(raw)
479
+ count += 1
480
+ return f"/blob/{count - 1}"
481
+
482
+ rewrite_blobs(data["model"], write_blob)
467
483
  # The viewer only resolves /blob/ URLs in the model and a columnar states
468
484
  # object; a legacy per-frame array's inline __b64__ fields are decoded
469
485
  # in place, so they must stay inline.
470
486
  if is_columnar(data["states"]):
471
- externalize(data["states"])
487
+ rewrite_blobs(data["states"], write_blob)
472
488
  for name in ("model", "states"):
473
489
  (out / f"{name}.json").write_text(json.dumps(data[name]))
@@ -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:
@@ -27,6 +27,7 @@ import orjson
27
27
 
28
28
  from .columnar import (
29
29
  STATE_FIELD_WIDTHS,
30
+ batch_rows,
30
31
  blob_floats,
31
32
  encode_floats,
32
33
  expand_columnar_states,
@@ -212,12 +213,8 @@ def _decode_per_batch(
212
213
  return value
213
214
  flat = blob_floats(value)
214
215
  per_row = resolution * (vector_width or 1)
215
- if len(flat) == per_row:
216
- # One shared row (deduplicated singleton terrain).
217
- chunks = [list(flat)] * batch_size
218
- elif len(flat) == batch_size * per_row:
219
- chunks = [list(flat[i : i + per_row]) for i in range(0, len(flat), per_row)]
220
- else:
216
+ chunks = batch_rows(flat, per_row, batch_size)
217
+ if chunks is None:
221
218
  raise ValueError(
222
219
  f"terrain blob has {len(flat)} floats; expected {per_row} (one "
223
220
  f"shared row) or {batch_size * per_row} ({batch_size} batches "
@@ -316,11 +313,14 @@ def _validate_doc(doc: dict, label: str) -> None:
316
313
  def _expand_batched(
317
314
  values: list | str, is_singleton: bool, batch_size: int, field: str, label: str
318
315
  ) -> list | str:
319
- # b64 blobs pass through untouched: _decode_per_batch resolves their row
320
- # layout by length (one shared row for a deduplicated singleton terrain,
321
- # or batch_size rows for per-batch / legacy broadcast-singleton data) and
322
- # replicates the shared case itself. Only plain lists (where the row count
323
- # 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
+ """
324
324
  if is_blob(values):
325
325
  return values
326
326
 
@@ -636,8 +636,8 @@ def _merge_terrain(
636
636
  )
637
637
 
638
638
  def _bound(name: str, key: str, pick) -> float | None:
639
- # A null bound means "unknown" (json-format.md), and one unknown
640
- # makes the merged range unknown too.
639
+ # A null bound means "unknown", and one unknown makes the merged
640
+ # range unknown too.
641
641
  values = [t["properties"][name].get(key) for t in terrains]
642
642
  return None if any(v is None for v in values) else pick(values)
643
643
 
@@ -679,15 +679,6 @@ def _nearest_index(sorted_times: list[float], t: float) -> int:
679
679
  return i - 1 if (t - before) <= (after - t) else i
680
680
 
681
681
 
682
- def _state_body_lookup(
683
- states: list[dict], file_idx: int, state_idx: int, cache: dict
684
- ) -> dict[str, dict]:
685
- key = (file_idx, state_idx)
686
- if key not in cache:
687
- cache[key] = bodies_by_name(states[state_idx].get("bodies"))
688
- return cache[key]
689
-
690
-
691
682
  def _merge_states(
692
683
  states_list: list[list[dict]],
693
684
  batch_sizes: list[int],
@@ -696,6 +687,13 @@ def _merge_states(
696
687
  labels: list[str],
697
688
  selections: list[list[int] | None],
698
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
+ """
699
697
  ref_times = [s["time"] for s in states_list[0]]
700
698
  times_by_file = [[s["time"] for s in states] for states in states_list]
701
699
  total_batches = sum(_output_sizes(batch_sizes, selections))
@@ -731,7 +729,11 @@ def _merge_states(
731
729
  zip(states_list, batch_sizes, selections)
732
730
  ):
733
731
  state_idx = state_idx_by_file[file_idx]
734
- lookup = _state_body_lookup(states, file_idx, state_idx, lookup_cache)
732
+ lookup = lookup_cache.get((file_idx, state_idx))
733
+ if lookup is None:
734
+ lookup = lookup_cache[(file_idx, state_idx)] = bodies_by_name(
735
+ states[state_idx].get("bodies")
736
+ )
735
737
  body_state = lookup.get(name)
736
738
  if body_state is None:
737
739
  raise ValueError(
@@ -934,7 +936,7 @@ def merge_simulation_files(
934
936
  if dropped:
935
937
  logger.warning(
936
938
  "Dropping episode boundaries from %s: they index their own "
937
- "timeline, and the merged scene uses '%s'’s timeline.",
939
+ "timeline, and the merged scene uses '%s's timeline.",
938
940
  ", ".join(f"'{label}'" for label in dropped),
939
941
  labels[0],
940
942
  )