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.
- {simview-4.0.0/simview.egg-info → simview-4.1.0}/PKG-INFO +2 -1
- {simview-4.0.0 → simview-4.1.0}/README.md +1 -0
- {simview-4.0.0 → simview-4.1.0}/pyproject.toml +1 -1
- {simview-4.0.0 → simview-4.1.0}/simview/__init__.py +3 -0
- {simview-4.0.0 → simview-4.1.0}/simview/__main__.py +84 -29
- simview-4.1.0/simview/columnar.py +380 -0
- {simview-4.0.0 → simview-4.1.0}/simview/diff.py +218 -36
- {simview-4.0.0 → simview-4.1.0}/simview/info.py +31 -10
- simview-4.1.0/simview/live.py +354 -0
- {simview-4.0.0 → simview-4.1.0}/simview/merge.py +202 -22
- {simview-4.0.0 → simview-4.1.0}/simview/model.py +75 -3
- simview-4.1.0/simview/remote.py +359 -0
- {simview-4.0.0 → simview-4.1.0}/simview/scene.py +105 -10
- {simview-4.0.0 → simview-4.1.0}/simview/server.py +176 -244
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/SimView.js +84 -1
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/AnimationController.js +3 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/BatchManager.js +80 -4
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/InteractionController.js +4 -273
- simview-4.1.0/simview/static/js/components/InteractionControls.js +63 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/StateStore.js +20 -3
- simview-4.1.0/simview/static/js/components/WindowedField.js +153 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/config.js +0 -16
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/Body.js +191 -64
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/StaticObject.js +22 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/Terrain.js +48 -7
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/utils.js +10 -2
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/Controls.js +19 -2
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/PlaybackControls.js +168 -1
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/ScalarPlotter.js +113 -1
- simview-4.1.0/simview/static/js/utils/batchVisibility.js +46 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/blobCodec.js +3 -1
- simview-4.1.0/simview/static/js/utils/blobWindow.js +107 -0
- simview-4.1.0/simview/static/js/utils/episodes.js +120 -0
- {simview-4.0.0 → simview-4.1.0}/simview/terrain.py +5 -1
- {simview-4.0.0 → simview-4.1.0/simview.egg-info}/PKG-INFO +2 -1
- {simview-4.0.0 → simview-4.1.0}/simview.egg-info/SOURCES.txt +9 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_cli.py +153 -0
- simview-4.1.0/tests/test_columnar_ondisk.py +380 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_columnar_states.py +1 -1
- {simview-4.0.0 → simview-4.1.0}/tests/test_diff.py +115 -0
- simview-4.1.0/tests/test_episodes.py +171 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_live.py +136 -5
- {simview-4.0.0 → simview-4.1.0}/tests/test_merge.py +246 -19
- {simview-4.0.0 → simview-4.1.0}/tests/test_pointcloud_features.py +4 -2
- simview-4.1.0/tests/test_remote.py +375 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_roundtrip.py +13 -8
- {simview-4.0.0 → simview-4.1.0}/tests/test_scene.py +22 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_show.py +28 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_terrain.py +7 -3
- simview-4.0.0/simview/live.py +0 -216
- simview-4.0.0/simview/static/js/components/InteractionControls.js +0 -116
- {simview-4.0.0 → simview-4.1.0}/LICENSE +0 -0
- {simview-4.0.0 → simview-4.1.0}/MANIFEST.in +0 -0
- {simview-4.0.0 → simview-4.1.0}/setup.cfg +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/launcher.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/py.typed +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/render.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/state.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/css/controls.css +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/components/Scene.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/main.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/colormap.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/objects/similarity.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/AnalysisPanel.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/BatchLegend.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/BodyStateWindow.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/ErrorMetrics.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/Legend.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/ui/TerrainProfile.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/batchPresets.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/bodyTransforms.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/csv.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/errorMath.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/injectStyles.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/interpolate.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/liveFollow.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/loadRecordingLibs.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/terrainSample.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/js/utils/viewState.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/index.min.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/Color.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/chroma.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/average.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/random.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/input.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/get.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/set.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/index.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/last.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/type.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/chroma-js-3.1.2/src/version.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/download.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/js-colormaps.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/tar.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/three.core.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/three-0.174.0/three.module.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/uPlot.esm.js +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/lib/uPlot.min.css +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/textures/contacts/red-cross0.png +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/textures/points/ball0.png +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/static/textures/points/ball1.png +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/templates/index.html +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview/utils.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview.egg-info/dependency_links.txt +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview.egg-info/entry_points.txt +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview.egg-info/requires.txt +0 -0
- {simview-4.0.0 → simview-4.1.0}/simview.egg-info/top_level.txt +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_contacts.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_info.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_launcher.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_server.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_terrain_query.py +0 -0
- {simview-4.0.0 → simview-4.1.0}/tests/test_trajectory.py +0 -0
- {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.
|
|
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
|
|
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 = [
|
|
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
|