reforge-core 2.0.2__tar.gz → 2.0.4__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 (176) hide show
  1. {reforge_core-2.0.2 → reforge_core-2.0.4}/PKG-INFO +1 -1
  2. {reforge_core-2.0.2 → reforge_core-2.0.4}/pyproject.toml +1 -1
  3. reforge_core-2.0.4/reforge_core/calibration/base_joint_planning.py +543 -0
  4. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/calibration/data_acquisition.py +654 -19
  5. reforge_core-2.0.4/reforge_core/calibration/run_helpers.py +1144 -0
  6. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/utility.py +179 -2
  7. {reforge_core-2.0.2 → reforge_core-2.0.4}/CMakeLists.txt +0 -0
  8. {reforge_core-2.0.2 → reforge_core-2.0.4}/README.md +0 -0
  9. {reforge_core-2.0.2 → reforge_core-2.0.4}/__init__.py +0 -0
  10. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/CMakeLists.txt +0 -0
  11. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/__init__.py +0 -0
  12. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/calibration/README.md +0 -0
  13. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/calibration/__init__.py +0 -0
  14. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/calibration/api.py +0 -0
  15. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/CMakeLists.txt +0 -0
  16. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/README.md +0 -0
  17. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/__init__.py +0 -0
  18. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/base_shaper.cpp +0 -0
  19. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/base_shaper.hpp +0 -0
  20. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/base_shaper_bindings.cpp +0 -0
  21. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/covalent_wrapper.cpp +0 -0
  22. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/covalent_wrapper.hpp +0 -0
  23. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/covalent_wrapper_binding.cpp +0 -0
  24. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/map_loader.cpp +0 -0
  25. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/include/map_loader.hpp +0 -0
  26. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/joint_tracker/__init__.py +0 -0
  27. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/joint_tracker/common_types.py +0 -0
  28. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/joint_tracker/joint_calibration_compensator.py +0 -0
  29. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/joint_tracker/joint_mpc_runtime.py +0 -0
  30. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/joint_tracker/tracker_interface.py +0 -0
  31. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/kinecal/__init__.py +0 -0
  32. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/kinecal/compensation.py +0 -0
  33. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/__init__.py +0 -0
  34. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/base_shaper.py +0 -0
  35. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/control_settings.py +0 -0
  36. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/covalent_wrapper.py +0 -0
  37. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/example_usage.py +0 -0
  38. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/lifted_mpc.py +0 -0
  39. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/map_loader.py +0 -0
  40. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/model_types.py +0 -0
  41. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/python/windowed_buffer.py +0 -0
  42. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/shaper/__init__.py +0 -0
  43. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/shaper/shaper_interface.py +0 -0
  44. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/control/shaper/shaper_window_helpers.py +0 -0
  45. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/hw_interfaces/arm_client.py +0 -0
  46. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/hw_interfaces/arm_imu_config.py +0 -0
  47. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/hw_interfaces/arm_imu_manager.py +0 -0
  48. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/hw_interfaces/arm_imu_time_calibration.py +0 -0
  49. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/hw_interfaces/imu_recorder.py +0 -0
  50. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/.gitignore +0 -0
  51. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/README.md +0 -0
  52. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/__init__.py +0 -0
  53. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/base_client.py +0 -0
  54. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/ble_client.py +0 -0
  55. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/client.py +0 -0
  56. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/data.py +0 -0
  57. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/frequency.py +0 -0
  58. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/full_scale.py +0 -0
  59. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/.gitignore +0 -0
  60. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/README.md +0 -0
  61. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/__init__.py +0 -0
  62. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/artifacts/.gitignore +0 -0
  63. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/baselines/README.md +0 -0
  64. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/conftest.py +0 -0
  65. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/test_data_integrity.py +0 -0
  66. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/test_sensor_full_scale_cycle.py +0 -0
  67. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/hwtests/test_transport_handoff.py +0 -0
  68. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/imu_codec.py +0 -0
  69. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/integrity.py +0 -0
  70. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/protocol.py +0 -0
  71. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/__init__.py +0 -0
  72. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/check_data_integrity.py +0 -0
  73. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/configure_device.py +0 -0
  74. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/diagnostics_plots/.gitignore +0 -0
  75. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/get_imu_stats.py +0 -0
  76. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/integrity_artifacts/.gitignore +0 -0
  77. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/plot_acquisition.py +0 -0
  78. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/probe_sensor_full_scale_limits.py +0 -0
  79. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/run_imu_diagnostics.py +0 -0
  80. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/tools/usb_diagnostics.py +0 -0
  81. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/transport_shared.py +0 -0
  82. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/types.py +0 -0
  83. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/usb_client.py +0 -0
  84. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/usb_transport.py +0 -0
  85. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/utils/__init__.py +0 -0
  86. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/utils/capture.py +0 -0
  87. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/imu/utils/sensor_full_scale_utils.py +0 -0
  88. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/README.md +0 -0
  89. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/__init__.py +0 -0
  90. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/artifacts.py +0 -0
  91. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/beep.py +0 -0
  92. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/cli.py +0 -0
  93. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/collision_checks.py +0 -0
  94. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/config.py +0 -0
  95. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/demo.py +0 -0
  96. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/export.py +0 -0
  97. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/interaction.py +0 -0
  98. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/kinematics.py +0 -0
  99. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/logging_setup.py +0 -0
  100. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/manual_teaching.py +0 -0
  101. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/options.py +0 -0
  102. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/outlier_detection.py +0 -0
  103. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/probe_params.toml +0 -0
  104. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/probed_urdf.py +0 -0
  105. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/recording_methods.py +0 -0
  106. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/references.py +0 -0
  107. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/result_cli.py +0 -0
  108. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/runner.py +0 -0
  109. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/socket_collection.py +0 -0
  110. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/tcp_config.py +0 -0
  111. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/tcp_sequence.py +0 -0
  112. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/transfers.py +0 -0
  113. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/kinecal/visualization.py +0 -0
  114. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/CMakeLists.txt +0 -0
  115. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/README.md +0 -0
  116. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/__init__.py +0 -0
  117. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/collision/__init__.py +0 -0
  118. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/collision/collision.py +0 -0
  119. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/geometry.py +0 -0
  120. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/native/CMakeLists.txt +0 -0
  121. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/native/include/robot_dynamics.cpp +0 -0
  122. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/native/include/robot_dynamics.hpp +0 -0
  123. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/native/include/robot_dynamics_bindings.cpp +0 -0
  124. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/notes/batched_dynamics/designNotes.md +0 -0
  125. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/notes/feedrate_timing/feedrate_math.md +0 -0
  126. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/paths.py +0 -0
  127. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/planning/__init__.py +0 -0
  128. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/planning/common.py +0 -0
  129. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/planning/joint_space_planning.py +0 -0
  130. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/robot_dynamics.py +0 -0
  131. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/robot_dynamics_original.py +0 -0
  132. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/shared_api_utils.py +0 -0
  133. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/README.md +0 -0
  134. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/__init__.py +0 -0
  135. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/joint_limits.py +0 -0
  136. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/joint_trajectory.py +0 -0
  137. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/path_timing.py +0 -0
  138. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/resampling.py +0 -0
  139. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/retiming.py +0 -0
  140. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/timing_utils.py +0 -0
  141. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/timing/trajectory_plotting.py +0 -0
  142. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/trajectory.py +0 -0
  143. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/trajopt/__init__.py +0 -0
  144. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/trajopt/traj_qp.py +0 -0
  145. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/units.py +0 -0
  146. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/urdf_to_dh/generate_dh.py +0 -0
  147. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/urdf_to_dh/geometry_helpers.py +0 -0
  148. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/urdf_to_dh/kinematics_helpers.py +0 -0
  149. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/urdf_to_dh/urdf_helpers.py +0 -0
  150. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/validate_shaper.py +0 -0
  151. {reforge_core-2.0.2 → reforge_core-2.0.4}/reforge_core/util/vibration_test.py +0 -0
  152. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/calibration/test_api.py +0 -0
  153. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/control/test_lifted_mpc.py +0 -0
  154. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/imu/test_client.py +0 -0
  155. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/imu/test_tools.py +0 -0
  156. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_artifacts.py +0 -0
  157. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_collision_checks.py +0 -0
  158. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_compensation.py +0 -0
  159. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_datacol_cli.py +0 -0
  160. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_datacol_demo_run.py +0 -0
  161. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_datacol_entrypoint.py +0 -0
  162. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_kinematics.py +0 -0
  163. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_outlier_detection.py +0 -0
  164. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_probed_urdf.py +0 -0
  165. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_recording_methods.py +0 -0
  166. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_socket_collection.py +0 -0
  167. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/kinecal/test_transfers.py +0 -0
  168. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_cartesian_vibration_test.py +0 -0
  169. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_collision_model.py +0 -0
  170. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_geometry.py +0 -0
  171. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_joint_limits.py +0 -0
  172. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_kinecal_logging_setup.py +0 -0
  173. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_planning.py +0 -0
  174. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_resampling.py +0 -0
  175. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_trajectory_plotting.py +0 -0
  176. {reforge_core-2.0.2 → reforge_core-2.0.4}/tests/util/test_trajopt_qp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: reforge-core
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Reforge Core SDK by Reforge Robotics.
5
5
  Author-Email: Reforge Robotics <info@reforgerobotics.com>
6
6
  License: MIT
@@ -9,7 +9,7 @@ build-backend = "scikit_build_core.build"
9
9
 
10
10
  [project]
11
11
  name = "reforge-core"
12
- version = "2.0.2"
12
+ version = "2.0.4"
13
13
  description = "Reforge Core SDK by Reforge Robotics."
14
14
  readme = "README.md"
15
15
  requires-python = ">=3.11"
@@ -0,0 +1,543 @@
1
+ """Pure planning helpers for base-joint shaper calibration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from dataclasses import dataclass
7
+ from itertools import product
8
+ import math
9
+
10
+ import numpy as np
11
+
12
+ BASE_JOINT_AXIS_PARALLEL_ATOL = 1.0e-6
13
+ CALIBRATION_SCHEMA_VERSION = 2
14
+ DEFAULT_BASE_JOINT_ANGLE_COUNT = 4
15
+ MAX_SAVED_SAMPLE_FREQUENCY_HZ = 500.0
16
+ RUN_ROLE_NORMAL_FULL_AXIS = "normal_full_axis"
17
+ RUN_ROLE_BASE_ONLY_AXIS = "base_only_axis"
18
+ MOTION_MODE_SINE_SWEEP = "sine_sweep"
19
+ MOTION_MODE_POINT_TO_POINT_TEST = "point_to_point_test"
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class BaseJointLimit:
24
+ """Store one base-joint lower and upper position limit.
25
+
26
+ Args:
27
+ lower_rad: Lower position limit [rad].
28
+ upper_rad: Upper position limit [rad].
29
+ """
30
+
31
+ lower_rad: float
32
+ upper_rad: float
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class BaseAngleGrid:
37
+ """Describe sampled base-joint angles and the margin rule used.
38
+
39
+ Args:
40
+ angles_rad: Grid positions for one base joint [rad].
41
+ margin_source: Rule used to avoid joint-limit endpoints.
42
+ """
43
+
44
+ angles_rad: tuple[float, ...]
45
+ margin_source: str
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class CalibrationRunDescriptor:
50
+ """Describe one planned calibration run before robot motion.
51
+
52
+ Args:
53
+ run_id: Stable logical run identifier.
54
+ run_role: Run role such as `normal_full_axis` or `base_only_axis`.
55
+ motion_mode: Execution mode such as `sine_sweep` or `point_to_point_test`.
56
+ swept_axis: Axis commanded by this run [joint index].
57
+ pose_index: Pose-grid index [count].
58
+ axis_index: Axis-loop index within the role [count].
59
+ base_joint_angles_rad: Active base-joint coordinates [rad].
60
+ v_rad: Pose elevation angle from horizontal [rad].
61
+ r_m: Pose radius from base [m].
62
+ uses_joint_tracker_mpc: Whether joint-tracker MPC compensation is planned.
63
+ """
64
+
65
+ run_id: str
66
+ run_role: str
67
+ motion_mode: str
68
+ swept_axis: int
69
+ pose_index: int
70
+ axis_index: int
71
+ base_joint_angles_rad: tuple[float, ...]
72
+ v_rad: float
73
+ r_m: float
74
+ uses_joint_tracker_mpc: bool = False
75
+
76
+ @property
77
+ def v_deg(self) -> float:
78
+ """Return the planned pose elevation angle in degrees.
79
+
80
+ Returns:
81
+ `float` pose elevation angle [deg].
82
+ """
83
+ return math.degrees(self.v_rad)
84
+
85
+ @property
86
+ def r_mm(self) -> float:
87
+ """Return the planned pose radius in millimeters.
88
+
89
+ Returns:
90
+ `float` pose radius [mm].
91
+ """
92
+ return 1000.0 * self.r_m
93
+
94
+
95
+ def parse_base_joint_limit_specs(
96
+ limit_specs: Sequence[str] | None,
97
+ ) -> list[BaseJointLimit]:
98
+ """Parse CLI base-joint limit strings into typed lower/upper pairs.
99
+
100
+ Args:
101
+ limit_specs: Strings formatted as `"lower,upper"` in radians.
102
+ Returns:
103
+ `list[BaseJointLimit]` parsed limits [rad].
104
+ """
105
+ if not limit_specs:
106
+ return []
107
+
108
+ limits = []
109
+ for index, limit_spec in enumerate(limit_specs):
110
+ pieces = [piece.strip() for piece in limit_spec.split(",")]
111
+ if len(pieces) != 2:
112
+ raise ValueError(
113
+ f"Base joint limit {index} must contain exactly two comma-separated values."
114
+ )
115
+ lower_rad, upper_rad = (float(pieces[0]), float(pieces[1]))
116
+ if not (math.isfinite(lower_rad) and math.isfinite(upper_rad)):
117
+ raise ValueError(f"Base joint limit {index} values must be finite.")
118
+ if lower_rad >= upper_rad:
119
+ raise ValueError(
120
+ f"Base joint limit {index} lower bound must be less than upper bound."
121
+ )
122
+ limits.append(BaseJointLimit(lower_rad=lower_rad, upper_rad=upper_rad))
123
+ return limits
124
+
125
+
126
+ def serialize_float_sequence(values: Sequence[float]) -> str:
127
+ """Serialize a float sequence as a semicolon-delimited metadata field.
128
+
129
+ Args:
130
+ values: Values to serialize.
131
+ Returns:
132
+ `str` semicolon-delimited float values.
133
+ """
134
+ return ";".join(f"{float(value):.17g}" for value in values)
135
+
136
+
137
+ def deserialize_float_sequence(value: str) -> list[float]:
138
+ """Deserialize a semicolon-delimited metadata field into floats.
139
+
140
+ Args:
141
+ value: Semicolon-delimited float values.
142
+ Returns:
143
+ `list[float]` parsed values.
144
+ Raises:
145
+ ValueError: If any entry cannot be parsed as a float.
146
+ """
147
+ if value == "":
148
+ return []
149
+ return [float(piece) for piece in value.split(";") if piece != ""]
150
+
151
+
152
+ def serialize_int_sequence(values: Sequence[int]) -> str:
153
+ """Serialize an integer sequence as a semicolon-delimited metadata field.
154
+
155
+ Args:
156
+ values: Values to serialize.
157
+ Returns:
158
+ `str` semicolon-delimited integer values.
159
+ """
160
+ return ";".join(str(int(value)) for value in values)
161
+
162
+
163
+ def build_shaper_feature_metadata(base_joint_count: int) -> tuple[str, str]:
164
+ """Return serialized feature names and units for shaper model inputs.
165
+
166
+ Args:
167
+ base_joint_count: Number of consecutive base joints [count].
168
+ Returns:
169
+ `tuple[str, str]` serialized feature names and units.
170
+ Raises:
171
+ ValueError: If `base_joint_count` is less than one.
172
+ """
173
+ if base_joint_count < 1:
174
+ raise ValueError("base_joint_count must be >= 1.")
175
+ feature_names = [f"j{index}_rad" for index in range(base_joint_count)]
176
+ feature_names.extend(["v_deg", "r_mm", "inertia"])
177
+ feature_units = ["rad" for _ in range(base_joint_count)]
178
+ feature_units.extend(["deg", "mm", "kg_m2"])
179
+ return ";".join(feature_names), ";".join(feature_units)
180
+
181
+
182
+ def build_base_angle_grid(
183
+ lower_rad: float,
184
+ upper_rad: float,
185
+ home_rad: float,
186
+ max_displacement_rad: float,
187
+ base_joint_angle_count: int,
188
+ ) -> BaseAngleGrid:
189
+ """Build a base-joint angle grid while avoiding joint-limit endpoints.
190
+
191
+ Args:
192
+ lower_rad: Lower joint position limit [rad].
193
+ upper_rad: Upper joint position limit [rad].
194
+ home_rad: Reference/home joint angle [rad].
195
+ max_displacement_rad: Maximum sine displacement used as endpoint margin [rad].
196
+ base_joint_angle_count: Number of grid samples [count].
197
+ Returns:
198
+ `BaseAngleGrid` positions and margin rule.
199
+ Raises:
200
+ ValueError: If limits, displacement, or count are invalid.
201
+ """
202
+ if base_joint_angle_count <= 0:
203
+ raise ValueError("base_joint_angle_count must be > 0.")
204
+ if not lower_rad < upper_rad:
205
+ raise ValueError("Base joint lower limit must be less than upper limit.")
206
+ if max_displacement_rad < 0.0 or not math.isfinite(max_displacement_rad):
207
+ raise ValueError("max_displacement_rad must be finite and non-negative.")
208
+
209
+ joint_range = upper_rad - lower_rad
210
+ if joint_range > 2.0 * max_displacement_rad:
211
+ start = lower_rad + max_displacement_rad
212
+ stop = upper_rad - max_displacement_rad
213
+ margin_source = "max_displacement"
214
+ elif joint_range > max_displacement_rad:
215
+ half_margin = max_displacement_rad / 2.0
216
+ start = lower_rad + half_margin
217
+ stop = upper_rad - half_margin
218
+ margin_source = "half_max_displacement"
219
+ else:
220
+ if base_joint_angle_count == 1:
221
+ clipped_home = min(max(home_rad, lower_rad), upper_rad)
222
+ return BaseAngleGrid((float(clipped_home),), "home_fallback")
223
+ spacing = joint_range / float(base_joint_angle_count + 1)
224
+ start = lower_rad + spacing
225
+ stop = upper_rad - spacing
226
+ margin_source = "interior_fallback"
227
+
228
+ angles = np.linspace(start, stop, base_joint_angle_count, dtype=float)
229
+ return BaseAngleGrid(tuple(float(angle) for angle in angles), margin_source)
230
+
231
+
232
+ def validate_base_joint_configuration(
233
+ num_joints: int,
234
+ axes_to_command: int,
235
+ base_joint_count: int,
236
+ joint_axes: Sequence[Sequence[float]],
237
+ joint_limits: tuple[Sequence[float], Sequence[float]],
238
+ explicit_limits: Sequence[BaseJointLimit] | None = None,
239
+ axis_parallel_atol: float = BASE_JOINT_AXIS_PARALLEL_ATOL,
240
+ ) -> list[BaseJointLimit]:
241
+ """Validate base-joint count, axes, and limits for calibration planning.
242
+
243
+ Args:
244
+ num_joints: Robot joint count [count].
245
+ axes_to_command: Number of shaped axes in the calibration [count].
246
+ base_joint_count: Consecutive base-joint count starting at index zero [count].
247
+ joint_axes: World-frame joint axes for the reference configuration.
248
+ joint_limits: Lower and upper joint limits for all joints [rad].
249
+ explicit_limits: Optional CLI-provided limits for leading base joints [rad].
250
+ axis_parallel_atol: Absolute tolerance for z-parallel axis checks.
251
+ Returns:
252
+ `list[BaseJointLimit]` resolved base-joint limits [rad].
253
+ Raises:
254
+ ValueError: If the requested base-joint configuration is unsupported.
255
+ """
256
+ if base_joint_count < 1:
257
+ raise ValueError("base_joint_count must be >= 1.")
258
+ if base_joint_count > num_joints:
259
+ raise ValueError("base_joint_count must be <= num_joints.")
260
+ if base_joint_count > axes_to_command:
261
+ raise ValueError("base_joint_count must be <= axes_to_command.")
262
+ if len(joint_axes) < base_joint_count:
263
+ raise ValueError("Not enough joint axes to validate requested base joints.")
264
+
265
+ lower_limits, upper_limits = joint_limits
266
+ explicit_limits = explicit_limits or []
267
+ resolved_limits = []
268
+ world_z = np.array([0.0, 0.0, 1.0], dtype=float)
269
+
270
+ for joint_index in range(base_joint_count):
271
+ axis = np.asarray(joint_axes[joint_index], dtype=float)
272
+ norm = np.linalg.norm(axis)
273
+ if norm <= 0.0:
274
+ raise ValueError(f"Base joint {joint_index} axis has zero length.")
275
+ normalized_axis = axis / norm
276
+ is_parallel = np.allclose(
277
+ np.abs(normalized_axis), world_z, atol=axis_parallel_atol
278
+ )
279
+ if not is_parallel:
280
+ raise ValueError(
281
+ f"Base joint {joint_index} axis is not parallel to world z."
282
+ )
283
+
284
+ if joint_index < len(explicit_limits):
285
+ resolved_limits.append(explicit_limits[joint_index])
286
+ continue
287
+
288
+ lower_rad = float(lower_limits[joint_index])
289
+ upper_rad = float(upper_limits[joint_index])
290
+ if not (
291
+ math.isfinite(lower_rad)
292
+ and math.isfinite(upper_rad)
293
+ and lower_rad < upper_rad
294
+ ):
295
+ raise ValueError(
296
+ f"Base joint {joint_index} is missing finite lower/upper limits; "
297
+ "provide --base_joint_limits."
298
+ )
299
+ resolved_limits.append(BaseJointLimit(lower_rad=lower_rad, upper_rad=upper_rad))
300
+
301
+ return resolved_limits
302
+
303
+
304
+ def resolve_base_joint_limits_from_model(
305
+ robot_model: object,
306
+ reference_joint_angles: Sequence[float],
307
+ axes_to_command: int,
308
+ base_joint_count: int,
309
+ explicit_limits: Sequence[BaseJointLimit] | None = None,
310
+ ) -> list[BaseJointLimit]:
311
+ """Validate base joints using the dynamics model API and return limits.
312
+
313
+ Args:
314
+ robot_model: Dynamics-like object exposing `joint_limits` and
315
+ `get_joint_axes(reference_joint_angles)`.
316
+ reference_joint_angles: Reference joint configuration [rad].
317
+ axes_to_command: Number of shaped axes [count].
318
+ base_joint_count: Consecutive base-joint count starting at joint zero [count].
319
+ explicit_limits: Optional CLI-provided limits for leading base joints [rad].
320
+ Returns:
321
+ `list[BaseJointLimit]` resolved limits [rad].
322
+ Raises:
323
+ RuntimeError: If the robot model is missing.
324
+ ValueError: If model limits or world-frame axes do not satisfy the base-joint rules.
325
+ """
326
+ if robot_model is None:
327
+ raise RuntimeError(
328
+ "Base-joint validation requires a loaded robot dynamics model."
329
+ )
330
+ num_joints = int(getattr(robot_model, "num_joints"))
331
+ joint_axes = robot_model.get_joint_axes(reference_joint_angles)
332
+ joint_limits = robot_model.joint_limits
333
+ return validate_base_joint_configuration(
334
+ num_joints=num_joints,
335
+ axes_to_command=axes_to_command,
336
+ base_joint_count=base_joint_count,
337
+ joint_axes=joint_axes,
338
+ joint_limits=joint_limits,
339
+ explicit_limits=explicit_limits,
340
+ )
341
+
342
+
343
+ def build_base_angle_grids(
344
+ base_joint_limits: Sequence[BaseJointLimit],
345
+ reference_base_angles_rad: Sequence[float],
346
+ max_displacement_rad: float,
347
+ base_joint_angle_count: int,
348
+ ) -> list[BaseAngleGrid]:
349
+ """Build one endpoint-safe base-angle grid per base joint.
350
+
351
+ Args:
352
+ base_joint_limits: Base joint limits [rad].
353
+ reference_base_angles_rad: Reference/home angles for base joints [rad].
354
+ max_displacement_rad: Maximum sine displacement used as endpoint margin [rad].
355
+ base_joint_angle_count: Number of grid samples per base joint [count].
356
+ Returns:
357
+ `list[BaseAngleGrid]` one grid per base joint.
358
+ Raises:
359
+ ValueError: If inputs are inconsistent or invalid.
360
+ """
361
+ if len(base_joint_limits) != len(reference_base_angles_rad):
362
+ raise ValueError(
363
+ "Base joint limits and reference angles must have equal length."
364
+ )
365
+ return [
366
+ build_base_angle_grid(
367
+ lower_rad=limit.lower_rad,
368
+ upper_rad=limit.upper_rad,
369
+ home_rad=reference_base_angles_rad[index],
370
+ max_displacement_rad=max_displacement_rad,
371
+ base_joint_angle_count=base_joint_angle_count,
372
+ )
373
+ for index, limit in enumerate(base_joint_limits)
374
+ ]
375
+
376
+
377
+ def estimate_shaper_run_count(
378
+ base_joint_count: int,
379
+ base_joint_angle_count: int,
380
+ pose_count: int,
381
+ axes_to_command: int,
382
+ includes_joint_tracker_mpc: bool = False,
383
+ ) -> int:
384
+ """Estimate planned shaper calibration run count.
385
+
386
+ Args:
387
+ base_joint_count: Consecutive base-joint count [count].
388
+ base_joint_angle_count: Grid samples per base joint [count].
389
+ pose_count: Number of `V`, `R` poses [count].
390
+ axes_to_command: Number of normal full-axis sweeps [count].
391
+ includes_joint_tracker_mpc: Whether a one-pose joint-tracker prepass is included.
392
+ Returns:
393
+ `int` total planned sweep count.
394
+ Raises:
395
+ ValueError: If any count is invalid.
396
+ """
397
+ if min(base_joint_count, base_joint_angle_count, pose_count, axes_to_command) <= 0:
398
+ raise ValueError("All run-count inputs must be positive.")
399
+ normal_count = pose_count * axes_to_command
400
+ base_count = sum(
401
+ (base_joint_angle_count ** (base_joint_count - index)) * pose_count
402
+ for index in range(base_joint_count)
403
+ )
404
+ joint_tracker_count = axes_to_command if includes_joint_tracker_mpc else 0
405
+ return int(joint_tracker_count + normal_count + base_count)
406
+
407
+
408
+ def generate_calibration_run_descriptors(
409
+ v_list: Sequence[float],
410
+ r_list: Sequence[float],
411
+ axes_to_command: int,
412
+ start_axis: int,
413
+ reference_base_angles_rad: Sequence[float],
414
+ base_angle_grids: Sequence[BaseAngleGrid],
415
+ test_mode: bool = False,
416
+ uses_joint_tracker_mpc: bool = False,
417
+ ) -> list[CalibrationRunDescriptor]:
418
+ """Generate ordered normal and base-grid calibration run descriptors.
419
+
420
+ Args:
421
+ v_list: Pose-grid elevation angles [rad].
422
+ r_list: Pose-grid radii [m].
423
+ axes_to_command: Number of normal full-axis sweeps [count].
424
+ start_axis: First commanded axis [joint index].
425
+ reference_base_angles_rad: Fixed reference base-joint angles [rad].
426
+ base_angle_grids: Base-joint grids [rad].
427
+ test_mode: Whether descriptors represent point-to-point test motion.
428
+ uses_joint_tracker_mpc: Whether joint-tracker MPC compensation is planned.
429
+ Returns:
430
+ `list[CalibrationRunDescriptor]` ordered planned runs.
431
+ Raises:
432
+ ValueError: If the base metadata is inconsistent.
433
+ """
434
+ base_joint_count = len(base_angle_grids)
435
+ if base_joint_count < 1:
436
+ raise ValueError("At least one base angle grid is required.")
437
+ if len(reference_base_angles_rad) != base_joint_count:
438
+ raise ValueError("Reference base angles must match base angle grid count.")
439
+
440
+ motion_mode = (
441
+ MOTION_MODE_POINT_TO_POINT_TEST if test_mode else MOTION_MODE_SINE_SWEEP
442
+ )
443
+ pose_pairs = [
444
+ (pose_index, float(v_value), float(r_value))
445
+ for pose_index, (v_value, r_value) in enumerate(product(v_list, r_list))
446
+ ]
447
+ descriptors = []
448
+
449
+ for pose_index, v_value, r_value in pose_pairs:
450
+ for axis_offset, move_axis in enumerate(
451
+ range(start_axis, start_axis + axes_to_command)
452
+ ):
453
+ descriptors.append(
454
+ CalibrationRunDescriptor(
455
+ run_id=f"normal-p{pose_index:04d}-a{move_axis:02d}",
456
+ run_role=RUN_ROLE_NORMAL_FULL_AXIS,
457
+ motion_mode=motion_mode,
458
+ swept_axis=move_axis,
459
+ pose_index=pose_index,
460
+ axis_index=axis_offset,
461
+ base_joint_angles_rad=tuple(
462
+ float(v) for v in reference_base_angles_rad
463
+ ),
464
+ v_rad=v_value,
465
+ r_m=r_value,
466
+ uses_joint_tracker_mpc=uses_joint_tracker_mpc,
467
+ )
468
+ )
469
+
470
+ for swept_base_joint in reversed(range(base_joint_count)):
471
+ downstream_grid_values = [
472
+ base_angle_grids[index].angles_rad
473
+ for index in range(swept_base_joint, base_joint_count)
474
+ ]
475
+ for permutation_index, downstream_angles in enumerate(
476
+ product(*downstream_grid_values)
477
+ ):
478
+ base_angles = list(float(value) for value in reference_base_angles_rad)
479
+ for offset, angle_rad in enumerate(downstream_angles):
480
+ base_angles[swept_base_joint + offset] = float(angle_rad)
481
+ for pose_index, v_value, r_value in pose_pairs:
482
+ descriptors.append(
483
+ CalibrationRunDescriptor(
484
+ run_id=(
485
+ f"base-j{swept_base_joint}-g{permutation_index:04d}"
486
+ f"-p{pose_index:04d}"
487
+ ),
488
+ run_role=RUN_ROLE_BASE_ONLY_AXIS,
489
+ motion_mode=motion_mode,
490
+ swept_axis=swept_base_joint,
491
+ pose_index=pose_index,
492
+ axis_index=swept_base_joint,
493
+ base_joint_angles_rad=tuple(base_angles),
494
+ v_rad=v_value,
495
+ r_m=r_value,
496
+ uses_joint_tracker_mpc=uses_joint_tracker_mpc,
497
+ )
498
+ )
499
+ return descriptors
500
+
501
+
502
+ def resolve_saved_sample_frequency(sample_frequency_hz: float) -> float:
503
+ """Return the sample frequency used for persisted calibration files.
504
+
505
+ Args:
506
+ sample_frequency_hz: Acquisition sample frequency [Hz].
507
+ Returns:
508
+ `float` saved sample frequency [Hz].
509
+ Raises:
510
+ ValueError: If `sample_frequency_hz` is not positive and finite.
511
+ """
512
+ if sample_frequency_hz <= 0.0 or not math.isfinite(sample_frequency_hz):
513
+ raise ValueError("sample_frequency_hz must be positive and finite.")
514
+ return min(float(sample_frequency_hz), MAX_SAVED_SAMPLE_FREQUENCY_HZ)
515
+
516
+
517
+ def downsample_indices_for_storage(
518
+ sample_count: int, sample_frequency_hz: float
519
+ ) -> np.ndarray:
520
+ """Return row indices that cap saved calibration data at 500 Hz.
521
+
522
+ Args:
523
+ sample_count: Number of recorded samples [count].
524
+ sample_frequency_hz: Acquisition sample frequency [Hz].
525
+ Returns:
526
+ `np.ndarray` integer sample indices to keep.
527
+ Raises:
528
+ ValueError: If inputs are invalid.
529
+ """
530
+ if sample_count < 0:
531
+ raise ValueError("sample_count must be >= 0.")
532
+ if sample_count == 0:
533
+ return np.array([], dtype=int)
534
+ saved_frequency_hz = resolve_saved_sample_frequency(sample_frequency_hz)
535
+ if sample_frequency_hz <= saved_frequency_hz:
536
+ return np.arange(sample_count, dtype=int)
537
+
538
+ step = sample_frequency_hz / saved_frequency_hz
539
+ indices = np.floor(np.arange(0.0, float(sample_count), step)).astype(int)
540
+ indices = np.unique(np.clip(indices, 0, sample_count - 1))
541
+ if indices[-1] != sample_count - 1:
542
+ indices = np.append(indices, sample_count - 1)
543
+ return indices.astype(int)