diff --git a/.gitignore b/.gitignore index f6ee3643d..c460c06eb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ log/ .vscode/ MUJOCO_LOG.TXT .ccache/ +__pycache__/ +.pytest_cache/ +.venv/ diff --git a/docker-compose.yaml b/docker-compose.yaml index ce6e08906..8686bd397 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,11 @@ services: base: {} # Starts the MoveIt Pro Agent and the Bridge between the Agent and the Web UI. - agent_bridge: {} + agent_bridge: + environment: + # /joint_states carries all fifteen joints; the recorded `observation.state` + # must be the eight the policies use. See script/joint_command_bridge.py. + - MOVEIT_PRO_TRAIN_JOINT_STATES_TOPIC=/observed_joint_states # Starts the robot drivers. drivers: @@ -23,6 +27,10 @@ services: # Developer specific configuration when running `moveit_pro dev`. dev: + environment: + # /joint_states carries all fifteen joints; the recorded `observation.state` + # must be the eight the policies use. See script/joint_command_bridge.py. + - MOVEIT_PRO_TRAIN_JOINT_STATES_TOPIC=/observed_joint_states volumes: # Allow access to host hardware e.g. RealSense cameras - /dev:/dev diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/CMakeLists.txt b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/CMakeLists.txt new file mode 100644 index 000000000..24670efce --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.22) +project(kinova_vla_test_sim) + +find_package(ament_cmake REQUIRED) + +install( + DIRECTORY + config + description + launch + objectives + DESTINATION + share/${PROJECT_NAME} +) + +install(PROGRAMS + script/get_action_chunk_adapter.py + script/joint_command_bridge.py + DESTINATION lib/${PROJECT_NAME} +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + find_package(ament_cmake_pytest REQUIRED) + set(_pytest_tests + test/test_get_action_chunk_adapter.py + test/test_joint_command_bridge.py) + + foreach(_test_path ${_pytest_tests}) + get_filename_component(_test_name ${_test_path} NAME_WE) + ament_add_pytest_test(${_test_name} ${_test_path} + APPEND_ENV PYTHONPATH=${CMAKE_CURRENT_BINARY_DIR} + TIMEOUT 60 + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + endforeach() + + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/README.md b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/README.md new file mode 100644 index 000000000..f3f4e46cb --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/README.md @@ -0,0 +1,8 @@ +# kinova_vla_test_sim + +A MoveIt Pro configuration to simulate a Kinova Gen3 (7-DoF) + Robotiq 2F-85 stacking colored cubes in MuJoCo, for testing VLA policy execution. + +- `train/`: how the pi0.5 checkpoint is trained, as a config file plus a pinned environment +- `docker/`: how that checkpoint is served to the `Execute Color-Stack Policy` objective + +For detailed documentation see: [MoveIt Pro Documentation](https://docs.picknik.ai/) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/config.yaml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/config.yaml new file mode 100644 index 000000000..b67fb8b51 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/config.yaml @@ -0,0 +1,48 @@ +############################################################### +# +# This configures the robot to work with MoveIt Pro +# +############################################################### + +based_on_package: "kinova_sim" + +hardware: + simulated: True + launch_control_node: True + launch_robot_state_publisher: True + simulated_hardware_launch_file: + package: "kinova_vla_test_sim" + path: "launch/simulated_extras.launch.py" + + robot_description: + urdf: + package: "kinova_vla_test_sim" + path: "description/picknik_kinova_gen3.xacro" + srdf: + package: "kinova_vla_test_sim" + path: "config/moveit/picknik_kinova_gen3.srdf" + urdf_params: + - mujoco_keyframe: "default" + - arm: "gen3" + - dof: "7" + - vision: "false" + - gripper: "robotiq_2f_85" + - use_fake_hardware: "%>> hardware.simulated" + - fake_sensor_commands: "false" + - use_internal_bus_gripper_comm: "false" + - mujoco_model: "description/mujoco/scene.xml" + - mujoco_viewer: false + +moveit_params: + joint_limits: + package: "kinova_vla_test_sim" + path: "config/moveit/joint_limits.yaml" + +objectives: + behavior_loader_plugins: + kinova_vla_test_sim: + - "kinova_vla_test_sim_behaviors::KinovaVlaTestSimBehaviorsLoader" + objective_library_paths: + colorstack_objectives: + package_name: "kinova_vla_test_sim" + relative_path: "objectives" diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/joint_limits.yaml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/joint_limits.yaml new file mode 100644 index 000000000..13199dd35 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/joint_limits.yaml @@ -0,0 +1,46 @@ +# joint_limits.yaml allows the dynamics properties specified in the URDF to be overwritten or augmented as needed +# Specific joint properties can be changed with the keys [max_position, min_position, max_velocity, max_acceleration] +# Joint limits can be turned off with [has_velocity_limits, has_acceleration_limits] +# +# Matches external_dependencies/ros2_kortex/kortex_moveit_config/ +# kinova_gen3_7dof_robotiq_2f_85_moveit_config/config/joint_limits.yaml (the vendor's +# own MoveIt config for this arm+gripper combo), rather than kinova_sim's conservative +# sim defaults, so ExecutePolicy's velocity/acceleration gate doesn't reject +# policy-commanded trajectories that are within hardware limits but above the sim +# defaults. +joint_limits: + joint_1: + has_velocity_limits: true + max_velocity: 1.3963 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_2: + has_velocity_limits: true + max_velocity: 1.3963 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_3: + has_velocity_limits: true + max_velocity: 1.3963 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_4: + has_velocity_limits: true + max_velocity: 1.3963 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_5: + has_velocity_limits: true + max_velocity: 1.2218 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_6: + has_velocity_limits: true + max_velocity: 1.2218 + has_acceleration_limits: true + max_acceleration: 8.6 + joint_7: + has_velocity_limits: true + max_velocity: 1.2218 + has_acceleration_limits: true + max_acceleration: 8.6 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/picknik_kinova_gen3.srdf b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/picknik_kinova_gen3.srdf new file mode 100644 index 000000000..0585e62ad --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/config/moveit/picknik_kinova_gen3.srdf @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.STL new file mode 100644 index 000000000..5f6aff0d1 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e38517e653d58f4160cfdcee34c9dea983d8b3ecb8f2c147444f58235335260 +size 183884 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.mtl new file mode 100644 index 000000000..a771bd2d5 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.mtl @@ -0,0 +1,22 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl Blue_002 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.000000 0.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 + +newmtl White_002 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.obj new file mode 100644 index 000000000..502aa742f --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/base_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d209049c7784b700dacd3ff6b92e5a5e653b175b81cd40cdc3ac08c2c4115a63 +size 221550 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.STL new file mode 100644 index 000000000..705d64f18 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b2ec7dafbf6a3c9a2cac717d18132a8a1efd9fcb8f2c40d9c12c8ece7323ec8 +size 1334384 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.mtl new file mode 100644 index 000000000..66af0bf77 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.obj new file mode 100644 index 000000000..a17b9d033 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_no_vision_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:190b2a97e289f208cd1e9cc053d1fdd2d7bd147909e026784d3a854d473b0c20 +size 1362966 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.STL new file mode 100644 index 000000000..72f81c50b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bb05e420a88532b4eee3948a0047fb8e7f341c3e5cc971d54e97e2aaffcf878 +size 1164084 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.mtl new file mode 100644 index 000000000..fdb35325d --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl white +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.obj new file mode 100644 index 000000000..66914c558 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:479465e12fa09738aa1853d6033839e61bb2f8976088f2b180842feca3fe39d7 +size 1560243 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_0.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_0.obj new file mode 100644 index 000000000..696d39d25 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_0.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:200c5946064cb5c5219057c7af7f5a3a7c638ce5058d8f62ea434a00ea45a33a +size 95754 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_1.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_1.obj new file mode 100644 index 000000000..7fa43cd2b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:120e230afb4145c9cdcd19f21086456508e7b6b83800511555a3bea1cdc84362 +size 5220 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_10.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_10.obj new file mode 100644 index 000000000..f913d1b8c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_10.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e24cf26a6a62031d3ccb3602949c21f89b5249b19c78d974ef4fc348ff85f52 +size 14375 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_11.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_11.obj new file mode 100644 index 000000000..6d8321db5 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_11.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e7a61525c6148600ce9d0269c75fbb32e7224426407b13cede83b12bc8bedc8 +size 21037 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_12.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_12.obj new file mode 100644 index 000000000..86702f43a --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_12.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efa7b3b5c50fe5841425048071dbb74489a40a88ca1b42bc4ef13f19dd1c1600 +size 18640 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_13.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_13.obj new file mode 100644 index 000000000..b7703742d --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_13.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8391b80722e39ed690f938260c7e85dbf551d4c2ac011d9e953c918339ed650e +size 1428 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_14.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_14.obj new file mode 100644 index 000000000..28307b6a8 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_14.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c718a92fa738801de98ff2c6e1c056abb253127759c1e04a785fffb10edec6f7 +size 23919 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_15.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_15.obj new file mode 100644 index 000000000..8951a0e5e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_15.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3502178cd329fa8cddc20d18c53326d7910ce395460b0b9a92b1c95817df26b5 +size 7435 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_16.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_16.obj new file mode 100644 index 000000000..66e1ad113 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_16.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c710c70d5393bed97cb30627b2095cc56acb628837c94d219e94115624ee454a +size 17957 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_17.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_17.obj new file mode 100644 index 000000000..95ce5a363 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_17.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c131978369b705f13e1add72c239364b9882e3426019db72ff4746d7a4d43c51 +size 23426 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_18.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_18.obj new file mode 100644 index 000000000..8675b08f0 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_18.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c2d8e8e15a8ea372d59d72382f8fc0e243dca814e17e9f02b90aaa8096f5dfd +size 9073 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_2.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_2.obj new file mode 100644 index 000000000..486ceccf0 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_2.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee058b877e20e1fc6e48a0adb7a5c2dbaf75f7ac191e829923064f3e326f0453 +size 5434 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_3.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_3.obj new file mode 100644 index 000000000..6c2383041 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_3.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:664d46c25e091ddbfacb31102be55e1743bab1ab3829b40ccb3439ca6ab5f88a +size 37408 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_4.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_4.obj new file mode 100644 index 000000000..3abf22743 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_4.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b1a1689e7a4509860147bed4ba130a9ce20aa3f3fd56022feec4e22805ae5e1 +size 18107 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_5.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_5.obj new file mode 100644 index 000000000..d89e99e4e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_5.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a06f880f48a046fb78cfe3e45f3136691374371da4c82810349216c466abad11 +size 30633 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_6.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_6.obj new file mode 100644 index 000000000..c6c80a932 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_6.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80160c18c3ea6370906c2555639ff2c9ecdbcf968cd3196504544d174e974efa +size 21843 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_7.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_7.obj new file mode 100644 index 000000000..08730b1c7 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_7.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ea628bef2b03f7c24f74dad186e5e4f8d975aeacf9d9a778a0ee84d46c13ea9 +size 26696 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_8.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_8.obj new file mode 100644 index 000000000..d4f0d6782 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_8.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a41cc720f7916d370207b2893077b5f5025e2b3672e046fb43e8d2756b613c33 +size 23518 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_9.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_9.obj new file mode 100644 index 000000000..f2a05c8ae --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/bracelet_with_vision_link_convex/bracelet_convex_9.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a67149b83d2c80a14cdd4fff7c3110afb043c948d478ea468aec05e23eea155 +size 14182 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/end_effector_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/end_effector_link.STL new file mode 100644 index 000000000..3e2754219 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/end_effector_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b6fb58e61fa475939767d68a446f97f1bff02c0e5935a3ea8bb51e6515783d8 +size 80 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.STL new file mode 100644 index 000000000..0c616b5f7 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a00d96d2c7ec37e9fe4c5fb6d3fa2b4386dfd48fea5ac25632f4cb83b3505105 +size 456284 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.mtl new file mode 100644 index 000000000..b435e8789 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.001 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.obj new file mode 100644 index 000000000..92be4a04e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/forearm_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72520fe6116385b46ad20320e03f7b1ca8741ecab6b1cec196c554fdb903b58f +size 507752 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.STL new file mode 100644 index 000000000..00b636d56 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8df461a71780d8e2d09104a507584424279d85173adf14867e359f7e58b3bba7 +size 514184 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.mtl new file mode 100644 index 000000000..ac03f8c26 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.002 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.obj new file mode 100644 index 000000000..e6d13e0d9 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_1_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5c370bf5b89c7aad4387a52c1feead7e70e0d00b515e2d90aa096bad930a7c0 +size 562125 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.STL new file mode 100644 index 000000000..375f68608 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a9f38de21648804b3ab7076ec73726031f2ade7c95eb2541d070553e025407b +size 489184 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.mtl new file mode 100644 index 000000000..07c787567 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.003 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.obj new file mode 100644 index 000000000..b8847e002 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/half_arm_2_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bff684ba381830badc7e51e402394e16648d05a16639fa9934dac566b33b6663 +size 485542 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.STL new file mode 100644 index 000000000..71dbb2133 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a98e232e5197f09e9cd520e7a450c21bcec74bc49e2a19ce7cf86c5dc25f2e18 +size 476984 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.mtl new file mode 100644 index 000000000..37d1f301d --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.004 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.obj new file mode 100644 index 000000000..beb8cdb93 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/shoulder_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4d304cba1afc3fc7266159a5e34c26385d30a1b64764d3f04100d1192698bf4f +size 424456 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.STL new file mode 100644 index 000000000..cc0413db9 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b97a5e3b2de535344f8e8d4176d77f0cdb3455b235f35430fa0413208856e66 +size 546484 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.mtl new file mode 100644 index 000000000..8b4a00c2c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.005 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.obj new file mode 100644 index 000000000..c92ff7ba8 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_1_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:322c381b28eefca630d170cd434182434c063b6a36ebc1871152ccb490fa1864 +size 585957 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.STL b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.STL new file mode 100644 index 000000000..aa941bfe5 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:12271fb7d0ee78fb5642424cf5078be20f0476061e74203db964b674031ce879 +size 516984 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.mtl new file mode 100644 index 000000000..d012aae9e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'None' +# www.blender.org + +newmtl White.006 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.000000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.obj new file mode 100644 index 000000000..a38660556 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/kinova/spherical_wrist_2_link.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:873e40ba691e3b15dcbd65ab9aad604c27b5747bbb674b1f38bfe3e0b2f648e1 +size 532462 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.mtl new file mode 100644 index 000000000..65a2fec44 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'part.blend' +# www.blender.org + +newmtl Material +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.034536 0.034536 0.034536 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.obj new file mode 100644 index 000000000..a1f64d567 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4f6548123631dacb30a0a54516c522cf9aa6c556d9fbc2d42bc4938742c2032 +size 8371 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.mtl new file mode 100644 index 000000000..0fcf20253 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'rafti_finger.blend' +# www.blender.org + +newmtl Material.001 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.034536 0.034536 0.034536 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.obj new file mode 100644 index 000000000..fdf6a470c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_1.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f1163f54bd7763f89c5a5ac2a2db71e6e7303d2676d60c3c34162234568e093 +size 682 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.mtl new file mode 100644 index 000000000..0fcf20253 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'rafti_finger.blend' +# www.blender.org + +newmtl Material.001 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.034536 0.034536 0.034536 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.obj new file mode 100644 index 000000000..108f0754e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_2.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:086bc1bda883756dea3c9a5306da2016547a812adb22e70811de0956eb043a98 +size 1465 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.mtl new file mode 100644 index 000000000..0fcf20253 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'rafti_finger.blend' +# www.blender.org + +newmtl Material.001 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.034536 0.034536 0.034536 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.obj new file mode 100644 index 000000000..6110aa1fd --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_3.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4236029a257ad3f15ca5b32caa8cf8b40920dee83a1ae8ac9dadb4e8b006a40a +size 771 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.mtl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.mtl new file mode 100644 index 000000000..0fcf20253 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.mtl @@ -0,0 +1,12 @@ +# Blender 4.1.1 MTL File: 'rafti_finger.blend' +# www.blender.org + +newmtl Material.001 +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Kd 0.034536 0.034536 0.034536 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.450000 +d 1.000000 +illum 2 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.obj b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.obj new file mode 100644 index 000000000..a7dccfb5b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/rafti_finger_collision_4.obj @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5708a8d40a1848936f725031f090109eca1632b61e01d04d683fd33ce96ffd95 +size 3890 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/base.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/base.stl new file mode 100644 index 000000000..22a9c2b3b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/base.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c7b9f2bd92d705fc4e897c94e905973a3c05f406845e942229433deb7041453 +size 1712484 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_link.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_link.stl new file mode 100644 index 000000000..cbfd80ed7 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_link.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5ee95e62f8415bf5b6e503c831a958f5fc1990bf9b2865329ec38a28932727c +size 89084 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_tip_link.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_tip_link.stl new file mode 100644 index 000000000..9614b95e1 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/finger_tip_link.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:633793332c081641ce200df27a40643cc293b29956e3cb5cb29cb811c33ef1c7 +size 110484 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/inner_knuckle_link.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/inner_knuckle_link.stl new file mode 100644 index 000000000..01ec1e2a5 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/inner_knuckle_link.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fb74a8a0d76c0e471cf19fd48bc676fd5b19123798e7a39eb6aa56869354283 +size 84884 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/knuckle_link.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/knuckle_link.stl new file mode 100644 index 000000000..0b3f90a5c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/knuckle_link.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bbd6e868b1778bead60d2516c7ede581d7ad4e431744b1828757d6ea5129c112 +size 67084 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/pad.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/pad.stl new file mode 100644 index 000000000..231413751 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/pad.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4af1fa8d9bb285abecbf5dfc88e74cb30f059a7a074c2030dc35e9ee4316019d +size 15084 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/silicone_pad.stl b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/silicone_pad.stl new file mode 100644 index 000000000..6915244ae --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/assets/robotiq_2f85/silicone_pad.stl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca20c0fa61e6d3ce7b04bed25360e048e31dfd88ace04187aa6d9e8f4ca3fd0f +size 15084 diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/gen3_7dof.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/gen3_7dof.xml new file mode 100644 index 000000000..18839d953 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/gen3_7dof.xml @@ -0,0 +1,775 @@ + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/keyframes.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/keyframes.xml new file mode 100644 index 000000000..470f21122 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/keyframes.xml @@ -0,0 +1,517 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/scene.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/scene.xml new file mode 100644 index 000000000..c6bd04c50 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/mujoco/scene.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/picknik_kinova_gen3.xacro b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/picknik_kinova_gen3.xacro new file mode 100644 index 000000000..b1ac103ea --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/description/picknik_kinova_gen3.xacro @@ -0,0 +1,682 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 100000.0 + 100000.0 + + + + + 1e+5 + 1 + 0 + 0.2 + 0.002 + 0 + + + + + + + + + + + + + + + + + + + + + + + + 100000.0 + 100000.0 + + + + + 1e+5 + 1 + 0 + 0.2 + 0.002 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${-2*pi} + ${2*pi} + + + 0.0 + + + + + + + -2.41 + 2.41 + + + 0.0 + + + + + + + ${-2*pi} + ${2*pi} + + + -3.14 + + + + + + + -2.66 + 2.66 + + + -2.51 + + + + + + + ${-2*pi} + ${2*pi} + + + 0.0 + + + + + + + -2.23 + 2.23 + + + 0.96 + + + + + + + ${-2*pi} + ${2*pi} + + + 1.57 + + + + + + + + 0.7929 + + + + + + + + + + + + + + + + + + + + + + + + + picknik_mujoco_ros/MujocoSystem + $(arg mujoco_model) + kinova_vla_test_sim + 10 + 60 + $(arg mujoco_viewer) + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/Dockerfile.serve_policy b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/Dockerfile.serve_policy new file mode 100644 index 000000000..89f8a0d4b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/Dockerfile.serve_policy @@ -0,0 +1,9 @@ +FROM python:3.12-slim + +RUN pip install --no-cache-dir "lerobot[pi,smolvla]==0.6.0" + +COPY serve_policy.py /app/serve_policy.py +WORKDIR /app + +EXPOSE 8973 +ENTRYPOINT ["python", "serve_policy.py"] diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/README.md b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/README.md new file mode 100644 index 000000000..80345b9fe --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/README.md @@ -0,0 +1,30 @@ +# Policy serving + +Runs a trained policy against the `Execute Color-Stack Policy` objective via MoveIt Pro's `ExecutePolicy` behavior and the `/get_action_chunk` service. + +## 1. Serve (model container) + +```bash +docker compose run --rm --service-ports serve_policy \ + --checkpoint --policy-class pi05 --state-dim 8 +``` + +If `SERVE_POLICY_PORT` is set, also point the bridge's `INFER_URL` at +the same port (it defaults to `http://172.17.0.1:8973/infer`). + +## 2. Run (MoveIt Pro UI) + +The adapter starts automatically with Pro. To run manually without the whole stack: + +```bash +ros2 run kinova_vla_test_sim get_action_chunk_adapter.py +``` + +## Tests + +`../test/test_get_action_chunk_adapter.py` runs under `colcon test`. `test_serve_policy.py` needs `lerobot`/`torch`, so run it in model container: + +```bash +env -u LD_LIBRARY_PATH -m pip install pytest +env -u LD_LIBRARY_PATH -m pytest test_serve_policy.py +``` diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/docker-compose.yaml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/docker-compose.yaml new file mode 100644 index 000000000..7f80b0296 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/docker-compose.yaml @@ -0,0 +1,20 @@ +services: + serve_policy: + build: + context: . + dockerfile: Dockerfile.serve_policy + runtime: nvidia + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu, compute, utility] + ports: + - "${SERVE_POLICY_PORT:-8973}:8973" + environment: + - HF_HUB_OFFLINE=1 + volumes: + - /data/checkpoints:/data/checkpoints:ro + - /data/cache/huggingface:/root/.cache/huggingface:ro diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/serve_policy.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/serve_policy.py new file mode 100644 index 000000000..0bc41f0a0 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/serve_policy.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +"""lerobot RTC inference server for MoveIt Pro ExecutePolicy (host side). + +Serves POST /infer and GET /health over HTTP. Generalized over any lerobot RTC +policy via --policy-class (pi05 | smolvla | ...); feeds dataset-native camera +keys and lets each checkpoint's baked rename_map map them onto the model's +slots, so IMG_KEY_MAP is policy-agnostic. + +Runs on the host; the in-container bridge node (get_action_chunk_adapter.py) +reaches it at http://172.17.0.1:/infer. + + env -u LD_LIBRARY_PATH serve_policy.py \ + --checkpoint --policy-class pi05 +""" +import argparse +import base64 +import json +import threading +import traceback +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +import cv2 +import numpy as np +import torch + +from lerobot.configs.types import RTCAttentionSchedule +from lerobot.policies.factory import get_policy_class, make_pre_post_processors +from lerobot.policies.rtc.configuration_rtc import RTCConfig + +# pi0.5 checkpoints save a processor pipeline that references +# 'relative_actions_processor', an alias lerobot does not always auto-register. +# Register it before loading, else make_pre_post_processors raises +# "Processor step 'relative_actions_processor' not found". +from lerobot.processor import ProcessorStepRegistry +from lerobot.processor.relative_action_processor import RelativeActionsProcessorStep + +try: + ProcessorStepRegistry.get("relative_actions_processor") +except Exception: + ProcessorStepRegistry.register("relative_actions_processor")( + RelativeActionsProcessorStep + ) + +# Request image key -> the checkpoint's dataset-native camera key. The training +# rename_map is baked into the saved processor, so make_pre_post_processors +# renames these to the model's slots itself; feed native keys and let it rename. +IMG_KEY_MAP = {"front": "scene", "wrist": "wrist", "overview": "overview"} + + +def decode_image(b64: str) -> torch.Tensor: + """base64 JPEG -> CHW float32 [0,1] RGB tensor.""" + buf = np.frombuffer(base64.b64decode(b64), dtype=np.uint8) + bgr = cv2.imdecode(buf, cv2.IMREAD_COLOR) # HxWx3 BGR uint8 + if bgr is None: + raise ValueError("cv2.imdecode failed on /infer image") + rgb = np.ascontiguousarray(bgr[:, :, ::-1]) + return torch.from_numpy(rgb).float().permute(2, 0, 1) / 255.0 + + +class PolicyServer: + def __init__( + self, + checkpoint: str, + device: str, + fps: float, + execution_horizon: int, + policy_class: str = "pi05", + state_dim: int = 0, + ): + self.device = device + self.fps = fps + self.state_dim = state_dim # >0 overrides config shape for warmup + self.lock = threading.Lock() # serialize: one model, stateful RTC processor + + self.policy = get_policy_class(policy_class).from_pretrained(checkpoint) + self.policy.to(device) + self.policy.eval() + + # RTC guidance configured once, server-side; applies when the Objective + # leaves guidance_horizon=0. A non-zero guidance_horizon value arrives per + # call as execution_horizon. + self.policy.config.rtc_config = RTCConfig( + enabled=True, + execution_horizon=execution_horizon, + prefix_attention_schedule=RTCAttentionSchedule.EXP, + ) + self.policy.init_rtc_processor() + + self.pre, self.post = make_pre_post_processors( + policy_cfg=self.policy.config, + pretrained_path=checkpoint, + preprocessor_overrides={"device_processor": {"device": self.device}}, + postprocessor_overrides={"device_processor": {"device": self.device}}, + ) + + @torch.no_grad() + def infer(self, payload: dict) -> dict: + obs = { + f"observation.images.{IMG_KEY_MAP[k]}": decode_image( + payload["images"][k] + ).to(self.device) + for k in IMG_KEY_MAP + } + obs["observation.state"] = torch.tensor( + payload["state"], dtype=torch.float32, device=self.device + ) + obs["task"] = payload["task"] + + kwargs = {} + prev = payload.get("prev_chunk_left_over") + if prev: + kwargs["prev_chunk_left_over"] = torch.tensor( + prev, dtype=torch.float32, device=self.device + ) + kwargs["inference_delay"] = int(payload.get("inference_delay", 0)) + if payload.get("execution_horizon") is not None: + kwargs["execution_horizon"] = int(payload["execution_horizon"]) + + self.policy.reset() + chunk = self.policy.predict_action_chunk( + self.pre(obs), **kwargs + ) # (1, T, A) normalized + raw = chunk.squeeze(0).detach().cpu().numpy() + steps = [self.post(chunk[:, i, :]) for i in range(chunk.shape[1])] + actions = ( + torch.stack(steps, dim=1).squeeze(0).detach().cpu().numpy() + ) # (T, A) absolute + return { + "action_chunk": actions.tolist(), + "action_chunk_raw": raw.tolist(), + "dt": 1.0 / self.fps, + } + + def warmup(self): + """Full-size warmup so the first real chunk does not pay for CUDA graph / cache setup.""" + h = w = 224 + blank = base64.b64encode( + cv2.imencode(".jpg", np.zeros((h, w, 3), np.uint8))[1] + ).decode("ascii") + state_dim = self.state_dim or int( + self.policy.config.input_features["observation.state"].shape[0] + ) + self.infer( + { + "images": {k: blank for k in IMG_KEY_MAP}, + "state": [0.0] * state_dim, + "task": "warmup", + } + ) + + +def make_handler(server: PolicyServer): + class Handler(BaseHTTPRequestHandler): + def _send(self, code, obj): + body = json.dumps(obj).encode() + self.send_response(code) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self): + if self.path == "/health": + self._send(200, {"status": "ok", "warm": True}) + else: + self._send(404, {"error": "not found"}) + + def do_POST(self): + if self.path != "/infer": + self._send(404, {"error": "not found"}) + return + try: + n = int(self.headers.get("Content-Length", 0)) + payload = json.loads(self.rfile.read(n)) + except (ValueError, json.JSONDecodeError) as exc: + self._send(400, {"error": f"bad request: {exc}"}) + return + try: + with server.lock: + result = server.infer(payload) + self._send(200, result) + except ( + Exception + ) as exc: # surfaced to the bridge as {"error": ...} -> success=false + traceback.print_exc() + self._send(200, {"error": f"{type(exc).__name__}: {exc}"}) + + def log_message(self, *args): + pass # quiet; call counting is done adapter-side + + return Handler + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--checkpoint", required=True) + ap.add_argument( + "--policy-class", + default="pi05", + help="lerobot policy class: pi05 | smolvla | ...", + ) + ap.add_argument( + "--state-dim", + type=int, + default=0, + help="override observation.state dim for warmup (0=use config)", + ) + ap.add_argument( + "--fps", type=float, default=10.0, help="training fps; response dt=1/fps" + ) + ap.add_argument("--port", type=int, default=8973) + ap.add_argument("--device", default="cuda") + ap.add_argument( + "--execution-horizon", type=int, default=20, help="server RTC default window" + ) + args = ap.parse_args() + + print( + f"[rtc-server] loading {args.policy_class} {args.checkpoint} on {args.device} ...", + flush=True, + ) + srv = PolicyServer( + args.checkpoint, + args.device, + args.fps, + args.execution_horizon, + policy_class=args.policy_class, + state_dim=args.state_dim, + ) + print("[rtc-server] warmup inference ...", flush=True) + try: + srv.warmup() + except ( + Exception + ) as exc: # warmup only pre-pays CUDA setup; never let it kill the server. + traceback.print_exc() + print( + f"[rtc-server] WARNING: warmup skipped ({type(exc).__name__}: {exc}); " + f"pass --state-dim for a merged checkpoint to warm up cleanly", + flush=True, + ) + httpd = ThreadingHTTPServer(("0.0.0.0", args.port), make_handler(srv)) + print( + f"[rtc-server] ready on 0.0.0.0:{args.port} (dt={1.0/args.fps:.4f}, ehz={args.execution_horizon})", + flush=True, + ) + httpd.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/test_serve_policy.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/test_serve_policy.py new file mode 100644 index 000000000..60dd9aa42 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/docker/test_serve_policy.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Tests for serve_policy.py: image decoding and the /infer, /health HTTP handlers. + +Runs in the same Python environment as serve_policy.py itself (lerobot/torch/cv2), +not the ROS workspace's pytest suite — see docker/README.md for how to run this. +""" + +import base64 +import http.client +import json +import threading +import unittest + +import cv2 +import numpy as np +import torch +from http.server import ThreadingHTTPServer + +from serve_policy import decode_image, make_handler + + +def encode_bgr_jpeg_b64(bgr: np.ndarray) -> str: + ok, buf = cv2.imencode(".jpg", bgr) + assert ok + return base64.b64encode(buf.tobytes()).decode("ascii") + + +class TestDecodeImage(unittest.TestCase): + """decode_image: base64 JPEG -> CHW float32 [0,1] RGB tensor.""" + + def test_invalid_base64_raises(self) -> None: + """Malformed image bytes fail loudly (ValueError) instead of returning garbage.""" + with self.assertRaises(ValueError): + decode_image(base64.b64encode(b"not a jpeg").decode("ascii")) + + def test_output_shape_and_dtype(self) -> None: + """A 4x2 BGR frame decodes to a (3, 4, 2) float32 tensor scaled to [0, 1].""" + bgr = np.zeros((4, 2, 3), dtype=np.uint8) + tensor = decode_image(encode_bgr_jpeg_b64(bgr)) + + self.assertEqual(tuple(tensor.shape), (3, 4, 2)) + self.assertEqual(tensor.dtype, torch.float32) + self.assertGreaterEqual(float(tensor.min()), 0.0) + self.assertLessEqual(float(tensor.max()), 1.0) + + def test_bgr_to_rgb_channel_order(self) -> None: + """A pure-blue BGR frame decodes with the red channel near zero (BGR -> RGB swap).""" + bgr = np.zeros((8, 8, 3), dtype=np.uint8) + bgr[:, :, 0] = 255 # BGR channel 0 = blue + tensor = decode_image(encode_bgr_jpeg_b64(bgr)) + + # channel 0 = red after the BGR->RGB swap, so it should stay dark despite the + # source being fully saturated on the blue channel; channel 2 = blue, saturated. + self.assertLess(float(tensor[0].mean()), 0.2) + self.assertGreater(float(tensor[2].mean()), 0.8) + + +class TestHttpHandler(unittest.TestCase): + """The /health and /infer HTTP endpoints, isolated from real policy inference.""" + + class FakeServer: + """Stands in for PolicyServer: same .lock/.infer(payload) contract, no model.""" + + def __init__(self, infer_result=None, infer_error=None) -> None: + self.lock = threading.Lock() + self._infer_result = infer_result + self._infer_error = infer_error + + def infer(self, payload: dict) -> dict: + if self._infer_error is not None: + raise self._infer_error + return self._infer_result + + def _start( + self, fake_server: "TestHttpHandler.FakeServer" + ) -> http.client.HTTPConnection: + httpd = ThreadingHTTPServer(("127.0.0.1", 0), make_handler(fake_server)) + threading.Thread(target=httpd.serve_forever, daemon=True).start() + self.addCleanup(httpd.shutdown) + return http.client.HTTPConnection("127.0.0.1", httpd.server_address[1]) + + def test_health_returns_ok(self) -> None: + """GET /health reports ok without touching the (possibly still-loading) server.""" + conn = self._start(self.FakeServer()) + conn.request("GET", "/health") + resp = conn.getresponse() + + self.assertEqual(resp.status, 200) + self.assertEqual(json.loads(resp.read())["status"], "ok") + + def test_unknown_path_is_404(self) -> None: + """A request to any path other than /health or /infer is rejected, not routed.""" + conn = self._start(self.FakeServer()) + conn.request("GET", "/unknown") + resp = conn.getresponse() + + self.assertEqual(resp.status, 404) + + def test_infer_success_returns_result(self) -> None: + """A valid POST /infer body is forwarded to server.infer() and its result returned.""" + conn = self._start( + self.FakeServer(infer_result={"action_chunk": [[0.1]], "dt": 0.05}) + ) + body = json.dumps({"task": "warmup"}).encode() + conn.request("POST", "/infer", body=body) + resp = conn.getresponse() + + self.assertEqual(resp.status, 200) + self.assertEqual(json.loads(resp.read())["dt"], 0.05) + + def test_infer_malformed_json_is_400(self) -> None: + """A body that isn't valid JSON is rejected before it ever reaches server.infer().""" + conn = self._start(self.FakeServer()) + conn.request("POST", "/infer", body=b"not json") + resp = conn.getresponse() + + self.assertEqual(resp.status, 400) + + def test_infer_exception_returns_200_with_error_field(self) -> None: + """server.infer() raising is caught and reported as {"error": ...}, not a 500. + + The bridge (get_action_chunk_adapter.py) treats any 2xx body containing an + "error" key as a failed chunk; a raw 500 would instead surface as an unhandled + requests.HTTPError from raise_for_status(). + """ + conn = self._start(self.FakeServer(infer_error=RuntimeError("model not warm"))) + conn.request("POST", "/infer", body=json.dumps({"task": "x"}).encode()) + resp = conn.getresponse() + + self.assertEqual(resp.status, 200) + self.assertIn("model not warm", json.loads(resp.read())["error"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/launch/simulated_extras.launch.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/launch/simulated_extras.launch.py new file mode 100644 index 000000000..b20ef4407 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/launch/simulated_extras.launch.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 + +from launch import LaunchDescription +from launch_ros.actions import Node + + +def generate_launch_description(): + return LaunchDescription( + [ + # /get_action_chunk bridge to the host inference server (docker/serve_policy.py); + # ExecutePolicy calls this service, so it must be up wherever the Agent runs. + Node( + package="kinova_vla_test_sim", + executable="get_action_chunk_adapter.py", + name="get_action_chunk_adapter", + output="log", + ), + # Republishes the trajectory controller's commanded setpoint (plus the + # latched gripper command) as /joint_commands, which the Trainer records + # and Forge maps to the training `action`. Without it a Behavior-Tree + # oracle records no command stream and conversion falls back to + # next-state action labels. + Node( + package="kinova_vla_test_sim", + executable="joint_command_bridge.py", + name="joint_command_bridge", + output="log", + ), + ] + ) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/collect_color_stack_demo.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/collect_color_stack_demo.xml new file mode 100644 index 000000000..b199b1088 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/collect_color_stack_demo.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/command_color_stack_gripper.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/command_color_stack_gripper.xml new file mode 100644 index 000000000..d25618836 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/command_color_stack_gripper.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_oracle.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_oracle.xml new file mode 100644 index 000000000..07e8bc625 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_oracle.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_policy.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_policy.xml new file mode 100644 index 000000000..8518a7a2a --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/execute_color_stack_policy.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/move_along_color_stack_keyposes.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/move_along_color_stack_keyposes.xml new file mode 100644 index 000000000..585643d2b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/move_along_color_stack_keyposes.xml @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/prepare_color_stack_scene.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/prepare_color_stack_scene.xml new file mode 100644 index 000000000..dfaa4961a --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/prepare_color_stack_scene.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_green.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_green.xml new file mode 100644 index 000000000..d783dec90 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_green.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_red.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_red.xml new file mode 100644 index 000000000..859d2e661 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_blue_on_red.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_episode.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_episode.xml new file mode 100644 index 000000000..ed7cd20b5 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_episode.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_blue.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_blue.xml new file mode 100644 index 000000000..33c6cfe70 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_blue.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_red.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_red.xml new file mode 100644 index 000000000..1205f83e8 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_green_on_red.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_blue.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_blue.xml new file mode 100644 index 000000000..7d451d276 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_blue.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_green.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_green.xml new file mode 100644 index 000000000..2d39a33ca --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/record_color_stack_red_on_green.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/reset_to_eval_layout.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/reset_to_eval_layout.xml new file mode 100644 index 000000000..4acb6d518 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/reset_to_eval_layout.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/run_color_stack_oracle.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/run_color_stack_oracle.xml new file mode 100644 index 000000000..2b075fef8 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/objectives/run_color_stack_oracle.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/package.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/package.xml new file mode 100644 index 000000000..5f55542f2 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/package.xml @@ -0,0 +1,38 @@ + + + kinova_vla_test_sim + 6.5.0 + + + MuJoCo simulation of a Kinova Gen3 (7-DoF) + Robotiq 2F-85 stacking + colored cubes, for testing VLA policy execution. + + + Dan Wahl + + BSD-3-Clause + + ament_cmake + + control_msgs + kinova_sim + kinova_vla_test_sim_behaviors + kinova_gen3_base_config + joint_trajectory_admittance_controller + kortex_description + moveit_pro_ml_msgs + moveit_studio_kinova_pstop_manager + picknik_mujoco_ros + picknik_accessories + rclpy + realsense2_description + sensor_msgs + velocity_force_controller + + ament_lint_auto + ament_cmake_pytest + + + ament_cmake + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/get_action_chunk_adapter.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/get_action_chunk_adapter.py new file mode 100755 index 000000000..1a835db59 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/get_action_chunk_adapter.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""GetActionChunk adapter: serves ExecutePolicy from an inference process over HTTP. + +Policy-agnostic: runs inside the MoveIt Pro dev container, forwards observations +to the host inference server at INFER_URL and returns chunks. Run with the +overlay sourced so moveit_pro_ml_msgs resolves. +""" + +import base64 +import os + +import cv2 +import numpy as np +import requests + +import rclpy +from rclpy.node import Node +from std_msgs.msg import Float64MultiArray, MultiArrayDimension +from trajectory_msgs.msg import JointTrajectory, JointTrajectoryPoint + +from moveit_pro_ml_msgs.srv import GetActionChunk + +INFER_URL = os.environ.get("INFER_URL", "http://172.17.0.1:8973/infer") + + +def encode_jpeg_b64(img) -> str: + """sensor_msgs/Image -> base64 JPEG. Accepts rgb8/bgr8/rgba8/bgra8 frames.""" + encoding = str(img.encoding).lower() + if encoding not in ("rgb8", "bgr8", "rgba8", "bgra8"): + raise ValueError(f"unsupported image encoding '{img.encoding}'") + channels = 4 if encoding in ("rgba8", "bgra8") else 3 + # step is the row stride in bytes; slice off any row padding before reshaping. + rows = np.frombuffer(bytes(img.data), dtype=np.uint8).reshape(img.height, img.step) + arr = rows[:, : img.width * channels].reshape(img.height, img.width, channels)[ + :, :, :3 + ] + # cv2.imencode expects BGR input, so bgr frames pass through and rgb frames flip once. + if encoding.startswith("rgb"): + arr = arr[:, :, ::-1] + ok, buf = cv2.imencode(".jpg", np.ascontiguousarray(arr)) + if not ok: + raise RuntimeError("cv2.imencode failed") + return base64.b64encode(buf.tobytes()).decode("ascii") + + +class GetActionChunkAdapter(Node): + def __init__(self) -> None: + super().__init__("get_action_chunk_adapter") + self.create_service(GetActionChunk, "/get_action_chunk", self._on_request) + self._calls = 0 + + def _on_request(self, request, response): + # The request carries the policy's full trained state: the arm group's + # joints plus the gripper joint appended last when the Objective configures one. + state = list(request.robot_state.position) + if len(request.images) != len(request.image_names): + response.success = False + response.message = ( + f"images ({len(request.images)}) and image_names " + f"({len(request.image_names)}) length mismatch" + ) + return response + try: + images = { + request.image_names[i]: encode_jpeg_b64(img) + for i, img in enumerate(request.images) + } + except (ValueError, RuntimeError) as exc: + response.success = False + response.message = f"image encode failed: {exc}" + return response + payload = { + "state": state, + "task": request.prompt, + "images": images, + "new_episode": bool(request.new_episode), + } + + # RTC carryover: forward the previous chunk's unexecuted tail and the overlap depth. + # A Float64MultiArray is a flat row-major buffer plus a layout, so reshape it back + # into (steps, action width) rows; it arrives empty on the first call and when RTC is off. + prev = request.previous_action_chunk + if prev.data and len(prev.layout.dim) == 2: + steps, width = prev.layout.dim[0].size, prev.layout.dim[1].size + payload["prev_chunk_left_over"] = ( + np.asarray(prev.data, dtype=float).reshape(steps, width).tolist() + ) + payload["inference_delay"] = int(request.frozen_prefix_steps) + # A non-zero guidance_horizon is the Objective overriding the server's RTC window. + if request.guidance_horizon > 0: + payload["execution_horizon"] = int(request.guidance_horizon) + + try: + resp = requests.post(INFER_URL, json=payload, timeout=30.0) + resp.raise_for_status() + data = resp.json() + except (requests.RequestException, ValueError) as exc: + response.success = False + response.message = f"/infer request failed: {exc}" + return response + if isinstance(data, dict) and data.get("error"): + response.success = False + response.message = f"/infer error: {data['error']}" + return response + chunk = data.get("action_chunk") if isinstance(data, dict) else None + if not chunk or "dt" not in data: + response.success = False + response.message = ( + "/infer response is missing a non-empty action_chunk or dt" + ) + return response + expected_dims = len(request.robot_state.name) + mismatched_width = next( + (len(step) for step in chunk if len(step) != expected_dims), None + ) + if mismatched_width is not None: + response.success = False + response.message = ( + f"/infer chunk width {mismatched_width} does not match the observed " + f"joint count {expected_dims}" + ) + return response + + # The chunk: absolute joint positions. The action columns line up with the request's + # state entries, so the request's joint names are also the chunk's joint names. + traj = JointTrajectory() + traj.joint_names = list(request.robot_state.name) + for step in chunk: + point = JointTrajectoryPoint() + point.positions = [float(v) for v in step] + traj.points.append(point) + response.success = True + response.chunk = traj + response.native_control_period = float(data["dt"]) + + # RTC echo: the normalized model output, one row per step, one column per action dimension. + raw = data.get("action_chunk_raw") + if raw: + arr = Float64MultiArray() + steps, width = len(raw), len(raw[0]) + arr.layout.data_offset = 0 + arr.layout.dim = [ + MultiArrayDimension(label="steps", size=steps, stride=steps * width), + MultiArrayDimension(label="dims", size=width, stride=width), + ] + arr.data = np.asarray(raw, dtype=float).ravel().tolist() + response.policy_action_chunk = arr + + self._calls += 1 + if self._calls % 10 == 0: + self.get_logger().info(f"get_action_chunk: served {self._calls} chunks") + return response + + +def main() -> None: + rclpy.init() + node = GetActionChunkAdapter() + try: + rclpy.spin(node) + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/joint_command_bridge.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/joint_command_bridge.py new file mode 100755 index 000000000..2406f55c1 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/script/joint_command_bridge.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +"""Publish the training `action` and `observation.state` streams. + +Forge labels `action` from a joint command topic and falls back to next-state +labels when there is none; no MoveIt Pro Behavior publishes one. This node +assembles the trajectory controller's reference setpoint with the latched +gripper command and republishes both as a `sensor_msgs/JointState`. + +`/joint_states` carries all fifteen joints, eight of them passive Robotiq +linkage. Recording it as `observation.state` gives a fifteen-wide state the +policies are not trained on, so this node republishes the same eight joints the +action uses. Point MOVEIT_PRO_TRAIN_JOINT_STATES_TOPIC at that topic. + + ros2 run kinova_vla_test_sim joint_command_bridge.py \\ + --ros-args -p publish_rate:=10.0 +""" + +from __future__ import annotations + +import rclpy +from control_msgs.msg import JointTrajectoryControllerState +from rclpy.node import Node +from sensor_msgs.msg import JointState + +# joint_trajectory_admittance_controller publishes ~/controller_state only while +# a goal executes, so a reference older than this means no trajectory is running +# and the arm should be labelled with the pose it is holding. +DEFAULT_REFERENCE_TIMEOUT_S = 0.5 + +# The arm joints, in the order the trajectory controller reports them. Fixing the +# layout here rather than waiting for the controller to announce it is what lets +# both streams publish from the first tick: the controller is silent until a +# trajectory runs, which is several frames into an episode. +DEFAULT_ARM_JOINT_NAMES = [f"joint_{index}" for index in range(1, 8)] + + +def is_reference_fresh(now: float, stamp: float, timeout: float) -> bool: + return (now - stamp) < timeout + + +def assemble_joint_command( + joint_order: list[str], + gripper_joint: str | None, + reference_names: list[str] | None, + reference_positions: list[float] | None, + reference_fresh: bool, + measured: dict[str, float], + gripper_cmd: float | None, + gripper_fallback: float, +) -> list[float] | None: + """One position per name in ``joint_order``, or None if a joint has no source. + + Arm joints take the controller's setpoint while it is fresh, otherwise their + measured position. The gripper takes the latched command: its + ``GripperCommand`` goal is not observable on any topic, and it shares the + arm's radian scale through the ``split`` tendon in + ``description/mujoco/gen3_7dof.xml``. + """ + reference_map: dict[str, float] = {} + if ( + reference_fresh + and reference_names + and reference_positions + and len(reference_names) == len(reference_positions) + ): + reference_map = dict(zip(reference_names, reference_positions)) + + positions: list[float] = [] + for joint in joint_order: + if gripper_joint is not None and joint == gripper_joint: + positions.append( + float(gripper_cmd) + if gripper_cmd is not None + else float(gripper_fallback) + ) + elif joint in reference_map: + positions.append(float(reference_map[joint])) + elif joint in measured: + positions.append(float(measured[joint])) + else: + return None + return positions + + +class JointCommandBridge(Node): + def __init__(self) -> None: + super().__init__("joint_command_bridge") + + self._controller_state_topic = self._param( + "controller_state_topic", + "/joint_trajectory_admittance_controller/controller_state", + ) + self._joint_states_topic = self._param("joint_states_topic", "/joint_states") + self._joint_command_topic = self._param( + "joint_command_topic", "/joint_commands" + ) + self._observation_state_topic = self._param( + "observation_state_topic", "/observed_joint_states" + ) + self._arm_joint_names = list( + self._param("arm_joint_names", DEFAULT_ARM_JOINT_NAMES) + ) + self._gripper_joint = self._param( + "gripper_joint_name", "robotiq_85_left_knuckle_joint" + ) + self._gripper_fallback = float(self._param("gripper_rest_position", 0.0)) + # float(): an integer in YAML would declare this INTEGER and reject every + # SetROS2Parameter double at collection time. + self.declare_parameter("gripper_command_position", self._gripper_fallback) + # Forge resamples every stream onto the dataset's 1/fps grid. + self._publish_rate = float(self._param("publish_rate", 10.0)) + if self._publish_rate <= 0.0: + raise ValueError(f"publish_rate must be > 0, got {self._publish_rate}") + self._reference_timeout = float( + self._param("reference_timeout", DEFAULT_REFERENCE_TIMEOUT_S) + ) + + # One tuple: a torn read could zip new names against old positions of the + # same length, which the length guard above cannot catch. + self._reference: tuple[list[str], list[float]] | None = None + self._reference_stamp = 0.0 + self._measured: dict[str, float] = {} + self._measured_stamp = 0.0 + # ExecutePolicy's layout: the planning group's order, gripper last. + self._joint_order = self._arm_joint_names + ( + [self._gripper_joint] if self._gripper_joint else [] + ) + + self._publisher = self.create_publisher( + JointState, self._joint_command_topic, 10 + ) + self._state_publisher = self.create_publisher( + JointState, self._observation_state_topic, 10 + ) + self.create_subscription( + JointTrajectoryControllerState, + self._controller_state_topic, + self._on_controller_state, + 10, + ) + self.create_subscription( + JointState, self._joint_states_topic, self._on_joint_states, 10 + ) + self.create_timer(1.0 / self._publish_rate, self._publish) + self.get_logger().info( + f"bridging {self._controller_state_topic} -> {self._joint_command_topic} " + f"and {self._joint_states_topic} -> {self._observation_state_topic} " + f"at {self._publish_rate:g} Hz for {self._joint_order}" + ) + + def _param(self, name: str, default): + return self.declare_parameter(name, default).value + + def _now(self) -> float: + return self.get_clock().now().nanoseconds * 1e-9 + + def _on_controller_state(self, msg: JointTrajectoryControllerState) -> None: + self._reference = (list(msg.joint_names), list(msg.reference.positions)) + self._reference_stamp = self._now() + if msg.joint_names and list(msg.joint_names) != self._arm_joint_names: + # Reordering the channels partway through an episode would corrupt it, + # so the configured layout stands and the mismatch is reported instead. + self.get_logger().error( + f"{self._controller_state_topic} reports {list(msg.joint_names)}, not " + f"the configured arm_joint_names {self._arm_joint_names}; recorded " + "channels will not match the controller. Fix arm_joint_names.", + throttle_duration_sec=30.0, + ) + + def _on_joint_states(self, msg: JointState) -> None: + self._measured = dict(zip(msg.name, msg.position)) + self._measured_stamp = self._now() + + def _publish(self) -> None: + self._publish_action() + self._publish_observation() + + def _publish_action(self) -> None: + if ( + self._gripper_joint + and self._measured + and self._gripper_joint not in self._measured + ): + self.get_logger().warning( + f"gripper_joint_name '{self._gripper_joint}' is not in " + f"{self._joint_states_topic}; not publishing " + f"{self._joint_command_topic}. Check the configured joint name.", + throttle_duration_sec=30.0, + ) + return + + reference_names, reference_positions = self._reference or (None, None) + positions = assemble_joint_command( + joint_order=self._joint_order, + gripper_joint=self._gripper_joint or None, + reference_names=reference_names, + reference_positions=reference_positions, + reference_fresh=is_reference_fresh( + self._now(), self._reference_stamp, self._reference_timeout + ), + measured=self._measured, + gripper_cmd=self.get_parameter("gripper_command_position").value, + gripper_fallback=self._gripper_fallback, + ) + if positions is None: + self.get_logger().warning( + f"no setpoint or measured position for some joint in " + f"{self._joint_order}; not publishing {self._joint_command_topic}.", + throttle_duration_sec=10.0, + ) + return + self._publisher.publish(self._joint_state(positions)) + + def _publish_observation(self) -> None: + # Republishing the last positions under a fresh stamp would record a frozen + # arm as a still one, which nothing downstream could tell apart. + if not is_reference_fresh( + self._now(), self._measured_stamp, self._reference_timeout + ): + self.get_logger().warning( + f"no {self._joint_states_topic} within {self._reference_timeout:g}s; " + f"not publishing {self._observation_state_topic}.", + throttle_duration_sec=10.0, + ) + return + missing = [j for j in self._joint_order if j not in self._measured] + if missing: + self.get_logger().warning( + f"{self._joint_states_topic} carries no {missing}; not publishing " + f"{self._observation_state_topic}.", + throttle_duration_sec=10.0, + ) + return + self._state_publisher.publish( + self._joint_state([self._measured[j] for j in self._joint_order]) + ) + + def _joint_state(self, positions: list[float]) -> JointState: + msg = JointState() + msg.header.stamp = self.get_clock().now().to_msg() + msg.name = list(self._joint_order) + msg.position = positions + return msg + + +def main() -> None: + rclpy.init() + node = JointCommandBridge() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_get_action_chunk_adapter.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_get_action_chunk_adapter.py new file mode 100644 index 000000000..db1eb908d --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_get_action_chunk_adapter.py @@ -0,0 +1,318 @@ +#!/usr/bin/env python3 +"""Tests for the GetActionChunk bridge: image encoding and request/response mapping.""" + +import importlib.util +import sys +import unittest +from pathlib import Path +from unittest.mock import MagicMock, patch + +import rclpy +import requests +from sensor_msgs.msg import Image, JointState +from std_msgs.msg import Float64MultiArray, MultiArrayDimension, MultiArrayLayout + +from moveit_pro_ml_msgs.srv import GetActionChunk + +# get_action_chunk_adapter.py is a standalone ROS executable (install(PROGRAMS ...) in +# CMakeLists.txt), not part of an importable Python package, so it's loaded by file path. +_SCRIPT_PATH = ( + Path(__file__).resolve().parent.parent / "script" / "get_action_chunk_adapter.py" +) +_spec = importlib.util.spec_from_file_location("get_action_chunk_adapter", _SCRIPT_PATH) +get_action_chunk_adapter = importlib.util.module_from_spec(_spec) +sys.modules["get_action_chunk_adapter"] = get_action_chunk_adapter +_spec.loader.exec_module(get_action_chunk_adapter) + +GetActionChunkAdapter = get_action_chunk_adapter.GetActionChunkAdapter +encode_jpeg_b64 = get_action_chunk_adapter.encode_jpeg_b64 + + +def make_image(encoding: str, height: int = 2, width: int = 2) -> Image: + """A flat-colored sensor_msgs/Image with no row padding (step == width * channels).""" + channels = 4 if encoding in ("rgba8", "bgra8") else 3 + img = Image() + img.encoding = encoding + img.height = height + img.width = width + img.step = width * channels + img.data = bytes([128] * (height * img.step)) + return img + + +def make_request(**overrides) -> GetActionChunk.Request: + request = GetActionChunk.Request() + request.robot_state = JointState(name=["j1", "j2"], position=[0.1, 0.2]) + request.images = [make_image("rgb8")] + request.image_names = ["front"] + request.prompt = "stack the blocks" + request.new_episode = False + for key, value in overrides.items(): + setattr(request, key, value) + return request + + +class TestEncodeJpegB64(unittest.TestCase): + """encode_jpeg_b64: sensor_msgs/Image -> base64 JPEG round trip.""" + + def test_unsupported_encoding_raises(self) -> None: + """A mono8 (or any non-{r,b}gb[a]8) image is rejected, not silently reinterpreted.""" + img = make_image("mono8") + with self.assertRaises(ValueError): + encode_jpeg_b64(img) + + def test_rgb8_and_bgr8_roundtrip_to_same_pixels(self) -> None: + """rgb8 and bgr8 inputs carrying the same visual color decode to matching JPEG bytes.""" + rgb = make_image("rgb8") + rgb.data = bytes([10, 20, 30] * (rgb.height * rgb.width)) + bgr = make_image("bgr8") + bgr.data = bytes([30, 20, 10] * (bgr.height * bgr.width)) + + rgb_b64 = encode_jpeg_b64(rgb) + bgr_b64 = encode_jpeg_b64(bgr) + + self.assertEqual(rgb_b64, bgr_b64) + + def test_rgba8_drops_alpha_channel(self) -> None: + """Encoding a 4-channel frame does not crash and yields a valid JPEG (3-channel).""" + img = make_image("rgba8") + img.data = bytes([10, 20, 30, 255] * (img.height * img.width)) + b64 = encode_jpeg_b64(img) + self.assertTrue(len(b64) > 0) + + def test_row_padding_is_stripped(self) -> None: + """step wider than width*channels (row padding) must not corrupt the decoded pixels.""" + img = make_image("rgb8") + pad = 4 + img.step = img.width * 3 + pad + img.data = bytes([10, 20, 30] * img.width + [0] * pad) * img.height + padded_b64 = encode_jpeg_b64(img) + + unpadded = make_image("rgb8") + unpadded.data = bytes([10, 20, 30] * (unpadded.height * unpadded.width)) + unpadded_b64 = encode_jpeg_b64(unpadded) + + self.assertEqual(padded_b64, unpadded_b64) + + +class TestOnRequest(unittest.TestCase): + """GetActionChunkAdapter._on_request: HTTP call shaping and response translation.""" + + @classmethod + def setUpClass(cls) -> None: + rclpy.init() + + @classmethod + def tearDownClass(cls) -> None: + rclpy.shutdown() + + def setUp(self) -> None: + self.node = GetActionChunkAdapter() + + def tearDown(self) -> None: + self.node.destroy_node() + + @patch("get_action_chunk_adapter.requests.post") + def test_successful_chunk_populates_trajectory(self, mock_post: MagicMock) -> None: + """A valid /infer response becomes a JointTrajectory with matching joint names.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.5, 0.6], [0.7, 0.8]], + "dt": 0.05, + } + request = make_request() + response = self.node._on_request(request, GetActionChunk.Response()) + + self.assertTrue(response.success, response.message) + self.assertEqual(list(response.chunk.joint_names), ["j1", "j2"]) + self.assertEqual(len(response.chunk.points), 2) + self.assertEqual(list(response.chunk.points[0].positions), [0.5, 0.6]) + self.assertAlmostEqual(response.native_control_period, 0.05) + + @patch("get_action_chunk_adapter.requests.post") + def test_prompt_forwarded_as_task_field(self, mock_post: MagicMock) -> None: + """The fixed request.prompt field is sent to /infer under the 'task' key.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0]], + "dt": 0.05, + } + request = make_request(prompt="pick the red cube") + self.node._on_request(request, GetActionChunk.Response()) + + sent_payload = mock_post.call_args.kwargs["json"] + self.assertEqual(sent_payload["task"], "pick the red cube") + + def test_unsupported_image_encoding_fails_without_http_call(self) -> None: + """An image the encoder can't handle fails locally; /infer is never called.""" + request = make_request(images=[make_image("mono8")]) + with patch("get_action_chunk_adapter.requests.post") as mock_post: + response = self.node._on_request(request, GetActionChunk.Response()) + mock_post.assert_not_called() + self.assertFalse(response.success) + self.assertIn("image encode failed", response.message) + + def test_images_and_image_names_length_mismatch_fails_without_http_call( + self, + ) -> None: + """A malformed request (arrays not lined up by index, per the .srv contract) + fails locally instead of raising IndexError out of the service callback.""" + request = make_request(images=[make_image("rgb8"), make_image("rgb8")]) + with patch("get_action_chunk_adapter.requests.post") as mock_post: + response = self.node._on_request(request, GetActionChunk.Response()) + mock_post.assert_not_called() + self.assertFalse(response.success) + self.assertIn("length mismatch", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_http_error_surfaces_as_failure(self, mock_post: MagicMock) -> None: + """A network-level failure calling /infer is reported, not raised.""" + mock_post.side_effect = requests.ConnectionError("refused") + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertFalse(response.success) + self.assertIn("/infer request failed", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_server_error_field_surfaces_as_failure(self, mock_post: MagicMock) -> None: + """A server-reported {"error": ...} body fails the request rather than being ignored.""" + mock_post.return_value.json.return_value = {"error": "checkpoint not loaded"} + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertFalse(response.success) + self.assertIn("checkpoint not loaded", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_missing_dt_fails(self, mock_post: MagicMock) -> None: + """A response with a chunk but no dt is rejected rather than defaulting silently.""" + mock_post.return_value.json.return_value = {"action_chunk": [[0.0, 0.0]]} + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertFalse(response.success) + self.assertIn("missing", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_chunk_width_mismatch_fails(self, mock_post: MagicMock) -> None: + """A chunk whose column count doesn't match the requested joint count is rejected.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0, 0.0]], # 3 columns, request has 2 joints + "dt": 0.05, + } + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertFalse(response.success) + self.assertIn("does not match", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_chunk_width_mismatch_reports_the_offending_row( + self, mock_post: MagicMock + ) -> None: + """The error names the row that actually mismatched, not always row 0.""" + mock_post.return_value.json.return_value = { + # row 0 matches the request's 2 joints; row 1 is the actual offender. + "action_chunk": [[0.0, 0.0], [0.0, 0.0, 0.0]], + "dt": 0.05, + } + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertFalse(response.success) + self.assertIn("chunk width 3", response.message) + + @patch("get_action_chunk_adapter.requests.post") + def test_previous_chunk_reshaped_and_forwarded(self, mock_post: MagicMock) -> None: + """A populated previous_action_chunk is reshaped from its flat layout before sending.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0]], + "dt": 0.05, + } + prev = Float64MultiArray( + layout=MultiArrayLayout( + dim=[ + MultiArrayDimension(label="steps", size=2, stride=4), + MultiArrayDimension(label="dims", size=2, stride=2), + ] + ), + data=[1.0, 2.0, 3.0, 4.0], + ) + request = make_request(previous_action_chunk=prev, frozen_prefix_steps=3) + self.node._on_request(request, GetActionChunk.Response()) + + sent_payload = mock_post.call_args.kwargs["json"] + self.assertEqual(sent_payload["prev_chunk_left_over"], [[1.0, 2.0], [3.0, 4.0]]) + self.assertEqual(sent_payload["inference_delay"], 3) + + @patch("get_action_chunk_adapter.requests.post") + def test_empty_previous_chunk_omits_rtc_fields(self, mock_post: MagicMock) -> None: + """The first call of an episode (empty previous_action_chunk) sends no RTC carryover.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0]], + "dt": 0.05, + } + self.node._on_request(make_request(), GetActionChunk.Response()) + + sent_payload = mock_post.call_args.kwargs["json"] + self.assertNotIn("prev_chunk_left_over", sent_payload) + self.assertNotIn("inference_delay", sent_payload) + + @patch("get_action_chunk_adapter.requests.post") + def test_zero_guidance_horizon_omits_execution_horizon( + self, mock_post: MagicMock + ) -> None: + """guidance_horizon=0 defers to the server's own RTC default, per the .srv contract.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0]], + "dt": 0.05, + } + self.node._on_request( + make_request(guidance_horizon=0), GetActionChunk.Response() + ) + + sent_payload = mock_post.call_args.kwargs["json"] + self.assertNotIn("execution_horizon", sent_payload) + + @patch("get_action_chunk_adapter.requests.post") + def test_nonzero_guidance_horizon_overrides_execution_horizon( + self, mock_post: MagicMock + ) -> None: + """A nonzero guidance_horizon from the Objective is forwarded as execution_horizon.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.0, 0.0]], + "dt": 0.05, + } + self.node._on_request( + make_request(guidance_horizon=7), GetActionChunk.Response() + ) + + sent_payload = mock_post.call_args.kwargs["json"] + self.assertEqual(sent_payload["execution_horizon"], 7) + + @patch("get_action_chunk_adapter.requests.post") + def test_action_chunk_raw_echoed_as_policy_action_chunk( + self, mock_post: MagicMock + ) -> None: + """When the server echoes action_chunk_raw, it is reflected back as policy_action_chunk.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.5, 0.6], [0.7, 0.8]], + "action_chunk_raw": [[-0.1, 0.1], [0.2, -0.2]], + "dt": 0.05, + } + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + arr = response.policy_action_chunk + self.assertEqual([d.size for d in arr.layout.dim], [2, 2]) + self.assertEqual(list(arr.data), [-0.1, 0.1, 0.2, -0.2]) + + @patch("get_action_chunk_adapter.requests.post") + def test_no_action_chunk_raw_leaves_policy_action_chunk_empty( + self, mock_post: MagicMock + ) -> None: + """A policy without RTC support (no action_chunk_raw) leaves policy_action_chunk unset.""" + mock_post.return_value.json.return_value = { + "action_chunk": [[0.5, 0.6]], + "dt": 0.05, + } + response = self.node._on_request(make_request(), GetActionChunk.Response()) + + self.assertEqual(list(response.policy_action_chunk.data), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_joint_command_bridge.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_joint_command_bridge.py new file mode 100644 index 000000000..b921c5de6 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/test/test_joint_command_bridge.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 + +# Copyright 2026 PickNik Inc. +# All rights reserved. +# +# Unauthorized copying of this code base via any medium is strictly prohibited. +# Proprietary and confidential. + +"""Tests for the commanded-joint assembly that becomes the recorded `action`. + +Each case is a way the action could silently degrade into a copy of +`observation.state`, or into a constant, either of which trains a policy that +cannot move. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "script")) +from joint_command_bridge import ( # noqa: E402 + DEFAULT_REFERENCE_TIMEOUT_S, + assemble_joint_command, + is_reference_fresh, +) + +ARM = ["joint_a", "joint_b"] +GRIPPER = "gripper_joint" +ORDER = [*ARM, GRIPPER] + + +def test_returns_none_when_arm_joint_has_no_source() -> None: + """Publishing a short vector would misalign every downstream joint.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=None, + reference_positions=None, + reference_fresh=False, + measured={"joint_a": 0.1}, # joint_b absent + gripper_cmd=0.4, + gripper_fallback=0.0, + ) + assert result is None + + +def test_uses_controller_reference_for_arm_when_fresh() -> None: + """The setpoint leads the measured position, and that lead is the learning signal.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=True, + measured={"joint_a": 9.0, "joint_b": 9.0}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + assert result == [1.0, 2.0, 0.6] + + +def test_falls_back_to_measured_when_reference_stale() -> None: + """A gripper dwell is labelled with the held pose, not the last goal's setpoint.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=False, + measured={"joint_a": 0.5, "joint_b": 0.6}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + assert result == [0.5, 0.6, 0.6] + + +def test_gripper_uses_fallback_before_first_command() -> None: + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=True, + measured={}, + gripper_cmd=None, + gripper_fallback=0.0, + ) + assert result == [1.0, 2.0, 0.0] + + +def test_gripper_zero_command_is_not_treated_as_unset() -> None: + """0.0 is the oracle's OPEN command and is falsy; the fallback differs to catch it.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=True, + measured={}, + gripper_cmd=0.0, + gripper_fallback=0.6, + ) + assert result == [1.0, 2.0, 0.0] + + +def test_mismatched_reference_lengths_are_ignored() -> None: + """Zipping them would bind joint_a to the wrong value and drop joint_b silently.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0], # length mismatch + reference_fresh=True, + measured={"joint_a": 0.5, "joint_b": 0.6}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + assert result == [0.5, 0.6, 0.6] + + +def test_no_gripper_joint_builds_arm_only_vector() -> None: + result = assemble_joint_command( + joint_order=ARM, + gripper_joint=None, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=True, + measured={}, + gripper_cmd=None, + gripper_fallback=0.0, + ) + assert result == [1.0, 2.0] + + +def test_order_follows_joint_order_not_controller_order() -> None: + """A positional copy of the controller's vector would train on permuted labels.""" + result = assemble_joint_command( + joint_order=["joint_b", "joint_a", GRIPPER], + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b"], + reference_positions=[1.0, 2.0], + reference_fresh=True, + measured={}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + assert result == [2.0, 1.0, 0.6] + + +def test_gripper_never_takes_the_controller_reference() -> None: + """Checking the reference first would flatten the commanded open/close.""" + result = assemble_joint_command( + joint_order=ORDER, + gripper_joint=GRIPPER, + reference_names=["joint_a", "joint_b", GRIPPER], + reference_positions=[1.0, 2.0, 0.123], + reference_fresh=True, + measured={}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + assert result == [1.0, 2.0, 0.6] + + +def test_reference_freshness_pins_the_timeout_on_both_sides() -> None: + """The timeout decides whether a dwell is labelled with the held pose.""" + timeout = DEFAULT_REFERENCE_TIMEOUT_S + assert is_reference_fresh(now=timeout - 1e-6, stamp=0.0, timeout=timeout) + assert not is_reference_fresh(now=timeout + 1e-6, stamp=0.0, timeout=timeout) + assert not is_reference_fresh( + now=timeout, stamp=0.0, timeout=timeout + ), "the boundary itself is stale" + + +def test_empty_joint_order_builds_an_empty_vector() -> None: + """The node never calls it this way, but the contract is one entry per name.""" + assert ( + assemble_joint_command( + joint_order=[], + gripper_joint=GRIPPER, + reference_names=["joint_a"], + reference_positions=[1.0], + reference_fresh=True, + measured={}, + gripper_cmd=0.6, + gripper_fallback=0.0, + ) + == [] + ) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/README.md b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/README.md new file mode 100644 index 000000000..e13bbe224 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/README.md @@ -0,0 +1,48 @@ +# Train + +Recipe for the checkpoint served by `docker/serve_policy.py`. + +## Combine the recordings + +Each prompt is recorded as its own Pro dataset. Merge them into the one dataset +training reads, under `~/.local/share/moveit_pro/trainer/recordings`: + +```bash +uv run python combine_datasets.py -lerobot -lerobot ... +``` + +This also backfills the quantile statistics the Trainer's converter leaves out, +which pi0.5 needs to normalize state and action. Training a single unmerged +dataset needs that backfill on its own: + +```bash +uv run python backfill_stats.py -lerobot +``` + +## LeRobot Train + +```bash +uv sync +uv run lerobot-train \ + --config_path=pi05_colorstack_lora_backbone.yaml \ + --output_dir=/path/to/checkpoints/my_run \ + --job_name=my_run +``` + +Needs `HF_TOKEN` in the environment and a GPU with 40+ GB. On smaller GPUs, add +`--policy.path=lerobot/pi05_base --policy.gradient_checkpointing=true`. A 24 GB +card needs it: the recipe as written runs out of memory, and checkpointing brings +it down to 14.7 GB at ~1.1 step/s. + +## Merge, then serve + +`lerobot-train` saves a LoRA adapter; `serve_policy.py` needs a dense checkpoint: + +```bash +uv run python merge_lora_checkpoint.py /checkpoints//pretrained_model _merged +docker compose run --rm --service-ports serve_policy --checkpoint _merged --policy-class pi05 +``` + +## Gotcha: `--policy.*` overrides need `--policy.path` + +Otherwise the run fails with `DecodingError: policy: Expected a dict with a 'type' key`. diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/backfill_stats.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/backfill_stats.py new file mode 100644 index 000000000..dce74fe9c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/backfill_stats.py @@ -0,0 +1,48 @@ +"""Add the quantile statistics a Pro-recorded dataset is missing. + +The Trainer's converter records only min/max/mean/std/count for state and +action. Policies that normalize by quantile -- pi0.5 maps both that way -- +read q01/q99 and cannot train without them. + +LeRobot recomputes these from the written parquet in well under a second, so +run this once per converted dataset, before training or before merging: + + uv run python backfill_stats.py [ ...] + +Image statistics stay absent. LeRobot has no recompute path for them, and +pi0.5 normalizes images with IDENTITY, so nothing reads them -- but the +training config must then set `use_imagenet_stats: false`, or LeRobot fails +writing ImageNet constants into a camera entry that does not exist. +""" + +import sys +from pathlib import Path + +from lerobot.datasets.dataset_tools import recompute_stats +from lerobot.datasets.lerobot_dataset import LeRobotDataset + + +def backfill(root: Path) -> None: + """Recompute `root`'s statistics in place.""" + dataset = LeRobotDataset(repo_id=f"local/{root.name}", root=root) + before = sorted(dataset.meta.stats.get("action", {})) + dataset = recompute_stats(dataset) + after = sorted(dataset.meta.stats.get("action", {})) + print(f"{root.name}: action stats {before} -> {after}") + + +def main() -> int: + roots = [Path(arg).expanduser() for arg in sys.argv[1:]] + if not roots: + print(__doc__, file=sys.stderr) + return 2 + for root in roots: + if not (root / "meta" / "info.json").is_file(): + print(f"not a LeRobot dataset: {root}", file=sys.stderr) + return 1 + backfill(root) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/combine_datasets.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/combine_datasets.py new file mode 100644 index 000000000..65c453038 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/combine_datasets.py @@ -0,0 +1,52 @@ +"""Combine the per-prompt recordings into one dataset to train on. + +Each prompt is recorded as its own Pro dataset. Training wants them as one, +with the prompts unified into a single task table: + + uv run python combine_datasets.py ... + +Statistics are backfilled into each source first. Aggregation pools the +per-dataset statistics rather than recomputing them, so a source still missing +its quantiles would carry that gap into the merged dataset. +""" + +import sys +from pathlib import Path + +from lerobot.datasets.aggregate import aggregate_datasets + +from backfill_stats import backfill + + +def combine(output: Path, sources: list[Path]) -> None: + """Backfill each source's statistics, then merge them into `output`.""" + for source in sources: + backfill(source) + aggregate_datasets( + repo_ids=[f"local/{source.name}" for source in sources], + aggr_repo_id=f"local/{output.name}", + roots=sources, + aggr_root=output, + ) + print(f"combined {len(sources)} dataset(s) into {output}") + + +def main() -> int: + args = [Path(arg).expanduser() for arg in sys.argv[1:]] + if len(args) < 2: + print(__doc__, file=sys.stderr) + return 2 + output, sources = args[0], args[1:] + for source in sources: + if not (source / "meta" / "info.json").is_file(): + print(f"not a LeRobot dataset: {source}", file=sys.stderr) + return 1 + if output.exists(): + print(f"output already exists: {output}", file=sys.stderr) + return 1 + combine(output, sources) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/merge_lora_checkpoint.py b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/merge_lora_checkpoint.py new file mode 100644 index 000000000..96689c5c7 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/merge_lora_checkpoint.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +"""Merge a lerobot pi0.5 LoRA adapter into a dense checkpoint. + +`lerobot-train` saves adapter-only checkpoints, which the eval/serve loaders can't +read directly. This loads the base policy, applies the adapter, merges the LoRA +deltas into the base weights, and saves a dense checkpoint with the trained +processors copied alongside. + +Usage: + merge_lora_checkpoint.py IN_ADAPTER_DIR OUT_DENSE_DIR +""" + +import json +import shutil +import sys +from pathlib import Path + +import torch +from lerobot.policies.pi05.modeling_pi05 import PI05Policy +from lerobot.processor import ProcessorStepRegistry +from lerobot.processor.relative_action_processor import RelativeActionsProcessorStep + +try: + ProcessorStepRegistry.get("relative_actions_processor") +except Exception: + ProcessorStepRegistry.register("relative_actions_processor")( + RelativeActionsProcessorStep + ) + +from peft import PeftConfig, PeftModel # noqa: E402 + + +def main() -> None: + in_dir, out_dir = sys.argv[1], sys.argv[2] + in_p, out_p = Path(in_dir), Path(out_dir) + + peft_cfg = PeftConfig.from_pretrained(in_dir) + base_ref = peft_cfg.base_model_name_or_path or "lerobot/pi05_base" + print(f"[merge] base={base_ref} adapter={in_dir}") + + base = PI05Policy.from_pretrained(base_ref) + model = PeftModel.from_pretrained(base, in_dir) + merged = model.merge_and_unload().to(torch.bfloat16) # PEFT merges in float32 + + out_p.mkdir(parents=True, exist_ok=True) + merged.save_pretrained(out_p) + + # save_pretrained writes the base policy's config; overwrite it with the trained + # config so the merged checkpoint keeps the right action shape/dtype and no adapter flag. + trained_cfg = json.loads((in_p / "config.json").read_text()) + trained_cfg["use_peft"] = False + trained_cfg["dtype"] = "bfloat16" + (out_p / "config.json").write_text(json.dumps(trained_cfg, indent=2)) + + copied = [] + for f in in_p.iterdir(): + if ( + f.name.startswith("policy_pre") + or f.name.startswith("policy_post") + or f.name == "train_config.json" + ): + shutil.copy2(f, out_p / f.name) + copied.append(f.name) + + print(f"[merge] copied processors: {sorted(copied)}") + print(f"[merge] dense checkpoint -> {out_p}") + print(f"[merge] contents: {sorted(p.name for p in out_p.iterdir())}") + + +if __name__ == "__main__": + main() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pi05_colorstack_lora_backbone.yaml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pi05_colorstack_lora_backbone.yaml new file mode 100644 index 000000000..6ac519292 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pi05_colorstack_lora_backbone.yaml @@ -0,0 +1,56 @@ +# pi0.5 LoRA-backbone recipe for the Kinova color-cube stacking dataset. + +policy: + path: lerobot/pi05_base + push_to_hub: false + dtype: bfloat16 + gradient_checkpointing: false # enable to prevent OOM on smaller GPUs + train_expert_only: false + use_relative_actions: false + optimizer_lr: 2.5e-5 + optimizer_weight_decay: 0.01 + scheduler_decay_steps: 50000 # keep in sync with `steps` below + +dataset: + repo_id: PickNikRobotics/kinova-color-cube-stack-mujoco + revision: main + video_backend: pyav + # Pro-recorded datasets carry no image statistics, and pi0.5 normalizes images + # with IDENTITY so nothing reads them. Left true, LeRobot fails writing ImageNet + # constants into a camera entry that does not exist. See backfill_stats.py. + use_imagenet_stats: false + image_transforms: + enable: false # enable to improve robustness to image variance + +# LoRA the gemma_2b backbone LM; train the action expert and projection heads +# densely; leave SigLIP frozen. +peft: + method_type: LORA + r: 32 + lora_alpha: 64 + target_modules: '.*paligemma\.model\.language_model\.layers\.\d+\.(self_attn\.(q|k|v|o)_proj|mlp\.(gate|up|down)_proj)' + full_training_modules: + - gemma_expert.model + - action_in_proj + - action_out_proj + - time_mlp_in + - time_mlp_out + +# This config's cameras -> pi0.5's DROID slots. +rename_map: + observation.images.overview: observation.images.base_0_rgb + observation.images.wrist: observation.images.left_wrist_0_rgb + observation.images.scene: observation.images.right_wrist_0_rgb + +steps: 50000 +batch_size: 4 +save_freq: 10000 +seed: 1000 + +num_workers: 8 # lower this first if the run OOMs at a checkpoint save + +wandb: + enable: false + disable_artifact: true + # project: project_name + # entity: entity_name diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pyproject.toml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pyproject.toml new file mode 100644 index 000000000..e41073176 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/pyproject.toml @@ -0,0 +1,26 @@ +# Environment for the training recipes in this directory. Standalone so it can be +# copied elsewhere (e.g. a RunPod box) and still resolve the same versions. + +[project] +name = "lerobot-training-recipes" +version = "0.1.0" +description = "Pinned environment for running the training recipes in this directory" +requires-python = ">=3.12,<3.13" + +dependencies = [ + "lerobot[pi,peft,training]==0.6.0", + "torch==2.10.0", + "torchvision==0.25.0", +] + +[tool.uv] +package = false # a dependency set, not an importable package + +[[tool.uv.index]] +name = "pytorch-cu128" +url = "https://download.pytorch.org/whl/cu128" +explicit = true + +[tool.uv.sources] +torch = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }] +torchvision = [{ index = "pytorch-cu128", marker = "sys_platform == 'linux'" }] diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/uv.lock b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/uv.lock new file mode 100644 index 000000000..709f56bde --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim/train/uv.lock @@ -0,0 +1,1649 @@ +version = 1 +revision = 3 +requires-python = "==3.12.*" +resolution-markers = [ + "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux')", + "platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'x86_64' and sys_platform == 'linux'", + "platform_machine == 'arm64' and sys_platform == 'darwin'", + "sys_platform == 'win32'", + "(platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] + +[[package]] +name = "accelerate" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/75/94cd5d389649578aca399e5aa822637eec18319a1dadc400ffe2f9a7493f/accelerate-1.14.0.tar.gz", hash = "sha256:41b9c4377a54e0b460a959b0defa1b736e4ca0a2373252d9a539964c2afe3c8d", size = 412167, upload-time = "2026-06-11T13:45:52.326Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "av" +version = "15.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e9/c3/83e6e73d1592bc54436eae0bc61704ae0cff0c3cfbde7b58af9ed67ebb49/av-15.1.0.tar.gz", hash = "sha256:39cda2dc810e11c1938f8cb5759c41d6b630550236b3365790e67a313660ec85", size = 3774192, upload-time = "2025-08-30T04:41:56.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/58/de78b276d20db6ffcd4371283df771721a833ba525a3d57e753d00a9fe79/av-15.1.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:40c5df37f4c354ab8190c6fd68dab7881d112f527906f64ca73da4c252a58cee", size = 21760991, upload-time = "2025-08-30T04:40:00.801Z" }, + { url = "https://files.pythonhosted.org/packages/56/cc/45f85775304ae60b66976360d82ba5b152ad3fd91f9267d5020a51e9a828/av-15.1.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:af455ce65ada3d361f80c90c810d9bced4db5655ab9aa513024d6c71c5c476d5", size = 26953097, upload-time = "2025-08-30T04:40:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f8/2d781e5e71d02fc829487e775ccb1185e72f95340d05f2e84eb57a11e093/av-15.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86226d2474c80c3393fa07a9c366106029ae500716098b72b3ec3f67205524c3", size = 38319710, upload-time = "2025-08-30T04:40:07.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/13/37737ef2193e83862ccacff23580c39de251da456a1bf0459e762cca273c/av-15.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:11326f197e7001c4ca53a83b2dbc67fd39ddff8cdf62ce6be3b22d9f3f9338bd", size = 39915519, upload-time = "2025-08-30T04:40:11.066Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e8032c7b8f2a4129a03f63f896544f8b7cf068e2db2950326fa2400d5c47/av-15.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a631ea879cc553080ee62874f4284765c42ba08ee0279851a98a85e2ceb3cc8d", size = 40286166, upload-time = "2025-08-30T04:40:14.561Z" }, + { url = "https://files.pythonhosted.org/packages/e2/23/612c0fd809444d04b8387a2dfd942ccc77829507bd78a387ff65a9d98c24/av-15.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8f383949b010c3e731c245f80351d19dc0c08f345e194fc46becb1cb279be3ff", size = 41150592, upload-time = "2025-08-30T04:40:17.951Z" }, + { url = "https://files.pythonhosted.org/packages/15/74/6f8e38a3b0aea5f28e72813672ff45b64615f2c69e6a4a558718c95edb9f/av-15.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:d5921aa45f4c1f8c1a8d8185eb347e02aa4c3071278a2e2dd56368d54433d643", size = 31336093, upload-time = "2025-08-30T04:40:21.393Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" }, + { url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" }, + { url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" }, + { url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" }, + { url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" }, + { url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" }, + { url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" }, + { url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "cmake" +version = "4.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/17/f8f42ae205604319cc36f46d9929bd9bfbd83d3d02d6314c44fa97c42006/cmake-4.1.3.tar.gz", hash = "sha256:89f48ddc2570eb62447e33311cffc6dfeb09631bd0a19423d8a59cec8af030f1", size = 34998, upload-time = "2025-11-19T22:41:27.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/79/1bf4009d7ef16d62e0b92ddb78efeda830ca5903149abf9dc01d270c3d4e/cmake-4.1.3-py3-none-macosx_10_10_universal2.whl", hash = "sha256:3b6b25ce8fecc768881b36a1dfbca0013adac10a299c73e24cf4cbb99e4c37d6", size = 49246088, upload-time = "2025-11-19T22:40:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/4d/9d/14e076406388efa2bbea2366ec0bbe85e2536787ebbb374dda792f068222/cmake-4.1.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3893eb9c20d8a8bac3d951bbef9a4ce9d5495cd35a08b4e08d76215f5ead5897", size = 30381441, upload-time = "2025-11-19T22:40:31.55Z" }, + { url = "https://files.pythonhosted.org/packages/f7/9e/0f7216dfef03f1cbac0cdf4685da6994559f5ede3452e563335a35d6a6cb/cmake-4.1.3-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:487faf892ff5e05084c6a7f229dd9e568d0542b88487386acb42f0cb2f6634b6", size = 30781002, upload-time = "2025-11-19T22:40:35.325Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2e/69d9b1eee7b7c68e9ce53f8449e372151b4967c223ecd43c7083a4dece8d/cmake-4.1.3-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3dbddc52f839df0ebc1c6b6915bd78d63d0805137c6f419fbddd587404276c28", size = 32613762, upload-time = "2025-11-19T22:40:39.488Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9b/deac4d6f8cf4adcaa61d7f16d1ec42d41d471bf330ffcdac4d29c83e46a3/cmake-4.1.3-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b42e99eb6e976f455f29283dd7583270d611b55c7687b5fe8d022d9ae7c95de5", size = 28577197, upload-time = "2025-11-19T22:40:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6c/323c40671c6f1b3e02bb4a7404fbe2bf653190a56e63cf4b6a4f06e876bc/cmake-4.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81f11b72bc59cbe547d9f283487ef0519bf68176edffcdfa1a4dc5a52f292369", size = 29690899, upload-time = "2025-11-19T22:40:45.363Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/ab7866f55ee11a07aa446ee31b91b8f337f1b702b9546fc7b18e23d0566f/cmake-4.1.3-py3-none-manylinux_2_31_armv7l.whl", hash = "sha256:fd633c4395b1522caedf0b64034d1a48ea0e483f19e9c2985d14ee7152b21593", size = 26522320, upload-time = "2025-11-19T22:40:48.463Z" }, + { url = "https://files.pythonhosted.org/packages/db/21/a99ed3f1192c85d6d565e61c0cd0161f8046afcf0b0951e6492be632f2f2/cmake-4.1.3-py3-none-manylinux_2_35_riscv64.whl", hash = "sha256:ea40a64b8027f2b7fb1684312a2f170e4d0904b7a4f123cd96e7290103bb1ed4", size = 28869263, upload-time = "2025-11-19T22:40:51.618Z" }, + { url = "https://files.pythonhosted.org/packages/13/66/3c32bb2d5e72f00a0861066b29cc6981cbffcf9786f7339317f151a4d4be/cmake-4.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e3782d5f82e8960290e50747b1fb5ff8396363a656ad5716a3aedc77334ca94f", size = 41751469, upload-time = "2025-11-19T22:40:54.75Z" }, + { url = "https://files.pythonhosted.org/packages/05/60/922c05d62ba5b422afd211966877673ddceb634e95552893bf9a11cc4e58/cmake-4.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:44b011b8374aac8f3d7a7fb319b3c25d54c2fd9342d94a855ae3a64240efe828", size = 35040544, upload-time = "2025-11-19T22:40:57.669Z" }, + { url = "https://files.pythonhosted.org/packages/71/ae/957336b0489f7d3050cd19010585d4ab5ebcdef485292b9baee68ebbeccf/cmake-4.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f29e924fd6d1a4f2f731eb743cc687b82063f73f15f0b4fb8e2b8a8211faba8", size = 45811680, upload-time = "2025-11-19T22:41:01.124Z" }, + { url = "https://files.pythonhosted.org/packages/85/0d/41e2ac694b156b249bfaccec071897c46b21deeb4db1ec51d949e7843f4b/cmake-4.1.3-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:466cdce904392f18b201471a3a6429cc12b4d98a166faa3ee0ad4461f3043083", size = 45859079, upload-time = "2025-11-19T22:41:04.694Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/42ca38f001b1f1327c19734e4c0080557a7991db832aacfe4b193ba7743a/cmake-4.1.3-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d37db26f98ac26f0858cf6a30157a4be83b29cb195afeb640b355b097f1d94d7", size = 39946757, upload-time = "2025-11-19T22:41:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/73/ab/a3965bfce6376894c76e17af095b0e360a9e1a1719e3df1e244ea6d6d893/cmake-4.1.3-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:18e1e2b7b226763017521ba8721c74d1a2a3cd7d1ec8e889b0b869d4e939370b", size = 44016695, upload-time = "2025-11-19T22:41:11.84Z" }, + { url = "https://files.pythonhosted.org/packages/a4/66/fa0e8d3c66459a616f0baf9d22933e14137c259f4b62f0dad9c3723cf42d/cmake-4.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6966746b25d1e9c8d32c731452e220e84331b5133544f710b21bd228a93812ca", size = 43357408, upload-time = "2025-11-19T22:41:15.302Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8f/5c43c6465af62bb16159de113438365c789c5a69261dad36746aa1ec74b8/cmake-4.1.3-py3-none-win32.whl", hash = "sha256:b1c890af27bb548d0a2c0e1affc81ad180fc17d8dfa9545e0658153446fe7db4", size = 34268275, upload-time = "2025-11-19T22:41:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/c1/51/2bc56a4d8d9c2680913f1a7e0b7a33e48100f336df91176b74dda6dff8b3/cmake-4.1.3-py3-none-win_amd64.whl", hash = "sha256:fd5a2ea9a38c6109036d8c912a7db4df2de241cfbc00b7424ae246494387da80", size = 37545974, upload-time = "2025-11-19T22:41:21.85Z" }, + { url = "https://files.pythonhosted.org/packages/36/a5/ec213d5c228ab7a205abeb51cc23aa1be9b586041c40cdccc157c325822a/cmake-4.1.3-py3-none-win_arm64.whl", hash = "sha256:79bd8f92a3385cc6641949b0274cd10ee9a4f45a2c13840121b68b2e90b5af3a", size = 36337597, upload-time = "2025-11-19T22:41:24.968Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c2/65bfd79292b8ff18be4dd7f7442cea37bcbc1a228c1886f1dea515c45b67/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:694ba35023846625ef471257e6b5a4bc8af690f961d197d77d34b1d1db393f56", size = 11760260, upload-time = "2025-10-21T14:51:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" }, +] + +[[package]] +name = "datasets" +version = "4.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/34/14cd8e76f907f7d4dca2334cfeec9f81d30fd15c25a015f99aaea694eaed/datasets-4.8.5.tar.gz", hash = "sha256:0f0c1c3d56ffff2c93b2f4c63c95bac94f3d7e8621aea2a2a576275233bba772", size = 605649, upload-time = "2026-04-27T15:43:57.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/99/00f3196036501b53032c4b1ab8337a0b978dee832ed276dae3815df4e8b5/datasets-4.8.5-py3-none-any.whl", hash = "sha256:5079900781719c0e063a8efdd2cd95a31ad0c63209178669cd23cf1b926149ff", size = 528973, upload-time = "2026-04-27T15:43:53.702Z" }, +] + +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "draccus" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "pyyaml" }, + { name = "pyyaml-include" }, + { name = "toml" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4e/e2/f5012fda17ee5d1eaf3481b6ca3e11dffa5348e5e08ab745538fdc8041bb/draccus-0.10.0.tar.gz", hash = "sha256:8dd08304219becdcd66cd16058ba98e9c3e6b7bfe48ccb9579dae39f8d37ae19", size = 62243, upload-time = "2025-02-05T07:27:48.182Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/9a/a83083b230d352ee5d205757b74006dbe084448ca45e3bc5ca99215b1e55/draccus-0.10.0-py3-none-any.whl", hash = "sha256:90243418ae0e9271c390a59cafb6acfd37001193696ed36fcc8525f791a83282", size = 71783, upload-time = "2025-02-05T07:27:46.1Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "farama-notifications" +version = "0.0.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/91/14397890dde30adc4bee6462158933806207bc5dd10d7b4d09d5c33845cf/farama_notifications-0.0.6.tar.gz", hash = "sha256:b19acac4bb41d76e59e03394b5dd165f4761c86fa327f56307a35cbee3b60158", size = 2517, upload-time = "2026-04-24T08:43:57.603Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/f0/21f81892e4ed10f4ec3ef2e7cf8635fb76e7c0907c55d0da66be50094760/farama_notifications-0.0.6-py3-none-any.whl", hash = "sha256:f84839188efa1ce5bb361c2a84881b2dc2c0d0d7fb661ff00421820170930935", size = 2897, upload-time = "2026-04-24T08:43:56.785Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/80/8232b582c4b318b817cf1274ba74976b07b34d35ef439b3eb948f98645a1/filelock-3.32.0.tar.gz", hash = "sha256:7be2ad23a14607ccc71808e68fe30848aeace7058ace17852f68e2a68e310402", size = 213757, upload-time = "2026-07-21T13:17:42.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/79/b4c714bef36bc4ec2beeae1e0c124f0223888cd8c6feb1cdc56038116920/filelock-3.32.0-py3-none-any.whl", hash = "sha256:d396bea984af47333ef05e50eae7eff88c84256de6112aea0ec48a233c064fe3", size = 97732, upload-time = "2026-07-21T13:17:41.55Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.57" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/0d/132ed135c871b6bf91adf16a0e43797cd535b81d4973b5d09291c54fc5ee/gitpython-3.1.57.tar.gz", hash = "sha256:c493ec57c0ef6b19743798b6a5af859c71814b524e7e6f97baa2f8e658961488", size = 225898, upload-time = "2026-07-26T07:33:26.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/6e/2139de986d9c7c3ac86f1f8be43858ce90bdfe2f7175e6c80c650ba15242/gitpython-3.1.57-py3-none-any.whl", hash = "sha256:4ccf7d73c10f5c9e76043fbb2675ac5a1b3ff5b41e648f56bcbed5f63792ecaf", size = 217151, upload-time = "2026-07-26T07:33:24.838Z" }, +] + +[[package]] +name = "gymnasium" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cloudpickle" }, + { name = "farama-notifications" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/ff/14b6880d703dfaca204490979d3254ccd280c99550798993319902873658/gymnasium-1.3.0.tar.gz", hash = "sha256:6939e86e835d6b71b6ba6bfd360487420876deafc79bfb7bacba83a7c446bcf3", size = 830646, upload-time = "2026-04-22T13:47:14.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/73/fda6a25f3beeb5e49d74330b44092b9e5a547395ccd478d1103ddcbff1fc/gymnasium-1.3.0-py3-none-any.whl", hash = "sha256:6b8c159a8540dcbcb221722d7efda24d78ebbcbc3bd2ea1c2611aa2a34471fc2", size = 953904, upload-time = "2026-04-22T13:47:12.13Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/67be8d71f900d9a55761b6022821d6679fb56c64f1b6063d5af2c2606727/hf_xet-1.5.2.tar.gz", hash = "sha256:73044bd31bae33c984af832d19c752a0dffb67518fee9ddbd91d616e1101cf47", size = 903674, upload-time = "2026-07-16T17:29:56.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/ba/2b70603c7552db82baeb2623e2336898304a17328845151be4fe1f48d420/hf_xet-1.5.2-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f922b8f5fb84f1dd3d7ab7a1316354a1bca9b1c73ecfc19c76e51a2a49d29799", size = 4033760, upload-time = "2026-07-16T17:29:43.884Z" }, + { url = "https://files.pythonhosted.org/packages/60/ac/b097a86a1e4a6098f3a79382643ab09d5733d87ccc864877ad1e12b49b70/hf_xet-1.5.2-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:045f84440c55cdeb659cf1a1dd48c77bcd0d2e93632e2fea8f2c3bdee79f38ed", size = 3841438, upload-time = "2026-07-16T17:29:45.539Z" }, + { url = "https://files.pythonhosted.org/packages/d3/35/db860aa3a0780660324a506ad4b3d322ddc6ecbba4b9340aed0942cbf21c/hf_xet-1.5.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db78c39c83d6279daddc98e2238f373ab8980685556d42472b4ec51abcf03e8c", size = 4428006, upload-time = "2026-07-16T17:29:46.996Z" }, + { url = "https://files.pythonhosted.org/packages/af/6b/832dd980af4b0c3ae0660e309285f2ffcdff2faa38129390dbb47aa4a3f9/hf_xet-1.5.2-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7db73c810500c54c6760be8c39d4b2e476974de85424c50063efc22fdda13025", size = 4221099, upload-time = "2026-07-16T17:29:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/9e/05/ae50f0d34e3254e6c3e208beb2519f6b8673016fc4b3643badaf6450d186/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6395cfe3c9cbead4f16b31808b0e67eac428b66c656f856e99636adaddea878f", size = 4420766, upload-time = "2026-07-16T17:29:50.092Z" }, + { url = "https://files.pythonhosted.org/packages/07/a9/c050bc2743a2bcd68928bfee157b08681667a164a24ec95fbfcfcd717e08/hf_xet-1.5.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cde8cd167126bb6109b2ceb19b844433a4988643e8f3e01dd9dd0e4a34535097", size = 4636716, upload-time = "2026-07-16T17:29:51.62Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/68b01c5c2edb56ac9a67b3d076ffddcb90867abaee923923eb34e7a14e76/hf_xet-1.5.2-cp38-abi3-win_amd64.whl", hash = "sha256:ecf63d1cb69a9a7319910f8f83fcf9b46e7a32dfcf4b8f8eeddb55f647306e65", size = 3988373, upload-time = "2026-07-16T17:29:53.395Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/988383e9dc17294d536fcbcd6fd16eed882e411ad16c954984a53e47b09c/hf_xet-1.5.2-cp38-abi3-win_arm64.whl", hash = "sha256:1da28519496eb7c8094c11e4d25509b4a468457a0302d58136099db2fd9a671d", size = 3816957, upload-time = "2026-07-16T17:29:54.991Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/db3771a6e4fad4bd28fb055d4363b51cb0ae98c1aa504b79d41fdcab5483/huggingface_hub-1.25.1.tar.gz", hash = "sha256:21129595ca7a753be479b319913e22cc8808361ac118bd76cc413db831b28a99", size = 928426, upload-time = "2026-07-27T09:24:10.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/3f/21e816831c6d16f88a6c784974413fa0421ce8a5d04380c2666ed5b503e5/huggingface_hub-1.25.1-py3-none-any.whl", hash = "sha256:004d4e70350517e24c68a7dbb7dc5e40b2b6aefef8f94bf7a85f6f9835102ea5", size = 774909, upload-time = "2026-07-27T09:24:08.079Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonlines" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/87/bcda8e46c88d0e34cad2f09ee2d0c7f5957bccdb9791b0b934ec84d84be4/jsonlines-4.0.0.tar.gz", hash = "sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74", size = 11359, upload-time = "2023-09-01T12:34:44.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/62/d9ba6323b9202dd2fe166beab8a86d29465c41a0288cbe229fac60c1ab8d/jsonlines-4.0.0-py3-none-any.whl", hash = "sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55", size = 8701, upload-time = "2023-09-01T12:34:42.563Z" }, +] + +[[package]] +name = "lerobot" +version = "0.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cmake" }, + { name = "draccus" }, + { name = "einops" }, + { name = "gymnasium" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "setuptools" }, + { name = "termcolor" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/18/b2999bbb52399d404ddcf05645536d3902bd65575ba0ad6bb7bef990a723/lerobot-0.6.0.tar.gz", hash = "sha256:6cad660816fdb72570ea7345ecb23bf0f74d36324e2d7c816a260d31a0c29186", size = 1367952, upload-time = "2026-07-06T10:42:05.281Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/20/9a96311c19e9d256e65584ca83c49c5782d0f204836e84ceeb420d4d493e/lerobot-0.6.0-py3-none-any.whl", hash = "sha256:b38a564fbc441d98380576863bf68635dde5fc2c42ddc2a39d0486640dc9e9a8", size = 1743768, upload-time = "2026-07-06T10:42:03.165Z" }, +] + +[package.optional-dependencies] +peft = [ + { name = "peft" }, + { name = "transformers" }, +] +pi = [ + { name = "scipy" }, + { name = "transformers" }, +] +training = [ + { name = "accelerate" }, + { name = "av" }, + { name = "datasets" }, + { name = "jsonlines" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "torchcodec", marker = "(platform_machine == 'arm64' and sys_platform == 'darwin') or (platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'win32'" }, + { name = "wandb" }, +] + +[[package]] +name = "lerobot-training-recipes" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "lerobot", extra = ["peft", "pi", "training"] }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "torchvision", version = "0.25.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torchvision", version = "0.25.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, +] + +[package.metadata] +requires-dist = [ + { name = "lerobot", extras = ["pi", "peft", "training"], specifier = "==0.6.0" }, + { name = "torch", marker = "sys_platform != 'linux'", specifier = "==2.10.0" }, + { name = "torch", marker = "sys_platform == 'linux'", specifier = "==2.10.0", index = "https://download.pytorch.org/whl/cu128" }, + { name = "torchvision", marker = "sys_platform != 'linux'", specifier = "==0.25.0" }, + { name = "torchvision", marker = "sys_platform == 'linux'", specifier = "==0.25.0", index = "https://download.pytorch.org/whl/cu128" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/99/db44d685f0e257ff0e213ade1964fc459b4a690a73293220e98feb3307cf/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b86f6dd8935884615a0683b663891d43781b819ac4f2ba2b0c9604676af346d0", size = 590537124, upload-time = "2025-03-07T01:43:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/b3bd73445e5cb342727fd24fe1f7b748f690b460acadc27ea22f904502c8/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4412396548808ddfed3f17a467b104ba7751e6b58678a4b840675c56d21cf7ed", size = 9533318, upload-time = "2025-03-07T01:40:10.421Z" }, + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d1/e50d0acaab360482034b84b6e27ee83c6738f7d32182b987f9c7a4e32962/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc1fec1e1637854b4c0a65fb9a8346b51dd9ee69e61ebaccc82058441f15bce8", size = 43106076, upload-time = "2025-03-07T01:41:59.817Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/75/f865a3b236e4647605ea34cc450900854ba123834a5f1598e160b9530c3a/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:52bf7bbee900262ffefe5e9d5a2a69a30d97e2bc5bb6cc866688caa976966e3d", size = 965265, upload-time = "2025-03-07T01:39:43.533Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/41/e79269ce215c857c935fd86bcfe91a451a584dfc27f1e068f568b9ad1ab7/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c9132cc3f8958447b4910a1720036d9eff5928cc3179b0a51fb6d167c6cc87d8", size = 705026878, upload-time = "2025-06-06T21:52:51.348Z" }, + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/bc/7771846d3a0272026c416fbb7e5f4c1f146d6d80704534d0b187dd6f4800/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:848ef7224d6305cdb2a4df928759dca7b1201874787083b6e7550dd6765ce69a", size = 193109211, upload-time = "2025-03-07T01:44:56.873Z" }, + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f5/5607710447a6fe9fd9b3283956fceeee8a06cda1d2f56ce31371f595db2a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:4beb6d4cce47c1a0f1013d72e02b0994730359e17801d395bdcbf20cfb3bb00a", size = 1120705, upload-time = "2025-03-07T01:45:41.434Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/5e/92aa15eca622a388b80fbf8375d4760738df6285b1e92c43d37390a33a9a/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:dfab99248034673b779bc6decafdc3404a8a6f502462201f2f31f11354204acd", size = 63625754, upload-time = "2025-03-07T01:46:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/32/f7cd6ce8a7690544d084ea21c26e910a97e077c9b7f07bf5de623ee19981/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:db9ed69dbef9715071232caa9b69c52ac7de3a95773c2db65bdba85916e4e5c0", size = 267229841, upload-time = "2025-03-07T01:46:54.356Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/f7/cd777c4109681367721b00a106f491e0d0d15cfa1fd59672ce580ce42a97/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b6c161cb130be1a07a27ea6923df8141f3c295852f4b260c65f18f3e0a091dc", size = 288117129, upload-time = "2025-03-07T01:47:40.407Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/b9/598f6ff36faaece4b3c50d26f50e38661499ff34346f00e057760b35cc9d/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8878dce784d0fac90131b6817b607e803c36e629ba34dc5b433471382196b6a5", size = 283835557, upload-time = "2025-02-26T00:16:54.265Z" }, + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/1c/857979db0ef194ca5e21478a0612bcdbbe59458d7694361882279947b349/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:31432ad4d1fb1004eb0c56203dc9bc2178a1ba69d1d9e02d64a6938ab5e40e7a", size = 322400625, upload-time = "2025-06-26T04:11:04.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/8cee5da30d13430e87bf99bb33455d2724d0a4a9cb5d7926d80ccb96d008/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:adccd7161ace7261e01bb91e44e88da350895c270d23f744f0820c818b7229e7", size = 38386204, upload-time = "2025-03-07T01:49:43.612Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/6a/03aa43cc9bd3ad91553a88b5f6fb25ed6a3752ae86ce2180221962bc2aa5/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0b48363fc6964dede448029434c6abed6c5e37f823cb43c3bcde7ecfc0457e15", size = 138936938, upload-time = "2025-09-06T00:32:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/c0/1b303feea90d296f6176f32a2a70b5ef230f9bdeb3a72bddb0dc922dc137/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d7ad891da111ebafbf7e015d34879f7112832fc239ff0d7d776b6cb685274615", size = 91161, upload-time = "2025-03-07T01:42:23.922Z" }, + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, +] + +[[package]] +name = "peft" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/9b/560e4be8e26f6fd133a03630a8df0c663b9e8d61b4ade152b72005aec83b/platformdirs-4.11.0.tar.gz", hash = "sha256:0555d18370482847566ffabcaa53ad7c6c1c29f195989ae1ed634a05f76ea1e0", size = 31953, upload-time = "2026-07-21T13:09:36.565Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/68/d8d58938dfb1370b266a1a729e6d77a985be23689a0496498ee17b2cbf90/platformdirs-4.11.0-py3-none-any.whl", hash = "sha256:360ccded2b7fce0af0ff80cc8f5942a1c5d99b0e856033acb030bfc634709e74", size = 23247, upload-time = "2026-07-21T13:09:35.422Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://files.pythonhosted.org/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://files.pythonhosted.org/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://files.pythonhosted.org/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2026.3.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, +] + +[[package]] +name = "pyyaml-include" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/be/2d07ad85e3d593d69640876a8686eae2c533db8cb7bf298d25c421b4d2d5/pyyaml-include-1.4.1.tar.gz", hash = "sha256:1a96e33a99a3e56235f5221273832464025f02ff3d8539309a3bf00dec624471", size = 20592, upload-time = "2024-03-25T14:56:43.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/ca/6a2cc3a73170d10b5af1f1613baa2ed1f8f46f62dd0bfab2bffd2c2fe260/pyyaml_include-1.4.1-py3-none-any.whl", hash = "sha256:323c7f3a19c82fbc4d73abbaab7ef4f793e146a13383866831631b26ccc7fb00", size = 19079, upload-time = "2024-03-25T14:56:41.274Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.66.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/6f/d59cad0889d15fde85254cf58e701484de3f3f0406003b3197746910b19b/sentry_sdk-2.66.1.tar.gz", hash = "sha256:f882fb08710c5f8bfc603aafa3e901b384009a19cc3f76a572b863392ee81cdc", size = 940543, upload-time = "2026-07-22T12:26:54.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/d3/726bd88f0eece09ddf431bea4c9191c18e7a8d070b854eb0014d447712ee/sentry_sdk-2.66.1-py3-none-any.whl", hash = "sha256:86002793161d9a95ef04bdd8d442e9bfece5d989b755f05d6360215094a7aff6", size = 505555, upload-time = "2026-07-22T12:26:52.71Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "smmap" +version = "5.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "platform_machine == 'arm64' and sys_platform == 'darwin'", + "sys_platform == 'win32'", + "(platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "filelock", marker = "sys_platform != 'linux'" }, + { name = "fsspec", marker = "sys_platform != 'linux'" }, + { name = "jinja2", marker = "sys_platform != 'linux'" }, + { name = "networkx", marker = "sys_platform != 'linux'" }, + { name = "setuptools", marker = "sys_platform != 'linux'" }, + { name = "sympy", marker = "sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +resolution-markers = [ + "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux')", + "platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "filelock", marker = "sys_platform == 'linux'" }, + { name = "fsspec", marker = "sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'linux'" }, + { name = "networkx", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'linux'" }, + { name = "sympy", marker = "sys_platform == 'linux'" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6f09cdf2415516be028ae82e6b985bcfc3eac37bc52ab401142689f6224516ca", upload-time = "2026-01-21T15:22:03Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torch-2.10.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:628e89bd5110ced7debee2a57c69959725b7fbc64eab81a39dd70e46c7e28ba5", upload-time = "2026-01-21T15:22:11Z" }, +] + +[[package]] +name = "torchcodec" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/85/38f4843ff2a6bf7dfb71a153acd99024dadb96749965a67524c2f1cc1894/torchcodec-0.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:57056e91d1d883d0fb77ca7759e304be9c0bdb4ea0e37bde5c2e361347063b8c", size = 4368988, upload-time = "2026-04-14T18:24:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3b41034b0f1289423745f918ace2a1e1e86b9c578c2e2461b6afcbb5354a/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f1aee486a84247fcaa67870ac5005aa8d382a9839e91e476fa71b5b3d9fda9b7", size = 2397532, upload-time = "2026-04-14T18:24:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a9/a2b6ee3e84c55bdd0c45fd991dde71c95a99115ec9e26938b212b4545dcf/torchcodec-0.11.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6c26e90e7aa982302644d0af8cb706318682bb390f48a80ecbfeab03499acd04", size = 2329883, upload-time = "2026-04-14T18:24:55.467Z" }, + { url = "https://files.pythonhosted.org/packages/82/48/683114a4ed6b59f76b6919532a5db0f4068787be26bab92cc18a1dfa6794/torchcodec-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:3fd2d10e0e0a5f455c1c87dc1380b3bd43b77dd5eeeaf479470643b1c04a2dd2", size = 1921066, upload-time = "2026-04-14T18:24:57.102Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "platform_machine == 'arm64' and sys_platform == 'darwin'", + "sys_platform == 'win32'", + "(platform_machine != 'arm64' and sys_platform == 'darwin') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", +] +dependencies = [ + { name = "numpy", marker = "sys_platform != 'linux'" }, + { name = "pillow", marker = "sys_platform != 'linux'" }, + { name = "torch", version = "2.10.0", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'linux'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0+cu128" +source = { registry = "https://download.pytorch.org/whl/cu128" } +resolution-markers = [ + "(platform_machine == 'AMD64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')", + "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'arm64' and sys_platform == 'linux')", + "platform_machine != 'AMD64' and platform_machine != 'aarch64' and platform_machine != 'arm64' and platform_machine != 'x86_64' and sys_platform == 'linux'", +] +dependencies = [ + { name = "numpy", marker = "sys_platform == 'linux'" }, + { name = "pillow", marker = "sys_platform == 'linux'" }, + { name = "torch", version = "2.10.0+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "sys_platform == 'linux'" }, +] +wheels = [ + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8623e534ef6a815bd6407d4b52dd70c7154e2eda626ad4b9cb895d36c5a3305b", upload-time = "2026-01-21T22:32:23Z" }, + { url = "https://download-r2.pytorch.org/whl/cu128/torchvision-0.25.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1255a0ca2bf987acf9f103b96c5c4cfe3415fc4a1eef17fa08af527a04a4f573", upload-time = "2026-01-21T22:32:24Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/1e/1e244ab2ab50a863e6b52cc55761910567fa532b69a6740f6e99c5fdbd98/transformers-5.5.4.tar.gz", hash = "sha256:2e67cadba81fc7608cc07c4dd54f524820bc3d95b1cabd0ef3db7733c4f8b82e", size = 8227649, upload-time = "2026-04-13T16:55:55.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/fb/162a66789c65e5afa3b051309240c26bf37fbc8fea285b4546ae747995a2/transformers-5.5.4-py3-none-any.whl", hash = "sha256:0bd6281b82966fe5a7a16f553ea517a9db1dee6284d7cb224dfd88fc0dd1c167", size = 10236696, upload-time = "2026-04-13T16:55:51.497Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, +] + +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "wandb" +version = "0.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "gitpython" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "sentry-sdk" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/a2/53ca062f430178e3af48ebc137396481d0ee885fb94a554c0df464cd8afa/wandb-0.27.2.tar.gz", hash = "sha256:c81ff93ab63f4dabc5a27b90ac3d12310fbfa6a14ca99201626921c99b2845be", size = 40300451, upload-time = "2026-06-06T01:47:02.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/95/18d3625558667b459d91c19630f7cecfbc133f87f5b144a7fb755e473e8c/wandb-0.27.2-py3-none-macosx_12_0_arm64.whl", hash = "sha256:978400b3c4b7d97e927c32264453da5e4a0040a3468d5b77a00d9c480613f370", size = 23990048, upload-time = "2026-06-06T01:46:38.902Z" }, + { url = "https://files.pythonhosted.org/packages/43/14/72c26f67b0b6cb307cbb76659465c6ab7d99ea27c268d1b4f5aa82c4d8e5/wandb-0.27.2-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:de8099f02f540c743069617db7d034511a64c193748783aa6d2d98310918d170", size = 25165812, upload-time = "2026-06-06T01:46:42.068Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a3/f9fe31ca72b4f5854d1e488403d6310783127a6b7e267c28577e9bd51b43/wandb-0.27.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e0779592410215a2762063c3585d3dcad73c7dca9cb6d63c4dcc1588267c1392", size = 24554366, upload-time = "2026-06-06T01:46:44.57Z" }, + { url = "https://files.pythonhosted.org/packages/76/e2/7a5064aba235ddb855b8c2250e07e6187fcc8382332e237e545d4de094ee/wandb-0.27.2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:55bfebf4d382116a8e9610848cadc0de50d406bacd3d0a390d12dabde196f009", size = 26380293, upload-time = "2026-06-06T01:46:47.487Z" }, + { url = "https://files.pythonhosted.org/packages/ed/4c/0c845edac5ff0fd0930e881bec2569f2e2af2a4fc873249855600546eee0/wandb-0.27.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:566aa2fcd67d2a23c08713da75e9daf82f30f7136af76763ef1d7db3d901d940", size = 24728823, upload-time = "2026-06-06T01:46:49.887Z" }, + { url = "https://files.pythonhosted.org/packages/e1/7a/a6f7a02a0e6bf73e163b61caca03aaba3452836a02dbe2b64f9e1a3c6afc/wandb-0.27.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:41158181fc5b691438b3d04fee0a8c061e3f1f407a3258096afbebfe1db24e72", size = 26691957, upload-time = "2026-06-06T01:46:52.384Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9b/aa94eb8265b0c55dc6c3e435c11241b3f885c7a1720718046efd7cbd8361/wandb-0.27.2-py3-none-win32.whl", hash = "sha256:5c55fad8c7be9d345dcebdc9dc10f7d2ac5af5bede62acbcd79a412ccaf48c87", size = 24151396, upload-time = "2026-06-06T01:46:54.793Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0b/9e442779f5f24baaca044daf7546a735f41a811886b84ae12740d51b7f9d/wandb-0.27.2-py3-none-win_amd64.whl", hash = "sha256:87204d4fe40fbd9a1fe89a05927ce4ddb8be34d8210045457819fb4a35e0bcea", size = 24151404, upload-time = "2026-06-06T01:46:56.994Z" }, + { url = "https://files.pythonhosted.org/packages/aa/3a/01ab3afc1f6f962df93db34be8183147c9821e4dce82dc03313fb8d08635/wandb-0.27.2-py3-none-win_arm64.whl", hash = "sha256:32ed7456f40443c971e95dd63704d840fce66c24f88049a9bda8a09dfe85effe", size = 22063373, upload-time = "2026-06-06T01:47:00.263Z" }, +] + +[[package]] +name = "xxhash" +version = "3.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/63/71aa56b151a1b28770037a61bd4e461c2619cfc8866a4fcaf1548605e325/xxhash-3.8.1.tar.gz", hash = "sha256:b0de4bf3aa66363552d52c6a89003c479911f12098cd48a53d44a0f7a25f7c46", size = 86223, upload-time = "2026-07-06T10:49:58.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/91/f65c34a7aa7b4e7cf4854f8e6ef3f7ee32ceac41d4f008da0780db0612f6/xxhash-3.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e6e49370822c1f4d8d90e678b06dbcb08b51a026a7c4b55479e7d467f2e813bc", size = 34680, upload-time = "2026-07-06T10:44:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/57/04/b10a245a4c09a9cfa88f8e9ae755029413ad1ac17047f9a61906e5ae0799/xxhash-3.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:220d68130f83f7cc86d6edfdeab176adc73d7200bf3a8ec10c629e8cf605c215", size = 32397, upload-time = "2026-07-06T10:44:42.196Z" }, + { url = "https://files.pythonhosted.org/packages/3a/75/45ab795b5945b6388583bd75202106af505537935566c15a1577797a0e08/xxhash-3.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d365ee1892c1fa803536f8c6ce21d24b29c9718ec75eb856095c07830f8c478", size = 220549, upload-time = "2026-07-06T10:44:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/13/44/5ba2bd0a14ddf4193fc7d8ec29625f659f22c06d60b28f04bf46305d8330/xxhash-3.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:852bfe059720632e2f16a6a4745e41d20937b2bf2a42a401e2412046bb6971cc", size = 241186, upload-time = "2026-07-06T10:44:45.534Z" }, + { url = "https://files.pythonhosted.org/packages/23/32/c4147def4d1e4538b906f82731e0ba23424377fc50a7cddd03cd284c8f63/xxhash-3.8.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2f8c25a7061d952de589bd0ea0eaadee32378ff83dd6a677b267f9cd86f401f8", size = 264852, upload-time = "2026-07-06T10:44:47.199Z" }, + { url = "https://files.pythonhosted.org/packages/6c/bd/71ed14f4f0318bb7fd7b2ec51999413487fa8da8d41208e84d50d1ef0f98/xxhash-3.8.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:868a8dcaff1a84ba78038e1cef14fc88ccf84d9b4d12ea604696e0693296aa56", size = 242663, upload-time = "2026-07-06T10:44:48.846Z" }, + { url = "https://files.pythonhosted.org/packages/91/09/70af22c565a8473b3f2ae73f88e7721af281bc4a575236dbd1970c9f76f6/xxhash-3.8.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6536d8677d2fff7e64cd0b98b976df9de7aee0e69590044c2af5f51b76b7a170", size = 473510, upload-time = "2026-07-06T10:44:50.695Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/34db781c8f0cf99c544ca1f2bc2e5bf55426e1eb4ca6de8ea5da56a9f352/xxhash-3.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c0cedd280eab2e8291270e6c04894dbc096f8159a39dcf1807429f026ca3cc", size = 220469, upload-time = "2026-07-06T10:44:52.422Z" }, + { url = "https://files.pythonhosted.org/packages/93/5f/9a184f615fa5a4dce30c01534f62946ce5a11ce40f73785cbd356ccabaa9/xxhash-3.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daa86e4b68221d38e669bb236ba112d0335353829fb627c82e5909e4bbe8694c", size = 310290, upload-time = "2026-07-06T10:44:54.142Z" }, + { url = "https://files.pythonhosted.org/packages/a9/dc/9b9a9789011ee153723a5eb9e7dd7fcbae2ba9b3fe7a729249ca7c252056/xxhash-3.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2bc7113e6f2b6b3922dd61796ca9f36af09da3773898e7003038dc992fc83b8d", size = 238173, upload-time = "2026-07-06T10:44:55.693Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/71c6005ada9dcb608a4e1902e8475ecadb5f3fbfa04e1e244d276a2d0c43/xxhash-3.8.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5eed32dad81d6ba8e62dc7b9ffa0500199385d7810a8dd9d4eafaceb8c6e20bb", size = 269026, upload-time = "2026-07-06T10:44:57.424Z" }, + { url = "https://files.pythonhosted.org/packages/2f/87/d6c036ba25dfbd9c8633be5aa86fc9474bbb9e2c68212a841d090abe7344/xxhash-3.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:83697b0ea1f10e7f5d8b26a4906fa851393c61546c63839643a2b7fe2d868061", size = 224970, upload-time = "2026-07-06T10:44:59.085Z" }, + { url = "https://files.pythonhosted.org/packages/48/62/4c1f035a41c5752aa05e195b6c904c07b94fe9061a16de61e72a6e6b135f/xxhash-3.8.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:36fc69160465ae75c6ec4ac9f781bb2aa16ae7ff869e73c26fee85fbb11b9887", size = 240820, upload-time = "2026-07-06T10:45:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/d39d565069b87e86d21a2af2a31d04db79249d25aa8d5b62959056a89857/xxhash-3.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:445e0f5a31f2f3546ae0895d4811e159518cdc9d824c11419898d40cfadb677e", size = 300619, upload-time = "2026-07-06T10:45:02.716Z" }, + { url = "https://files.pythonhosted.org/packages/13/22/75467acc887edc8cf71c97ab1708feb3df7a88bda589b9f399765c6387d2/xxhash-3.8.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:dfe0580fbfd5e4af87d0cc52d2044f155d55ebd8c8a93568758a2ea7d8e15975", size = 443267, upload-time = "2026-07-06T10:45:04.653Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b6/1da3baa5fa6ef705e3425fddd382be7dfc4dfba2686df90a20f16e9c7b1b/xxhash-3.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:095e1323fa108be1292c54c86da3ef3c7a7dc015b105a52133973bc07a6ad11a", size = 217338, upload-time = "2026-07-06T10:45:06.304Z" }, + { url = "https://files.pythonhosted.org/packages/78/dd/b5295a9f97484e7a1c2b283a742ca45e3104991c55a1ef670dde161829ba/xxhash-3.8.1-cp312-cp312-win32.whl", hash = "sha256:bf28f55e427e0483acb1f666bd0d869b6d5e5a716680c216ad7befe3d4cfba2e", size = 31970, upload-time = "2026-07-06T10:45:07.823Z" }, + { url = "https://files.pythonhosted.org/packages/ec/31/3fa0b807d7e21515cd975e7fe5c039d52ac3e9401a96d6ad68dae6305215/xxhash-3.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:2256e80e4960ee282f63428adb349cb7f8bd8efe4db770d88eb815f4b9860724", size = 32741, upload-time = "2026-07-06T10:45:09.42Z" }, + { url = "https://files.pythonhosted.org/packages/b8/05/86feada74e239600e6875aa507afb40482a89b92700aa74a92da83bdcb77/xxhash-3.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:9df56e6df96a60590935e22373041cccc91fd55858763dcffb55bf63b3a2b396", size = 29234, upload-time = "2026-07-06T10:45:10.809Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/CMakeLists.txt b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/CMakeLists.txt new file mode 100644 index 000000000..bcff6b4f9 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/CMakeLists.txt @@ -0,0 +1,58 @@ +cmake_minimum_required(VERSION 3.22) +project(kinova_vla_test_sim_behaviors) + +find_package(moveit_pro_package REQUIRED) +moveit_pro_package() + +set(THIS_PACKAGE_INCLUDE_DEPENDS + control_msgs + geometry_msgs + moveit_pro_base + moveit_pro_behavior + moveit_pro_behavior_interface + moveit_studio_internal_msgs + nlohmann_json + pluginlib + rclcpp_action + sensor_msgs + tf2_eigen + trajectory_msgs) +foreach(package IN ITEMS ${THIS_PACKAGE_INCLUDE_DEPENDS}) + find_package(${package} REQUIRED) +endforeach() + +add_library( + kinova_vla_test_sim_behaviors + SHARED + src/compute_top_down_keyposes.cpp + src/plan_joint_spline_through_poses.cpp + src/send_gripper_command.cpp + src/wait_for_episode_start.cpp + src/register_behaviors.cpp) +target_include_directories( + kinova_vla_test_sim_behaviors + PUBLIC $ + $) +ament_target_dependencies(kinova_vla_test_sim_behaviors ${THIS_PACKAGE_INCLUDE_DEPENDS}) + +install( + TARGETS kinova_vla_test_sim_behaviors + EXPORT kinova_vla_test_sim_behaviorsTargets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + INCLUDES + DESTINATION include) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + moveit_pro_behavior_test(kinova_vla_test_sim_behaviors) + ament_lint_auto_find_test_dependencies() +endif() + +pluginlib_export_plugin_description_file( + moveit_pro_behavior_interface kinova_vla_test_sim_behaviors_plugin_description.xml) + +ament_export_targets(kinova_vla_test_sim_behaviorsTargets HAS_LIBRARY_TARGET) +ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) +ament_package() diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/compute_top_down_keyposes.hpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/compute_top_down_keyposes.hpp new file mode 100644 index 000000000..136b98340 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/compute_top_down_keyposes.hpp @@ -0,0 +1,79 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace kinova_vla_test_sim_behaviors +{ +/** + * @brief Orientation of a top-down grasp whose jaw axis is spun to @p yaw about world +Z. + * + * The tip frame's +Z is the approach axis and +Y the jaw open/close axis, so pointing the + * approach axis at world -Z is Rx(pi). + */ +[[nodiscard]] Eigen::Quaterniond topDownGraspOrientation(double yaw); + +/** @brief Rotation about world +Z of @p orientation. */ +[[nodiscard]] double yawOf(const Eigen::Quaterniond& orientation); + +/** + * @brief The one of a cube's four symmetry-equivalent top-down yaws that @p cost scores lowest. + * + * A cube is 4-fold symmetric about its vertical axis, so all four grasps are visually identical + * and any of them stacks; which one the arm takes is purely a question of how far it has to + * move. The oracle scores them by IK joint distance from the arm's current pose + * (`oracle.py::_choose_topdown_yaw`), and this reproduces that with @p cost injected so the + * ranking is testable without a robot model. + * + * @returns nullopt when @p cost scores no candidate, i.e. none is reachable. + */ +[[nodiscard]] std::optional chooseTopDownYawByCost(double cube_yaw, + const std::function(double)>& cost); + +/** @brief Sum of squared per-joint differences, the oracle's IK cost. */ +[[nodiscard]] double jointDistanceCost(const std::vector& from, const std::vector& to); + +/** + * @brief One top-down waypoint @p height above @p aim_pose, at @p orientation. + * + * @p held_object_offset is the held object's origin in the tip frame; it shifts the waypoint so + * the *object* lands on the aim point rather than the tip. Pass zero when the tip itself is the + * thing being positioned. + */ +[[nodiscard]] Eigen::Isometry3d topDownKeypose(const Eigen::Isometry3d& aim_pose, const Eigen::Quaterniond& orientation, + const Eigen::Vector3d& held_object_offset, double height); + +/** @brief topDownKeypose() at each of @p heights, all sharing @p orientation. */ +[[nodiscard]] std::vector computeTopDownKeyposes(const Eigen::Isometry3d& aim_pose, + const Eigen::Quaterniond& orientation, + const Eigen::Vector3d& held_object_offset, + const std::vector& heights); + +/** + * @brief Builds the Cartesian path for one segment of the color-cube stacking oracle. + */ +class ComputeTopDownKeyposes : public moveit_pro::behaviors::SharedResourcesNode +{ +public: + ComputeTopDownKeyposes(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources); + + [[nodiscard]] static BT::PortsList providedPorts(); + [[nodiscard]] static BT::KeyValueVector metadata(); + + BT::NodeStatus tick() override; +}; +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/plan_joint_spline_through_poses.hpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/plan_joint_spline_through_poses.hpp new file mode 100644 index 000000000..e7a0561f3 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/plan_joint_spline_through_poses.hpp @@ -0,0 +1,101 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace kinova_vla_test_sim_behaviors +{ +/** + * @brief Clamped cubic spline through joint-space knots. + * + * Passes through every knot exactly, is C1 continuous across them, and has zero first + * derivative at both ends. So the arm eases from rest, flows through the interior knots + * without stopping, and arrives at the last knot at rest. + * + * The parameter is dimensionless: divide the derivatives by the trajectory duration to + * get joint velocities and accelerations. + */ +class JointSpline +{ +public: + /** @throws std::invalid_argument unless there are >= 2 knots, all of one width, at + * strictly increasing @p parameters of the same count. */ + JointSpline(const std::vector& parameters, const std::vector& knots); + + [[nodiscard]] Eigen::VectorXd position(double s) const; + [[nodiscard]] Eigen::VectorXd velocity(double s) const; + [[nodiscard]] Eigen::VectorXd acceleration(double s) const; + + /** @brief Largest |d position / d parameter| over the spline, per joint. */ + [[nodiscard]] Eigen::VectorXd peakSpeed() const; + +private: + /** Where a parameter falls: the segment holding it, and the basis weights within it. */ + struct Segment + { + std::size_t index; + double width; + double from; + double to; + }; + + /** @p s saturated to the knot range, resolved against the segment containing it. */ + [[nodiscard]] Segment locate(double s) const; + + std::vector parameters_; + std::vector knots_; + std::vector moments_; +}; + +/** + * @brief How long one straight knot-to-knot segment should take. + * + * The slower of two budgets: covering @p cartesian_length at @p cartesian_speed, and + * holding every joint's peak speed under @p joint_velocity_cap. + */ +[[nodiscard]] double segmentDuration(double cartesian_length, const Eigen::VectorXd& joint_delta, + double cartesian_speed, const Eigen::VectorXd& joint_velocity_cap); + +/** + * @brief Knot parameters spanning [0, 1], each segment's share proportional to @p segment_durations. + * + * Giving a long or slow segment more of the parameter is what makes one spline over the + * whole chain respect a single speed budget instead of rushing its longest leg. + */ +[[nodiscard]] std::vector splineKnotParameters(const std::vector& segment_durations); + +/** + * @brief Duration to run @p spline over, as the slower of the same two budgets. + * + * Measured against the spline itself rather than the straight knot-to-knot deltas, since + * flowing through an interior knot overshoots what those deltas predict. + */ +[[nodiscard]] double splineDuration(const JointSpline& spline, double cartesian_length, double cartesian_speed, + const Eigen::VectorXd& joint_velocity_cap); + +/** + * @brief Plans one segment of the color-cube stacking oracle as a joint-space trajectory. + */ +class PlanJointSplineThroughPoses : public moveit_pro::behaviors::SharedResourcesNode +{ +public: + PlanJointSplineThroughPoses(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources); + + [[nodiscard]] static BT::PortsList providedPorts(); + [[nodiscard]] static BT::KeyValueVector metadata(); + + BT::NodeStatus tick() override; +}; +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/send_gripper_command.hpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/send_gripper_command.hpp new file mode 100644 index 000000000..8a1aa0ae6 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/send_gripper_command.hpp @@ -0,0 +1,37 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace kinova_vla_test_sim_behaviors +{ +/** + * @brief Sends a gripper position goal and succeeds without waiting for it. + */ +class SendGripperCommand : public moveit_pro::behaviors::SharedResourcesNode +{ +public: + SendGripperCommand(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources); + + [[nodiscard]] static BT::PortsList providedPorts(); + [[nodiscard]] static BT::KeyValueVector metadata(); + + BT::NodeStatus tick() override; + +private: + rclcpp_action::Client::SharedPtr client_; + std::string action_name_; +}; +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/wait_for_episode_start.hpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/wait_for_episode_start.hpp new file mode 100644 index 000000000..cab7ea51b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/include/kinova_vla_test_sim_behaviors/wait_for_episode_start.hpp @@ -0,0 +1,48 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#pragma once + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace kinova_vla_test_sim_behaviors +{ +using GetActiveRecordingSrv = moveit_studio_internal_msgs::srv::GetActiveRecording; + +/** + * @brief Blocks until the active Trainer recording session has opened an episode. + */ +class WaitForEpisodeStart : public moveit_pro::behaviors::AsyncBehaviorBase +{ +public: + WaitForEpisodeStart(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources); + + [[nodiscard]] static BT::PortsList providedPorts(); + [[nodiscard]] static BT::KeyValueVector metadata(); + + tl::expected doWork() override; + tl::expected doHalt() override; + + std::shared_future>& getFuture() override + { + return future_; + } + +private: + std::shared_future> future_; + std::unique_ptr> client_; + std::atomic_bool halted_{ false }; +}; +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/kinova_vla_test_sim_behaviors_plugin_description.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/kinova_vla_test_sim_behaviors_plugin_description.xml new file mode 100644 index 000000000..c664490de --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/kinova_vla_test_sim_behaviors_plugin_description.xml @@ -0,0 +1,7 @@ + + + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/package.xml b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/package.xml new file mode 100644 index 000000000..ee37d1e71 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/package.xml @@ -0,0 +1,39 @@ + + + kinova_vla_test_sim_behaviors + 6.5.0 + + Behaviors for collecting color-cube stacking demonstrations + + + Dan Wahl + Dan Wahl + + BSD-3-Clause + + ament_cmake + + moveit_pro_package + + control_msgs + geometry_msgs + moveit_pro_base + moveit_pro_behavior + moveit_pro_behavior_interface + moveit_studio_internal_msgs + nlohmann_json + pluginlib + rclcpp_action + sensor_msgs + trajectory_msgs + tf2_eigen + + ament_lint_auto + ament_cmake_gtest + ament_clang_format + ament_clang_tidy + + + ament_cmake + + diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/compute_top_down_keyposes.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/compute_top_down_keyposes.cpp new file mode 100644 index 000000000..a1b4e19ca --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/compute_top_down_keyposes.cpp @@ -0,0 +1,283 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +inline constexpr auto kDescriptionComputeTopDownKeyposes = R"( +

+ Builds the Cartesian path for one segment of the color-cube stacking oracle: + a top-down waypoint at each of heights above aim_pose, + all sharing one grasp orientation. +

+

+ The orientation points the tip's approach axis straight down, spun to whichever + of the cube's four symmetry-equivalent yaws costs the least arm motion from + seed_joint_state, scored by inverse kinematics. Set + held_object_offset to the grasped object's transform in the tip + frame to aim the object at the waypoints instead of the tip, which is what + places a carried cube on a target cube. +

+

+ Wire reuse_orientation from an earlier segment's + orientation once the gripper has closed. Choosing again while + holding an object would twist it in the jaws, since a different yaw is equally + cheap for the arm but not the same grip. +

+

+ Feed the scene from GetCurrentPlanningScene into + GetRobotJointState, whose joint_state output is this + Behavior's seed_joint_state. Send path on to + PlanJointSplineThroughPoses. +

+ )"; + +constexpr auto kPortIDAimPose = "aim_pose"; +constexpr auto kPortIDSeedJointState = "seed_joint_state"; +constexpr auto kPortIDPlanningGroupName = "planning_group_name"; +constexpr auto kPortIDTipLink = "tip_link"; +constexpr auto kPortIDHeldObjectOffset = "held_object_offset"; +constexpr auto kPortIDHeights = "heights"; +constexpr auto kPortIDReuseOrientation = "reuse_orientation"; +constexpr auto kPortIDPath = "path"; +constexpr auto kPortIDOrientation = "orientation"; +} // namespace + +namespace kinova_vla_test_sim_behaviors +{ +Eigen::Quaterniond topDownGraspOrientation(double yaw) +{ + return Eigen::Quaterniond(Eigen::AngleAxisd(yaw, Eigen::Vector3d::UnitZ()) * + Eigen::AngleAxisd(M_PI, Eigen::Vector3d::UnitX())); +} + +double yawOf(const Eigen::Quaterniond& orientation) +{ + const Eigen::Matrix3d rotation = orientation.normalized().toRotationMatrix(); + return std::atan2(rotation(1, 0), rotation(0, 0)); +} + +std::optional chooseTopDownYawByCost(double cube_yaw, const std::function(double)>& cost) +{ + std::optional best_yaw; + std::optional best_cost; + for (int quarter_turns = 0; quarter_turns < 4; ++quarter_turns) + { + const double yaw = cube_yaw + quarter_turns * (M_PI / 2.0); + const std::optional candidate = cost(yaw); + if (candidate.has_value() && (!best_cost.has_value() || candidate.value() < best_cost.value())) + { + best_cost = candidate; + best_yaw = yaw; + } + } + return best_yaw; +} + +double jointDistanceCost(const std::vector& from, const std::vector& to) +{ + double cost = 0.0; + for (std::size_t i = 0; i < std::min(from.size(), to.size()); ++i) + { + const double difference = to[i] - from[i]; + cost += difference * difference; + } + return cost; +} + +Eigen::Isometry3d topDownKeypose(const Eigen::Isometry3d& aim_pose, const Eigen::Quaterniond& orientation, + const Eigen::Vector3d& held_object_offset, double height) +{ + // The offset is rigid in the tip frame, so rotating the wrist from the grasp orientation to + // this one carries the object with it. + const Eigen::Vector3d tip_to_object = orientation * held_object_offset; + Eigen::Isometry3d keypose(orientation); + keypose.translation() = aim_pose.translation() + Eigen::Vector3d(0.0, 0.0, height) - tip_to_object; + return keypose; +} + +std::vector computeTopDownKeyposes(const Eigen::Isometry3d& aim_pose, + const Eigen::Quaterniond& orientation, + const Eigen::Vector3d& held_object_offset, + const std::vector& heights) +{ + std::vector keyposes; + keyposes.reserve(heights.size()); + for (const double height : heights) + { + keyposes.push_back(topDownKeypose(aim_pose, orientation, held_object_offset, height)); + } + return keyposes; +} + +ComputeTopDownKeyposes::ComputeTopDownKeyposes( + const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources) + : SharedResourcesNode(name, config, shared_resources) +{ +} + +BT::PortsList ComputeTopDownKeyposes::providedPorts() +{ + return { + BT::InputPort(kPortIDAimPose, "{aim_pose}", + "Pose the waypoints are stacked above, and whose yaw the grasp " + "aligns to. The cube to grasp or to stack onto; for a straight " + "retract, the current tip pose."), + BT::InputPort(kPortIDSeedJointState, "{seed_joint_state}", + "Arm's current joint positions, from GetRobotJointState. Both the IK " + "seed and the pose the grasp candidates are scored against."), + BT::InputPort(kPortIDPlanningGroupName, "manipulator", + "SRDF joint group the grasp candidates are solved for."), + BT::InputPort(kPortIDTipLink, "grasp_link", + "Link the waypoints position. Any fixed offset between this and the frame the " + "trajectory controls lies along the shared approach axis, so it shifts all four " + "candidates alike and cannot change which one wins."), + BT::InputPort(kPortIDHeldObjectOffset, "", + "Grasped object's transform in the frame the waypoints " + "position, which is tip_link shifted by the planner's " + "tip_offset, not tip_link itself. Read it with " + "GetLatestTransform using that frame as the target. Leave " + "unwired to position the frame itself."), + BT::InputPort>(kPortIDHeights, + "Heights above aim_pose, in meters, one waypoint each, semicolon separated."), + BT::InputPort(kPortIDReuseOrientation, "", + "Grasp orientation to reuse instead of choosing one. Wire this " + "from an earlier segment's orientation for any move that carries " + "a grasped object."), + BT::OutputPort>(kPortIDPath, "{path}", + "Cartesian path for PlanJointSplineThroughPoses, in " + "the frame of aim_pose."), + BT::OutputPort(kPortIDOrientation, "{orientation}", + "The grasp orientation these waypoints share."), + }; +} + +BT::KeyValueVector ComputeTopDownKeyposes::metadata() +{ + return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Color-Cube Stacking" }, + { moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionComputeTopDownKeyposes } }; +} + +BT::NodeStatus ComputeTopDownKeyposes::tick() +{ + const auto ports = moveit_pro::behaviors::getRequiredInputs( + getInput(kPortIDAimPose), + getInput(kPortIDSeedJointState), getInput>(kPortIDHeights), + getInput(kPortIDPlanningGroupName), getInput(kPortIDTipLink)); + if (!ports.has_value()) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), "Failed to get required values from input data ports: " + ports.error()); + return BT::NodeStatus::FAILURE; + } + const auto& [aim_pose_msg, seed_joint_state, heights, planning_group_name, tip_link] = ports.value(); + + if (heights.empty()) + { + getBehaviorContext()->logger->publishFailureMessage(name(), "heights is empty, so the path would have no " + "waypoints."); + return BT::NodeStatus::FAILURE; + } + + const auto& robot_model = getBehaviorContext()->robot_model; + const auto* joint_group = robot_model ? robot_model->getJointModelGroup(planning_group_name) : nullptr; + if (joint_group == nullptr) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), fmt::format("No planning group '{}' in the robot model.", planning_group_name)); + return BT::NodeStatus::FAILURE; + } + + moveit_pro::base::RobotState seed_state(robot_model); + seed_state.setToDefaultValues(); + for (std::size_t i = 0; i < seed_joint_state.name.size() && i < seed_joint_state.position.size(); ++i) + { + if (robot_model->hasJointModel(seed_joint_state.name[i])) + { + seed_state.setJointPositions(seed_joint_state.name[i], { seed_joint_state.position[i] }); + } + } + seed_state.update(); + std::vector seed_positions; + seed_state.copyJointGroupPositions(joint_group, seed_positions); + + Eigen::Vector3d held_object_offset = Eigen::Vector3d::Zero(); + if (const auto offset = getInput(kPortIDHeldObjectOffset); offset.has_value()) + { + held_object_offset = tf2::transformToEigen(offset.value()).translation(); + } + + Eigen::Isometry3d aim_pose; + tf2::fromMsg(aim_pose_msg.pose, aim_pose); + + // Score each candidate where the arm arrives first, so the cost is the motion actually spent + // getting there rather than to the end of the segment. + const double approach_height = heights.front(); + const std::string ik_tip_link = tip_link; + const auto cost_of = [&](double yaw) -> std::optional { + const Eigen::Quaterniond orientation = topDownGraspOrientation(yaw); + const Eigen::Isometry3d keypose = topDownKeypose(aim_pose, orientation, held_object_offset, approach_height); + moveit_pro::base::RobotState candidate(seed_state); + if (!candidate.setFromIK(joint_group, keypose, ik_tip_link)) + { + return std::nullopt; + } + std::vector solution; + candidate.copyJointGroupPositions(joint_group, solution); + return jointDistanceCost(seed_positions, solution); + }; + + Eigen::Quaterniond orientation; + if (const auto reused = getInput(kPortIDReuseOrientation); reused.has_value()) + { + tf2::fromMsg(reused.value(), orientation); + } + else + { + const double cube_yaw = yawOf(Eigen::Quaterniond(aim_pose.rotation())); + const std::optional yaw = chooseTopDownYawByCost(cube_yaw, cost_of); + if (!yaw.has_value()) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), "No top-down grasp of this pose is reachable: inverse kinematics failed for all four " + "symmetry-equivalent yaws."); + return BT::NodeStatus::FAILURE; + } + orientation = topDownGraspOrientation(yaw.value()); + } + + const std::vector keyposes = + computeTopDownKeyposes(aim_pose, orientation, held_object_offset, heights); + + std::vector path; + path.reserve(keyposes.size()); + for (const auto& keypose : keyposes) + { + geometry_msgs::msg::PoseStamped pose_msg; + pose_msg.header = aim_pose_msg.header; + pose_msg.pose = tf2::toMsg(keypose); + path.push_back(pose_msg); + } + setOutput(kPortIDPath, path); + setOutput(kPortIDOrientation, tf2::toMsg(orientation)); + + return BT::NodeStatus::SUCCESS; +} +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/plan_joint_spline_through_poses.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/plan_joint_spline_through_poses.cpp new file mode 100644 index 000000000..b188e267e --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/plan_joint_spline_through_poses.cpp @@ -0,0 +1,422 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +inline constexpr auto kDescriptionPlanJointSplineThroughPoses = R"( +

+ Plans a Cartesian path as a single joint-space trajectory: + each waypoint is solved for inverse kinematics once, warm-started from the + previous solution, and the resulting joint knots are joined by a clamped + cubic spline. The arm eases from rest, passes through every waypoint exactly + without stopping, and arrives at the last one at rest. +

+

+ Interpolating in joint space between same-branch solutions is what keeps a + redundant arm's posture stable. Solving inverse kinematics per interpolated + waypoint instead, as a Cartesian planner does, lets the null space drift + between waypoints and can swing the wrist through a different posture on a + path whose endpoints both look correct. +

+

+ The duration is the slower of covering the path at + cartesian_speed and holding every joint under its model velocity + limit scaled by joint_velocity_scale, so the tip holds a roughly + constant speed instead of a constant joint rate. +

+

+ Feed path from ComputeTopDownKeyposes, + seed_joint_state from GetRobotJointState, and the + output to ExecuteTrajectory. +

+ )"; + +constexpr auto kPortIDPath = "path"; +constexpr auto kPortIDSeedJointState = "seed_joint_state"; +constexpr auto kPortIDPlanningGroupName = "planning_group_name"; +constexpr auto kPortIDTipLink = "tip_link"; +constexpr auto kPortIDTipOffset = "tip_offset"; +constexpr auto kPortIDCartesianSpeed = "cartesian_speed"; +constexpr auto kPortIDJointVelocityScale = "joint_velocity_scale"; +constexpr auto kPortIDSamplingRate = "sampling_rate"; +constexpr auto kPortIDJointTrajectory = "joint_trajectory_msg"; + +// Peak-to-mean speed ratio used only to size each segment's nominal duration, which sets how +// much of the spline parameter it gets. The spline's true peak is measured afterwards, so this +// is a weighting, not a limit; it is the oracle's figure, from the smootherstep profile that +// preceded the spline. +constexpr double kNominalPeakSpeedRatio = 1.875; + +// A trajectory long enough to be worth executing, and a ceiling so an unreachable waypoint +// produces a failure rather than an arm that creeps for minutes. +constexpr double kMinimumDuration = 0.2; +constexpr double kMaximumDuration = 60.0; +} // namespace + +namespace kinova_vla_test_sim_behaviors +{ +JointSpline::JointSpline(const std::vector& parameters, const std::vector& knots) + : parameters_(parameters), knots_(knots) +{ + const std::size_t count = knots.size(); + if (count < 2) + { + throw std::invalid_argument(fmt::format("A spline needs at least 2 knots, got {}.", count)); + } + if (parameters.size() != count) + { + throw std::invalid_argument( + fmt::format("Got {} knots but {} parameters; they must correspond.", count, parameters.size())); + } + const Eigen::Index width = knots.front().size(); + for (std::size_t i = 0; i < count; ++i) + { + if (knots[i].size() != width) + { + throw std::invalid_argument( + fmt::format("Knot {} has width {}, but knot 0 has width {}.", i, knots[i].size(), width)); + } + if (i > 0 && !(parameters[i] > parameters[i - 1])) + { + throw std::invalid_argument(fmt::format("Parameters must strictly increase, but parameter {} is {} and {} is {}.", + i - 1, parameters[i - 1], i, parameters[i])); + } + } + + // Second derivatives at the knots, from the standard tridiagonal moment formulation with + // both ends clamped to zero first derivative. + Eigen::MatrixXd system = Eigen::MatrixXd::Zero(count, count); + Eigen::MatrixXd right_hand_side = Eigen::MatrixXd::Zero(count, width); + const auto span = [&](std::size_t i) { return parameters_[i + 1] - parameters_[i]; }; + const auto slope = [&](std::size_t i) -> Eigen::RowVectorXd { + return (knots_[i + 1] - knots_[i]).transpose() / span(i); + }; + + for (std::size_t i = 1; i + 1 < count; ++i) + { + system(i, i - 1) = span(i - 1); + system(i, i) = 2.0 * (span(i - 1) + span(i)); + system(i, i + 1) = span(i); + right_hand_side.row(i) = 6.0 * (slope(i) - slope(i - 1)); + } + system(0, 0) = 2.0 * span(0); + system(0, 1) = span(0); + right_hand_side.row(0) = 6.0 * slope(0); + system(count - 1, count - 1) = 2.0 * span(count - 2); + system(count - 1, count - 2) = span(count - 2); + right_hand_side.row(count - 1) = -6.0 * slope(count - 2); + + const Eigen::MatrixXd moments = system.colPivHouseholderQr().solve(right_hand_side); + moments_.reserve(count); + for (std::size_t i = 0; i < count; ++i) + { + moments_.push_back(moments.row(i).transpose()); + } +} + +JointSpline::Segment JointSpline::locate(double s) const +{ + // Saturating rather than extrapolating: a cubic continued past its last knot diverges fast. + s = std::clamp(s, parameters_.front(), parameters_.back()); + const auto upper = std::upper_bound(parameters_.begin(), parameters_.end(), s); + const auto found = static_cast(std::distance(parameters_.begin(), upper)); + const std::size_t index = std::clamp(found == 0 ? 0 : found - 1, 0, parameters_.size() - 2); + const double width = parameters_[index + 1] - parameters_[index]; + return { index, width, (parameters_[index + 1] - s) / width, (s - parameters_[index]) / width }; +} + +Eigen::VectorXd JointSpline::position(double s) const +{ + const auto [i, h, a, b] = locate(s); + return a * knots_[i] + b * knots_[i + 1] + + ((a * a * a - a) * moments_[i] + (b * b * b - b) * moments_[i + 1]) * (h * h / 6.0); +} + +Eigen::VectorXd JointSpline::velocity(double s) const +{ + const auto [i, h, a, b] = locate(s); + return (knots_[i + 1] - knots_[i]) / h + + ((1.0 - 3.0 * a * a) * moments_[i] + (3.0 * b * b - 1.0) * moments_[i + 1]) * (h / 6.0); +} + +Eigen::VectorXd JointSpline::acceleration(double s) const +{ + const auto [i, h, a, b] = locate(s); + return a * moments_[i] + b * moments_[i + 1]; +} + +Eigen::VectorXd JointSpline::peakSpeed() const +{ + Eigen::VectorXd peak = Eigen::VectorXd::Zero(knots_.front().size()); + for (std::size_t i = 0; i + 1 < parameters_.size(); ++i) + { + // Speed is quadratic within a segment, so its extremes are at the ends or where the + // acceleration crosses zero. + std::vector candidates = { parameters_[i], parameters_[i + 1] }; + for (Eigen::Index j = 0; j < peak.size(); ++j) + { + const double denominator = moments_[i + 1][j] - moments_[i][j]; + if (std::abs(denominator) > std::numeric_limits::epsilon()) + { + const double a = moments_[i + 1][j] / denominator; + if (a >= 0.0 && a <= 1.0) + { + candidates.push_back(parameters_[i + 1] - a * (parameters_[i + 1] - parameters_[i])); + } + } + } + for (const double s : candidates) + { + peak = peak.cwiseMax(velocity(s).cwiseAbs()); + } + } + return peak; +} + +double segmentDuration(double cartesian_length, const Eigen::VectorXd& joint_delta, double cartesian_speed, + const Eigen::VectorXd& joint_velocity_cap) +{ + const double cartesian = cartesian_speed > 0.0 ? cartesian_length / cartesian_speed : 0.0; + double joint = 0.0; + for (Eigen::Index j = 0; j < joint_delta.size() && j < joint_velocity_cap.size(); ++j) + { + if (joint_velocity_cap[j] > 0.0) + { + joint = std::max(joint, kNominalPeakSpeedRatio * std::abs(joint_delta[j]) / joint_velocity_cap[j]); + } + } + return std::max(cartesian, joint); +} + +std::vector splineKnotParameters(const std::vector& segment_durations) +{ + std::vector parameters{ 0.0 }; + parameters.reserve(segment_durations.size() + 1); + // A zero-length segment would collapse two knots onto one parameter, which the spline + // rejects, so give every segment a positive share. + const double floor = std::numeric_limits::epsilon(); + for (const double duration : segment_durations) + { + parameters.push_back(parameters.back() + std::max(duration, floor)); + } + const double total = parameters.back(); + for (double& parameter : parameters) + { + parameter /= total; + } + return parameters; +} + +double splineDuration(const JointSpline& spline, double cartesian_length, double cartesian_speed, + const Eigen::VectorXd& joint_velocity_cap) +{ + const double cartesian = cartesian_speed > 0.0 ? cartesian_length / cartesian_speed : 0.0; + const Eigen::VectorXd peak = spline.peakSpeed(); + double joint = 0.0; + for (Eigen::Index j = 0; j < peak.size() && j < joint_velocity_cap.size(); ++j) + { + if (joint_velocity_cap[j] > 0.0) + { + joint = std::max(joint, peak[j] / joint_velocity_cap[j]); + } + } + return std::clamp(std::max(cartesian, joint), kMinimumDuration, kMaximumDuration); +} + +PlanJointSplineThroughPoses::PlanJointSplineThroughPoses( + const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources) + : SharedResourcesNode(name, config, shared_resources) +{ +} + +BT::PortsList PlanJointSplineThroughPoses::providedPorts() +{ + return { + BT::InputPort>(kPortIDPath, "{path}", + "Waypoints to pass through, in order."), + BT::InputPort(kPortIDSeedJointState, "{seed_joint_state}", + "Joint positions the trajectory starts from, from " + "GetRobotJointState. Also seeds the first inverse kinematics " + "solve, which fixes the branch the whole path stays on."), + BT::InputPort(kPortIDPlanningGroupName, "manipulator", "SRDF joint group to plan for."), + BT::InputPort(kPortIDTipLink, "grasp_link", "Link the waypoints are solved for."), + BT::InputPort>(kPortIDTipOffset, "0;0;0", + "Translation from tip_link to the frame the waypoints actually position, " + "in the tip_link frame, semicolon separated."), + BT::InputPort(kPortIDCartesianSpeed, "0.065", "Tip speed budget along the path, in meters per second."), + BT::InputPort(kPortIDJointVelocityScale, "0.5", + "Fraction of each joint's model velocity limit the trajectory may reach. Binds only " + "where the path is short enough that cartesian_speed would exceed it."), + BT::InputPort(kPortIDSamplingRate, "100", "Output trajectory sampling rate, in Hz."), + BT::OutputPort(kPortIDJointTrajectory, "{joint_trajectory_msg}", + "Timed trajectory for ExecuteTrajectory."), + }; +} + +BT::KeyValueVector PlanJointSplineThroughPoses::metadata() +{ + return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Color-Cube Stacking" }, + { moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionPlanJointSplineThroughPoses } }; +} + +BT::NodeStatus PlanJointSplineThroughPoses::tick() +{ + const auto ports = moveit_pro::behaviors::getRequiredInputs( + getInput>(kPortIDPath), + getInput(kPortIDSeedJointState), getInput(kPortIDPlanningGroupName), + getInput(kPortIDTipLink), getInput>(kPortIDTipOffset), + getInput(kPortIDCartesianSpeed), getInput(kPortIDJointVelocityScale), + getInput(kPortIDSamplingRate)); + if (!ports.has_value()) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), "Failed to get required values from input data ports: " + ports.error()); + return BT::NodeStatus::FAILURE; + } + const auto& [path, seed_joint_state, planning_group_name, tip_link, tip_offset, cartesian_speed, joint_velocity_scale, + sampling_rate] = ports.value(); + + if (path.empty()) + { + getBehaviorContext()->logger->publishFailureMessage(name(), "path is empty, so there is nothing to plan."); + return BT::NodeStatus::FAILURE; + } + if (tip_offset.size() != 3) + { + getBehaviorContext()->logger->publishFailureMessage(name(), fmt::format("tip_offset needs 3 values, got {}.", + tip_offset.size())); + return BT::NodeStatus::FAILURE; + } + if (sampling_rate == 0) + { + getBehaviorContext()->logger->publishFailureMessage(name(), "sampling_rate must be positive."); + return BT::NodeStatus::FAILURE; + } + + const auto& robot_model = getBehaviorContext()->robot_model; + const auto* joint_group = robot_model ? robot_model->getJointModelGroup(planning_group_name) : nullptr; + if (joint_group == nullptr) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), fmt::format("No planning group '{}' in the robot model.", planning_group_name)); + return BT::NodeStatus::FAILURE; + } + + moveit_pro::base::RobotState state(robot_model); + state.setToDefaultValues(); + for (std::size_t i = 0; i < seed_joint_state.name.size() && i < seed_joint_state.position.size(); ++i) + { + if (robot_model->hasJointModel(seed_joint_state.name[i])) + { + state.setJointPositions(seed_joint_state.name[i], { seed_joint_state.position[i] }); + } + } + state.update(); + + // The waypoints position a frame offset from tip_link, so the link itself has to arrive + // the same offset short of each one. + const Eigen::Translation3d offset(-tip_offset[0], -tip_offset[1], -tip_offset[2]); + + std::vector knots; + std::vector tip_positions; + { + std::vector positions; + state.copyJointGroupPositions(joint_group, positions); + knots.push_back(Eigen::Map(positions.data(), static_cast(positions.size()))); + tip_positions.push_back((state.getGlobalLinkTransform(tip_link) * offset.inverse()).translation()); + } + + for (std::size_t i = 0; i < path.size(); ++i) + { + Eigen::Isometry3d waypoint; + tf2::fromMsg(path[i].pose, waypoint); + if (!state.setFromIK(joint_group, waypoint * offset, tip_link)) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), fmt::format("Inverse kinematics failed for waypoint {} of {}.", i + 1, path.size())); + return BT::NodeStatus::FAILURE; + } + std::vector positions; + state.copyJointGroupPositions(joint_group, positions); + knots.push_back(Eigen::Map(positions.data(), static_cast(positions.size()))); + tip_positions.push_back(waypoint.translation()); + } + + Eigen::VectorXd velocity_cap(knots.front().size()); + const auto& bounds = joint_group->getActiveJointModelsBounds(); + for (Eigen::Index j = 0; j < velocity_cap.size(); ++j) + { + const auto index = static_cast(j); + const bool bounded = index < bounds.size() && !bounds[index]->empty() && bounds[index]->front().velocity_bounded_; + velocity_cap[j] = + bounded ? bounds[index]->front().max_velocity_ * joint_velocity_scale : std::numeric_limits::infinity(); + } + + std::vector segment_durations; + double cartesian_length = 0.0; + segment_durations.reserve(knots.size() - 1); + for (std::size_t i = 0; i + 1 < knots.size(); ++i) + { + const double length = (tip_positions[i + 1] - tip_positions[i]).norm(); + cartesian_length += length; + segment_durations.push_back(segmentDuration(length, knots[i + 1] - knots[i], cartesian_speed, velocity_cap)); + } + + std::unique_ptr spline; + try + { + spline = std::make_unique(splineKnotParameters(segment_durations), knots); + } + catch (const std::invalid_argument& exception) + { + getBehaviorContext()->logger->publishFailureMessage(name(), fmt::format("Could not fit a spline through the " + "inverse kinematics solutions: {}", + exception.what())); + return BT::NodeStatus::FAILURE; + } + + const double duration = splineDuration(*spline, cartesian_length, cartesian_speed, velocity_cap); + const auto steps = static_cast(std::ceil(duration * sampling_rate)); + + trajectory_msgs::msg::JointTrajectory trajectory; + trajectory.header = path.front().header; + trajectory.joint_names = joint_group->getVariableNames(); + trajectory.points.reserve(steps + 1); + for (std::size_t step = 0; step <= steps; ++step) + { + const double s = static_cast(step) / static_cast(steps); + const Eigen::VectorXd position = spline->position(s); + const Eigen::VectorXd velocity = spline->velocity(s) / duration; + const Eigen::VectorXd acceleration = spline->acceleration(s) / (duration * duration); + + trajectory_msgs::msg::JointTrajectoryPoint point; + point.positions.assign(position.data(), position.data() + position.size()); + point.velocities.assign(velocity.data(), velocity.data() + velocity.size()); + point.accelerations.assign(acceleration.data(), acceleration.data() + acceleration.size()); + point.time_from_start = rclcpp::Duration::from_seconds(s * duration); + trajectory.points.push_back(std::move(point)); + } + setOutput(kPortIDJointTrajectory, trajectory); + + return BT::NodeStatus::SUCCESS; +} +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/register_behaviors.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/register_behaviors.cpp new file mode 100644 index 000000000..a59e0844c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/register_behaviors.cpp @@ -0,0 +1,36 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace kinova_vla_test_sim_behaviors +{ +class KinovaVlaTestSimBehaviorsLoader : public moveit_pro::behaviors::SharedResourcesNodeLoaderBase +{ +public: + void registerBehaviors(BT::BehaviorTreeFactory& factory, + const std::shared_ptr& shared_resources) override + { + moveit_pro::behaviors::registerBehavior(factory, "ComputeTopDownKeyposes", shared_resources); + moveit_pro::behaviors::registerBehavior(factory, "PlanJointSplineThroughPoses", + shared_resources); + moveit_pro::behaviors::registerBehavior(factory, "SendGripperCommand", shared_resources); + moveit_pro::behaviors::registerBehavior(factory, "WaitForEpisodeStart", shared_resources); + } +}; +} // namespace kinova_vla_test_sim_behaviors + +PLUGINLIB_EXPORT_CLASS(kinova_vla_test_sim_behaviors::KinovaVlaTestSimBehaviorsLoader, + moveit_pro::behaviors::SharedResourcesNodeLoaderBase); diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/send_gripper_command.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/send_gripper_command.cpp new file mode 100644 index 000000000..5ecdfaff6 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/send_gripper_command.cpp @@ -0,0 +1,110 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include + +#include +#include +#include + +namespace +{ +inline constexpr auto kDescriptionSendGripperCommand = R"( +

+ Sends the gripper a position goal and succeeds as soon as the server accepts + it, without waiting for the jaws to arrive. +

+

+ This is how ExecutePolicy drives the gripper at deploy time: one + goal per change of target, never awaited and never cancelled, while the arm + keeps moving. Collecting demonstrations any other way records a grip the + policy cannot reproduce. MoveGripperAction blocks instead, and + closing onto an object never reaches the commanded position, so it returns + only once the jaws stop compressing it, seconds after the grip is solid. + Cancelling that wait is not a fix: this controller rewrites its target to + wherever the jaws are, which pins a weaker grip than a live goal would hold. +

+

+ Pair with a WaitForDuration to hold the arm still while the jaws + travel. +

+ )"; + +constexpr auto kPortIDActionName = "gripper_command_action_name"; +constexpr auto kPortIDPosition = "position"; +constexpr auto kPortIDMaxEffort = "max_effort"; +constexpr auto kPortIDServerTimeout = "wait_for_server_timeout"; +} // namespace + +namespace kinova_vla_test_sim_behaviors +{ +SendGripperCommand::SendGripperCommand(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources) + : SharedResourcesNode(name, config, shared_resources) +{ +} + +BT::PortsList SendGripperCommand::providedPorts() +{ + return { + BT::InputPort(kPortIDActionName, "/robotiq_gripper_controller/gripper_cmd", + "GripperCommand action that actuates the gripper."), + BT::InputPort(kPortIDPosition, "Gripper joint target position."), + BT::InputPort(kPortIDMaxEffort, "0.0", "Effort ceiling; 0 leaves it to the controller."), + BT::InputPort(kPortIDServerTimeout, "3.0", "Seconds to wait for the action server to appear."), + }; +} + +BT::KeyValueVector SendGripperCommand::metadata() +{ + return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Color-Cube Stacking" }, + { moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionSendGripperCommand } }; +} + +BT::NodeStatus SendGripperCommand::tick() +{ + const auto ports = + moveit_pro::behaviors::getRequiredInputs(getInput(kPortIDActionName), + getInput(kPortIDPosition), getInput(kPortIDMaxEffort), + getInput(kPortIDServerTimeout)); + if (!ports.has_value()) + { + getBehaviorContext()->logger->publishFailureMessage( + name(), "Failed to get required values from input data ports: " + ports.error()); + return BT::NodeStatus::FAILURE; + } + const auto& [action_name, position, max_effort, server_timeout] = ports.value(); + + // The client is kept between ticks: it outlives the goal request either way, and the + // shared node's executor is what delivers it. + if (client_ == nullptr || action_name_ != action_name) + { + client_ = + rclcpp_action::create_client(getBehaviorContext()->node, action_name); + action_name_ = action_name; + } + + const auto timeout = std::chrono::duration(server_timeout); + if (!client_->wait_for_action_server(std::chrono::duration_cast(timeout))) + { + getBehaviorContext()->logger->publishFailureMessage(name(), fmt::format("No GripperCommand action server on '{}'.", + action_name)); + return BT::NodeStatus::FAILURE; + } + + control_msgs::action::GripperCommand::Goal goal; + goal.command.position = position; + goal.command.max_effort = max_effort; + // The goal handle is deliberately dropped: waiting on it is the blocking this Behavior + // exists to avoid, so a rejected goal shows up as a gripper that did not move rather + // than a FAILURE here. + client_->async_send_goal(goal); + + return BT::NodeStatus::SUCCESS; +} +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/wait_for_episode_start.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/wait_for_episode_start.cpp new file mode 100644 index 000000000..df5b41193 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/src/wait_for_episode_start.cpp @@ -0,0 +1,151 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include +#include + +#include +#include +#include +#include + +namespace +{ +inline constexpr auto kDescriptionWaitForEpisodeStart = R"( +

+ Blocks until the active Trainer recording session reports that it has opened + an episode, then succeeds. +

+

+ RecordEpisode returns as soon as the recorder process has been + spawned, but the episode does not begin until the Trainer publishes its start + marker, seconds later. Motion before that marker is silently discarded at + conversion: the converter segments on markers and drops any segment holding + none, treating it as recorder spillover. Demonstrations then begin partway + through the motion, with no error to notice. Tick this between + RecordEpisode and the demonstrated motion. +

+

+ This polls the session state rather than watching for the marker, so it cannot + miss the transition by subscribing a moment too late. It is still worth pairing + with a short WaitForDuration, which keeps the arm still across the + few frames straddling the marker; the converter's idle trimming removes them. +

+

+ Fails when no episode opens within timeout, naming the last state + seen. +

+ )"; + +constexpr auto kPortIDServiceName = "service_name"; +constexpr auto kPortIDTimeout = "timeout"; + +constexpr auto kDefaultServiceName = "/trainer/active_recording"; + +// The RecordingState value that means the episode's start marker has been published. +constexpr auto kRecordingState = "recording"; + +constexpr std::chrono::milliseconds kPollPeriod{ 50 }; +constexpr std::chrono::duration kServerTimeout{ 5.0 }; +constexpr std::chrono::duration kResponseTimeout{ 5.0 }; + +/** @brief The session's state, or "" when no session is active or the payload has no state. */ +std::string stateOf(const std::string& session_json) +{ + if (session_json.empty()) + { + return ""; + } + const auto session = nlohmann::json::parse(session_json, nullptr, false); + if (session.is_discarded() || !session.contains("state") || !session["state"].is_string()) + { + return ""; + } + return session["state"].get(); +} +} // namespace + +namespace kinova_vla_test_sim_behaviors +{ +WaitForEpisodeStart::WaitForEpisodeStart(const std::string& name, const BT::NodeConfiguration& config, + const std::shared_ptr& shared_resources) + : AsyncBehaviorBase(name, config, shared_resources) + , client_(std::make_unique>(shared_resources)) +{ +} + +BT::PortsList WaitForEpisodeStart::providedPorts() +{ + return { + BT::InputPort(kPortIDServiceName, kDefaultServiceName, "Name of the Trainer active_recording service."), + BT::InputPort(kPortIDTimeout, "30.0", "Seconds to wait for an episode to open."), + }; +} + +BT::KeyValueVector WaitForEpisodeStart::metadata() +{ + return { { moveit_pro::behaviors::kSubcategoryMetadataKey, "Color-Cube Stacking" }, + { moveit_pro::behaviors::kDescriptionMetadataKey, kDescriptionWaitForEpisodeStart } }; +} + +tl::expected WaitForEpisodeStart::doWork() +{ + const auto ports = moveit_pro::behaviors::getRequiredInputs(getInput(kPortIDServiceName), + getInput(kPortIDTimeout)); + if (!ports.has_value()) + { + return tl::make_unexpected("Failed to get required values from input data ports: " + ports.error()); + } + const auto& [service_name, timeout] = ports.value(); + + halted_ = false; + client_->initialize(service_name, kServerTimeout, kResponseTimeout); + if (!client_->waitForServiceServer()) + { + return tl::make_unexpected(fmt::format("No Trainer active_recording service on '{}'.", service_name)); + } + // Nothing here holds a goal, so a halt may interrupt the poll as soon as it arrives. + notifyCanHalt(); + + const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration(timeout); + std::string last_state; + while (!halted_) + { + const auto response = client_->syncSendRequest(GetActiveRecordingSrv::Request{}); + if (!response.has_value()) + { + return tl::make_unexpected("Failed to read the active recording: " + response.error()); + } + if (!response.value().status.success) + { + return tl::make_unexpected("The Trainer refused to report the active recording: " + + response.value().status.error_message); + } + last_state = stateOf(response.value().session_json); + if (last_state == kRecordingState) + { + return true; + } + if (std::chrono::steady_clock::now() >= deadline) + { + return tl::make_unexpected( + fmt::format("No recording episode opened within {:g}s; the session's last state was '{}'.", timeout, + last_state.empty() ? "no active session" : last_state)); + } + std::this_thread::sleep_for(kPollPeriod); + } + return false; +} + +tl::expected WaitForEpisodeStart::doHalt() +{ + halted_ = true; + client_->cancelRequest(); + return {}; +} +} // namespace kinova_vla_test_sim_behaviors diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/CMakeLists.txt b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/CMakeLists.txt new file mode 100644 index 000000000..fe9ea895b --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/CMakeLists.txt @@ -0,0 +1,24 @@ +find_package(ament_cmake_gtest REQUIRED) + +# rclcpp::init wedges on discovery when it shares a ROS domain with a live backend, +# so every test that brings up a node gets a domain of its own -- one each, since +# colcon runs them in parallel and they would otherwise discover each other. The +# domain is read while the rmw library loads, too early for the test's own main(). +ament_add_gtest(test_behavior_plugins test_behavior_plugins.cpp ENV "ROS_DOMAIN_ID=89") +ament_target_dependencies(test_behavior_plugins ${THIS_PACKAGE_INCLUDE_DEPENDS}) + +ament_add_gtest(test_compute_top_down_keyposes test_compute_top_down_keyposes.cpp) +target_link_libraries(test_compute_top_down_keyposes kinova_vla_test_sim_behaviors) +ament_target_dependencies(test_compute_top_down_keyposes ${THIS_PACKAGE_INCLUDE_DEPENDS}) + +ament_add_gtest(test_plan_joint_spline_through_poses test_plan_joint_spline_through_poses.cpp) +target_link_libraries(test_plan_joint_spline_through_poses kinova_vla_test_sim_behaviors) +ament_target_dependencies(test_plan_joint_spline_through_poses ${THIS_PACKAGE_INCLUDE_DEPENDS}) + +ament_add_gtest(test_send_gripper_command test_send_gripper_command.cpp ENV "ROS_DOMAIN_ID=90") +target_link_libraries(test_send_gripper_command kinova_vla_test_sim_behaviors) +ament_target_dependencies(test_send_gripper_command ${THIS_PACKAGE_INCLUDE_DEPENDS}) + +ament_add_gtest(test_wait_for_episode_start test_wait_for_episode_start.cpp ENV "ROS_DOMAIN_ID=91") +target_link_libraries(test_wait_for_episode_start kinova_vla_test_sim_behaviors) +ament_target_dependencies(test_wait_for_episode_start ${THIS_PACKAGE_INCLUDE_DEPENDS}) diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_behavior_plugins.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_behavior_plugins.cpp new file mode 100644 index 000000000..68d27dcd1 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_behavior_plugins.cpp @@ -0,0 +1,47 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include +#include +#include +#include + +/** + * @brief This test makes sure that the Behaviors provided in this package can be successfully registered and + * instantiated by the behavior tree factory. + */ +TEST(BehaviorTests, test_load_behavior_plugins) +{ + pluginlib::ClassLoader class_loader( + "moveit_pro_behavior_interface", "moveit_pro::behaviors::SharedResourcesNodeLoaderBase"); + + auto node = std::make_shared("test_node"); + auto shared_resources = std::make_shared(node); + + BT::BehaviorTreeFactory factory; + { + auto plugin_instance = + class_loader.createUniqueInstance("kinova_vla_test_sim_behaviors::KinovaVlaTestSimBehaviorsLoader"); + ASSERT_NO_THROW(plugin_instance->registerBehaviors(factory, shared_resources)); + } + // Test that ClassLoader is able to find and instantiate each Behavior using the package's plugin description info. + for (const auto& behavior_name : + { "ComputeTopDownKeyposes", "PlanJointSplineThroughPoses", "SendGripperCommand", "WaitForEpisodeStart" }) + { + EXPECT_NO_THROW((void)factory.instantiateTreeNode("test_behavior_name", behavior_name, BT::NodeConfiguration())) + << "Behavior '" << behavior_name << "' is registered but could not be instantiated."; + } +} + +int main(int argc, char** argv) +{ + rclcpp::init(argc, argv); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_compute_top_down_keyposes.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_compute_top_down_keyposes.cpp new file mode 100644 index 000000000..f348ea743 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_compute_top_down_keyposes.cpp @@ -0,0 +1,229 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include + +#include +#include +#include + +#include + +namespace +{ +using kinova_vla_test_sim_behaviors::chooseTopDownYawByCost; +using kinova_vla_test_sim_behaviors::computeTopDownKeyposes; +using kinova_vla_test_sim_behaviors::jointDistanceCost; +using kinova_vla_test_sim_behaviors::topDownGraspOrientation; +using kinova_vla_test_sim_behaviors::yawOf; + +constexpr double kEpsilon = 1e-9; +constexpr double kQuarterTurn = M_PI / 2.0; + +Eigen::Isometry3d makePose(const Eigen::Vector3d& position, double yaw) +{ + Eigen::Isometry3d pose(Eigen::AngleAxisd(yaw, Eigen::Vector3d::UnitZ())); + pose.translation() = position; + return pose; +} + +// The oracle's Q_TOPDOWN, in the (x, y, z, w) order Eigen's constructor takes last. +Eigen::Quaterniond oracleTopDown() +{ + return Eigen::Quaterniond(0.0, 1.0, 0.0, 0.0); +} +} // namespace + +TEST(TopDownGraspOrientation, ZeroYawMatchesTheOracleBaseOrientation) +{ + // mujoco_ik.py pins Q_TOPDOWN = (w, x, y, z) = (0, 1, 0, 0). A different sign convention or + // axis here would still point the jaws down but spin them 90 degrees off the cube. + EXPECT_NEAR(std::abs(topDownGraspOrientation(0.0).dot(oracleTopDown())), 1.0, kEpsilon); +} + +TEST(TopDownGraspOrientation, ApproachAxisPointsDown) +{ + for (const double yaw : { -2.0, 0.0, 0.7, 3.0 }) + { + const Eigen::Vector3d approach = topDownGraspOrientation(yaw) * Eigen::Vector3d::UnitZ(); + EXPECT_NEAR(approach.z(), -1.0, kEpsilon) << "yaw " << yaw; + } +} + +TEST(TopDownGraspOrientation, SpinsTheJawAxisByYaw) +{ + // The jaw axis is the tip's +Y. Spinning by yaw must rotate it in the horizontal plane, or the + // grasp does not line up with the cube's faces. + // Rx(pi) flips +Y to -Y, then Rz(pi/2) carries that onto +X. + const Eigen::Vector3d jaw = topDownGraspOrientation(kQuarterTurn) * Eigen::Vector3d::UnitY(); + EXPECT_NEAR(jaw.x(), 1.0, kEpsilon); + EXPECT_NEAR(jaw.z(), 0.0, kEpsilon); +} + +TEST(YawOf, RoundTripsTopDownGraspOrientation) +{ + for (const double yaw : { -1.5, -0.2, 0.0, 0.9 }) + { + EXPECT_NEAR(yawOf(topDownGraspOrientation(yaw)), yaw, kEpsilon) << "yaw " << yaw; + } +} + +TEST(JointDistanceCost, IsZeroForTheSamePose) +{ + const std::vector pose{ 0.1, -0.2, 0.3 }; + EXPECT_NEAR(jointDistanceCost(pose, pose), 0.0, kEpsilon); +} + +TEST(JointDistanceCost, SumsTheSquaredPerJointDifferences) +{ + // The oracle's score, so a joint that moves twice as far counts four times as much and one + // big wrist swing outweighs several small arm adjustments. + EXPECT_NEAR(jointDistanceCost({ 0.0, 0.0 }, { 3.0, 4.0 }), 25.0, kEpsilon); +} + +TEST(ChooseTopDownYawByCost, ReturnsACubeYawPlusAWholeNumberOfQuarterTurns) +{ + // Every candidate must be a symmetry of the cube. A yaw that is not one grasps a corner. + const double chosen = + chooseTopDownYawByCost(0.3, [](double yaw) { return std::optional(std::abs(yaw)); }).value(); + const double turns = (chosen - 0.3) / kQuarterTurn; + EXPECT_NEAR(turns, std::round(turns), kEpsilon); +} + +TEST(ChooseTopDownYawByCost, PicksTheCheapestCandidateRatherThanTheNearest) +{ + // The whole point of scoring by IK: on eval_0 the oracle takes a candidate 180 degrees from + // the wrist's current yaw because it holds joint_5 still. A nearest-yaw rule cannot do that. + const double cube_yaw = 0.0; + const auto cost = [](double yaw) -> std::optional { + // cheapest at two quarter turns, i.e. the candidate a half turn away + return std::abs(std::remainder(yaw - M_PI, 2.0 * M_PI)); + }; + EXPECT_NEAR(chooseTopDownYawByCost(cube_yaw, cost).value(), M_PI, kEpsilon); +} + +TEST(ChooseTopDownYawByCost, SkipsUnreachableCandidates) +{ + // IK fails on candidates that would put the wrist past a limit; those must not be chosen + // even when a reachable one scores worse. + const auto cost = [](double yaw) -> std::optional { + if (std::abs(std::remainder(yaw, 2.0 * M_PI)) < kEpsilon) + { + return 0.0; // cheapest, but pretend it is the only reachable one below + } + return std::nullopt; + }; + EXPECT_NEAR(chooseTopDownYawByCost(0.0, cost).value(), 0.0, kEpsilon); +} + +TEST(ChooseTopDownYawByCost, ReturnsNulloptWhenNothingIsReachable) +{ + // The segment must fail loudly rather than plan a path the arm cannot follow. + EXPECT_FALSE(chooseTopDownYawByCost(0.4, [](double) { return std::nullopt; }).has_value()); +} + +TEST(ChooseTopDownYawByCost, IsUnchangedByCubeYawsAQuarterTurnApart) +{ + // The cube's own yaw is only known modulo a quarter turn, so equivalent readings of the same + // physical cube must produce the same grasp. + const auto cost = [](double yaw) -> std::optional { return std::abs(std::remainder(yaw - 0.9, 2.0 * M_PI)); }; + const double base = chooseTopDownYawByCost(0.2, cost).value(); + EXPECT_NEAR(std::remainder(chooseTopDownYawByCost(0.2 + kQuarterTurn, cost).value() - base, 2.0 * M_PI), 0.0, + kEpsilon); + EXPECT_NEAR(std::remainder(chooseTopDownYawByCost(0.2 - kQuarterTurn, cost).value() - base, 2.0 * M_PI), 0.0, + kEpsilon); +} + +TEST(ComputeTopDownKeyposes, StacksOneWaypointPerHeightAboveTheAimPose) +{ + const auto keyposes = computeTopDownKeyposes(makePose({ 0.5, -0.1, 0.115 }, 0.0), topDownGraspOrientation(0.0), + Eigen::Vector3d::Zero(), { 0.12, 0.0 }); + + ASSERT_EQ(keyposes.size(), 2u); + EXPECT_NEAR(keyposes[0].translation().z(), 0.235, kEpsilon); + EXPECT_NEAR(keyposes[1].translation().z(), 0.115, kEpsilon); + for (const auto& keypose : keyposes) + { + EXPECT_NEAR(keypose.translation().x(), 0.5, kEpsilon); + EXPECT_NEAR(keypose.translation().y(), -0.1, kEpsilon); + } +} + +TEST(ComputeTopDownKeyposes, GivesEveryWaypointTheChosenOrientation) +{ + // The oracle descends straight down onto the cube. Re-deriving the orientation per waypoint + // would let the wrist rotate mid-descent and shear the grasp. + const Eigen::Quaterniond orientation = topDownGraspOrientation(0.4); + const auto keyposes = computeTopDownKeyposes(makePose({ 0.5, 0.0, 0.115 }, 0.4), orientation, Eigen::Vector3d::Zero(), + { 0.12, 0.06, 0.0 }); + + ASSERT_EQ(keyposes.size(), 3u); + for (const auto& keypose : keyposes) + { + EXPECT_NEAR(std::abs(Eigen::Quaterniond(keypose.rotation()).dot(orientation)), 1.0, kEpsilon); + } +} + +TEST(ComputeTopDownKeyposes, PlacesTheHeldObjectRatherThanTheTipWhenOffsetIsSet) +{ + // The place segment aims the carried cube at the target, so the tip must land offset by + // exactly the grip, rotated into the world. + const Eigen::Vector3d grip_offset(0.0, 0.0, 0.02); + const auto keyposes = + computeTopDownKeyposes(makePose({ 0.4, 0.2, 0.115 }, 0.0), topDownGraspOrientation(0.0), grip_offset, { 0.031 }); + + ASSERT_EQ(keyposes.size(), 1u); + // The tip's +Z points down, so an object 20 mm along +Z sits 20 mm below the tip: the tip goes + // that much higher for the object to land on the aim point. + EXPECT_NEAR(keyposes[0].translation().z(), 0.115 + 0.031 + 0.02, kEpsilon); + EXPECT_NEAR(keyposes[0].translation().x(), 0.4, kEpsilon); + EXPECT_NEAR(keyposes[0].translation().y(), 0.2, kEpsilon); +} + +TEST(ComputeTopDownKeyposes, RotatesALateralHeldObjectOffsetIntoTheWorld) +{ + // A grip that is off-center laterally must be corrected in the direction the wrist is actually + // pointing. Ignoring the rotation would put the cube down on the wrong side of the target. + const Eigen::Vector3d grip_offset(0.01, 0.0, 0.0); + const auto keyposes = computeTopDownKeyposes(makePose({ 0.4, 0.2, 0.115 }, kQuarterTurn), + topDownGraspOrientation(kQuarterTurn), grip_offset, { 0.0 }); + + ASSERT_EQ(keyposes.size(), 1u); + // Rz(pi/2) * Rx(pi) maps the tip's +X onto world +Y, so the tip shifts back along -Y. + EXPECT_NEAR(keyposes[0].translation().x(), 0.4, kEpsilon); + EXPECT_NEAR(keyposes[0].translation().y(), 0.19, kEpsilon); +} + +TEST(ComputeTopDownKeyposes, ReturnsAnEmptyPathForNoHeights) +{ + EXPECT_TRUE(computeTopDownKeyposes(makePose({ 0.5, 0.0, 0.115 }, 0.0), topDownGraspOrientation(0.0), + Eigen::Vector3d::Zero(), {}) + .empty()); +} + +TEST(ComputeTopDownKeyposes, TakesOnlyThePositionFromATiltedAimPose) +{ + // Cube poses come from live physics and are never exactly level. The waypoint must still sit + // straight above the cube, since the arm approaches vertically. + Eigen::Isometry3d tilted(Eigen::AngleAxisd(0.3, Eigen::Vector3d::UnitZ()) * + Eigen::AngleAxisd(0.05, Eigen::Vector3d::UnitX())); + tilted.translation() = Eigen::Vector3d(0.5, 0.0, 0.115); + + const auto keyposes = computeTopDownKeyposes(tilted, topDownGraspOrientation(0.3), Eigen::Vector3d::Zero(), { 0.12 }); + + ASSERT_EQ(keyposes.size(), 1u); + EXPECT_NEAR(keyposes[0].translation().x(), 0.5, kEpsilon); + EXPECT_NEAR(keyposes[0].translation().y(), 0.0, kEpsilon); + EXPECT_NEAR(keyposes[0].translation().z(), 0.235, kEpsilon); + const Eigen::Vector3d approach = keyposes[0].rotation() * Eigen::Vector3d::UnitZ(); + EXPECT_NEAR(approach.z(), -1.0, kEpsilon); +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_plan_joint_spline_through_poses.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_plan_joint_spline_through_poses.cpp new file mode 100644 index 000000000..3c8c7b7ee --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_plan_joint_spline_through_poses.cpp @@ -0,0 +1,181 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include +#include + +#include + +#include + +namespace +{ +using kinova_vla_test_sim_behaviors::JointSpline; +using kinova_vla_test_sim_behaviors::segmentDuration; +using kinova_vla_test_sim_behaviors::splineDuration; +using kinova_vla_test_sim_behaviors::splineKnotParameters; + +Eigen::VectorXd vec(std::initializer_list values) +{ + Eigen::VectorXd result(static_cast(values.size())); + Eigen::Index i = 0; + for (const double value : values) + { + result[i++] = value; + } + return result; +} + +/** Two knots one unit apart on a single joint, over the full parameter span. */ +JointSpline twoKnotSpline() +{ + return JointSpline({ 0.0, 1.0 }, { vec({ 0.0 }), vec({ 1.0 }) }); +} + +/** Three knots, the interior one deliberately off the midpoint so corner-cutting shows. */ +JointSpline threeKnotSpline() +{ + return JointSpline({ 0.0, 0.25, 1.0 }, { vec({ 0.0, 0.0 }), vec({ 0.5, -1.0 }), vec({ 2.0, 1.0 }) }); +} +} // namespace + +TEST(JointSpline, RejectsFewerThanTwoKnots) +{ + EXPECT_THROW(JointSpline({ 0.0 }, { vec({ 0.0 }) }), std::invalid_argument); + EXPECT_THROW(JointSpline({}, {}), std::invalid_argument); +} + +TEST(JointSpline, RejectsParameterCountMismatch) +{ + EXPECT_THROW(JointSpline({ 0.0, 0.5, 1.0 }, { vec({ 0.0 }), vec({ 1.0 }) }), std::invalid_argument); +} + +TEST(JointSpline, RejectsNonIncreasingParameters) +{ + EXPECT_THROW(JointSpline({ 0.0, 0.0 }, { vec({ 0.0 }), vec({ 1.0 }) }), std::invalid_argument); + EXPECT_THROW(JointSpline({ 1.0, 0.0 }, { vec({ 0.0 }), vec({ 1.0 }) }), std::invalid_argument); +} + +TEST(JointSpline, RejectsKnotsOfDifferentWidths) +{ + EXPECT_THROW(JointSpline({ 0.0, 1.0 }, { vec({ 0.0 }), vec({ 1.0, 2.0 }) }), std::invalid_argument); +} + +TEST(JointSpline, PassesThroughEveryKnotExactly) +{ + const JointSpline spline = threeKnotSpline(); + EXPECT_NEAR(spline.position(0.0)[0], 0.0, 1e-12); + EXPECT_NEAR(spline.position(0.25)[0], 0.5, 1e-12); + EXPECT_NEAR(spline.position(0.25)[1], -1.0, 1e-12); + EXPECT_NEAR(spline.position(1.0)[0], 2.0, 1e-12); +} + +TEST(JointSpline, StartsAndEndsAtRest) +{ + const JointSpline spline = threeKnotSpline(); + EXPECT_NEAR(spline.velocity(0.0).cwiseAbs().maxCoeff(), 0.0, 1e-9); + EXPECT_NEAR(spline.velocity(1.0).cwiseAbs().maxCoeff(), 0.0, 1e-9); +} + +TEST(JointSpline, FlowsThroughInteriorKnotsWithoutStopping) +{ + // The reason for one spline over the whole chain rather than a clamped move per segment: + // chaining would pin the interior knot to zero velocity and stop the arm at every waypoint. + const JointSpline spline = threeKnotSpline(); + EXPECT_GT(spline.velocity(0.25).cwiseAbs().maxCoeff(), 0.5); +} + +TEST(JointSpline, IsContinuousInVelocityAcrossAnInteriorKnot) +{ + const JointSpline spline = threeKnotSpline(); + const Eigen::VectorXd before = spline.velocity(0.25 - 1e-7); + const Eigen::VectorXd after = spline.velocity(0.25 + 1e-7); + EXPECT_LT((after - before).cwiseAbs().maxCoeff(), 1e-5); +} + +TEST(JointSpline, PeaksAtOneAndAHalfTimesTheMeanOnASingleSegment) +{ + // Pins the profile: a clamped cubic peaks at 1.5x its mean speed. Smootherstep would be + // 1.875 and a linear ramp 1.0, so this fails if the interpolation is swapped out. + EXPECT_NEAR(twoKnotSpline().peakSpeed()[0], 1.5, 1e-9); + EXPECT_NEAR(twoKnotSpline().velocity(0.5)[0], 1.5, 1e-9); +} + +TEST(JointSpline, ReportsPeakSpeedPerJointIndependently) +{ + const JointSpline spline = JointSpline({ 0.0, 1.0 }, { vec({ 0.0, 0.0 }), vec({ 1.0, 4.0 }) }); + const Eigen::VectorXd peak = spline.peakSpeed(); + EXPECT_NEAR(peak[0], 1.5, 1e-9); + EXPECT_NEAR(peak[1], 6.0, 1e-9); +} + +TEST(JointSpline, SaturatesOutsideTheParameterRangeRatherThanExtrapolating) +{ + // A cubic run past its last knot diverges fast, so evaluating off the end holds the knot. + const JointSpline spline = twoKnotSpline(); + EXPECT_NEAR(spline.position(-0.5)[0], 0.0, 1e-12); + EXPECT_NEAR(spline.position(1.5)[0], 1.0, 1e-12); + EXPECT_NEAR(spline.velocity(1.5)[0], 0.0, 1e-9); +} + +TEST(SegmentDuration, TakesTheCartesianBudgetWhenTheJointCapIsSlack) +{ + // 0.2 m at 0.065 m/s is 3.08 s; the joint cap allows far quicker, so it does not bind. + EXPECT_NEAR(segmentDuration(0.2, vec({ 0.1, 0.1 }), 0.065, vec({ 0.695, 0.695 })), 0.2 / 0.065, 1e-9); +} + +TEST(SegmentDuration, TakesTheJointCapWhenThePathIsShortButTheArmTurnsFar) +{ + // A wrist flip in place: no Cartesian distance to pay for, but the joint still has to + // stay under its limit. + const double duration = segmentDuration(0.0, vec({ 3.0 }), 0.065, vec({ 0.695 })); + EXPECT_NEAR(duration, 1.875 * 3.0 / 0.695, 1e-9); +} + +TEST(SegmentDuration, IgnoresJointsWithoutAVelocityLimit) +{ + EXPECT_NEAR(segmentDuration(0.1, vec({ 3.0 }), 0.065, vec({ 0.0 })), 0.1 / 0.065, 1e-9); +} + +TEST(SplineKnotParameters, SpansZeroToOneInProportionToDuration) +{ + const std::vector parameters = splineKnotParameters({ 1.0, 3.0 }); + ASSERT_EQ(parameters.size(), 3u); + EXPECT_NEAR(parameters[0], 0.0, 1e-12); + EXPECT_NEAR(parameters[1], 0.25, 1e-12); + EXPECT_NEAR(parameters[2], 1.0, 1e-12); +} + +TEST(SplineKnotParameters, StaysStrictlyIncreasingThroughAZeroLengthSegment) +{ + // Two waypoints at the same place would otherwise collapse onto one parameter and make + // the spline unsolvable. + const std::vector parameters = splineKnotParameters({ 1.0, 0.0, 1.0 }); + EXPECT_GT(parameters[2], parameters[1]); + EXPECT_NO_THROW(JointSpline(parameters, { vec({ 0.0 }), vec({ 1.0 }), vec({ 1.0 }), vec({ 2.0 }) })); +} + +TEST(SplineDuration, SpendsTheCartesianLengthAtTheRequestedSpeed) +{ + // The oracle's regime: the tip speed budget binds and the joint caps stay slack, so the + // whole segment takes exactly length / speed. + const JointSpline spline = twoKnotSpline(); + EXPECT_NEAR(splineDuration(spline, 0.381, 0.065, vec({ 0.695 })), 0.381 / 0.065, 1e-9); +} + +TEST(SplineDuration, StretchesUntilThePeakJointSpeedFitsUnderTheCap) +{ + // Peak speed is 1.5 rad per unit parameter, so a 0.5 rad/s cap needs 3 s. + const JointSpline spline = twoKnotSpline(); + EXPECT_NEAR(splineDuration(spline, 0.0, 0.065, vec({ 0.5 })), 3.0, 1e-9); +} + +TEST(SplineDuration, ClampsToASaneRange) +{ + const JointSpline spline = twoKnotSpline(); + EXPECT_NEAR(splineDuration(spline, 1e-9, 0.065, vec({ 1e6 })), 0.2, 1e-9); + EXPECT_NEAR(splineDuration(spline, 1e6, 0.065, vec({ 1e6 })), 60.0, 1e-9); +} diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_send_gripper_command.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_send_gripper_command.cpp new file mode 100644 index 000000000..1c451eb42 --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_send_gripper_command.cpp @@ -0,0 +1,163 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include + +namespace +{ +using kinova_vla_test_sim_behaviors::SendGripperCommand; +using GripperCommand = control_msgs::action::GripperCommand; + +constexpr auto kActionName = "/test_gripper_cmd"; +constexpr double kTargetPosition = 0.6; + +/** + * @brief A gripper server that accepts every goal and never finishes it. + * + * Closing onto an object behaves this way for seconds at a time: the jaws stall short of the + * commanded position and the goal stays live. A Behavior that waited would block for all of it. + */ +class StallingGripperServer +{ +public: + explicit StallingGripperServer(const rclcpp::Node::SharedPtr& node) + { + server_ = rclcpp_action::create_server( + node, kActionName, + [this](const rclcpp_action::GoalUUID&, std::shared_ptr goal) { + received_.set_value(goal->command.position); + return rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE; + }, + [](const std::shared_ptr>&) { + return rclcpp_action::CancelResponse::ACCEPT; + }, + [this](const std::shared_ptr>& handle) { + // Held, never completed, exactly like jaws still compressing an object. + std::lock_guard lock(mutex_); + held_ = handle; + }); + } + + /** @brief Ends the held goal, which rclcpp_action can wedge on if left executing. */ + ~StallingGripperServer() + { + // Terminating the goal re-enters the server's own callbacks, so the lock is + // released first; holding it here throws EDEADLK out of a destructor. + std::shared_ptr> held; + { + std::lock_guard lock(mutex_); + held = held_; + } + if (held != nullptr && held->is_executing()) + { + held->abort(std::make_shared()); + } + } + + /** @brief Commanded position of the first goal the server received. */ + [[nodiscard]] std::future receivedPosition() + { + return received_.get_future(); + } + +private: + rclcpp_action::Server::SharedPtr server_; + std::mutex mutex_; + std::shared_ptr> held_; + std::promise received_; +}; + +/** @brief Spins @p node on its own thread for the lifetime of the fixture. */ +class SendGripperCommandTest : public ::testing::Test +{ +public: + SendGripperCommandTest() + : node_(std::make_shared("send_gripper_command_test")) + , context_(std::make_shared(node_)) + { + executor_.add_node(node_); + spin_thread_ = std::thread([this] { executor_.spin(); }); + } + + ~SendGripperCommandTest() override + { + executor_.cancel(); + if (spin_thread_.joinable()) + { + spin_thread_.join(); + } + } + + [[nodiscard]] static BT::NodeConfiguration configFor(const std::string& action_name, double timeout) + { + BT::NodeConfiguration config; + config.blackboard = BT::Blackboard::create(); + config.input_ports["gripper_command_action_name"] = action_name; + config.input_ports["position"] = std::to_string(kTargetPosition); + config.input_ports["max_effort"] = "0.0"; + config.input_ports["wait_for_server_timeout"] = std::to_string(timeout); + return config; + } + + rclcpp::Node::SharedPtr node_; + std::shared_ptr context_; + rclcpp::executors::MultiThreadedExecutor executor_; + std::thread spin_thread_; +}; +} // namespace + +TEST_F(SendGripperCommandTest, SucceedsWithoutWaitingForAGoalThatNeverFinishes) +{ + // GIVEN a gripper server that accepts the goal and never completes it + StallingGripperServer server(node_); + std::future received = server.receivedPosition(); + SendGripperCommand behavior("send_gripper", configFor(kActionName, 5.0), context_); + + // WHEN the Behavior is ticked + const BT::NodeStatus status = behavior.tick(); + + // THEN it reports success rather than blocking on the unfinished goal + EXPECT_EQ(status, BT::NodeStatus::SUCCESS); + + // AND the server received the commanded position + ASSERT_EQ(received.wait_for(std::chrono::seconds(10)), std::future_status::ready) + << "The goal never reached the action server."; + EXPECT_DOUBLE_EQ(received.get(), kTargetPosition); +} + +TEST_F(SendGripperCommandTest, FailsWhenNoActionServerIsAvailable) +{ + // GIVEN no server on the configured action name + SendGripperCommand behavior("send_gripper", configFor("/absent_gripper_cmd", 0.2), context_); + + // WHEN the Behavior is ticked + // THEN it fails rather than reporting a command nothing can execute + EXPECT_EQ(behavior.tick(), BT::NodeStatus::FAILURE); +} + +int main(int argc, char** argv) +{ + rclcpp::init(argc, argv); + testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); + rclcpp::shutdown(); + return result; +} diff --git a/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_wait_for_episode_start.cpp b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_wait_for_episode_start.cpp new file mode 100644 index 000000000..bb3c55d3c --- /dev/null +++ b/src/moveit_pro_kinova_configs/kinova_vla_test_sim_behaviors/test/test_wait_for_episode_start.cpp @@ -0,0 +1,217 @@ +// Copyright 2026 PickNik Inc. +// All rights reserved. +// +// Unauthorized copying of this code base via any medium is strictly prohibited. +// Proprietary and confidential. + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include + +namespace +{ +using kinova_vla_test_sim_behaviors::WaitForEpisodeStart; +using GetActiveRecording = moveit_studio_internal_msgs::srv::GetActiveRecording; + +constexpr auto kServiceName = "/test_active_recording"; + +/** @brief A Trainer stand-in whose reported session state the test controls. */ +class FakeTrainer +{ +public: + explicit FakeTrainer(const rclcpp::Node::SharedPtr& node) + { + server_ = node->create_service(kServiceName, + [this](const std::shared_ptr, + std::shared_ptr response) { + ++calls_; + std::lock_guard lock(mutex_); + response->status.success = true; + response->session_json = payload_; + }); + } + + void setState(const std::string& state) + { + std::lock_guard lock(mutex_); + payload_ = R"({"id":"rec-1","state":")" + state + R"(","current_episode":1})"; + } + + /** @brief Serve @p payload verbatim, for session_json a parser cannot use. */ + void setRawPayload(const std::string& payload) + { + std::lock_guard lock(mutex_); + payload_ = payload; + } + + [[nodiscard]] int calls() const + { + return calls_; + } + +private: + rclcpp::Service::SharedPtr server_; + // Written by the test thread, read by the executor's. + mutable std::mutex mutex_; + std::string payload_{ R"({"id":"rec-1","state":"preparing","current_episode":1})" }; + std::atomic_int calls_{ 0 }; +}; + +/** @brief Spins @p node on its own thread for the lifetime of the fixture. */ +class WaitForEpisodeStartTest : public ::testing::Test +{ +public: + WaitForEpisodeStartTest() + : node_(std::make_shared("wait_for_episode_start_test")) + , context_(std::make_shared(node_)) + { + executor_.add_node(node_); + spin_thread_ = std::thread([this] { executor_.spin(); }); + } + + ~WaitForEpisodeStartTest() override + { + executor_.cancel(); + if (spin_thread_.joinable()) + { + spin_thread_.join(); + } + } + + [[nodiscard]] static BT::NodeConfiguration configFor(const std::string& service_name, double timeout) + { + BT::NodeConfiguration config; + config.blackboard = BT::Blackboard::create(); + config.input_ports["service_name"] = service_name; + config.input_ports["timeout"] = std::to_string(timeout); + return config; + } + + /** @brief Ticks @p behavior until it leaves RUNNING, or returns RUNNING at @p deadline. + * + * executeTick() rather than tick(): the latter is StatefulActionNode's protected + * hook, and only executeTick() routes the first tick through onStart(). */ + static BT::NodeStatus tickUntilSettled(WaitForEpisodeStart& behavior, std::chrono::seconds deadline) + { + const auto give_up = std::chrono::steady_clock::now() + deadline; + BT::NodeStatus status = behavior.executeTick(); + while (status == BT::NodeStatus::RUNNING && std::chrono::steady_clock::now() < give_up) + { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + status = behavior.executeTick(); + } + return status; + } + + rclcpp::Node::SharedPtr node_; + std::shared_ptr context_; + rclcpp::executors::MultiThreadedExecutor executor_; + std::thread spin_thread_; +}; +} // namespace + +TEST_F(WaitForEpisodeStartTest, RunsUntilTheSessionReportsRecording) +{ + // GIVEN a session that is still preparing + FakeTrainer trainer(node_); + WaitForEpisodeStart behavior("wait_for_episode", configFor(kServiceName, 10.0), context_); + + // WHEN it is ticked while the session has not opened an episode + ASSERT_EQ(behavior.executeTick(), BT::NodeStatus::RUNNING); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // THEN it stays RUNNING rather than releasing the motion early + EXPECT_EQ(behavior.executeTick(), BT::NodeStatus::RUNNING); + + // WHEN the episode opens + trainer.setState("recording"); + + // THEN it succeeds + EXPECT_EQ(tickUntilSettled(behavior, std::chrono::seconds(10)), BT::NodeStatus::SUCCESS); + EXPECT_GT(trainer.calls(), 1) << "The state was read once and never re-polled."; +} + +TEST_F(WaitForEpisodeStartTest, FailsWhenNoEpisodeOpensWithinTheTimeout) +{ + // GIVEN a session that never leaves preparing + FakeTrainer trainer(node_); + WaitForEpisodeStart behavior("wait_for_episode", configFor(kServiceName, 0.5), context_); + + // WHEN it is ticked + // THEN it fails rather than waiting on a recording that will never start + EXPECT_EQ(tickUntilSettled(behavior, std::chrono::seconds(20)), BT::NodeStatus::FAILURE); + + // AND says which state it was stuck in, so the operator can tell a slow recorder + // from a session that never started + EXPECT_NE(context_->logger->consumeErrorLogBuffer().find("'preparing'"), std::string::npos); +} + +TEST_F(WaitForEpisodeStartTest, KeepsWaitingWhenTheSessionCannotBeRead) +{ + // GIVEN a Trainer answering with a payload no parser can use + FakeTrainer trainer(node_); + trainer.setRawPayload("{not json"); + WaitForEpisodeStart behavior("wait_for_episode", configFor(kServiceName, 10.0), context_); + + // WHEN it is ticked + ASSERT_EQ(behavior.executeTick(), BT::NodeStatus::RUNNING); + std::this_thread::sleep_for(std::chrono::milliseconds(300)); + + // THEN an unreadable payload is not mistaken for an open episode + EXPECT_EQ(behavior.executeTick(), BT::NodeStatus::RUNNING); + + // AND it recovers once the Trainer answers properly again + trainer.setState("recording"); + EXPECT_EQ(tickUntilSettled(behavior, std::chrono::seconds(10)), BT::NodeStatus::SUCCESS); +} + +TEST_F(WaitForEpisodeStartTest, HaltsWithoutWaitingOutTheTimeout) +{ + // GIVEN a long wait on a session that has not opened an episode + FakeTrainer trainer(node_); + WaitForEpisodeStart behavior("wait_for_episode", configFor(kServiceName, 600.0), context_); + ASSERT_EQ(behavior.executeTick(), BT::NodeStatus::RUNNING); + + // WHEN the tree halts it (haltNode(), since halt() is the protected hook) + const auto started = std::chrono::steady_clock::now(); + behavior.haltNode(); + const auto elapsed = std::chrono::steady_clock::now() - started; + + // THEN it returns promptly instead of blocking until the timeout expires + EXPECT_LT(elapsed, std::chrono::seconds(5)) + << "halt() waited " << std::chrono::duration_cast(elapsed).count() << "ms."; +} + +TEST_F(WaitForEpisodeStartTest, FailsWhenTheTrainerIsAbsent) +{ + // GIVEN no Trainer on the configured service name + WaitForEpisodeStart behavior("wait_for_episode", configFor("/absent_active_recording", 10.0), context_); + + // WHEN it is ticked + // THEN it fails instead of blocking on a service nothing serves + EXPECT_EQ(tickUntilSettled(behavior, std::chrono::seconds(30)), BT::NodeStatus::FAILURE); + + // AND names the service, since a typo here is the likely cause + EXPECT_NE(context_->logger->consumeErrorLogBuffer().find("/absent_active_recording"), std::string::npos); +} + +int main(int argc, char** argv) +{ + rclcpp::init(argc, argv); + testing::InitGoogleTest(&argc, argv); + const int result = RUN_ALL_TESTS(); + rclcpp::shutdown(); + return result; +}