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.
Files changed (252) hide show
  1. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/PKG-INFO +1 -1
  2. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/pyproject.toml +8 -8
  3. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/filters/filters.py +42 -46
  4. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/routers/mosaic.py +488 -0
  5. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/base.html +201 -10
  6. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/monitor.html +8 -8
  7. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/mosaic.html +3 -3
  8. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_chart.html +1085 -0
  9. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_header.html +322 -0
  10. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_json_editor.html +60 -0
  11. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_overlays.html +83 -0
  12. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/partials/trial_viewer/_series_panel.html +1009 -0
  13. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/__init__.py +0 -0
  14. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/main.js +277 -0
  15. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/monitor.js +212 -0
  16. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/mosaic.js +42 -0
  17. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/toast.js +4 -0
  18. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/js/trial-viewer.js +1388 -0
  19. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/main.css +22 -0
  20. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/static/vendored/__init__.py +0 -0
  21. mujoco_mojo-2.2.2/src/mujoco_mojo/utils/layers/dojo/templates/trial_viewer.html +55 -0
  22. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/PKG-INFO +1 -1
  23. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/SOURCES.txt +18 -6
  24. mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/routers/mosaic.py +0 -208
  25. mujoco_mojo-2.2.1/src/mujoco_mojo/utils/layers/dojo/templates/trial_viewer.html +0 -2241
  26. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/LICENSE +0 -0
  27. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/README.md +0 -0
  28. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/setup.cfg +0 -0
  29. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/__about__.py +0 -0
  30. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/__init__.py +0 -0
  31. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/base.py +0 -0
  32. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/meta.py +0 -0
  33. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/__init__.py +0 -0
  34. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/defaults.py +0 -0
  35. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/dependency_path.py +0 -0
  36. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/extension.py +0 -0
  37. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/__init__.py +0 -0
  38. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/frame.py +0 -0
  39. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/include.py +0 -0
  40. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/meta/replicate.py +0 -0
  41. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco.py +0 -0
  42. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/__init__.py +0 -0
  43. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator.py +0 -0
  44. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/__init__.py +0 -0
  45. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/adhesion.py +0 -0
  46. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/base.py +0 -0
  47. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/cylinder.py +0 -0
  48. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/damper.py +0 -0
  49. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/dcmotor.py +0 -0
  50. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/general.py +0 -0
  51. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/intvelocity.py +0 -0
  52. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/motor.py +0 -0
  53. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/muscle.py +0 -0
  54. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/plugin.py +0 -0
  55. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/position.py +0 -0
  56. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/actuator_attr/velocity.py +0 -0
  57. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset.py +0 -0
  58. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/__init__.py +0 -0
  59. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/hfield.py +0 -0
  60. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material.py +0 -0
  61. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material_attr/__init__.py +0 -0
  62. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/material_attr/layer.py +0 -0
  63. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/mesh.py +0 -0
  64. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/model.py +0 -0
  65. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/asset_attr/texture.py +0 -0
  66. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body.py +0 -0
  67. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/__init__.py +0 -0
  68. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/attach.py +0 -0
  69. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/camera.py +0 -0
  70. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite.py +0 -0
  71. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/__init__.py +0 -0
  72. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/geom.py +0 -0
  73. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/joint.py +0 -0
  74. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/site.py +0 -0
  75. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/composite_attr/skin.py +0 -0
  76. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp.py +0 -0
  77. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/__init__.py +0 -0
  78. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/contact.py +0 -0
  79. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/edge.py +0 -0
  80. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/elasticity.py +0 -0
  81. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/flexcomp_attr/pin.py +0 -0
  82. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/free_joint.py +0 -0
  83. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/geom.py +0 -0
  84. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/inertial.py +0 -0
  85. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/joint.py +0 -0
  86. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/light.py +0 -0
  87. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/plugin.py +0 -0
  88. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/body_attr/site.py +0 -0
  89. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler.py +0 -0
  90. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler_attr/__init__.py +0 -0
  91. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/compiler_attr/lengthrange.py +0 -0
  92. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact.py +0 -0
  93. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/__init__.py +0 -0
  94. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/exclude.py +0 -0
  95. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/contact_attr/pair.py +0 -0
  96. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable.py +0 -0
  97. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/__init__.py +0 -0
  98. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex.py +0 -0
  99. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/__init__.py +0 -0
  100. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/contact.py +0 -0
  101. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/edge.py +0 -0
  102. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/flex_attr/elasticity.py +0 -0
  103. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin.py +0 -0
  104. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin_attr/__init__.py +0 -0
  105. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/deformable_attr/skin_attr/bone.py +0 -0
  106. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality.py +0 -0
  107. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/__init__.py +0 -0
  108. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/connect.py +0 -0
  109. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/equality_base.py +0 -0
  110. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flex.py +0 -0
  111. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flexstrain.py +0 -0
  112. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/flexvert.py +0 -0
  113. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/joint.py +0 -0
  114. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/tendon.py +0 -0
  115. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/equality_attr/weld.py +0 -0
  116. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe.py +0 -0
  117. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe_attr/__init__.py +0 -0
  118. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/keyframe_attr/key.py +0 -0
  119. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option.py +0 -0
  120. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option_attr/__init__.py +0 -0
  121. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/option_attr/flag.py +0 -0
  122. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor.py +0 -0
  123. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/__init__.py +0 -0
  124. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/accelerometer.py +0 -0
  125. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorfrc.py +0 -0
  126. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorpos.py +0 -0
  127. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/actuatorvel.py +0 -0
  128. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/ballangvel.py +0 -0
  129. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/ballquat.py +0 -0
  130. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/base.py +0 -0
  131. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/base_collision.py +0 -0
  132. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/camprojection.py +0 -0
  133. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/clock.py +0 -0
  134. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/contact.py +0 -0
  135. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/distance.py +0 -0
  136. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/e_kinetic.py +0 -0
  137. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/e_potential.py +0 -0
  138. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/force.py +0 -0
  139. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameangacc.py +0 -0
  140. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameangvel.py +0 -0
  141. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framelinacc.py +0 -0
  142. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framelinvel.py +0 -0
  143. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framepos.py +0 -0
  144. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framequat.py +0 -0
  145. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framexaxis.py +0 -0
  146. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/frameyaxis.py +0 -0
  147. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/framezaxis.py +0 -0
  148. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/fromto.py +0 -0
  149. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/gyro.py +0 -0
  150. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/insidesite.py +0 -0
  151. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointactuatorfrc.py +0 -0
  152. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitfrc.py +0 -0
  153. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitpos.py +0 -0
  154. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointlimitvel.py +0 -0
  155. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointpos.py +0 -0
  156. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/jointvel.py +0 -0
  157. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/magnetometer.py +0 -0
  158. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/normal.py +0 -0
  159. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/plugin.py +0 -0
  160. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/rangefinder.py +0 -0
  161. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreeangmom.py +0 -0
  162. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreecom.py +0 -0
  163. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/subtreelinvel.py +0 -0
  164. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tactile.py +0 -0
  165. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonactuatorfrc.py +0 -0
  166. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitfrc.py +0 -0
  167. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitpos.py +0 -0
  168. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonlimitvel.py +0 -0
  169. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonpos.py +0 -0
  170. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/tendonvel.py +0 -0
  171. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/torque.py +0 -0
  172. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/touch.py +0 -0
  173. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/user.py +0 -0
  174. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/sensor_attr/velocimeter.py +0 -0
  175. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/size.py +0 -0
  176. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/statistic.py +0 -0
  177. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon.py +0 -0
  178. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/__init__.py +0 -0
  179. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed.py +0 -0
  180. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed_attr/__init__.py +0 -0
  181. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/fixed_attr/joint.py +0 -0
  182. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial.py +0 -0
  183. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/__init__.py +0 -0
  184. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/geom.py +0 -0
  185. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/pulley.py +0 -0
  186. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/spatial_attr/site.py +0 -0
  187. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/tendon_attr/tendon_base.py +0 -0
  188. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual.py +0 -0
  189. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/__init__.py +0 -0
  190. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/global_.py +0 -0
  191. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/headlight.py +0 -0
  192. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/map.py +0 -0
  193. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/quality.py +0 -0
  194. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/rgba.py +0 -0
  195. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/mujoco_attr/visual_attr/scale.py +0 -0
  196. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/orientation.py +0 -0
  197. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/plugin.py +0 -0
  198. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/pose.py +0 -0
  199. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/position.py +0 -0
  200. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mjcf/xml_model.py +0 -0
  201. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/mojo_model.py +0 -0
  202. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/__init__.py +0 -0
  203. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/load.py +0 -0
  204. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/runtime_manager.py +0 -0
  205. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/signal_manager.py +0 -0
  206. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/runtime/video_recorder.py +0 -0
  207. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/stochas/__init__.py +0 -0
  208. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/typing.py +0 -0
  209. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/__init__.py +0 -0
  210. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/color.py +0 -0
  211. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/dataframe.py +0 -0
  212. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/defaults.py +0 -0
  213. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/filters/__init__.py +0 -0
  214. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/interp.py +0 -0
  215. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/__init__.py +0 -0
  216. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/cli.py +0 -0
  217. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/__init__.py +0 -0
  218. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/main.py +0 -0
  219. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/__init__.py +0 -0
  220. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/monitor.py +0 -0
  221. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/routers/morph.py +0 -0
  222. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/shared.py +0 -0
  223. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/__init__.py +0 -0
  224. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/error.html +0 -0
  225. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/morph.html +0 -0
  226. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/__init__.py +0 -0
  227. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/chime.mp3 +0 -0
  228. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/dark-logo.svg +0 -0
  229. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/light-logo.svg +0 -0
  230. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/main.js +0 -0
  231. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/monitor.js +0 -0
  232. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/mosaic.js +0 -0
  233. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/dojo/templates/static/trial-viewer.js +0 -0
  234. {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
  235. {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
  236. {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
  237. {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
  238. {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
  239. {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
  240. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/layers/reloaded.py +0 -0
  241. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/log.py +0 -0
  242. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/proximity.py +0 -0
  243. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/proximity_mixin.py +0 -0
  244. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/runner.py +0 -0
  245. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/statusing.py +0 -0
  246. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/utils/utils.py +0 -0
  247. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo/visualization.py +0 -0
  248. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/dependency_links.txt +0 -0
  249. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/entry_points.txt +0 -0
  250. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/requires.txt +0 -0
  251. {mujoco_mojo-2.2.1 → mujoco_mojo-2.2.2}/src/mujoco_mojo.egg-info/top_level.txt +0 -0
  252. {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.1
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.1"
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.scripts]
72
- mujoco-mojo = "mujoco_mojo.utils.layers.cli:cli_app"
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.optional-dependencies]
98
- reloaded = ["mjviser>=0.0.11", "viser>=1.0.26"]
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
- to_unit: SignalUnit
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")