simview 4.1.0__tar.gz → 4.2.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.1.0/simview.egg-info → simview-4.2.0}/PKG-INFO +2 -1
- {simview-4.1.0 → simview-4.2.0}/README.md +1 -0
- {simview-4.1.0 → simview-4.2.0}/pyproject.toml +1 -1
- {simview-4.1.0 → simview-4.2.0}/simview/__main__.py +43 -16
- {simview-4.1.0 → simview-4.2.0}/simview/merge.py +275 -51
- {simview-4.1.0 → simview-4.2.0}/simview/model.py +61 -2
- {simview-4.1.0 → simview-4.2.0}/simview/scene.py +9 -0
- {simview-4.1.0 → simview-4.2.0}/simview/server.py +11 -2
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/Controls.js +8 -3
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/ErrorMetrics.js +48 -20
- {simview-4.1.0 → simview-4.2.0/simview.egg-info}/PKG-INFO +2 -1
- {simview-4.1.0 → simview-4.2.0}/simview.egg-info/SOURCES.txt +1 -0
- simview-4.2.0/tests/test_batch_selection.py +73 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_cli.py +103 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_merge.py +230 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_roundtrip.py +62 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_server.py +17 -0
- {simview-4.1.0 → simview-4.2.0}/LICENSE +0 -0
- {simview-4.1.0 → simview-4.2.0}/MANIFEST.in +0 -0
- {simview-4.1.0 → simview-4.2.0}/setup.cfg +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/__init__.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/columnar.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/diff.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/info.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/launcher.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/live.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/py.typed +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/remote.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/render.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/state.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/css/controls.css +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/SimView.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/AnimationController.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/BatchManager.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/InteractionController.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/InteractionControls.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/Scene.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/StateStore.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/components/WindowedField.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/config.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/main.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/Body.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/StaticObject.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/Terrain.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/colormap.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/similarity.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/objects/utils.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/AnalysisPanel.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/BatchLegend.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/BodyStateWindow.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/Legend.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/PlaybackControls.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/ScalarPlotter.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/ui/TerrainProfile.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/batchPresets.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/batchVisibility.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/blobCodec.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/blobWindow.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/bodyTransforms.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/csv.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/episodes.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/errorMath.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/injectStyles.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/interpolate.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/liveFollow.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/loadRecordingLibs.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/terrainSample.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/js/utils/viewState.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/index.min.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/Color.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/chroma.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/colors/colorbrewer.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/colors/w3cx11.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/average.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/bezier.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/blend.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/cubehelix.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/mix.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/random.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/generator/scale.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/_hsx.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hcg.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsi.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsl.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/hsv.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lab.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lch.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/lrgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/num.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklab.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/oklch.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/interpolator/rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/cmyk2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/cmyk/rgb2cmyk.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/css2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/hsl2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lab2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/lch2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklab2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/oklch2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/css/rgb2css.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/gl/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/hcg2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hcg/rgb2hcg.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/hex2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hex/rgb2hex.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/hsi2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsi/rgb2hsi.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/hsl2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsl/rgb2hsl.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/hsv2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/hsv/rgb2hsv.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/input.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab-constants.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/lab2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lab/rgb2lab.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/hcl2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lab2lch.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2lab.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/lch2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/lch/rgb2lch.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/named/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/num2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/num/rgb2num.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/oklab2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklab/rgb2oklab.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/oklch2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/oklch/rgb2oklch.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/rgb/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/rgb2temperature.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/io/temp/temperature2rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/alpha.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/clipped.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/darken.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/get.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/luminance.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/mix.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/premultiply.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/saturate.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/set.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/ops/shade.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/analyze.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/clip_rgb.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrast.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/contrastAPCA.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/delta-e.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/distance.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/index.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/last.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/limit.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/multiply-matrices.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/scales.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/type.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/unpack.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/utils/valid.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/chroma-js-3.1.2/src/version.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/download.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/js-colormaps.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/tar.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/three-0.174.0/addons/controls/OrbitControls.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/three-0.174.0/addons/libs/lil-gui.module.min.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/three-0.174.0/three.core.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/three-0.174.0/three.module.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/uPlot.esm.js +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/lib/uPlot.min.css +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/textures/contacts/red-cross0.png +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/textures/points/ball0.png +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/static/textures/points/ball1.png +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/templates/index.html +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/terrain.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview/utils.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview.egg-info/dependency_links.txt +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview.egg-info/entry_points.txt +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview.egg-info/requires.txt +0 -0
- {simview-4.1.0 → simview-4.2.0}/simview.egg-info/top_level.txt +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_columnar_ondisk.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_columnar_states.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_contacts.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_diff.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_episodes.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_info.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_launcher.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_live.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_pointcloud_features.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_remote.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_scene.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_show.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_terrain.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_terrain_query.py +0 -0
- {simview-4.1.0 → simview-4.2.0}/tests/test_trajectory.py +0 -0
- {simview-4.1.0 → simview-4.2.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.2.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
|
|
@@ -63,6 +63,7 @@ Want to try it without installing anything? There's a
|
|
|
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
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.
|
|
66
|
+
- **Comparing Runs**: Merge several scene files into one, taking whole files or just picked batches — `simview gt.json method_a.json#1 method_b.json#1`.
|
|
66
67
|
|
|
67
68
|
---
|
|
68
69
|
|
|
@@ -30,6 +30,7 @@ Want to try it without installing anything? There's a
|
|
|
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
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.
|
|
33
|
+
- **Comparing Runs**: Merge several scene files into one, taking whole files or just picked batches — `simview gt.json method_a.json#1 method_b.json#1`.
|
|
33
34
|
|
|
34
35
|
---
|
|
35
36
|
|
|
@@ -300,16 +300,22 @@ def run_render(path: Path, args: argparse.Namespace) -> None:
|
|
|
300
300
|
sys.exit(1)
|
|
301
301
|
|
|
302
302
|
|
|
303
|
-
def save_merged(
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
303
|
+
def save_merged(
|
|
304
|
+
paths: list[Path],
|
|
305
|
+
out_path: Path,
|
|
306
|
+
selections: list[str | None] | None = None,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Merge `paths` (must be >= 2, or 1 with a batch selection) and write the
|
|
309
|
+
result to `out_path`, gzipped if it ends in .gz, without starting the
|
|
310
|
+
server."""
|
|
311
|
+
selected = selections is not None and any(s is not None for s in selections)
|
|
312
|
+
if len(paths) < 2 and not selected:
|
|
307
313
|
logger.error("Error: --save-merged requires at least 2 input files to merge.")
|
|
308
314
|
sys.exit(1)
|
|
309
315
|
|
|
310
316
|
from simview.merge import merge_simulation_files
|
|
311
317
|
|
|
312
|
-
merged = merge_simulation_files(paths)
|
|
318
|
+
merged = merge_simulation_files(paths, selections)
|
|
313
319
|
payload = json.dumps(merged).encode("utf-8")
|
|
314
320
|
if out_path.suffix == ".gz":
|
|
315
321
|
payload = gzip.compress(payload, compresslevel=1)
|
|
@@ -346,7 +352,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
346
352
|
"a PNG screenshot (needs the 'render' extra). Multiple "
|
|
347
353
|
"visualize-mode files are merged into one scene, each file's "
|
|
348
354
|
"batches appended as extra batches (e.g. a real-world recording "
|
|
349
|
-
"plus a simulated rerun).
|
|
355
|
+
"plus a simulated rerun). Append '#<batches>' to a file to merge "
|
|
356
|
+
"only some of its batches (e.g. 'run.json#1', 'run.json#0,2-3', "
|
|
357
|
+
"'run.json#-1' or 'run.json#<batch name>'), so a ground truth "
|
|
358
|
+
"shared by several files isn't merged once per file. Any input "
|
|
359
|
+
"may instead be an scp-style "
|
|
350
360
|
"'host:path' spec (e.g. 'rci:~/results/scene.json'), which is "
|
|
351
361
|
"fetched over ssh -- compressed on the wire -- into a local cache "
|
|
352
362
|
"and re-fetched only when the remote file changes."
|
|
@@ -674,18 +684,35 @@ def main():
|
|
|
674
684
|
clear_cache()
|
|
675
685
|
return
|
|
676
686
|
|
|
677
|
-
|
|
687
|
+
# Split "scene.json#1,3" into the file and the batches to take from it
|
|
688
|
+
# before resolving the file, so a remote spec can carry a selection too.
|
|
689
|
+
from simview.merge import split_batch_spec
|
|
678
690
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
691
|
+
try:
|
|
692
|
+
split_inputs = [split_batch_spec(spec) for spec in args.inputs]
|
|
693
|
+
except ValueError as e:
|
|
694
|
+
logger.error("Error: %s", e)
|
|
695
|
+
sys.exit(1)
|
|
696
|
+
paths = [_resolve_input(spec, args) for spec, _ in split_inputs]
|
|
697
|
+
selections: list[str | None] = [selector for _, selector in split_inputs]
|
|
682
698
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
699
|
+
try:
|
|
700
|
+
if args.save_merged:
|
|
701
|
+
save_merged(paths, Path(args.save_merged), selections)
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
SimViewServer.start(
|
|
705
|
+
sim_path=paths if len(paths) > 1 else paths[0],
|
|
706
|
+
host=args.host,
|
|
707
|
+
preferred_port=args.port,
|
|
708
|
+
open_browser=not args.no_browser,
|
|
709
|
+
batch_selections=selections,
|
|
710
|
+
)
|
|
711
|
+
except ValueError as e:
|
|
712
|
+
# A bad batch selection ("scene.json#9" on a 2-batch file) is user
|
|
713
|
+
# error, not a crash: report it like any other bad argument.
|
|
714
|
+
logger.error("Error: %s", e)
|
|
715
|
+
sys.exit(1)
|
|
689
716
|
|
|
690
717
|
|
|
691
718
|
if __name__ == "__main__":
|
|
@@ -10,12 +10,18 @@ Files are not required to share a timeline: the first file's timestamps become
|
|
|
10
10
|
the merged timeline, and every other file is resampled onto it by nearest
|
|
11
11
|
timestamp (zero-order hold, no interpolation). Put the recording you care most
|
|
12
12
|
about matching frame-for-frame first.
|
|
13
|
+
|
|
14
|
+
Each file may contribute a *subset* of its own batches (see
|
|
15
|
+
`parse_batch_selection`), so comparing several methods that each shipped their
|
|
16
|
+
own copy of a shared ground truth doesn't mean merging that ground truth once
|
|
17
|
+
per file.
|
|
13
18
|
"""
|
|
14
19
|
|
|
15
20
|
import base64
|
|
16
21
|
import bisect
|
|
17
22
|
import json
|
|
18
23
|
import logging
|
|
24
|
+
import re
|
|
19
25
|
import struct
|
|
20
26
|
from pathlib import Path
|
|
21
27
|
from typing import Sequence
|
|
@@ -43,6 +49,137 @@ _STATE_FIELD_WIDTHS = {
|
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
|
|
52
|
+
# Separates a file from the batches to take out of it in a CLI input spec
|
|
53
|
+
# ("scene.json#1,3"). Deliberately not ':', which already means "remote host"
|
|
54
|
+
# (see simview.remote).
|
|
55
|
+
_BATCH_SPEC_SEP = "#"
|
|
56
|
+
_INDEX_RE = re.compile(r"^-?\d+$")
|
|
57
|
+
_RANGE_RE = re.compile(r"^(\d+)-(\d+)$")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def split_batch_spec(spec: str) -> tuple[str, str | None]:
|
|
61
|
+
"""Split a CLI input like ``scene.json#1,3`` into ``("scene.json", "1,3")``,
|
|
62
|
+
or return ``(spec, None)`` when it names no batch subset.
|
|
63
|
+
|
|
64
|
+
Purely syntactic apart from one filesystem check: an existing local file
|
|
65
|
+
wins over the selector reading (mirroring `remote.is_remote_spec`), so a
|
|
66
|
+
file literally named ``odd#name.json`` still opens as itself.
|
|
67
|
+
"""
|
|
68
|
+
if _BATCH_SPEC_SEP not in spec:
|
|
69
|
+
return spec, None
|
|
70
|
+
try:
|
|
71
|
+
if Path(spec).exists():
|
|
72
|
+
return spec, None
|
|
73
|
+
except OSError:
|
|
74
|
+
pass
|
|
75
|
+
head, _, selector = spec.rpartition(_BATCH_SPEC_SEP)
|
|
76
|
+
if not head or not selector:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Malformed batch selection '{spec}'; expected "
|
|
79
|
+
f"'file{_BATCH_SPEC_SEP}<batches>', e.g. 'scene.json{_BATCH_SPEC_SEP}1' "
|
|
80
|
+
f"or 'scene.json{_BATCH_SPEC_SEP}0,2-3'."
|
|
81
|
+
)
|
|
82
|
+
return head, selector
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_batch_selection(
|
|
86
|
+
selector: str | Sequence[int],
|
|
87
|
+
batch_size: int,
|
|
88
|
+
batch_names: Sequence[str] | None,
|
|
89
|
+
label: str,
|
|
90
|
+
) -> list[int]:
|
|
91
|
+
"""Resolve one file's batch selector to a list of that file's batch indices.
|
|
92
|
+
|
|
93
|
+
`selector` is either an explicit sequence of indices or a comma-separated
|
|
94
|
+
string whose entries are an index (``2``), a negative index counting from
|
|
95
|
+
the end (``-1``), an inclusive range (``0-3``), or one of the file's own
|
|
96
|
+
`batchNames`. Anything that parses as an index or a range is read as such,
|
|
97
|
+
so a batch *named* "2" can only be selected by its index. The selected
|
|
98
|
+
order is the merged order, and selecting the same batch twice is rejected
|
|
99
|
+
as a typo rather than silently duplicating it.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(selector, str):
|
|
102
|
+
raw: list[int] = []
|
|
103
|
+
for token in selector.split(","):
|
|
104
|
+
token = token.strip()
|
|
105
|
+
if not token:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"'{label}': empty entry in batch selection '{selector}'."
|
|
108
|
+
)
|
|
109
|
+
if _INDEX_RE.match(token):
|
|
110
|
+
raw.append(int(token))
|
|
111
|
+
continue
|
|
112
|
+
span = _RANGE_RE.match(token)
|
|
113
|
+
if span:
|
|
114
|
+
start, end = int(span.group(1)), int(span.group(2))
|
|
115
|
+
if end < start:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"'{label}': batch range '{token}' ends before it starts."
|
|
118
|
+
)
|
|
119
|
+
raw.extend(range(start, end + 1))
|
|
120
|
+
continue
|
|
121
|
+
names = list(batch_names or [])
|
|
122
|
+
if token in names:
|
|
123
|
+
if names.count(token) > 1:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"'{label}': '{token}' names {names.count(token)} of its "
|
|
126
|
+
"batches; select one of them by index instead."
|
|
127
|
+
)
|
|
128
|
+
raw.append(names.index(token))
|
|
129
|
+
continue
|
|
130
|
+
known = (
|
|
131
|
+
f"one of its batch names ({', '.join(names)})"
|
|
132
|
+
if names
|
|
133
|
+
else "a batch name (this file declares no 'batchNames')"
|
|
134
|
+
)
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"'{label}': '{token}' is not a batch index, an 'a-b' range, or "
|
|
137
|
+
f"{known}."
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
raw = [int(i) for i in selector]
|
|
141
|
+
|
|
142
|
+
resolved: list[int] = []
|
|
143
|
+
for index in raw:
|
|
144
|
+
# Negative indices count from the end, as in Python slicing, so '-1'
|
|
145
|
+
# means "the last batch" without knowing the file's batch count.
|
|
146
|
+
actual = index + batch_size if index < 0 else index
|
|
147
|
+
if not 0 <= actual < batch_size:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"'{label}': batch {index} is out of range; the file has "
|
|
150
|
+
f"{batch_size} batch(es) (valid indices 0-{batch_size - 1})."
|
|
151
|
+
)
|
|
152
|
+
resolved.append(actual)
|
|
153
|
+
if not resolved:
|
|
154
|
+
raise ValueError(f"'{label}': batch selection is empty.")
|
|
155
|
+
duplicates = sorted({i for i in resolved if resolved.count(i) > 1})
|
|
156
|
+
if duplicates:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"'{label}': batch(es) {', '.join(str(d) for d in duplicates)} "
|
|
159
|
+
"selected more than once."
|
|
160
|
+
)
|
|
161
|
+
return resolved
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _select(rows: Sequence, selection: list[int] | None) -> list:
|
|
165
|
+
"""Pick `selection`'s rows out of an already fully expanded per-batch list
|
|
166
|
+
(one row per batch of the source file). A None selection keeps every row,
|
|
167
|
+
which is what every field does when no subset was asked for."""
|
|
168
|
+
if selection is None:
|
|
169
|
+
return list(rows)
|
|
170
|
+
return [rows[i] for i in selection]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _output_sizes(
|
|
174
|
+
batch_sizes: list[int], selections: list[list[int] | None]
|
|
175
|
+
) -> list[int]:
|
|
176
|
+
"""How many batches each file contributes to the merged scene."""
|
|
177
|
+
return [
|
|
178
|
+
size if selection is None else len(selection)
|
|
179
|
+
for size, selection in zip(batch_sizes, selections)
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
|
|
46
183
|
def _decode_b64_floats(value) -> tuple[float, ...] | None:
|
|
47
184
|
"""Decode a binary ``__b64__`` blob to a flat tuple of floats, or return
|
|
48
185
|
None if `value` isn't such a blob (e.g. it's an already-batched plain
|
|
@@ -240,17 +377,23 @@ def _merge_bodies(models: list[dict], labels: list[str]) -> list[dict]:
|
|
|
240
377
|
return bodies
|
|
241
378
|
|
|
242
379
|
|
|
243
|
-
def _default_batch_names(
|
|
380
|
+
def _default_batch_names(
|
|
381
|
+
paths: Sequence[Path], batch_sizes: list[int], selections: list[list[int] | None]
|
|
382
|
+
) -> list[str]:
|
|
244
383
|
"""One name per output batch, derived from the source file it came from.
|
|
245
384
|
Single-batch files just use the file stem; multi-batch files get an index
|
|
246
|
-
suffix so batches from the same file are still distinguishable.
|
|
385
|
+
suffix so batches from the same file are still distinguishable. The suffix
|
|
386
|
+
is the batch's index *in its source file*, so a selected subset stays
|
|
387
|
+
traceable back to it (batch 2 of a 4-batch 'run.json' is 'run[2]', not
|
|
388
|
+
'run[0]')."""
|
|
247
389
|
names = []
|
|
248
|
-
for path, batch_size in zip(paths, batch_sizes):
|
|
390
|
+
for path, batch_size, selection in zip(paths, batch_sizes, selections):
|
|
249
391
|
stem = path.stem
|
|
250
392
|
if batch_size == 1:
|
|
251
393
|
names.append(stem)
|
|
252
394
|
else:
|
|
253
|
-
|
|
395
|
+
indices = range(batch_size) if selection is None else selection
|
|
396
|
+
names.extend(f"{stem}[{j}]" for j in indices)
|
|
254
397
|
return names
|
|
255
398
|
|
|
256
399
|
|
|
@@ -268,7 +411,10 @@ def _merge_scalar_names(models: list[dict], labels: list[str]) -> list[str]:
|
|
|
268
411
|
|
|
269
412
|
|
|
270
413
|
def _merge_static_objects(
|
|
271
|
-
models: list[dict],
|
|
414
|
+
models: list[dict],
|
|
415
|
+
batch_sizes: list[int],
|
|
416
|
+
labels: list[str],
|
|
417
|
+
selections: list[list[int] | None],
|
|
272
418
|
) -> list[dict]:
|
|
273
419
|
first = models[0].get("staticObjects") or []
|
|
274
420
|
names = [s["name"] for s in first]
|
|
@@ -294,14 +440,19 @@ def _merge_static_objects(
|
|
|
294
440
|
entry["shape"] = shape
|
|
295
441
|
else:
|
|
296
442
|
shapes = []
|
|
297
|
-
for model, batch_size, label in zip(
|
|
443
|
+
for model, batch_size, label, selection in zip(
|
|
444
|
+
models, batch_sizes, labels, selections
|
|
445
|
+
):
|
|
298
446
|
shapes.extend(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
447
|
+
_select(
|
|
448
|
+
_expand_batched(
|
|
449
|
+
model["staticObjects"][idx]["shapes"],
|
|
450
|
+
False,
|
|
451
|
+
batch_size,
|
|
452
|
+
"shapes",
|
|
453
|
+
label,
|
|
454
|
+
),
|
|
455
|
+
selection,
|
|
305
456
|
)
|
|
306
457
|
)
|
|
307
458
|
entry["shapes"] = shapes
|
|
@@ -310,7 +461,11 @@ def _merge_static_objects(
|
|
|
310
461
|
|
|
311
462
|
|
|
312
463
|
def _merge_embedding(
|
|
313
|
-
models: list[dict],
|
|
464
|
+
models: list[dict],
|
|
465
|
+
batch_sizes: list[int],
|
|
466
|
+
labels: list[str],
|
|
467
|
+
resolution: int,
|
|
468
|
+
selections: list[list[int] | None],
|
|
314
469
|
) -> str | None:
|
|
315
470
|
"""Concatenate the per-batch terrain `embeddingData` of every file into a
|
|
316
471
|
single ``__b64__`` blob (the encoding the viewer's Terrain.js splits per
|
|
@@ -336,7 +491,9 @@ def _merge_embedding(
|
|
|
336
491
|
|
|
337
492
|
per_file_rows: list[list[list[float]]] = []
|
|
338
493
|
widths: list[int] = []
|
|
339
|
-
for model, value, batch_size, label in zip(
|
|
494
|
+
for model, value, batch_size, label, selection in zip(
|
|
495
|
+
models, values, batch_sizes, labels, selections
|
|
496
|
+
):
|
|
340
497
|
flat = _decode_b64_floats(value)
|
|
341
498
|
if flat is None:
|
|
342
499
|
# Real producers always blob-encode embeddings; a plain list is
|
|
@@ -374,6 +531,7 @@ def _merge_embedding(
|
|
|
374
531
|
rows = [
|
|
375
532
|
list(flat[i : i + per_batch]) for i in range(0, len(flat), per_batch)
|
|
376
533
|
]
|
|
534
|
+
rows = _select(rows, selection)
|
|
377
535
|
per_file_rows.append(rows)
|
|
378
536
|
widths.append(len(rows[0]) // resolution)
|
|
379
537
|
|
|
@@ -393,7 +551,10 @@ def _merge_embedding(
|
|
|
393
551
|
|
|
394
552
|
|
|
395
553
|
def _merge_terrain(
|
|
396
|
-
models: list[dict],
|
|
554
|
+
models: list[dict],
|
|
555
|
+
batch_sizes: list[int],
|
|
556
|
+
labels: list[str],
|
|
557
|
+
selections: list[list[int] | None],
|
|
397
558
|
) -> dict:
|
|
398
559
|
first_terrain = models[0]["terrain"]
|
|
399
560
|
dims = first_terrain["dimensions"]
|
|
@@ -453,9 +614,12 @@ def _merge_terrain(
|
|
|
453
614
|
if not items:
|
|
454
615
|
return None
|
|
455
616
|
merged = []
|
|
456
|
-
for value, batch_size in items:
|
|
617
|
+
for value, batch_size, selection in items:
|
|
457
618
|
merged.extend(
|
|
458
|
-
|
|
619
|
+
_select(
|
|
620
|
+
_decode_per_batch(value, batch_size, resolution, vector_width),
|
|
621
|
+
selection,
|
|
622
|
+
)
|
|
459
623
|
)
|
|
460
624
|
return merged
|
|
461
625
|
|
|
@@ -465,7 +629,9 @@ def _merge_terrain(
|
|
|
465
629
|
property_min_max: dict[str, tuple[float | None, float | None]] = {
|
|
466
630
|
name: (None, None) for name in kept_properties
|
|
467
631
|
}
|
|
468
|
-
for model, batch_size, label in zip(
|
|
632
|
+
for model, batch_size, label, selection in zip(
|
|
633
|
+
models, batch_sizes, labels, selections
|
|
634
|
+
):
|
|
469
635
|
terrain = model["terrain"]
|
|
470
636
|
singleton = terrain.get("isSingleton", False)
|
|
471
637
|
height_data.append(
|
|
@@ -474,6 +640,7 @@ def _merge_terrain(
|
|
|
474
640
|
terrain["heightData"], singleton, batch_size, "heightData", label
|
|
475
641
|
),
|
|
476
642
|
batch_size,
|
|
643
|
+
selection,
|
|
477
644
|
)
|
|
478
645
|
)
|
|
479
646
|
normals.append(
|
|
@@ -482,6 +649,7 @@ def _merge_terrain(
|
|
|
482
649
|
terrain["normals"], singleton, batch_size, "normals", label
|
|
483
650
|
),
|
|
484
651
|
batch_size,
|
|
652
|
+
selection,
|
|
485
653
|
)
|
|
486
654
|
)
|
|
487
655
|
|
|
@@ -494,6 +662,7 @@ def _merge_terrain(
|
|
|
494
662
|
(
|
|
495
663
|
_expand_batched(prop["data"], singleton, batch_size, name, label),
|
|
496
664
|
batch_size,
|
|
665
|
+
selection,
|
|
497
666
|
)
|
|
498
667
|
)
|
|
499
668
|
cur_min, cur_max = property_min_max[name]
|
|
@@ -528,7 +697,7 @@ def _merge_terrain(
|
|
|
528
697
|
"normals": _concat_lists_or_b64(normals, vector_width=3),
|
|
529
698
|
"properties": merged_properties,
|
|
530
699
|
}
|
|
531
|
-
embedding = _merge_embedding(models, batch_sizes, labels, resolution)
|
|
700
|
+
embedding = _merge_embedding(models, batch_sizes, labels, resolution, selections)
|
|
532
701
|
if embedding is not None:
|
|
533
702
|
merged["embeddingData"] = embedding
|
|
534
703
|
return merged
|
|
@@ -568,10 +737,11 @@ def _merge_states(
|
|
|
568
737
|
bodies: list[dict],
|
|
569
738
|
scalar_names: list[str],
|
|
570
739
|
labels: list[str],
|
|
740
|
+
selections: list[list[int] | None],
|
|
571
741
|
) -> list[dict]:
|
|
572
742
|
ref_times = [s["time"] for s in states_list[0]]
|
|
573
743
|
times_by_file = [[s["time"] for s in states] for states in states_list]
|
|
574
|
-
total_batches = sum(batch_sizes)
|
|
744
|
+
total_batches = sum(_output_sizes(batch_sizes, selections))
|
|
575
745
|
lookup_cache: dict = {}
|
|
576
746
|
|
|
577
747
|
merged_states = []
|
|
@@ -597,8 +767,8 @@ def _merge_states(
|
|
|
597
767
|
}
|
|
598
768
|
contacts = [] if "contacts" in available else None
|
|
599
769
|
|
|
600
|
-
for file_idx, (states, batch_size) in enumerate(
|
|
601
|
-
zip(states_list, batch_sizes)
|
|
770
|
+
for file_idx, (states, batch_size, selection) in enumerate(
|
|
771
|
+
zip(states_list, batch_sizes, selections)
|
|
602
772
|
):
|
|
603
773
|
state_idx = state_idx_by_file[file_idx]
|
|
604
774
|
lookup = _state_body_lookup(states, file_idx, state_idx, lookup_cache)
|
|
@@ -609,12 +779,15 @@ def _merge_states(
|
|
|
609
779
|
f"t={states[state_idx]['time']}."
|
|
610
780
|
)
|
|
611
781
|
transform.extend(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
782
|
+
_select(
|
|
783
|
+
_normalize_per_batch(
|
|
784
|
+
_decode_state_field(
|
|
785
|
+
body_state["bodyTransform"],
|
|
786
|
+
_STATE_FIELD_WIDTHS["bodyTransform"],
|
|
787
|
+
),
|
|
788
|
+
batch_size,
|
|
616
789
|
),
|
|
617
|
-
|
|
790
|
+
selection,
|
|
618
791
|
)
|
|
619
792
|
)
|
|
620
793
|
for attr in attr_values:
|
|
@@ -624,11 +797,14 @@ def _merge_states(
|
|
|
624
797
|
f"available but is missing it at t={states[state_idx]['time']}."
|
|
625
798
|
)
|
|
626
799
|
attr_values[attr].extend(
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
800
|
+
_select(
|
|
801
|
+
_normalize_per_batch(
|
|
802
|
+
_decode_state_field(
|
|
803
|
+
body_state[attr], _STATE_FIELD_WIDTHS[attr]
|
|
804
|
+
),
|
|
805
|
+
batch_size,
|
|
630
806
|
),
|
|
631
|
-
|
|
807
|
+
selection,
|
|
632
808
|
)
|
|
633
809
|
)
|
|
634
810
|
if contacts is not None:
|
|
@@ -637,14 +813,15 @@ def _merge_states(
|
|
|
637
813
|
f"'{labels[file_idx]}' body '{name}' declares 'contacts' as "
|
|
638
814
|
f"available but is missing it at t={states[state_idx]['time']}."
|
|
639
815
|
)
|
|
640
|
-
contacts.extend(body_state["contacts"])
|
|
816
|
+
contacts.extend(_select(body_state["contacts"], selection))
|
|
641
817
|
|
|
642
818
|
if len(transform) != total_batches:
|
|
643
819
|
raise ValueError(
|
|
644
820
|
f"Merged 'bodyTransform' for body '{name}' has {len(transform)} "
|
|
645
|
-
f"rows; expected {total_batches} (sum of
|
|
646
|
-
f"{', '.join(repr(label) for label in labels)}).
|
|
647
|
-
"each file's per-body state rows match its
|
|
821
|
+
f"rows; expected {total_batches} (sum of the merged batch "
|
|
822
|
+
f"counts across {', '.join(repr(label) for label in labels)}). "
|
|
823
|
+
"Check that each file's per-body state rows match its "
|
|
824
|
+
"declared simBatches."
|
|
648
825
|
)
|
|
649
826
|
merged_body = {"name": name, "bodyTransform": transform, **attr_values}
|
|
650
827
|
if contacts is not None:
|
|
@@ -654,8 +831,8 @@ def _merge_states(
|
|
|
654
831
|
merged_state = {"time": t, "bodies": merged_bodies}
|
|
655
832
|
for scalar_name in scalar_names:
|
|
656
833
|
values = []
|
|
657
|
-
for file_idx, (states, batch_size) in enumerate(
|
|
658
|
-
zip(states_list, batch_sizes)
|
|
834
|
+
for file_idx, (states, batch_size, selection) in enumerate(
|
|
835
|
+
zip(states_list, batch_sizes, selections)
|
|
659
836
|
):
|
|
660
837
|
state_idx = state_idx_by_file[file_idx]
|
|
661
838
|
scalar_values = states[state_idx].get(scalar_name)
|
|
@@ -664,20 +841,44 @@ def _merge_states(
|
|
|
664
841
|
f"'{labels[file_idx]}' is missing scalar '{scalar_name}' at "
|
|
665
842
|
f"t={states[state_idx]['time']}."
|
|
666
843
|
)
|
|
667
|
-
values.extend(scalar_values)
|
|
844
|
+
values.extend(_select(scalar_values, selection))
|
|
668
845
|
merged_state[scalar_name] = values
|
|
669
846
|
merged_states.append(merged_state)
|
|
670
847
|
|
|
671
848
|
return merged_states
|
|
672
849
|
|
|
673
850
|
|
|
674
|
-
def merge_simulation_files(
|
|
851
|
+
def merge_simulation_files(
|
|
852
|
+
paths: Sequence[str | Path],
|
|
853
|
+
selections: Sequence[str | Sequence[int] | None] | None = None,
|
|
854
|
+
) -> dict:
|
|
675
855
|
"""Load and merge `paths` into a single `{"model": ..., "states": ...}` dict
|
|
676
|
-
where each file's batches are concatenated into the output's batch dimension.
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
856
|
+
where each file's batches are concatenated into the output's batch dimension.
|
|
857
|
+
|
|
858
|
+
`selections`, if given, must be parallel to `paths`: each entry is None
|
|
859
|
+
(contribute every batch of that file) or a batch selector -- a string like
|
|
860
|
+
``"1"``, ``"0,2"``, ``"1-3"``, ``"-1"`` or a batch name, or an explicit
|
|
861
|
+
sequence of indices (see `parse_batch_selection`). Selecting a subset is
|
|
862
|
+
what lets several files that each carry the same ground-truth batch be
|
|
863
|
+
merged without duplicating it, and it is the one case where a single file
|
|
864
|
+
is a valid merge input.
|
|
865
|
+
"""
|
|
680
866
|
resolved_paths = [Path(p) for p in paths]
|
|
867
|
+
if selections is None:
|
|
868
|
+
requested: list[str | Sequence[int] | None] = [None] * len(resolved_paths)
|
|
869
|
+
else:
|
|
870
|
+
requested = list(selections)
|
|
871
|
+
if len(requested) != len(resolved_paths):
|
|
872
|
+
raise ValueError(
|
|
873
|
+
f"merge_simulation_files got {len(requested)} batch selection(s) "
|
|
874
|
+
f"for {len(resolved_paths)} file(s); they must be parallel."
|
|
875
|
+
)
|
|
876
|
+
if len(resolved_paths) < 2 and all(s is None for s in requested):
|
|
877
|
+
raise ValueError(
|
|
878
|
+
"merge_simulation_files requires at least 2 files (or one file with "
|
|
879
|
+
"a batch selection, to keep only some of its batches)"
|
|
880
|
+
)
|
|
881
|
+
|
|
681
882
|
labels = [p.name for p in resolved_paths]
|
|
682
883
|
docs = [_load_json(p) for p in resolved_paths]
|
|
683
884
|
for doc, label in zip(docs, labels):
|
|
@@ -685,20 +886,41 @@ def merge_simulation_files(paths: Sequence[str | Path]) -> dict:
|
|
|
685
886
|
models = [doc["model"] for doc in docs]
|
|
686
887
|
states_list = [doc["states"] for doc in docs]
|
|
687
888
|
batch_sizes = [int(m.get("simBatches", 1)) for m in models]
|
|
889
|
+
# Resolved here rather than at the call site because a selector may name a
|
|
890
|
+
# batch by name or by a negative index, both of which need the file loaded.
|
|
891
|
+
batch_selections: list[list[int] | None] = [
|
|
892
|
+
None
|
|
893
|
+
if selector is None
|
|
894
|
+
else parse_batch_selection(selector, batch_size, model.get("batchNames"), label)
|
|
895
|
+
for selector, batch_size, model, label in zip(
|
|
896
|
+
requested, batch_sizes, models, labels
|
|
897
|
+
)
|
|
898
|
+
]
|
|
688
899
|
|
|
689
900
|
bodies = _merge_bodies(models, labels)
|
|
690
901
|
scalar_names = _merge_scalar_names(models, labels)
|
|
691
|
-
static_objects = _merge_static_objects(
|
|
692
|
-
|
|
902
|
+
static_objects = _merge_static_objects(
|
|
903
|
+
models, batch_sizes, labels, batch_selections
|
|
904
|
+
)
|
|
905
|
+
terrain = _merge_terrain(models, batch_sizes, labels, batch_selections)
|
|
693
906
|
merged_states = _merge_states(
|
|
694
|
-
states_list, batch_sizes, bodies, scalar_names, labels
|
|
907
|
+
states_list, batch_sizes, bodies, scalar_names, labels, batch_selections
|
|
695
908
|
)
|
|
696
909
|
|
|
697
|
-
|
|
698
|
-
|
|
910
|
+
out_sizes = _output_sizes(batch_sizes, batch_selections)
|
|
911
|
+
total_batches = sum(out_sizes)
|
|
912
|
+
offsets = [sum(out_sizes[:i]) for i in range(len(out_sizes))]
|
|
699
913
|
ranges = ", ".join(
|
|
700
|
-
f"'{label}'
|
|
701
|
-
|
|
914
|
+
f"'{label}'"
|
|
915
|
+
+ (
|
|
916
|
+
""
|
|
917
|
+
if selection is None
|
|
918
|
+
else f" (source batches {', '.join(str(i) for i in selection)})"
|
|
919
|
+
)
|
|
920
|
+
+ f" -> batches {offset}-{offset + size - 1}"
|
|
921
|
+
for label, offset, size, selection in zip(
|
|
922
|
+
labels, offsets, out_sizes, batch_selections
|
|
923
|
+
)
|
|
702
924
|
)
|
|
703
925
|
logger.info(
|
|
704
926
|
"Merged %d files into %d batches (%s)", len(paths), total_batches, ranges
|
|
@@ -706,7 +928,9 @@ def merge_simulation_files(paths: Sequence[str | Path]) -> dict:
|
|
|
706
928
|
|
|
707
929
|
merged_model = {
|
|
708
930
|
"simBatches": total_batches,
|
|
709
|
-
"batchNames": _default_batch_names(
|
|
931
|
+
"batchNames": _default_batch_names(
|
|
932
|
+
resolved_paths, batch_sizes, batch_selections
|
|
933
|
+
),
|
|
710
934
|
"scalarNames": scalar_names,
|
|
711
935
|
"dt": models[0].get("dt"),
|
|
712
936
|
"collapse": models[0].get("collapse", False),
|