simview 4.0.0__tar.gz → 4.1.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 (206) hide show
  1. {simview-4.0.0/simview.egg-info → simview-4.1.0}/PKG-INFO +2 -1
  2. {simview-4.0.0 → simview-4.1.0}/README.md +1 -0
  3. {simview-4.0.0 → simview-4.1.0}/pyproject.toml +1 -1
  4. {simview-4.0.0 → simview-4.1.0}/simview/__init__.py +3 -0
  5. {simview-4.0.0 → simview-4.1.0}/simview/__main__.py +84 -29
  6. simview-4.1.0/simview/columnar.py +380 -0
  7. {simview-4.0.0 → simview-4.1.0}/simview/diff.py +218 -36
  8. {simview-4.0.0 → simview-4.1.0}/simview/info.py +31 -10
  9. simview-4.1.0/simview/live.py +354 -0
  10. {simview-4.0.0 → simview-4.1.0}/simview/merge.py +202 -22
  11. {simview-4.0.0 → simview-4.1.0}/simview/model.py +75 -3
  12. simview-4.1.0/simview/remote.py +359 -0
  13. {simview-4.0.0 → simview-4.1.0}/simview/scene.py +105 -10
  14. {simview-4.0.0 → simview-4.1.0}/simview/server.py +176 -244
  15. {simview-4.0.0 → simview-4.1.0}/simview/static/js/SimView.js +84 -1
  16. {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/AnimationController.js +3 -0
  17. {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/BatchManager.js +80 -4
  18. {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/InteractionController.js +4 -273
  19. simview-4.1.0/simview/static/js/components/InteractionControls.js +63 -0
  20. {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/StateStore.js +20 -3
  21. simview-4.1.0/simview/static/js/components/WindowedField.js +153 -0
  22. {simview-4.0.0 → simview-4.1.0}/simview/static/js/config.js +0 -16
  23. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/Body.js +191 -64
  24. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/StaticObject.js +22 -0
  25. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/Terrain.js +48 -7
  26. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/utils.js +10 -2
  27. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/Controls.js +19 -2
  28. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/PlaybackControls.js +168 -1
  29. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/ScalarPlotter.js +113 -1
  30. simview-4.1.0/simview/static/js/utils/batchVisibility.js +46 -0
  31. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/blobCodec.js +3 -1
  32. simview-4.1.0/simview/static/js/utils/blobWindow.js +107 -0
  33. simview-4.1.0/simview/static/js/utils/episodes.js +120 -0
  34. {simview-4.0.0 → simview-4.1.0}/simview/terrain.py +5 -1
  35. {simview-4.0.0 → simview-4.1.0/simview.egg-info}/PKG-INFO +2 -1
  36. {simview-4.0.0 → simview-4.1.0}/simview.egg-info/SOURCES.txt +9 -0
  37. {simview-4.0.0 → simview-4.1.0}/tests/test_cli.py +153 -0
  38. simview-4.1.0/tests/test_columnar_ondisk.py +380 -0
  39. {simview-4.0.0 → simview-4.1.0}/tests/test_columnar_states.py +1 -1
  40. {simview-4.0.0 → simview-4.1.0}/tests/test_diff.py +115 -0
  41. simview-4.1.0/tests/test_episodes.py +171 -0
  42. {simview-4.0.0 → simview-4.1.0}/tests/test_live.py +136 -5
  43. {simview-4.0.0 → simview-4.1.0}/tests/test_merge.py +246 -19
  44. {simview-4.0.0 → simview-4.1.0}/tests/test_pointcloud_features.py +4 -2
  45. simview-4.1.0/tests/test_remote.py +375 -0
  46. {simview-4.0.0 → simview-4.1.0}/tests/test_roundtrip.py +13 -8
  47. {simview-4.0.0 → simview-4.1.0}/tests/test_scene.py +22 -0
  48. {simview-4.0.0 → simview-4.1.0}/tests/test_show.py +28 -0
  49. {simview-4.0.0 → simview-4.1.0}/tests/test_terrain.py +7 -3
  50. simview-4.0.0/simview/live.py +0 -216
  51. simview-4.0.0/simview/static/js/components/InteractionControls.js +0 -116
  52. {simview-4.0.0 → simview-4.1.0}/LICENSE +0 -0
  53. {simview-4.0.0 → simview-4.1.0}/MANIFEST.in +0 -0
  54. {simview-4.0.0 → simview-4.1.0}/setup.cfg +0 -0
  55. {simview-4.0.0 → simview-4.1.0}/simview/launcher.py +0 -0
  56. {simview-4.0.0 → simview-4.1.0}/simview/py.typed +0 -0
  57. {simview-4.0.0 → simview-4.1.0}/simview/render.py +0 -0
  58. {simview-4.0.0 → simview-4.1.0}/simview/state.py +0 -0
  59. {simview-4.0.0 → simview-4.1.0}/simview/static/css/controls.css +0 -0
  60. {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/Scene.js +0 -0
  61. {simview-4.0.0 → simview-4.1.0}/simview/static/js/main.js +0 -0
  62. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/colormap.js +0 -0
  63. {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/similarity.js +0 -0
  64. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/AnalysisPanel.js +0 -0
  65. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/BatchLegend.js +0 -0
  66. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/BodyStateWindow.js +0 -0
  67. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/ErrorMetrics.js +0 -0
  68. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/Legend.js +0 -0
  69. {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/TerrainProfile.js +0 -0
  70. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/batchPresets.js +0 -0
  71. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/bodyTransforms.js +0 -0
  72. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/csv.js +0 -0
  73. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/errorMath.js +0 -0
  74. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/injectStyles.js +0 -0
  75. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/interpolate.js +0 -0
  76. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/liveFollow.js +0 -0
  77. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/loadRecordingLibs.js +0 -0
  78. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/terrainSample.js +0 -0
  79. {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/viewState.js +0 -0
  80. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/index.min.js +0 -0
  81. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/Color.js +0 -0
  82. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/chroma.js +0 -0
  83. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +0 -0
  84. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +0 -0
  85. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/average.js +0 -0
  86. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +0 -0
  87. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +0 -0
  88. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +0 -0
  89. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +0 -0
  90. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/random.js +0 -0
  91. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +0 -0
  92. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +0 -0
  93. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +0 -0
  94. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +0 -0
  95. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +0 -0
  96. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +0 -0
  97. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +0 -0
  98. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +0 -0
  99. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +0 -0
  100. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +0 -0
  101. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +0 -0
  102. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +0 -0
  103. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +0 -0
  104. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +0 -0
  105. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +0 -0
  106. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +0 -0
  107. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +0 -0
  108. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +0 -0
  109. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +0 -0
  110. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +0 -0
  111. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +0 -0
  112. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +0 -0
  113. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +0 -0
  114. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +0 -0
  115. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +0 -0
  116. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +0 -0
  117. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +0 -0
  118. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +0 -0
  119. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +0 -0
  120. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +0 -0
  121. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +0 -0
  122. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +0 -0
  123. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +0 -0
  124. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +0 -0
  125. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +0 -0
  126. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +0 -0
  127. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +0 -0
  128. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +0 -0
  129. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +0 -0
  130. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +0 -0
  131. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +0 -0
  132. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/input.js +0 -0
  133. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +0 -0
  134. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +0 -0
  135. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +0 -0
  136. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +0 -0
  137. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +0 -0
  138. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +0 -0
  139. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +0 -0
  140. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +0 -0
  141. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +0 -0
  142. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +0 -0
  143. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +0 -0
  144. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +0 -0
  145. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +0 -0
  146. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +0 -0
  147. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +0 -0
  148. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +0 -0
  149. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +0 -0
  150. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +0 -0
  151. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +0 -0
  152. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +0 -0
  153. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +0 -0
  154. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +0 -0
  155. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +0 -0
  156. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +0 -0
  157. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +0 -0
  158. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +0 -0
  159. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +0 -0
  160. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/get.js +0 -0
  161. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +0 -0
  162. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +0 -0
  163. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +0 -0
  164. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +0 -0
  165. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/set.js +0 -0
  166. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +0 -0
  167. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +0 -0
  168. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +0 -0
  169. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +0 -0
  170. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +0 -0
  171. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +0 -0
  172. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +0 -0
  173. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/index.js +0 -0
  174. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/last.js +0 -0
  175. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +0 -0
  176. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +0 -0
  177. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +0 -0
  178. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/type.js +0 -0
  179. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +0 -0
  180. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +0 -0
  181. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/version.js +0 -0
  182. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/download.js +0 -0
  183. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/js-colormaps.js +0 -0
  184. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/tar.js +0 -0
  185. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
  186. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
  187. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/three.core.js +0 -0
  188. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/three.module.js +0 -0
  189. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/uPlot.esm.js +0 -0
  190. {simview-4.0.0 → simview-4.1.0}/simview/static/lib/uPlot.min.css +0 -0
  191. {simview-4.0.0 → simview-4.1.0}/simview/static/textures/contacts/red-cross0.png +0 -0
  192. {simview-4.0.0 → simview-4.1.0}/simview/static/textures/points/ball0.png +0 -0
  193. {simview-4.0.0 → simview-4.1.0}/simview/static/textures/points/ball1.png +0 -0
  194. {simview-4.0.0 → simview-4.1.0}/simview/templates/index.html +0 -0
  195. {simview-4.0.0 → simview-4.1.0}/simview/utils.py +0 -0
  196. {simview-4.0.0 → simview-4.1.0}/simview.egg-info/dependency_links.txt +0 -0
  197. {simview-4.0.0 → simview-4.1.0}/simview.egg-info/entry_points.txt +0 -0
  198. {simview-4.0.0 → simview-4.1.0}/simview.egg-info/requires.txt +0 -0
  199. {simview-4.0.0 → simview-4.1.0}/simview.egg-info/top_level.txt +0 -0
  200. {simview-4.0.0 → simview-4.1.0}/tests/test_contacts.py +0 -0
  201. {simview-4.0.0 → simview-4.1.0}/tests/test_info.py +0 -0
  202. {simview-4.0.0 → simview-4.1.0}/tests/test_launcher.py +0 -0
  203. {simview-4.0.0 → simview-4.1.0}/tests/test_server.py +0 -0
  204. {simview-4.0.0 → simview-4.1.0}/tests/test_terrain_query.py +0 -0
  205. {simview-4.0.0 → simview-4.1.0}/tests/test_trajectory.py +0 -0
  206. {simview-4.0.0 → simview-4.1.0}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simview
3
- Version: 4.0.0
3
+ Version: 4.1.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
@@ -62,6 +62,7 @@ Want to try it without installing anything? There's a
62
62
  - **Interactive UI**: Web-based controls for playback, camera, and data inspection.
63
63
  - **Python API**: Easy-to-use API for generating scenes and launching the visualizer directly from your code.
64
64
  - **JSON Support**: Load and save simulation data using a portable JSON format.
65
+ - **Remote Files**: Open a scene straight off a compute host with `simview host:path/to/scene.json` — fetched over SSH, compressed on the wire, and cached locally.
65
66
 
66
67
  ---
67
68
 
@@ -29,6 +29,7 @@ Want to try it without installing anything? There's a
29
29
  - **Interactive UI**: Web-based controls for playback, camera, and data inspection.
30
30
  - **Python API**: Easy-to-use API for generating scenes and launching the visualizer directly from your code.
31
31
  - **JSON Support**: Load and save simulation data using a portable JSON format.
32
+ - **Remote Files**: Open a scene straight off a compute host with `simview host:path/to/scene.json` — fetched over SSH, compressed on the wire, and cached locally.
32
33
 
33
34
  ---
34
35
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simview"
7
- version = "4.0.0"
7
+ version = "4.1.0"
8
8
  readme = "README.md"
9
9
  requires-python = ">=3.12"
10
10
  license = "BSD-3-Clause"
@@ -29,6 +29,7 @@ _LAZY_EXPORTS = {
29
29
  "SimViewStaticObject": "simview.model",
30
30
  "SimViewTerrain": "simview.model",
31
31
  "SimViewModel": "simview.model",
32
+ "SimViewEpisode": "simview.model",
32
33
  "BodyShapeType": "simview.model",
33
34
  "OptionalBodyStateAttribute": "simview.model",
34
35
  "SimViewBodyState": "simview.state",
@@ -42,6 +43,7 @@ if TYPE_CHECKING:
42
43
  BodyShapeType,
43
44
  OptionalBodyStateAttribute,
44
45
  SimViewBody,
46
+ SimViewEpisode,
45
47
  SimViewModel,
46
48
  SimViewStaticObject,
47
49
  SimViewTerrain,
@@ -58,6 +60,7 @@ __all__ = [
58
60
  "SimViewStaticObject",
59
61
  "SimViewTerrain",
60
62
  "SimViewModel",
63
+ "SimViewEpisode",
61
64
  "BodyShapeType",
62
65
  "OptionalBodyStateAttribute",
63
66
  "SimViewBodyState",
@@ -7,7 +7,7 @@ import sys
7
7
  import tempfile
8
8
  from pathlib import Path
9
9
 
10
- from simview import CACHE_DIR, __version__
10
+ from simview import CACHE_DIR, __version__, remote
11
11
  from simview.server import SimViewServer
12
12
 
13
13
  logger = logging.getLogger("simview.cli")
@@ -43,12 +43,33 @@ def _package_version() -> str:
43
43
  return __version__
44
44
 
45
45
 
46
+ def _dir_size(path: Path) -> int:
47
+ total = 0
48
+ for entry in path.rglob("*"):
49
+ try:
50
+ if entry.is_file():
51
+ total += entry.stat().st_size
52
+ except OSError:
53
+ pass
54
+ return total
55
+
56
+
46
57
  def clear_cache():
47
- # Legacy cache directories (kept for cleanup of older installs).
48
- for cache_dir in (Path("/tmp") / CACHE_DIR, Path.home() / ".cache" / CACHE_DIR):
49
- if cache_dir.exists():
50
- logger.info("Removing %s", cache_dir)
51
- shutil.rmtree(cache_dir, ignore_errors=True)
58
+ """Remove simview's on-disk cache: scene files fetched from remote hosts
59
+ (see `simview.remote`), plus locations left behind by older installs."""
60
+ freed = 0
61
+ seen: set[Path] = set()
62
+ for cache_dir in (
63
+ remote.cache_dir(),
64
+ Path("/tmp") / CACHE_DIR,
65
+ Path.home() / ".cache" / CACHE_DIR,
66
+ ):
67
+ if cache_dir in seen or not cache_dir.is_dir():
68
+ continue
69
+ seen.add(cache_dir)
70
+ freed += _dir_size(cache_dir)
71
+ logger.info("Removing %s", cache_dir)
72
+ shutil.rmtree(cache_dir, ignore_errors=True)
52
73
 
53
74
  # Temp scenes written by SimViewLauncher (tempfile.mkstemp with this prefix);
54
75
  # these leak if a launched viewer is killed before cleanup runs.
@@ -61,6 +82,8 @@ def clear_cache():
61
82
  logger.warning("Could not remove %s: %s", leftover, e)
62
83
  if removed:
63
84
  logger.info("Removed %d leftover temporary scene file(s).", removed)
85
+ if freed:
86
+ logger.info("Freed %s.", remote.human_bytes(freed))
64
87
 
65
88
  logger.info("Cache cleared.")
66
89
 
@@ -294,6 +317,21 @@ def save_merged(paths: list[Path], out_path: Path) -> None:
294
317
  logger.info("Merged scene written to %s", out_path)
295
318
 
296
319
 
320
+ def _resolve_input(spec: str, args: argparse.Namespace) -> Path:
321
+ """Resolve one CLI input to a local file, first fetching it over SSH if it
322
+ is an scp-style 'host:path' spec (see `simview.remote`). Everything
323
+ downstream only ever sees an ordinary local path."""
324
+ try:
325
+ path = remote.resolve_input(spec, refresh=args.refresh, offline=args.offline)
326
+ except remote.RemoteError as e:
327
+ logger.error("Error: %s", e)
328
+ sys.exit(1)
329
+ if not (path.exists() and path.is_file()):
330
+ logger.error("Error: File '%s' not found or is not a file.", path)
331
+ sys.exit(1)
332
+ return path
333
+
334
+
297
335
  def build_parser() -> argparse.ArgumentParser:
298
336
  parser = argparse.ArgumentParser(description="SimView CLI")
299
337
  parser.add_argument(
@@ -308,7 +346,10 @@ def build_parser() -> argparse.ArgumentParser:
308
346
  "a PNG screenshot (needs the 'render' extra). Multiple "
309
347
  "visualize-mode files are merged into one scene, each file's "
310
348
  "batches appended as extra batches (e.g. a real-world recording "
311
- "plus a simulated rerun)."
349
+ "plus a simulated rerun). Any input may instead be an scp-style "
350
+ "'host:path' spec (e.g. 'rci:~/results/scene.json'), which is "
351
+ "fetched over ssh -- compressed on the wire -- into a local cache "
352
+ "and re-fetched only when the remote file changes."
312
353
  ),
313
354
  )
314
355
  parser.add_argument(
@@ -420,7 +461,9 @@ def build_parser() -> argparse.ArgumentParser:
420
461
  "With 'simview diff <file>', restrict the trajectory diff to one "
421
462
  "body (matched by its full label, e.g. 'wheel_fl+wheel_fr', or by "
422
463
  "any single name inside a rigidly-grouped body). Default: diff "
423
- "every body present in states."
464
+ "every body present in states, plus any rigidly-attached body "
465
+ "resolvable from them. Poses are compared in world space (parent "
466
+ "chains resolved), matching the viewer's Error Metrics panel."
424
467
  ),
425
468
  )
426
469
  parser.add_argument(
@@ -488,6 +531,22 @@ def build_parser() -> argparse.ArgumentParser:
488
531
  "the next free port is used instead."
489
532
  ),
490
533
  )
534
+ parser.add_argument(
535
+ "--refresh",
536
+ action="store_true",
537
+ help=(
538
+ "Re-fetch remote 'host:path' inputs even if the cached copy still "
539
+ "matches the remote file."
540
+ ),
541
+ )
542
+ parser.add_argument(
543
+ "--offline",
544
+ action="store_true",
545
+ help=(
546
+ "Use the cached copy of remote 'host:path' inputs without contacting "
547
+ "the host at all; fails if nothing is cached for them yet."
548
+ ),
549
+ )
491
550
  parser.add_argument(
492
551
  "--no-browser",
493
552
  action="store_true",
@@ -551,6 +610,18 @@ def main():
551
610
  parser.print_help()
552
611
  sys.exit(1)
553
612
 
613
+ # Remote specs are an input-side convenience only; writing back over ssh is
614
+ # deliberately out of scope, so catch it here rather than at open() time.
615
+ for flag, value in (("--output", args.output), ("--save-merged", args.save_merged)):
616
+ if value is not None and remote.is_remote_spec(value):
617
+ logger.error(
618
+ "Error: %s must be a local path, but '%s' looks like a remote "
619
+ "'host:path' spec; remote specs are only supported for inputs.",
620
+ flag,
621
+ value,
622
+ )
623
+ sys.exit(1)
624
+
554
625
  if args.inputs and args.inputs[0] == "info":
555
626
  info_args = args.inputs[1:]
556
627
  if len(info_args) != 1:
@@ -559,10 +630,7 @@ def main():
559
630
  "'simview info scene.json'."
560
631
  )
561
632
  sys.exit(1)
562
- info_path = Path(info_args[0])
563
- if not (info_path.exists() and info_path.is_file()):
564
- logger.error("Error: File '%s' not found or is not a file.", info_path)
565
- sys.exit(1)
633
+ info_path = _resolve_input(info_args[0], args)
566
634
  run_info(info_path, as_json=args.json)
567
635
  return
568
636
 
@@ -574,10 +642,7 @@ def main():
574
642
  "e.g. 'simview terrain scene.json --point 0 0'."
575
643
  )
576
644
  sys.exit(1)
577
- terrain_path = Path(terrain_args[0])
578
- if not (terrain_path.exists() and terrain_path.is_file()):
579
- logger.error("Error: File '%s' not found or is not a file.", terrain_path)
580
- sys.exit(1)
645
+ terrain_path = _resolve_input(terrain_args[0], args)
581
646
  run_terrain(terrain_path, args)
582
647
  return
583
648
 
@@ -589,10 +654,7 @@ def main():
589
654
  "e.g. 'simview diff scene.json --batches 0 1'."
590
655
  )
591
656
  sys.exit(1)
592
- diff_path = Path(diff_args[0])
593
- if not (diff_path.exists() and diff_path.is_file()):
594
- logger.error("Error: File '%s' not found or is not a file.", diff_path)
595
- sys.exit(1)
657
+ diff_path = _resolve_input(diff_args[0], args)
596
658
  run_diff(diff_path, args)
597
659
  return
598
660
 
@@ -604,10 +666,7 @@ def main():
604
666
  "e.g. 'simview render scene.json --output frame.png'."
605
667
  )
606
668
  sys.exit(1)
607
- render_path = Path(render_args[0])
608
- if not (render_path.exists() and render_path.is_file()):
609
- logger.error("Error: File '%s' not found or is not a file.", render_path)
610
- sys.exit(1)
669
+ render_path = _resolve_input(render_args[0], args)
611
670
  run_render(render_path, args)
612
671
  return
613
672
 
@@ -615,11 +674,7 @@ def main():
615
674
  clear_cache()
616
675
  return
617
676
 
618
- paths = [Path(p) for p in args.inputs]
619
- for path in paths:
620
- if not (path.exists() and path.is_file()):
621
- logger.error("Error: File '%s' not found or is not a file.", path)
622
- sys.exit(1)
677
+ paths = [_resolve_input(spec, args) for spec in args.inputs]
623
678
 
624
679
  if args.save_merged:
625
680
  save_merged(paths, Path(args.save_merged))
@@ -0,0 +1,380 @@
1
+ """The columnar ("v4") states layout: one binary blob per body per field
2
+ covering a whole trajectory, instead of thousands of small per-frame objects.
3
+
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
6
+ **on-disk** format (`SimulationScene.save(columnar=True)`). The two are the
7
+ same document; only how a blob is referenced differs:
8
+
9
+ - on disk, a blob is an inline ``__b64__``-prefixed base64 string, exactly
10
+ like the per-frame binary fields the legacy layout already used;
11
+ - over HTTP, the server rewrites each of those into a ``/blob/{token}/{id}``
12
+ URL the browser fetches in parallel as a `Float32Array`.
13
+
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
16
+ rewriting for free.
17
+
18
+ Shape (see README.md "JSON Format Specification" for the authoritative spec)::
19
+
20
+ {"version": 4,
21
+ "times": [t0, t1, ...], # length T
22
+ "bodies": [{"name": str | [str, ...],
23
+ "fields": {"bodyTransform": <blob>, ...}, # each (T, B, k) float32
24
+ "contacts": [...]}], # optional, length T
25
+ "scalars": {"energy": <blob>, ...}} # each (T, B) float32
26
+
27
+ `columnarize_states` (numpy, writer side) and `expand_columnar_states`
28
+ (stdlib only, reader side) are inverses. Expansion is deliberately
29
+ dependency-free so the base install -- and the stdlib-only CLI tools
30
+ (`info.py`/`diff.py`/`terrain.py`) and `merge.py` -- can read columnar files
31
+ without numpy.
32
+ """
33
+
34
+ import base64
35
+ import logging
36
+ import struct
37
+ from typing import Any
38
+
39
+ try:
40
+ import numpy as np
41
+ except ImportError:
42
+ np = None
43
+
44
+ logger = logging.getLogger("simview.columnar")
45
+
46
+ COLUMNAR_VERSION = 4
47
+ BLOB_PREFIX = "__b64__"
48
+
49
+ # Per-body numeric state fields eligible for columnar (whole-trajectory)
50
+ # packing, with their trailing per-batch-row width. Same fields/widths
51
+ # SimViewBodyState/add_trajectory may binary-encode per frame (state.py,
52
+ # blobCodec.js); the columnar layout just packs a whole (T, B, k) run instead
53
+ # of one (B, k) blob per frame.
54
+ STATE_FIELD_WIDTHS = {
55
+ "bodyTransform": 7,
56
+ "velocity": 3,
57
+ "angularVelocity": 3,
58
+ "force": 3,
59
+ "torque": 3,
60
+ }
61
+
62
+
63
+ class StatesShapeMismatch(Exception):
64
+ """Raised internally by `columnarize_states` to bail out to the legacy
65
+ per-frame layout -- caught in one place rather than threading a bunch of
66
+ `if inconsistent: return None` checks through the nested loops below."""
67
+
68
+
69
+ def is_columnar(states_data: Any) -> bool:
70
+ """True if `states_data` is a columnar states document rather than the
71
+ legacy per-frame array."""
72
+ return (
73
+ isinstance(states_data, dict) and states_data.get("version") == COLUMNAR_VERSION
74
+ )
75
+
76
+
77
+ def inline_blob(raw: bytes) -> str:
78
+ """`register_blob` implementation for on-disk output: keep the bytes in the
79
+ document itself as a ``__b64__`` string (what the server instead rewrites
80
+ into a URL)."""
81
+ return BLOB_PREFIX + base64.b64encode(raw).decode()
82
+
83
+
84
+ def _blob_floats(value: Any) -> list[float]:
85
+ """Decode one ``__b64__`` blob (or pass through an already-plain nested
86
+ list, flattened) into a flat list of floats, without numpy."""
87
+ if isinstance(value, str):
88
+ if not value.startswith(BLOB_PREFIX):
89
+ raise ValueError(f"expected a {BLOB_PREFIX} blob, got {value[:32]!r}")
90
+ raw = base64.b64decode(value[len(BLOB_PREFIX) :])
91
+ return list(struct.unpack(f"<{len(raw) // 4}f", raw))
92
+
93
+ flat: list[float] = []
94
+
95
+ def _flatten(x: Any) -> None:
96
+ if isinstance(x, list):
97
+ for item in x:
98
+ _flatten(item)
99
+ else:
100
+ flat.append(float(x))
101
+
102
+ _flatten(value)
103
+ return flat
104
+
105
+
106
+ def _decode_state_field_rows(value, width: int, batch_size: int):
107
+ """Decode one state's per-body field value (either a ``__b64__`` blob or a
108
+ plain nested/flat JSON list) into a (batch_size, width) float32 array.
109
+
110
+ Mirrors the shapes SimViewBodyState.to_json()/add_trajectory produce: a
111
+ ``__b64__`` blob is always batch_size rows of `width` floats; a plain list
112
+ is either already nested (one row per batch) or, for a single-batch scene,
113
+ a flat list of `width` floats (see README "Authoring whole trajectories").
114
+ """
115
+ assert np is not None
116
+ if isinstance(value, str):
117
+ if not value.startswith(BLOB_PREFIX):
118
+ raise StatesShapeMismatch(f"unexpected string value for field: {value!r}")
119
+ flat = np.frombuffer(base64.b64decode(value[len(BLOB_PREFIX) :]), dtype="<f4")
120
+ else:
121
+ arr = np.asarray(value, dtype="<f4")
122
+ if arr.ndim == 1:
123
+ if batch_size != 1:
124
+ raise StatesShapeMismatch(
125
+ "flat (non-nested) field value with batch size != 1"
126
+ )
127
+ arr = arr[None, :]
128
+ flat = arr.reshape(-1)
129
+ if flat.size != batch_size * width:
130
+ raise StatesShapeMismatch(
131
+ f"field has {flat.size} floats; expected {batch_size * width} "
132
+ f"({batch_size} batches x {width})"
133
+ )
134
+ return flat.reshape(batch_size, width)
135
+
136
+
137
+ def body_key(name):
138
+ """Hashable key for a body's `name` (a string, or a list of grouped names
139
+ for bodies moving rigidly together -- see BodyTrajectory/SimViewBodyState).
140
+ The original `name` value (str or list) is what actually gets emitted in
141
+ the columnar payload; this is only used to identify "the same body slot"
142
+ across frames."""
143
+ return tuple(name) if isinstance(name, list) else name
144
+
145
+
146
+ def columnarize_states(states_data: list, model_data: dict | None, register_blob):
147
+ """Repack a legacy per-frame `states` array into the columnar document, or
148
+ return None if `states_data` doesn't meet the strict consistency
149
+ requirements (in which case the caller keeps the legacy layout).
150
+
151
+ `register_blob(bytes) -> str` decides how a whole-trajectory float32 blob
152
+ is referenced: `inline_blob` for on-disk output, or the server's
153
+ `/blob/{token}/{id}` registrar when serving over HTTP.
154
+
155
+ Strict by design: this trades a bit of coverage (an inconsistent scene just
156
+ doesn't get the win) for never risking a subtly wrong repack. The one
157
+ deliberate exception is `contacts`, which may legitimately come and go per
158
+ frame (see README) without disqualifying the rest of the scene.
159
+ """
160
+ if np is None or not states_data:
161
+ return None
162
+ if model_data is None:
163
+ return None
164
+
165
+ batch_size = int(model_data.get("simBatches", 1))
166
+
167
+ try:
168
+ times = []
169
+ # Per body: ordered list of field names (first frame's order/set is
170
+ # the contract every other frame must match), plus the accumulated
171
+ # (T, B, k) rows for each field, and the original name value to emit.
172
+ body_order: list = []
173
+ body_fields: dict[object, list[str]] = {}
174
+ body_name_value: dict[object, object] = {}
175
+ body_rows: dict[object, dict[str, list]] = {}
176
+ body_contacts: dict[object, list] = {}
177
+ any_contacts: set = set()
178
+
179
+ for state_idx, state in enumerate(states_data):
180
+ if "time" not in state:
181
+ raise StatesShapeMismatch(f"state {state_idx} is missing 'time'")
182
+ times.append(state["time"])
183
+
184
+ bodies = state.get("bodies") or []
185
+ seen_keys = set()
186
+ for body in bodies:
187
+ if not isinstance(body, dict) or "name" not in body:
188
+ raise StatesShapeMismatch(
189
+ f"state {state_idx} has a body entry missing 'name'"
190
+ )
191
+ name = body["name"]
192
+ if not isinstance(name, (str, list)):
193
+ raise StatesShapeMismatch(
194
+ f"state {state_idx} has a non-string/list body name"
195
+ )
196
+ key = body_key(name)
197
+ if key in seen_keys:
198
+ raise StatesShapeMismatch(
199
+ f"state {state_idx} lists body '{name}' more than once"
200
+ )
201
+ seen_keys.add(key)
202
+
203
+ fields = sorted(k for k in body if k in STATE_FIELD_WIDTHS)
204
+ if key not in body_fields:
205
+ if state_idx != 0 and body_rows.get(key) is None:
206
+ # A body appearing for the first time after frame 0
207
+ # would leave earlier frames' rows undefined -- bail
208
+ # rather than guess a fill value.
209
+ raise StatesShapeMismatch(
210
+ f"body '{name}' first appears at state {state_idx}, "
211
+ "not state 0"
212
+ )
213
+ body_order.append(key)
214
+ body_fields[key] = fields
215
+ body_name_value[key] = name
216
+ body_rows[key] = {f: [] for f in fields}
217
+ elif body_fields[key] != fields:
218
+ raise StatesShapeMismatch(
219
+ f"body '{name}' has inconsistent field set across frames"
220
+ )
221
+
222
+ for field in fields:
223
+ width = STATE_FIELD_WIDTHS[field]
224
+ rows = _decode_state_field_rows(body[field], width, batch_size)
225
+ body_rows[key][field].append(rows)
226
+
227
+ if "contacts" in body:
228
+ any_contacts.add(key)
229
+ body_contacts.setdefault(key, [None] * state_idx).append(
230
+ body["contacts"]
231
+ )
232
+ elif key in any_contacts:
233
+ body_contacts[key].append(None)
234
+
235
+ missing = set(body_order) - seen_keys
236
+ if missing:
237
+ raise StatesShapeMismatch(
238
+ f"state {state_idx} is missing bodies present in earlier "
239
+ f"frames: {sorted(str(m) for m in missing)}"
240
+ )
241
+ # A body with contacts not yet seen this frame (declared later than
242
+ # its own first appearance) still needs a None placeholder so its
243
+ # contacts list stays length == number of frames seen so far.
244
+ for key in any_contacts:
245
+ lst = body_contacts[key]
246
+ if len(lst) < state_idx + 1:
247
+ lst.append(None)
248
+
249
+ scalar_names = model_data.get("scalarNames") or []
250
+ for name in scalar_names:
251
+ if name not in state:
252
+ raise StatesShapeMismatch(
253
+ f"state {state_idx} is missing scalar '{name}'"
254
+ )
255
+
256
+ T = len(states_data)
257
+
258
+ bodies_payload = []
259
+ for key in body_order:
260
+ fields_payload = {}
261
+ for field, per_frame_rows in body_rows[key].items():
262
+ if len(per_frame_rows) != T:
263
+ raise StatesShapeMismatch(
264
+ f"body '{body_name_value[key]}' field '{field}' is "
265
+ "missing from some frames"
266
+ )
267
+ stacked = np.ascontiguousarray(
268
+ np.stack(per_frame_rows, axis=0), dtype="<f4"
269
+ ) # (T, B, k)
270
+ fields_payload[field] = register_blob(stacked.tobytes())
271
+ entry = {"name": body_name_value[key], "fields": fields_payload}
272
+ if key in any_contacts:
273
+ entry["contacts"] = body_contacts[key]
274
+ bodies_payload.append(entry)
275
+
276
+ scalars_payload = {}
277
+ for name in model_data.get("scalarNames") or []:
278
+ per_frame = []
279
+ for state in states_data:
280
+ row = np.asarray(state[name], dtype="<f4")
281
+ if row.ndim == 0:
282
+ row = row.reshape(1)
283
+ if row.shape != (batch_size,):
284
+ raise StatesShapeMismatch(
285
+ f"scalar '{name}' has shape {row.shape}; expected "
286
+ f"({batch_size},)"
287
+ )
288
+ per_frame.append(row)
289
+ stacked = np.ascontiguousarray(np.stack(per_frame, axis=0), dtype="<f4")
290
+ scalars_payload[name] = register_blob(stacked.tobytes())
291
+
292
+ return {
293
+ "version": COLUMNAR_VERSION,
294
+ "times": times,
295
+ "bodies": bodies_payload,
296
+ "scalars": scalars_payload,
297
+ }
298
+ except StatesShapeMismatch as e:
299
+ logger.warning(
300
+ "States data is not columnar-repackable, keeping the legacy "
301
+ "per-frame layout: %s",
302
+ e,
303
+ )
304
+ return None
305
+
306
+
307
+ def expand_columnar_states(states_doc: Any, batch_size: int) -> list[dict]:
308
+ """Inverse of `columnarize_states`: rebuild the legacy per-frame array.
309
+
310
+ Stdlib only (no numpy), so every reader -- `SimulationScene.load`,
311
+ `merge`, and the stdlib-only CLI tools -- can consume a columnar file.
312
+ Per-body numeric fields come back out as per-frame ``__b64__`` blobs
313
+ (byte-identical floats to what went in), so re-columnarizing a loaded
314
+ scene reproduces the same blobs.
315
+
316
+ Raises ValueError on a malformed/truncated document rather than emitting
317
+ silently wrong frames.
318
+ """
319
+ if not is_columnar(states_doc):
320
+ raise ValueError("not a columnar states document")
321
+
322
+ times = states_doc.get("times")
323
+ if not isinstance(times, list):
324
+ raise ValueError("columnar states document has no 'times' list")
325
+ T = len(times)
326
+ B = max(1, int(batch_size))
327
+
328
+ # Decode each blob once, then slice per frame.
329
+ bodies_in = states_doc.get("bodies") or []
330
+ decoded_bodies = []
331
+ for body in bodies_in:
332
+ name = body.get("name")
333
+ if name is None:
334
+ raise ValueError("columnar body entry is missing 'name'")
335
+ fields = {}
336
+ for field, blob in (body.get("fields") or {}).items():
337
+ width = STATE_FIELD_WIDTHS.get(field)
338
+ if width is None:
339
+ raise ValueError(f"unknown columnar field '{field}' for body '{name}'")
340
+ flat = _blob_floats(blob)
341
+ expected = T * B * width
342
+ if len(flat) != expected:
343
+ raise ValueError(
344
+ f"body '{name}' field '{field}' has {len(flat)} floats; "
345
+ f"expected {expected} (T={T} x B={B} x {width})"
346
+ )
347
+ fields[field] = (flat, width)
348
+ decoded_bodies.append((name, fields, body.get("contacts")))
349
+
350
+ decoded_scalars = {}
351
+ for name, blob in (states_doc.get("scalars") or {}).items():
352
+ flat = _blob_floats(blob)
353
+ if len(flat) != T * B:
354
+ raise ValueError(
355
+ f"scalar '{name}' has {len(flat)} floats; expected {T * B} "
356
+ f"(T={T} x B={B})"
357
+ )
358
+ decoded_scalars[name] = flat
359
+
360
+ states: list[dict] = []
361
+ for t in range(T):
362
+ body_states = []
363
+ for name, fields, contacts in decoded_bodies:
364
+ entry: dict = {"name": name}
365
+ for field, (flat, width) in fields.items():
366
+ start = t * B * width
367
+ chunk = flat[start : start + B * width]
368
+ entry[field] = inline_blob(struct.pack(f"<{len(chunk)}f", *chunk))
369
+ if contacts is not None:
370
+ value = contacts[t] if t < len(contacts) else None
371
+ if value is not None:
372
+ entry["contacts"] = value
373
+ body_states.append(entry)
374
+
375
+ state: dict = {"time": times[t], "bodies": body_states}
376
+ for name, flat in decoded_scalars.items():
377
+ state[name] = flat[t * B : (t + 1) * B]
378
+ states.append(state)
379
+
380
+ return states