mujoco-mojo 2.2.1__tar.gz → 2.2.2__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.
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/PKG-INFO +1 -1
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/pyproject.toml +8 -8
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/filters/filters.py +42 -46
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/routers/mosaic.py +488 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/base.html +201 -10
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/monitor.html +8 -8
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/mosaic.html +3 -3
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_chart.html +1085 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_header.html +322 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_json_editor.html +60 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_overlays.html +83 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_series_panel.html +1009 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/__init__.py +0 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/main.js +277 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/monitor.js +212 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/mosaic.js +42 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/toast.js +4 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/trial-viewer.js +1388 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/main.css +22 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored/__init__.py +0 -0
- mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/trial_viewer.html +55 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/PKG-INFO +1 -1
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/SOURCES.txt +18 -6
- mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/routers/mosaic.py +0 -208
- mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/trial_viewer.html +0 -2241
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/LICENSE +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/README.md +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/setup.cfg +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/__about__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/base.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/meta.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/defaults.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/dependency_path.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/extension.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/frame.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/include.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/replicate.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/adhesion.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/base.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/cylinder.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/damper.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/dcmotor.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/general.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/intvelocity.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/motor.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/muscle.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/plugin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/position.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/velocity.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/hfield.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material_attr/layer.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/mesh.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/model.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/texture.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/attach.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/camera.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/geom.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/joint.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/site.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/skin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/contact.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/edge.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/elasticity.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/pin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/free_joint.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/geom.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/inertial.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/joint.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/light.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/plugin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/site.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler_attr/lengthrange.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/exclude.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/pair.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/contact.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/edge.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/elasticity.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin_attr/bone.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/connect.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/equality_base.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flex.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flexstrain.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flexvert.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/joint.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/tendon.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/weld.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe_attr/key.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option_attr/flag.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/accelerometer.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorfrc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorpos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/ballangvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/ballquat.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/base.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/base_collision.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/camprojection.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/clock.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/contact.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/distance.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/e_kinetic.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/e_potential.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/force.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameangacc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameangvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framelinacc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framelinvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framepos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framequat.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framexaxis.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameyaxis.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framezaxis.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/fromto.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/gyro.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/insidesite.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointactuatorfrc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitfrc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitpos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointpos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/magnetometer.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/normal.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/plugin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/rangefinder.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreeangmom.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreecom.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreelinvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tactile.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonactuatorfrc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitfrc.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitpos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonpos.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonvel.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/torque.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/touch.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/user.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/velocimeter.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/size.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/statistic.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed_attr/joint.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/geom.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/pulley.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/site.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/tendon_base.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/global_.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/headlight.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/map.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/quality.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/rgba.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/scale.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/orientation.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/plugin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/pose.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/position.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/xml_model.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mojo_model.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/load.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/runtime_manager.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/signal_manager.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/video_recorder.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/stochas/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/typing.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/color.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/dataframe.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/defaults.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/filters/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/interp.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/cli.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/main.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/monitor.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/morph.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/shared.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/error.html +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/morph.html +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/__init__.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/chime.mp3 +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/dark-logo.svg +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/light-logo.svg +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/main.js +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/monitor.js +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/mosaic.js +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/trial-viewer.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/alpine.min.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/confetti.browser.min.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/iro.min.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/lz-string.min.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/plotly-3.4.0.min.js +0 -0
- {mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/static → mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored}/tailwind.min.js +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/reloaded.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/log.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/proximity.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/proximity_mixin.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/runner.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/statusing.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/utils.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/visualization.py +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/dependency_links.txt +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/entry_points.txt +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/requires.txt +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/top_level.txt +0 -0
- {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/tests/test_writer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mujoco-mojo
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: A complete MJCF lifecycle and trial orchestration suite for MuJoCo, powered by Pydantic v2.
|
|
5
5
|
Author-email: David Gable <dave.a.gable@gmail.com>
|
|
6
6
|
Maintainer-email: David Gable <dave.a.gable@gmail.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "mujoco-mojo"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.2"
|
|
8
8
|
description = "A complete MJCF lifecycle and trial orchestration suite for MuJoCo, powered by Pydantic v2."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -68,11 +68,8 @@ keywords = [
|
|
|
68
68
|
"mjcf",
|
|
69
69
|
]
|
|
70
70
|
|
|
71
|
-
[project.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
[tool.setuptools.package-data]
|
|
75
|
-
"*" = ["*.html", "*.js", "*.mp3", "*.svg", "*.css"]
|
|
71
|
+
[project.optional-dependencies]
|
|
72
|
+
reloaded = ["mjviser>=0.0.11", "viser>=1.0.26"]
|
|
76
73
|
|
|
77
74
|
[dependency-groups]
|
|
78
75
|
dev = [
|
|
@@ -94,8 +91,11 @@ dev = [
|
|
|
94
91
|
"zensical>=0.0.31",
|
|
95
92
|
]
|
|
96
93
|
|
|
97
|
-
[project.
|
|
98
|
-
|
|
94
|
+
[project.scripts]
|
|
95
|
+
mujoco-mojo = "mujoco_mojo.utils.layers.cli:cli_app"
|
|
96
|
+
|
|
97
|
+
[tool.setuptools.package-data]
|
|
98
|
+
"*" = ["*.html", "*.js", "*.mp3", "*.svg", "*.css"]
|
|
99
99
|
|
|
100
100
|
[tool.ruff]
|
|
101
101
|
exclude = ["typings", "scripts/templating/sensor_template.py"]
|
|
@@ -11,6 +11,7 @@ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, model_validator
|
|
|
11
11
|
from scipy.signal import savgol_filter
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
14
|
+
"UNIT_GROUPS",
|
|
14
15
|
"AbsoluteValueFilter",
|
|
15
16
|
"AnyFilter",
|
|
16
17
|
"ClipFilter",
|
|
@@ -302,56 +303,50 @@ class SavitzkyGolayFilter(BaseFilter):
|
|
|
302
303
|
|
|
303
304
|
|
|
304
305
|
ureg = pint.UnitRegistry()
|
|
305
|
-
# --- Kinematics ---
|
|
306
|
-
AngleUnit = Literal["rad", "deg", "rad/s", "deg/s", "rad/s^2", "deg/s^2", "rev", "rpm"]
|
|
307
|
-
LenUnit = Literal["m", "mm", "cm", "km", "in", "ft", "thou"]
|
|
308
|
-
VelUnit = Literal["m/s", "ft/s", "in/s", "km/h", "mph"]
|
|
309
|
-
AccUnit = Literal["m/s^2", "g", "ft/s^2", "in/s^2"]
|
|
310
|
-
|
|
311
|
-
# --- Dynamics & Statics ---
|
|
312
|
-
MassUnit = Literal["kg", "lbm", "slug", "g", "mg"]
|
|
313
|
-
ForceUnit = Literal["N", "lbf", "kN", "mN"]
|
|
314
|
-
TorqueUnit = Literal["N*m", "N*mm", "mN*m", "lbf*ft", "lbf*in", "ozf*in"]
|
|
315
|
-
InertiaUnit = Literal["kg*m^2", "lbm*in^2", "lbm*ft^2", "slug*ft^2"]
|
|
316
|
-
|
|
317
|
-
# --- Work & Thermodynamics ---
|
|
318
|
-
EnergyUnit = Literal["J", "kJ", "mJ", "W*s", "ft*lbf", "BTU"]
|
|
319
|
-
PowerUnit = Literal["W", "kW", "hp", "ft*lbf/s"]
|
|
320
|
-
PressureUnit = Literal["Pa", "kPa", "psi", "bar", "atm", "torr"]
|
|
321
|
-
|
|
322
|
-
# --- Temporal & Electronics ---
|
|
323
|
-
TimeUnit = Literal["s", "ms", "us", "ns", "min", "hr"]
|
|
324
|
-
FreqUnit = Literal["Hz", "kHz", "MHz", "rad/s"]
|
|
325
|
-
|
|
326
|
-
# --- Dimensionless & Ratios ---
|
|
327
|
-
MiscUnit = Literal["dimensionless", "pct", "count", "bit", "V", "A"]
|
|
328
|
-
|
|
329
|
-
SignalUnit = (
|
|
330
|
-
AngleUnit
|
|
331
|
-
| LenUnit
|
|
332
|
-
| VelUnit
|
|
333
|
-
| AccUnit
|
|
334
|
-
| MassUnit
|
|
335
|
-
| ForceUnit
|
|
336
|
-
| TorqueUnit
|
|
337
|
-
| InertiaUnit
|
|
338
|
-
| EnergyUnit
|
|
339
|
-
| PowerUnit
|
|
340
|
-
| PressureUnit
|
|
341
|
-
| TimeUnit
|
|
342
|
-
| FreqUnit
|
|
343
|
-
| MiscUnit
|
|
344
|
-
| str
|
|
345
|
-
)
|
|
346
|
-
|
|
347
306
|
try:
|
|
348
|
-
# explicitly map lbm and lbf to avoid ambiguity
|
|
349
307
|
ureg.define("lbm = pound")
|
|
350
308
|
ureg.define("lbf = force_pound")
|
|
309
|
+
ureg.define("ozf = force_ounce")
|
|
351
310
|
except pint.errors.RedefinitionError:
|
|
352
|
-
# already defined in some Pint versions
|
|
353
311
|
pass
|
|
354
312
|
|
|
313
|
+
# ---------------------------------------------------------------------------
|
|
314
|
+
# Unit groups — single source of truth for both the frontend smart dropdown
|
|
315
|
+
# and the SignalUnit type annotation on UnitFilter. To add a new unit,
|
|
316
|
+
# add it here; SignalUnit is derived automatically. Verify that pint can
|
|
317
|
+
# parse any new string via `ureg.parse_units(...)` before committing.
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
UNIT_GROUPS: list[tuple[str, list[str]]] = [
|
|
320
|
+
# --- Kinematics ---
|
|
321
|
+
("Angle", ["rad", "deg", "mrad", "rev", "rpm"]),
|
|
322
|
+
("Angular Velocity", ["rad/s", "deg/s"]),
|
|
323
|
+
("Angular Accel.", ["rad/s^2", "deg/s^2"]),
|
|
324
|
+
("Length", ["m", "mm", "cm", "um", "km", "in", "ft", "thou"]),
|
|
325
|
+
("Velocity", ["m/s", "mm/s", "cm/s", "ft/s", "in/s", "km/h", "mph"]),
|
|
326
|
+
("Acceleration", ["m/s^2", "mm/s^2", "ft/s^2", "in/s^2"]),
|
|
327
|
+
# --- Dynamics & Statics ---
|
|
328
|
+
("Mass", ["kg", "g", "mg", "lbm", "slug"]),
|
|
329
|
+
("Force", ["N", "mN", "uN", "kN", "lbf"]),
|
|
330
|
+
("Torque", ["N*m", "N*mm", "mN*m", "kN*m", "lbf*ft", "lbf*in", "ozf*in"]),
|
|
331
|
+
("Inertia", ["kg*m^2", "kg*mm^2", "lbm*in^2", "lbm*ft^2", "slug*ft^2"]),
|
|
332
|
+
# --- Work & Thermodynamics ---
|
|
333
|
+
("Energy", ["J", "mJ", "kJ", "W*s", "W*h", "kW*h", "ft*lbf", "BTU"]),
|
|
334
|
+
("Power", ["W", "mW", "kW", "MW", "hp", "ft*lbf/s"]),
|
|
335
|
+
("Pressure", ["Pa", "kPa", "MPa", "psi", "bar", "atm", "torr"]),
|
|
336
|
+
# --- Temporal & Electronics ---
|
|
337
|
+
("Time", ["s", "ms", "us", "ns", "min", "hr"]),
|
|
338
|
+
("Frequency", ["Hz", "kHz", "MHz"]),
|
|
339
|
+
("Voltage", ["V", "mV", "kV"]),
|
|
340
|
+
("Current", ["A", "mA"]),
|
|
341
|
+
# --- Dimensionless & Ratios ---
|
|
342
|
+
("Misc.", ["dimensionless", "pct", "count", "bit"]),
|
|
343
|
+
]
|
|
344
|
+
|
|
345
|
+
# Derived automatically — Literal[tuple_of_strings] is equivalent to Literal["a", "b", ...]
|
|
346
|
+
# in Python 3.9+ because x[a, b] and x[(a, b)] make the same __getitem__ call.
|
|
347
|
+
_ALL_UNITS: tuple[str, ...] = tuple(u for _, us in UNIT_GROUPS for u in us)
|
|
348
|
+
SignalUnit = Literal[_ALL_UNITS] | str
|
|
349
|
+
|
|
355
350
|
|
|
356
351
|
class UnitFilter(BaseFilter):
|
|
357
352
|
"""
|
|
@@ -362,9 +357,10 @@ class UnitFilter(BaseFilter):
|
|
|
362
357
|
type: Literal[FilterType.UNIT] = FilterType.UNIT
|
|
363
358
|
"""The discriminator type for Pydantic."""
|
|
364
359
|
|
|
365
|
-
from_unit: SignalUnit
|
|
360
|
+
from_unit: SignalUnit # pyright: ignore[reportInvalidTypeForm]
|
|
366
361
|
"""The original unit of the telemetry data (e.g., 'rad')."""
|
|
367
|
-
|
|
362
|
+
|
|
363
|
+
to_unit: SignalUnit # pyright: ignore[reportInvalidTypeForm]
|
|
368
364
|
"""The target unit for analysis/display (e.g., 'deg')."""
|
|
369
365
|
|
|
370
366
|
@model_validator(mode="after")
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import socket
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import get_args
|
|
9
|
+
|
|
10
|
+
import polars as pl
|
|
11
|
+
from fastapi import APIRouter, HTTPException, Query, Request
|
|
12
|
+
from fastapi.responses import HTMLResponse
|
|
13
|
+
|
|
14
|
+
from mujoco_mojo.utils.dataframe import ColumnManifest, MojoDataFrame
|
|
15
|
+
from mujoco_mojo.utils.filters.filters import UNIT_GROUPS as _UNIT_GROUPS
|
|
16
|
+
from mujoco_mojo.utils.filters.filters import AnyFilter as _AnyFilter
|
|
17
|
+
from mujoco_mojo.utils.filters.filters import FilterType as _FilterType
|
|
18
|
+
from mujoco_mojo.utils.filters.filters import filter_adapter as _filter_adapter
|
|
19
|
+
from mujoco_mojo.utils.log import get_logger
|
|
20
|
+
|
|
21
|
+
from .. import shared
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
# Derived from FilterType enum — automatically includes any new filter type added to filters.py.
|
|
28
|
+
# Used to identify the filter name in Pydantic error location tuples when formatting messages.
|
|
29
|
+
_FILTER_TYPE_NAMES: set[str] = {str(ft) for ft in _FilterType}
|
|
30
|
+
|
|
31
|
+
# Derived from AnyFilter's union members — automatically includes any new filter class.
|
|
32
|
+
# AnyFilter = Annotated[ScaleFilter | AbsoluteValueFilter | ..., Field(discriminator="type")]
|
|
33
|
+
# get_args(AnyFilter)[0] is the bare union; get_args of that gives the individual classes.
|
|
34
|
+
_annotated_args = get_args(_AnyFilter)
|
|
35
|
+
_FILTER_CLASSES: list[type] = (
|
|
36
|
+
list(get_args(_annotated_args[0])) if _annotated_args else []
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_CONSTRAINT_OPS = {
|
|
40
|
+
"less_than_equal": "≤",
|
|
41
|
+
"less_than": "<",
|
|
42
|
+
"greater_than_equal": "≥",
|
|
43
|
+
"greater_than": ">",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_filter_error(exc: Exception) -> str:
|
|
48
|
+
"""Format a Pydantic ValidationError into a short, human-readable message."""
|
|
49
|
+
from pydantic import ValidationError
|
|
50
|
+
|
|
51
|
+
if not isinstance(exc, ValidationError):
|
|
52
|
+
return str(exc).split("\n")[0] or "Filter error"
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
errors = exc.errors(include_url=False)
|
|
56
|
+
except TypeError:
|
|
57
|
+
errors = exc.errors() # older pydantic build without include_url kwarg
|
|
58
|
+
|
|
59
|
+
if not errors:
|
|
60
|
+
return "Filter validation failed — check filter settings"
|
|
61
|
+
|
|
62
|
+
first = errors[0]
|
|
63
|
+
loc: tuple = first.get("loc", ())
|
|
64
|
+
msg: str = first.get("msg", "")
|
|
65
|
+
err_type: str = first.get("type", "")
|
|
66
|
+
ctx: dict = first.get("ctx", {}) or {}
|
|
67
|
+
|
|
68
|
+
# Resolve filter type and field from location tuple
|
|
69
|
+
# e.g. ('/Bodies/xpos:x', 0, 'low_pass', 'alpha') → filter='Low Pass', field='alpha'
|
|
70
|
+
filter_label: str | None = None
|
|
71
|
+
field_name: str | None = None
|
|
72
|
+
for i, part in enumerate(loc):
|
|
73
|
+
if isinstance(part, str) and part in _FILTER_TYPE_NAMES:
|
|
74
|
+
filter_label = part.replace("_", " ").title()
|
|
75
|
+
if i + 1 < len(loc) and isinstance(loc[i + 1], str):
|
|
76
|
+
field_name = loc[i + 1]
|
|
77
|
+
|
|
78
|
+
prefix = f"{filter_label}: " if filter_label else ""
|
|
79
|
+
|
|
80
|
+
# ── Unit-specific model validator errors ──────────────────────────────
|
|
81
|
+
m = re.match(r"Value error, Unknown unit definition: '(.+?)' is not defined", msg)
|
|
82
|
+
if m:
|
|
83
|
+
return f"Unknown unit '{m.group(1)}'"
|
|
84
|
+
m = re.match(r"Value error, Incompatible units: (.+?) and (.+?) \(", msg)
|
|
85
|
+
if m:
|
|
86
|
+
return f"Incompatible units: {m.group(1)} → {m.group(2)}"
|
|
87
|
+
|
|
88
|
+
# ── Generic model validator (custom @model_validator) ─────────────────
|
|
89
|
+
if msg.startswith("Value error, "):
|
|
90
|
+
clean = msg.removeprefix("Value error, ")
|
|
91
|
+
return f"{prefix}{clean}"
|
|
92
|
+
|
|
93
|
+
# ── Field-level numeric constraint (gt, ge, lt, le) ───────────────────
|
|
94
|
+
if err_type in _CONSTRAINT_OPS:
|
|
95
|
+
op = _CONSTRAINT_OPS[err_type]
|
|
96
|
+
# Use next/in to avoid treating 0 as falsy (ctx.get("gt") or ... breaks on gt=0)
|
|
97
|
+
limit = next((ctx[k] for k in ("gt", "ge", "lt", "le") if k in ctx), None)
|
|
98
|
+
field_str = f"{field_name} " if field_name else ""
|
|
99
|
+
return f"{prefix}{field_str}must be {op} {limit}"
|
|
100
|
+
|
|
101
|
+
# ── Tagged-union discriminator mismatch (bad filter type string) ───────
|
|
102
|
+
if "union" in err_type or "tagged" in err_type:
|
|
103
|
+
return "Unknown filter type — check filter configuration"
|
|
104
|
+
|
|
105
|
+
# ── Fallback: clean up the raw Pydantic message ───────────────────────
|
|
106
|
+
clean = re.sub(r"\s*\[type=\w+.*?\]\s*$", "", msg).strip()
|
|
107
|
+
clean = clean.removeprefix("Value error,").strip()
|
|
108
|
+
return (
|
|
109
|
+
f"{prefix}{clean}"
|
|
110
|
+
if clean
|
|
111
|
+
else "Filter validation failed — check filter settings"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def get_network_ip():
|
|
116
|
+
"""Detects the primary local network IP of the host machine."""
|
|
117
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
118
|
+
try:
|
|
119
|
+
# Doesn't actually have to be reachable; just triggers IP selection
|
|
120
|
+
s.connect(("8.8.8.8", 1))
|
|
121
|
+
ip = s.getsockname()[0]
|
|
122
|
+
except Exception:
|
|
123
|
+
ip = "localhost"
|
|
124
|
+
finally:
|
|
125
|
+
s.close()
|
|
126
|
+
return ip
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@router.get("/", response_class=HTMLResponse)
|
|
130
|
+
async def get_mosaic(request: Request):
|
|
131
|
+
"""Serves the initial mosiac frame."""
|
|
132
|
+
return shared.templates.TemplateResponse(
|
|
133
|
+
request=request, name="mosaic.html", context={"request": request}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@router.get("/api/trials")
|
|
138
|
+
async def get_valid_trials():
|
|
139
|
+
"""Scans the workdir for folders containing 'telemetry.parquet'"""
|
|
140
|
+
job = shared.CURRENT_JOB
|
|
141
|
+
from mujoco_mojo.runtime.signal_manager import SignalManager
|
|
142
|
+
|
|
143
|
+
if job is None:
|
|
144
|
+
logger.warning("Mosaic accessed but CURRENT_JOB is None.")
|
|
145
|
+
return {"trials": []}
|
|
146
|
+
|
|
147
|
+
valid_trials = []
|
|
148
|
+
|
|
149
|
+
for tn in job.trial_nums:
|
|
150
|
+
trial_dir = job.trial_num_to_path(tn)
|
|
151
|
+
|
|
152
|
+
# if there is a db and the trial is actually done
|
|
153
|
+
if (
|
|
154
|
+
trial_dir / SignalManager.default_output_name()
|
|
155
|
+
).exists() and tn in job._cache:
|
|
156
|
+
valid_trials.append(trial_dir.name)
|
|
157
|
+
|
|
158
|
+
valid_trials.sort()
|
|
159
|
+
|
|
160
|
+
return {"trials": valid_trials}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@router.get("/{trial_id}", response_class=HTMLResponse)
|
|
164
|
+
async def get_trial_viewer(request: Request, trial_id: str):
|
|
165
|
+
"""Land here when clicking a trial."""
|
|
166
|
+
job = shared.CURRENT_JOB
|
|
167
|
+
|
|
168
|
+
if not job:
|
|
169
|
+
raise HTTPException(status_code=404, detail="No active job found.")
|
|
170
|
+
|
|
171
|
+
server_ip = get_network_ip()
|
|
172
|
+
port = request.url.port or 8000
|
|
173
|
+
|
|
174
|
+
# 1. Get the sorted list of all trials that actually have data
|
|
175
|
+
from mujoco_mojo.runtime.signal_manager import SignalManager
|
|
176
|
+
|
|
177
|
+
valid_ids = []
|
|
178
|
+
for tn in job.trial_nums:
|
|
179
|
+
path = job.trial_num_to_path(tn)
|
|
180
|
+
if (path / SignalManager.default_output_name()).exists():
|
|
181
|
+
valid_ids.append(path.name)
|
|
182
|
+
|
|
183
|
+
if trial_id not in valid_ids:
|
|
184
|
+
raise HTTPException(
|
|
185
|
+
status_code=404,
|
|
186
|
+
detail=f"Trial '{trial_id}' does not exist or has no telemetry data.",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# 2. Find the neighbors of the current trial_id
|
|
190
|
+
idx = valid_ids.index(trial_id)
|
|
191
|
+
prev_id = valid_ids[idx - 1] if idx > 0 else None
|
|
192
|
+
next_id = valid_ids[idx + 1] if idx < len(valid_ids) - 1 else None
|
|
193
|
+
|
|
194
|
+
return shared.templates.TemplateResponse(
|
|
195
|
+
request=request,
|
|
196
|
+
name="trial_viewer.html",
|
|
197
|
+
context={
|
|
198
|
+
"request": request,
|
|
199
|
+
"trial_id": trial_id,
|
|
200
|
+
"prev_id": prev_id,
|
|
201
|
+
"next_id": next_id,
|
|
202
|
+
"external_url": f"http://{server_ip}:{port}",
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.get("/api/filter-schema")
|
|
208
|
+
async def get_filter_schema():
|
|
209
|
+
"""
|
|
210
|
+
Returns metadata for all available filter types, derived from Pydantic models.
|
|
211
|
+
|
|
212
|
+
Filter classes are auto-discovered from AnyFilter's union — no changes needed here
|
|
213
|
+
when a new filter is added to filters.py.
|
|
214
|
+
"""
|
|
215
|
+
from pydantic_core import PydanticUndefined
|
|
216
|
+
|
|
217
|
+
def _infer_type(prop: dict) -> str:
|
|
218
|
+
if "anyOf" in prop:
|
|
219
|
+
non_null = [s for s in prop["anyOf"] if s.get("type") != "null"]
|
|
220
|
+
prop = non_null[0] if non_null else {}
|
|
221
|
+
t = prop.get("type", "")
|
|
222
|
+
if t == "integer":
|
|
223
|
+
return "int"
|
|
224
|
+
if t == "number":
|
|
225
|
+
return "float"
|
|
226
|
+
if t == "boolean":
|
|
227
|
+
return "bool"
|
|
228
|
+
return "string"
|
|
229
|
+
|
|
230
|
+
result = []
|
|
231
|
+
for cls in _FILTER_CLASSES:
|
|
232
|
+
schema = cls.model_json_schema()
|
|
233
|
+
props = schema.get("properties", {})
|
|
234
|
+
type_val = str(cls.model_fields["type"].default)
|
|
235
|
+
|
|
236
|
+
params = []
|
|
237
|
+
for name, field_info in cls.model_fields.items():
|
|
238
|
+
if name == "type":
|
|
239
|
+
continue
|
|
240
|
+
prop = props.get(name, {})
|
|
241
|
+
if "anyOf" in prop:
|
|
242
|
+
non_null = [s for s in prop["anyOf"] if s.get("type") != "null"]
|
|
243
|
+
prop_clean = {**prop, **(non_null[0] if non_null else {})}
|
|
244
|
+
else:
|
|
245
|
+
prop_clean = prop
|
|
246
|
+
|
|
247
|
+
default = field_info.default
|
|
248
|
+
if default is PydanticUndefined:
|
|
249
|
+
default = None
|
|
250
|
+
elif isinstance(default, float):
|
|
251
|
+
default = round(float(default), 8)
|
|
252
|
+
|
|
253
|
+
p: dict = {"name": name, "type": _infer_type(prop), "default": default}
|
|
254
|
+
if "minimum" in prop_clean:
|
|
255
|
+
p["min"] = prop_clean["minimum"]
|
|
256
|
+
if "maximum" in prop_clean:
|
|
257
|
+
p["max"] = prop_clean["maximum"]
|
|
258
|
+
if "exclusiveMinimum" in prop_clean:
|
|
259
|
+
p["exclusive_min"] = prop_clean["exclusiveMinimum"]
|
|
260
|
+
if "exclusiveMaximum" in prop_clean:
|
|
261
|
+
p["exclusive_max"] = prop_clean["exclusiveMaximum"]
|
|
262
|
+
params.append(p)
|
|
263
|
+
|
|
264
|
+
description = (cls.__doc__ or "").strip()
|
|
265
|
+
description = description.split("\n")[0].strip()
|
|
266
|
+
|
|
267
|
+
entry: dict = {
|
|
268
|
+
"type": type_val,
|
|
269
|
+
"label": type_val.replace("_", " ").title(),
|
|
270
|
+
"description": description,
|
|
271
|
+
"params": params,
|
|
272
|
+
}
|
|
273
|
+
if type_val == "unit":
|
|
274
|
+
entry["unit_groups"] = [
|
|
275
|
+
{"label": label, "units": units} for label, units in _UNIT_GROUPS
|
|
276
|
+
]
|
|
277
|
+
result.append(entry)
|
|
278
|
+
|
|
279
|
+
return result
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Profiles · named saved views stored under {workdir}/profiles/
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _get_profiles_dir() -> Path | None:
|
|
288
|
+
job = shared.CURRENT_JOB
|
|
289
|
+
if not job:
|
|
290
|
+
return None
|
|
291
|
+
d: Path = job.workdir / "profiles"
|
|
292
|
+
d.mkdir(exist_ok=True)
|
|
293
|
+
return d
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _sanitize_profile_name(name: str) -> str:
|
|
297
|
+
"""Return a filesystem-safe stem from the user-supplied profile name."""
|
|
298
|
+
name = name.strip()[:128]
|
|
299
|
+
name = re.sub(r"[^\w\s\-]", "", name) # keep word chars, whitespace, hyphens
|
|
300
|
+
name = re.sub(r"\s+", "_", name) # spaces → underscores
|
|
301
|
+
name = re.sub(r"_+", "_", name).strip("_")
|
|
302
|
+
return name[:64] or "profile"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@router.get("/api/profiles")
|
|
306
|
+
async def list_profiles():
|
|
307
|
+
"""List all saved profiles for the current job."""
|
|
308
|
+
d = _get_profiles_dir()
|
|
309
|
+
if d is None:
|
|
310
|
+
raise HTTPException(status_code=404, detail="No active job")
|
|
311
|
+
profiles = [
|
|
312
|
+
{"name": f.stem, "modified": int(f.stat().st_mtime * 1000)}
|
|
313
|
+
for f in sorted(d.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)
|
|
314
|
+
]
|
|
315
|
+
return profiles
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@router.get("/api/profiles/{name}")
|
|
319
|
+
async def get_profile(name: str):
|
|
320
|
+
"""Return the PlotConfig JSON for a saved profile."""
|
|
321
|
+
d = _get_profiles_dir()
|
|
322
|
+
if d is None:
|
|
323
|
+
raise HTTPException(status_code=404, detail="No active job")
|
|
324
|
+
path = d / f"{_sanitize_profile_name(name)}.json"
|
|
325
|
+
if not path.exists():
|
|
326
|
+
raise HTTPException(status_code=404, detail="Profile not found")
|
|
327
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@router.post("/api/profiles/{name}")
|
|
331
|
+
async def save_profile(name: str, request: Request):
|
|
332
|
+
"""Save the current PlotConfig as a named profile."""
|
|
333
|
+
d = _get_profiles_dir()
|
|
334
|
+
if d is None:
|
|
335
|
+
raise HTTPException(status_code=404, detail="No active job")
|
|
336
|
+
safe = _sanitize_profile_name(name)
|
|
337
|
+
if not safe:
|
|
338
|
+
raise HTTPException(status_code=400, detail="Invalid profile name")
|
|
339
|
+
body = await request.json()
|
|
340
|
+
(d / f"{safe}.json").write_text(
|
|
341
|
+
json.dumps(body, separators=(",", ":")), encoding="utf-8"
|
|
342
|
+
)
|
|
343
|
+
return {"name": safe}
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@router.delete("/api/profiles/{name}")
|
|
347
|
+
async def delete_profile(name: str):
|
|
348
|
+
"""Delete a saved profile."""
|
|
349
|
+
d = _get_profiles_dir()
|
|
350
|
+
if d is None:
|
|
351
|
+
raise HTTPException(status_code=404, detail="No active job")
|
|
352
|
+
path = d / f"{_sanitize_profile_name(name)}.json"
|
|
353
|
+
if not path.exists():
|
|
354
|
+
raise HTTPException(status_code=404, detail="Profile not found")
|
|
355
|
+
path.unlink()
|
|
356
|
+
return {"deleted": name}
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@lru_cache(maxsize=128)
|
|
360
|
+
def _get_column_manifest(path_str: str, mtime: float) -> ColumnManifest:
|
|
361
|
+
"""Retrieves all column names from the table schema."""
|
|
362
|
+
return MojoDataFrame.from_metadata(path_str).mojo.get_manifest()
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
@lru_cache(maxsize=2048)
|
|
366
|
+
def _get_atomic_column(path_str: str, col_name: str, mtime: float):
|
|
367
|
+
"""
|
|
368
|
+
Fetches a single column. 'mtime' is the cache-breaker. If the file changes, the mtime changes, triggering a fresh read even if the path and column name are the same.
|
|
369
|
+
"""
|
|
370
|
+
return pl.scan_parquet(path_str).select(col_name).collect().to_series().to_list()
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@router.get("/{trial_id}/data")
|
|
374
|
+
async def get_trial_data(
|
|
375
|
+
trial_id: str,
|
|
376
|
+
cols: str = Query(None),
|
|
377
|
+
rotate_by: str = Query(None),
|
|
378
|
+
filters: str = Query(None),
|
|
379
|
+
):
|
|
380
|
+
"""
|
|
381
|
+
Loops over the columns in the trial_id provided and returns their data. Optionally performs a rotation if requested and there are an associated x, y, and z column.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
trial_id (str): Trial to search (e.g. `"trial_001"`).
|
|
385
|
+
cols (str, optional): Comma separated list of column names to return data for (e.g. `"/Bodies/body1/xpos:x,/Bodies/body2/xpos:m"`). Defaults to Query(None).
|
|
386
|
+
rotate_by (str, optional): Quaternion family to rotate vectors by (e.g. `"/Bodies/body1/quat"`). Defaults to Query(None).
|
|
387
|
+
filters (str, optional): String representation of filters to be applied sequentially. Defaults to Query(None).
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
HTTPException: Raised if no shared.CURRENT_JOB was set.
|
|
391
|
+
HTTPException: Raised if the database path for the trial was found.
|
|
392
|
+
HTTPException: Raised if an error occured while extracting data from the database.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
dict: Dictionary containing split columns and their associated data.
|
|
396
|
+
|
|
397
|
+
"""
|
|
398
|
+
from mujoco_mojo.runtime.signal_manager import SignalManager
|
|
399
|
+
|
|
400
|
+
job = shared.CURRENT_JOB
|
|
401
|
+
if not job:
|
|
402
|
+
raise HTTPException(status_code=404, detail="No job active")
|
|
403
|
+
|
|
404
|
+
db_path = (
|
|
405
|
+
job.workdir / "trials" / trial_id / SignalManager.default_output_name()
|
|
406
|
+
).resolve()
|
|
407
|
+
|
|
408
|
+
if not db_path.exists():
|
|
409
|
+
raise HTTPException(
|
|
410
|
+
status_code=404, detail=f"Database not found for {trial_id}"
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# use the files last modified time as a cache breaker
|
|
414
|
+
mtime = db_path.stat().st_mtime
|
|
415
|
+
db_path_str = str(db_path)
|
|
416
|
+
|
|
417
|
+
# get the manifest of ALL columns in the df
|
|
418
|
+
column_manifest = _get_column_manifest(db_path_str, mtime)
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
requested = cols.split(",") if cols else []
|
|
422
|
+
available_cols = set(column_manifest["all"])
|
|
423
|
+
|
|
424
|
+
# determine columns to request
|
|
425
|
+
fetch_targets = [c for c in requested if c in available_cols]
|
|
426
|
+
|
|
427
|
+
if rotate_by:
|
|
428
|
+
q_family = [
|
|
429
|
+
f"{rotate_by}:x",
|
|
430
|
+
f"{rotate_by}:y",
|
|
431
|
+
f"{rotate_by}:z",
|
|
432
|
+
f"{rotate_by}:w",
|
|
433
|
+
]
|
|
434
|
+
for q in q_family:
|
|
435
|
+
if q in available_cols and q not in fetch_targets:
|
|
436
|
+
fetch_targets.append(q)
|
|
437
|
+
|
|
438
|
+
# early exit for no found columns
|
|
439
|
+
if not fetch_targets:
|
|
440
|
+
return {"columns": column_manifest, "data": {}}
|
|
441
|
+
|
|
442
|
+
# assemble dataframe
|
|
443
|
+
raw_data = {
|
|
444
|
+
col: _get_atomic_column(db_path_str, col, mtime) for col in fetch_targets
|
|
445
|
+
}
|
|
446
|
+
df = MojoDataFrame.from_dict(raw_data)
|
|
447
|
+
|
|
448
|
+
if rotate_by:
|
|
449
|
+
# rotate from world to rotate_by frame
|
|
450
|
+
df = df.mojo.with_rotation(quat_base=rotate_by, invert=True)
|
|
451
|
+
|
|
452
|
+
# parse validated filter stacks (col_name → list[AnyFilter])
|
|
453
|
+
col_filters: dict = {}
|
|
454
|
+
filter_errors: list[str] = []
|
|
455
|
+
if filters:
|
|
456
|
+
try:
|
|
457
|
+
col_filters = _filter_adapter.validate_python(json.loads(filters))
|
|
458
|
+
except Exception as e:
|
|
459
|
+
logger.warning(f"Could not parse filters for {trial_id}: {e}")
|
|
460
|
+
filter_errors.append(_format_filter_error(e))
|
|
461
|
+
|
|
462
|
+
# build response data, applying per-column filters where present
|
|
463
|
+
data: dict = {}
|
|
464
|
+
for col in requested:
|
|
465
|
+
if col not in df.columns:
|
|
466
|
+
continue
|
|
467
|
+
series = df[col]
|
|
468
|
+
filter_list = col_filters.get(col)
|
|
469
|
+
if filter_list:
|
|
470
|
+
if series.dtype != pl.Float64:
|
|
471
|
+
series = series.cast(pl.Float64)
|
|
472
|
+
tmp = pl.DataFrame({col: series})
|
|
473
|
+
expr = pl.col(col)
|
|
474
|
+
for f in filter_list:
|
|
475
|
+
expr = f.apply(expr)
|
|
476
|
+
tmp = tmp.with_columns(expr.alias(col))
|
|
477
|
+
series = tmp[col]
|
|
478
|
+
data[col] = series.to_list()
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
"columns": column_manifest,
|
|
482
|
+
"data": data,
|
|
483
|
+
"filter_errors": filter_errors,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
except Exception as e:
|
|
487
|
+
logger.error(f"Data retrieval failed for {trial_id}: {e}")
|
|
488
|
+
raise HTTPException(status_code=500, detail="Internal Server Error")
|