From 458c39668680744395d9e8464b5af23dad081b2f Mon Sep 17 00:00:00 2001 From: Szymon Paluch Date: Fri, 6 Mar 2026 19:11:18 +0100 Subject: [PATCH] fix: add 30s timeout to WebRTC connection_ready.wait() to prevent indefinite hang Blueprint.build() and direct UnitreeWebRTCConnection() hang forever when the WebRTC SDP exchange fails (port closed, another client connected, robot unreachable). connection_ready.wait() has no timeout, blocking the subprocess/main thread indefinitely with no error message. Add a 30-second timeout with thread cleanup and a descriptive TimeoutError listing common causes and troubleshooting steps. Common trigger: Go2 only allows one WebRTC client at a time. If the Unitree mobile app is connected, the SDP exchange returns 'reject' and the event never fires. Error now surfaces immediately instead of hanging forever. Tested: Go2 Pro, stock firmware, STA mode (home WiFi), DimOS v0.0.10.post2 --- .devcontainer/devcontainer.json | 39 - .dockerignore | 109 - .editorconfig | 27 - .envrc.nix | 11 - .envrc.venv | 7 - .gitattributes | 18 - .github/actions/docker-build/action.yml | 59 - .github/pull_request_template.md | 30 - .github/workflows/_docker-build-template.yml | 149 - .github/workflows/code-cleanup.yml | 37 - .github/workflows/docker.yml | 341 - .github/workflows/readme.md | 51 - .github/workflows/tests.yml | 42 - .gitignore | 71 - .pre-commit-config.yaml | 101 - .python-version | 1 - .style.yapf | 3 - CLA.md | 24 - LICENSE | 17 - MANIFEST.in | 21 - README.md | 280 - assets/dimensional-dark.svg | 23 - assets/dimensional-light.svg | 23 - assets/dimensional-text.svg | 20 - ...sional.command-center-extension-0.0.1.foxe | 3 - assets/dimensional.svg | 23 - assets/dimensionalascii.txt | 7 - assets/dimos_interface.gif | 3 - assets/dimos_terminal.png | 3 - assets/drone_foxglove_lcm_dashboard.json | 381 - assets/foxglove_dashboards/go2.json | 603 - .../old/foxglove_g1_detections.json | 915 -- .../old/foxglove_image_sharpness_test.json | 140 - .../old/foxglove_unitree_lcm_dashboard.json | 288 - .../old/foxglove_unitree_yolo.json | 849 -- assets/framecount.mp4 | 3 - assets/license_file_header.txt | 13 - assets/readme/agentic_control.gif | 3 - assets/readme/agents.png | 3 - assets/readme/dimos_demo.gif | 3 - assets/readme/lidar.gif | 3 - assets/readme/lidar.png | 3 - assets/readme/navigation.gif | 3 - assets/readme/navigation.png | 3 - assets/readme/perception.png | 3 - assets/readme/spacer.png | 3 - assets/readme/spatial_memory.gif | 3 - assets/simple_demo.mp4 | 3 - assets/simple_demo_small.gif | 3 - assets/trimmed_video_office.mov | 3 - bin/agent_web | 2 - bin/cuda/fix_ort.sh | 30 - bin/dev | 152 - bin/dockerbuild | 32 - bin/doclinks | 3 - bin/filter-errors-after-date | 77 - bin/filter-errors-for-user | 63 - bin/hooks/filter_commit_message.py | 49 - bin/hooks/largefiles_check | 62 - bin/hooks/lfs_check | 42 - bin/lfs_push | 97 - bin/mypy-ros | 44 - bin/pytest-fast | 6 - bin/pytest-mujoco | 6 - bin/pytest-slow | 6 - bin/re-ignore-mypy.py | 150 - bin/robot-debugger | 36 - bin/ros | 2 - data/.lfs/ab_lidar_frames.tar.gz | 3 - data/.lfs/apartment.tar.gz | 3 - data/.lfs/assets.tar.gz | 3 - data/.lfs/astar_corner_min_cost.png.tar.gz | 3 - data/.lfs/astar_min_cost.png.tar.gz | 3 - data/.lfs/big_office.ply.tar.gz | 3 - ...ig_office_height_cost_occupancy.png.tar.gz | 3 - .../big_office_simple_occupancy.png.tar.gz | 3 - data/.lfs/cafe-smol.jpg.tar.gz | 3 - data/.lfs/cafe.jpg.tar.gz | 3 - data/.lfs/chair-image.png.tar.gz | 3 - data/.lfs/command_center.html.tar.gz | 3 - data/.lfs/drone.tar.gz | 3 - data/.lfs/expected_occupancy_scene.xml.tar.gz | 3 - data/.lfs/g1_zed.tar.gz | 3 - data/.lfs/gradient_simple.png.tar.gz | 3 - data/.lfs/gradient_voronoi.png.tar.gz | 3 - data/.lfs/inflation_simple.png.tar.gz | 3 - data/.lfs/lcm_msgs.tar.gz | 3 - .../.lfs/make_navigation_map_mixed.png.tar.gz | 3 - .../make_navigation_map_simple.png.tar.gz | 3 - data/.lfs/make_path_mask_full.png.tar.gz | 3 - .../.lfs/make_path_mask_two_meters.png.tar.gz | 3 - data/.lfs/models_clip.tar.gz | 3 - data/.lfs/models_contact_graspnet.tar.gz | 3 - data/.lfs/models_edgetam.tar.gz | 3 - data/.lfs/models_fastsam.tar.gz | 3 - data/.lfs/models_graspgen.tar.gz | 3 - data/.lfs/models_mobileclip.tar.gz | 3 - data/.lfs/models_torchreid.tar.gz | 3 - data/.lfs/models_yolo.tar.gz | 3 - data/.lfs/models_yoloe.tar.gz | 3 - data/.lfs/mujoco_sim.tar.gz | 3 - data/.lfs/occupancy_general.png.tar.gz | 3 - data/.lfs/occupancy_simple.npy.tar.gz | 3 - data/.lfs/occupancy_simple.png.tar.gz | 3 - data/.lfs/office_building_1.tar.gz | 3 - data/.lfs/office_lidar.tar.gz | 3 - data/.lfs/osm_map_test.tar.gz | 3 - data/.lfs/overlay_occupied.png.tar.gz | 3 - data/.lfs/person.tar.gz | 3 - data/.lfs/piper_description.tar.gz | 3 - data/.lfs/raw_odometry_rotate_walk.tar.gz | 3 - data/.lfs/replay_g1.tar.gz | 3 - data/.lfs/replay_g1_run.tar.gz | 3 - data/.lfs/resample_path_simple.png.tar.gz | 3 - data/.lfs/resample_path_smooth.png.tar.gz | 3 - data/.lfs/rgbd_frames.tar.gz | 3 - data/.lfs/smooth_occupied.png.tar.gz | 3 - data/.lfs/three_paths.npy.tar.gz | 3 - data/.lfs/three_paths.ply.tar.gz | 3 - data/.lfs/three_paths.png.tar.gz | 3 - data/.lfs/unitree_go2_bigoffice.tar.gz | 3 - .../unitree_go2_bigoffice_map.pickle.tar.gz | 3 - data/.lfs/unitree_go2_lidar_corrected.tar.gz | 3 - data/.lfs/unitree_go2_office_walk2.tar.gz | 3 - data/.lfs/unitree_office_walk.tar.gz | 3 - data/.lfs/unitree_raw_webrtc_replay.tar.gz | 3 - data/.lfs/video.tar.gz | 3 - .../visualize_occupancy_rainbow.png.tar.gz | 3 - .../.lfs/visualize_occupancy_turbo.png.tar.gz | 3 - data/.lfs/xarm7.tar.gz | 3 - data/.lfs/xarm_description.tar.gz | 3 - default.env | 15 - dimos/__init__.py | 0 dimos/agents/agent.py | 205 - dimos/agents/agent_test_runner.py | 80 - dimos/agents/annotation.py | 24 - dimos/agents/conftest.py | 109 - dimos/agents/demo_agent.py | 32 - .../test_can_call_again_on_error[False].json | 34 - .../test_can_call_again_on_error[True].json | 34 - .../fixtures/test_can_call_tool[False].json | 22 - .../fixtures/test_can_call_tool[True].json | 22 - .../test_get_gps_position_for_queries.json | 25 - .../test_go_to_semantic_location.json | 21 - dimos/agents/fixtures/test_image.json | 23 - ...ple_tool_calls_with_multiple_messages.json | 98 - dimos/agents/fixtures/test_pounce.json | 21 - dimos/agents/fixtures/test_prompt.json | 8 - .../fixtures/test_set_gps_travel_points.json | 37 - .../test_set_gps_travel_points_multiple.json | 45 - .../fixtures/test_start_exploration.json | 21 - dimos/agents/fixtures/test_stop_movement.json | 19 - dimos/agents/fixtures/test_where_am_i.json | 19 - dimos/agents/ollama_agent.py | 39 - dimos/agents/skills/demo_calculator_skill.py | 43 - dimos/agents/skills/demo_google_maps_skill.py | 25 - dimos/agents/skills/demo_gps_nav.py | 25 - dimos/agents/skills/demo_robot.py | 40 - dimos/agents/skills/demo_skill.py | 23 - .../skills/google_maps_skill_container.py | 118 - dimos/agents/skills/gps_nav_skill.py | 109 - dimos/agents/skills/navigation.py | 324 - dimos/agents/skills/osm.py | 80 - dimos/agents/skills/person_follow.py | 270 - dimos/agents/skills/speak_skill.py | 104 - .../test_google_maps_skill_container.py | 96 - dimos/agents/skills/test_gps_nav_skills.py | 68 - dimos/agents/skills/test_navigation.py | 116 - .../skills/test_unitree_skill_container.py | 46 - dimos/agents/system_prompt.py | 53 - dimos/agents/test_agent.py | 212 - dimos/agents/testing.py | 194 - dimos/agents/utils.py | 94 - dimos/agents/vlm_agent.py | 131 - dimos/agents/vlm_stream_tester.py | 179 - dimos/agents/web_human_input.py | 87 - dimos/agents_deprecated/__init__.py | 0 dimos/agents_deprecated/agent.py | 917 -- dimos/agents_deprecated/agent_config.py | 55 - dimos/agents_deprecated/agent_message.py | 100 - dimos/agents_deprecated/agent_types.py | 255 - dimos/agents_deprecated/claude_agent.py | 738 -- dimos/agents_deprecated/memory/__init__.py | 0 dimos/agents_deprecated/memory/base.py | 134 - dimos/agents_deprecated/memory/chroma_impl.py | 182 - .../memory/image_embedding.py | 280 - .../memory/spatial_vector_db.py | 338 - .../memory/test_image_embedding.py | 211 - .../agents_deprecated/memory/visual_memory.py | 182 - dimos/agents_deprecated/modules/__init__.py | 15 - dimos/agents_deprecated/modules/base.py | 525 - dimos/agents_deprecated/modules/base_agent.py | 211 - .../modules/gateway/__init__.py | 20 - .../modules/gateway/client.py | 212 - .../modules/gateway/tensorzero_embedded.py | 280 - .../modules/gateway/tensorzero_simple.py | 106 - .../modules/gateway/utils.py | 156 - .../prompt_builder/__init__.py | 0 .../agents_deprecated/prompt_builder/impl.py | 224 - dimos/agents_deprecated/tokenizer/__init__.py | 0 dimos/agents_deprecated/tokenizer/base.py | 37 - .../tokenizer/huggingface_tokenizer.py | 89 - .../tokenizer/openai_tokenizer.py | 89 - .../assets/foxglove_dashboards/Overwatch.json | 522 - dimos/conftest.py | 169 - dimos/constants.py | 34 - dimos/control/README.md | 208 - dimos/control/__init__.py | 82 - dimos/control/blueprints.py | 638 - dimos/control/components.py | 82 - dimos/control/coordinator.py | 668 - dimos/control/examples/cartesian_ik_jogger.py | 349 - dimos/control/hardware_interface.py | 198 - dimos/control/task.py | 346 - dimos/control/tasks/__init__.py | 49 - dimos/control/tasks/cartesian_ik_task.py | 335 - dimos/control/tasks/servo_task.py | 242 - dimos/control/tasks/teleop_task.py | 332 - dimos/control/tasks/trajectory_task.py | 261 - dimos/control/tasks/velocity_task.py | 277 - dimos/control/test_control.py | 557 - dimos/control/tick_loop.py | 400 - dimos/core/__init__.py | 278 - dimos/core/_dask_exports.py | 17 - dimos/core/_protocol_exports.py | 19 - dimos/core/_test_future_annotations_helper.py | 36 - dimos/core/blueprints.py | 511 - dimos/core/colors.py | 43 - dimos/core/core.py | 40 - dimos/core/docker_build.py | 120 - dimos/core/docker_runner.py | 521 - dimos/core/global_config.py | 83 - dimos/core/introspection/__init__.py | 20 - .../core/introspection/blueprint/__init__.py | 24 - dimos/core/introspection/blueprint/dot.py | 253 - dimos/core/introspection/module/__init__.py | 45 - dimos/core/introspection/module/ansi.py | 96 - dimos/core/introspection/module/dot.py | 203 - dimos/core/introspection/module/info.py | 168 - dimos/core/introspection/module/render.py | 44 - dimos/core/introspection/svg.py | 57 - dimos/core/introspection/utils.py | 86 - dimos/core/module.py | 515 - dimos/core/module_coordinator.py | 108 - dimos/core/native_module.py | 296 - dimos/core/o3dpickle.py | 38 - dimos/core/resource.py | 45 - dimos/core/rpc_client.py | 150 - dimos/core/stream.py | 274 - dimos/core/test_blueprints.py | 505 - dimos/core/test_core.py | 139 - dimos/core/test_modules.py | 331 - dimos/core/test_native_module.py | 176 - dimos/core/test_rpcstress.py | 177 - dimos/core/test_stream.py | 256 - dimos/core/test_worker.py | 153 - dimos/core/testing.py | 83 - dimos/core/tests/native_echo.py | 48 - dimos/core/transport.py | 322 - dimos/core/worker.py | 227 - dimos/core/worker_manager.py | 74 - dimos/e2e_tests/conftest.py | 86 - dimos/e2e_tests/dimos_cli_call.py | 71 - dimos/e2e_tests/lcm_spy.py | 192 - dimos/e2e_tests/test_control_coordinator.py | 255 - dimos/e2e_tests/test_dimos_cli_e2e.py | 38 - dimos/e2e_tests/test_person_follow.py | 83 - dimos/e2e_tests/test_simulation_module.py | 86 - dimos/e2e_tests/test_spatial_memory.py | 60 - dimos/environment/__init__.py | 0 dimos/environment/environment.py | 178 - dimos/exceptions/__init__.py | 0 dimos/exceptions/agent_memory_exceptions.py | 93 - dimos/hardware/__init__.py | 0 dimos/hardware/end_effectors/__init__.py | 17 - dimos/hardware/end_effectors/end_effector.py | 21 - dimos/hardware/manipulators/README.md | 146 - dimos/hardware/manipulators/__init__.py | 51 - dimos/hardware/manipulators/mock/__init__.py | 28 - dimos/hardware/manipulators/mock/adapter.py | 261 - dimos/hardware/manipulators/piper/__init__.py | 26 - dimos/hardware/manipulators/piper/adapter.py | 528 - dimos/hardware/manipulators/registry.py | 99 - dimos/hardware/manipulators/spec.py | 261 - dimos/hardware/manipulators/xarm/__init__.py | 26 - dimos/hardware/manipulators/xarm/adapter.py | 379 - .../camera/gstreamer/gstreamer_camera.py | 317 - .../gstreamer/gstreamer_camera_test_script.py | 132 - .../camera/gstreamer/gstreamer_sender.py | 359 - dimos/hardware/sensors/camera/module.py | 135 - .../sensors/camera/realsense/README.md | 9 - .../sensors/camera/realsense/__init__.py | 43 - .../sensors/camera/realsense/camera.py | 487 - .../handeyeout_xarm6/calibration.json | 31 - dimos/hardware/sensors/camera/spec.py | 105 - dimos/hardware/sensors/camera/test_webcam.py | 60 - dimos/hardware/sensors/camera/webcam.py | 172 - dimos/hardware/sensors/camera/zed/__init__.py | 63 - dimos/hardware/sensors/camera/zed/camera.py | 536 - .../sensors/camera/zed/single_webcam.yaml | 27 - dimos/hardware/sensors/camera/zed/test_zed.py | 43 - dimos/hardware/sensors/fake_zed_module.py | 291 - dimos/hardware/sensors/lidar/__init__.py | 0 .../lidar/common/dimos_native_module.hpp | 86 - .../sensors/lidar/common/livox_sdk_config.hpp | 116 - .../sensors/lidar/fastlio2/__init__.py | 0 .../sensors/lidar/fastlio2/config/avia.yaml | 35 - .../lidar/fastlio2/config/horizon.yaml | 35 - .../sensors/lidar/fastlio2/config/marsim.yaml | 35 - .../sensors/lidar/fastlio2/config/mid360.yaml | 35 - .../lidar/fastlio2/config/ouster64.yaml | 36 - .../lidar/fastlio2/config/velodyne.yaml | 37 - .../sensors/lidar/fastlio2/cpp/CMakeLists.txt | 117 - .../sensors/lidar/fastlio2/cpp/README.md | 109 - .../lidar/fastlio2/cpp/cloud_filter.hpp | 51 - .../lidar/fastlio2/cpp/config/mid360.json | 38 - .../sensors/lidar/fastlio2/cpp/flake.lock | 135 - .../sensors/lidar/fastlio2/cpp/flake.nix | 59 - .../sensors/lidar/fastlio2/cpp/main.cpp | 522 - .../sensors/lidar/fastlio2/cpp/voxel_map.hpp | 297 - .../lidar/fastlio2/fastlio_blueprints.py | 50 - .../hardware/sensors/lidar/fastlio2/module.py | 148 - .../hardware/sensors/lidar/livox/__init__.py | 0 .../sensors/lidar/livox/cpp/CMakeLists.txt | 57 - .../sensors/lidar/livox/cpp/README.md | 114 - .../sensors/lidar/livox/cpp/flake.lock | 79 - .../sensors/lidar/livox/cpp/flake.nix | 71 - .../hardware/sensors/lidar/livox/cpp/main.cpp | 341 - .../sensors/lidar/livox/livox_blueprints.py | 22 - dimos/hardware/sensors/lidar/livox/module.py | 104 - dimos/hardware/sensors/lidar/livox/ports.py | 31 - dimos/manipulation/__init__.py | 29 - dimos/manipulation/control/__init__.py | 48 - .../control/coordinator_client.py | 713 - .../control/dual_trajectory_setter.py | 542 - .../control/servo_control/README.md | 477 - .../control/servo_control/__init__.py | 32 - .../cartesian_motion_controller.py | 720 - dimos/manipulation/control/target_setter.py | 222 - .../control/trajectory_controller/__init__.py | 31 - .../joint_trajectory_controller.py | 368 - .../control/trajectory_controller/spec.py | 101 - .../manipulation/control/trajectory_setter.py | 467 - dimos/manipulation/grasping/__init__.py | 30 - dimos/manipulation/grasping/demo_grasping.py | 48 - .../grasping/docker_context/Dockerfile | 72 - .../manipulation/grasping/graspgen_module.py | 279 - dimos/manipulation/grasping/grasping.py | 151 - .../manipulation/grasping/visualize_grasps.py | 90 - dimos/manipulation/manipulation_blueprints.py | 410 - dimos/manipulation/manipulation_interface.py | 266 - dimos/manipulation/manipulation_module.py | 1615 --- dimos/manipulation/planning/README.md | 178 - dimos/manipulation/planning/__init__.py | 84 - .../planning/examples/__init__.py | 17 - dimos/manipulation/planning/factory.py | 91 - .../planning/kinematics/__init__.py | 51 - .../kinematics/drake_optimization_ik.py | 269 - .../planning/kinematics/jacobian_ik.py | 430 - .../planning/kinematics/pinocchio_ik.py | 291 - .../manipulation/planning/monitor/__init__.py | 63 - .../planning/monitor/world_monitor.py | 483 - .../monitor/world_obstacle_monitor.py | 607 - .../planning/monitor/world_state_monitor.py | 331 - .../planning/planners/__init__.py | 41 - .../planning/planners/rrt_planner.py | 350 - dimos/manipulation/planning/spec/__init__.py | 51 - dimos/manipulation/planning/spec/config.py | 99 - dimos/manipulation/planning/spec/enums.py | 49 - dimos/manipulation/planning/spec/protocols.py | 231 - dimos/manipulation/planning/spec/types.py | 161 - .../planning/trajectory_generator/__init__.py | 25 - .../joint_trajectory_generator.py | 453 - .../planning/trajectory_generator/spec.py | 76 - dimos/manipulation/planning/utils/__init__.py | 51 - .../planning/utils/kinematics_utils.py | 296 - .../manipulation/planning/utils/mesh_utils.py | 354 - .../manipulation/planning/utils/path_utils.py | 299 - dimos/manipulation/planning/world/__init__.py | 27 - .../planning/world/drake_world.py | 1047 -- .../manipulation/test_manipulation_module.py | 293 - dimos/manipulation/test_manipulation_unit.py | 308 - dimos/mapping/__init__.py | 0 dimos/mapping/costmapper.py | 84 - dimos/mapping/google_maps/conftest.py | 38 - .../get_location_context_places_nearby.json | 965 -- .../get_location_context_reverse_geocode.json | 1140 -- .../google_maps/fixtures/get_position.json | 141 - .../fixtures/get_position_with_places.json | 53 - dimos/mapping/google_maps/google_maps.py | 192 - dimos/mapping/google_maps/test_google_maps.py | 139 - dimos/mapping/google_maps/types.py | 66 - dimos/mapping/occupancy/conftest.py | 30 - dimos/mapping/occupancy/extrude_occupancy.py | 235 - dimos/mapping/occupancy/gradient.py | 202 - dimos/mapping/occupancy/inflation.py | 53 - dimos/mapping/occupancy/operations.py | 88 - dimos/mapping/occupancy/path_map.py | 40 - dimos/mapping/occupancy/path_mask.py | 98 - dimos/mapping/occupancy/path_resampling.py | 256 - .../occupancy/test_extrude_occupancy.py | 28 - dimos/mapping/occupancy/test_gradient.py | 37 - dimos/mapping/occupancy/test_inflation.py | 31 - dimos/mapping/occupancy/test_operations.py | 40 - dimos/mapping/occupancy/test_path_map.py | 34 - dimos/mapping/occupancy/test_path_mask.py | 48 - .../mapping/occupancy/test_path_resampling.py | 50 - .../mapping/occupancy/test_visualizations.py | 31 - dimos/mapping/occupancy/visualizations.py | 159 - dimos/mapping/occupancy/visualize_path.py | 88 - dimos/mapping/osm/README.md | 43 - dimos/mapping/osm/__init__.py | 0 dimos/mapping/osm/current_location_map.py | 113 - dimos/mapping/osm/demo_osm.py | 25 - dimos/mapping/osm/osm.py | 183 - dimos/mapping/osm/query.py | 54 - dimos/mapping/osm/test_osm.py | 71 - .../pointclouds/accumulators/general.py | 77 - .../pointclouds/accumulators/protocol.py | 28 - dimos/mapping/pointclouds/demo.py | 86 - dimos/mapping/pointclouds/occupancy.py | 501 - dimos/mapping/pointclouds/test_occupancy.py | 128 - .../pointclouds/test_occupancy_speed.py | 57 - dimos/mapping/pointclouds/util.py | 58 - dimos/mapping/test_voxels.py | 207 - dimos/mapping/types.py | 27 - dimos/mapping/utils/distance.py | 48 - dimos/mapping/voxels.py | 244 - dimos/memory/embedding.py | 105 - dimos/memory/test_embedding.py | 53 - dimos/memory/timeseries/__init__.py | 41 - dimos/memory/timeseries/base.py | 367 - dimos/memory/timeseries/inmemory.py | 119 - dimos/memory/timeseries/legacy.py | 398 - dimos/memory/timeseries/pickledir.py | 198 - dimos/memory/timeseries/postgres.py | 312 - dimos/memory/timeseries/sqlite.py | 268 - dimos/memory/timeseries/test_base.py | 468 - dimos/memory/timeseries/test_legacy.py | 48 - dimos/models/__init__.py | 3 - dimos/models/base.py | 199 - dimos/models/depth/__init__.py | 0 dimos/models/depth/metric3d.py | 187 - dimos/models/depth/test_metric3d.py | 102 - dimos/models/embedding/__init__.py | 30 - dimos/models/embedding/base.py | 165 - dimos/models/embedding/clip.py | 112 - dimos/models/embedding/mobileclip.py | 119 - dimos/models/embedding/test_embedding.py | 152 - dimos/models/embedding/treid.py | 108 - dimos/models/qwen/video_query.py | 241 - .../models/segmentation/configs/edgetam.yaml | 138 - dimos/models/segmentation/edge_tam.py | 269 - dimos/models/test_base.py | 136 - dimos/models/vl/README.md | 67 - dimos/models/vl/__init__.py | 13 - dimos/models/vl/base.py | 342 - dimos/models/vl/florence.py | 170 - dimos/models/vl/moondream.py | 220 - dimos/models/vl/moondream_hosted.py | 148 - dimos/models/vl/openai.py | 106 - dimos/models/vl/qwen.py | 102 - dimos/models/vl/test_base.py | 146 - dimos/models/vl/test_captioner.py | 90 - dimos/models/vl/test_models.py | 0 dimos/models/vl/test_vlm.py | 317 - dimos/msgs/__init__.py | 4 - dimos/msgs/foxglove_msgs/Color.py | 65 - dimos/msgs/foxglove_msgs/ImageAnnotations.py | 38 - dimos/msgs/foxglove_msgs/__init__.py | 3 - dimos/msgs/geometry_msgs/Pose.py | 239 - dimos/msgs/geometry_msgs/PoseArray.py | 97 - dimos/msgs/geometry_msgs/PoseStamped.py | 140 - .../msgs/geometry_msgs/PoseWithCovariance.py | 197 - .../PoseWithCovarianceStamped.py | 110 - dimos/msgs/geometry_msgs/Quaternion.py | 268 - dimos/msgs/geometry_msgs/Transform.py | 305 - dimos/msgs/geometry_msgs/Twist.py | 121 - dimos/msgs/geometry_msgs/TwistStamped.py | 70 - .../msgs/geometry_msgs/TwistWithCovariance.py | 193 - .../TwistWithCovarianceStamped.py | 118 - dimos/msgs/geometry_msgs/Vector3.py | 434 - dimos/msgs/geometry_msgs/Wrench.py | 40 - dimos/msgs/geometry_msgs/WrenchStamped.py | 75 - dimos/msgs/geometry_msgs/__init__.py | 34 - dimos/msgs/geometry_msgs/test_Pose.py | 749 -- dimos/msgs/geometry_msgs/test_PoseStamped.py | 55 - .../geometry_msgs/test_PoseWithCovariance.py | 322 - .../test_PoseWithCovarianceStamped.py | 223 - dimos/msgs/geometry_msgs/test_Quaternion.py | 387 - dimos/msgs/geometry_msgs/test_Transform.py | 420 - dimos/msgs/geometry_msgs/test_Twist.py | 199 - dimos/msgs/geometry_msgs/test_TwistStamped.py | 66 - .../geometry_msgs/test_TwistWithCovariance.py | 356 - .../test_TwistWithCovarianceStamped.py | 252 - dimos/msgs/geometry_msgs/test_Vector3.py | 462 - dimos/msgs/geometry_msgs/test_publish.py | 54 - dimos/msgs/helpers.py | 53 - dimos/msgs/nav_msgs/OccupancyGrid.py | 592 - dimos/msgs/nav_msgs/Odometry.py | 235 - dimos/msgs/nav_msgs/Path.py | 214 - dimos/msgs/nav_msgs/__init__.py | 9 - dimos/msgs/nav_msgs/test_OccupancyGrid.py | 470 - dimos/msgs/nav_msgs/test_Odometry.py | 208 - dimos/msgs/nav_msgs/test_Path.py | 287 - dimos/msgs/protocol.py | 31 - dimos/msgs/sensor_msgs/CameraInfo.py | 479 - dimos/msgs/sensor_msgs/Image.py | 659 - dimos/msgs/sensor_msgs/Imu.py | 118 - dimos/msgs/sensor_msgs/JointCommand.py | 143 - dimos/msgs/sensor_msgs/JointState.py | 146 - dimos/msgs/sensor_msgs/Joy.py | 136 - dimos/msgs/sensor_msgs/PointCloud2.py | 751 -- dimos/msgs/sensor_msgs/RobotState.py | 188 - dimos/msgs/sensor_msgs/__init__.py | 20 - .../sensor_msgs/image_impls/AbstractImage.py | 19 - dimos/msgs/sensor_msgs/test_CameraInfo.py | 287 - dimos/msgs/sensor_msgs/test_Joy.py | 188 - dimos/msgs/sensor_msgs/test_PointCloud2.py | 160 - dimos/msgs/sensor_msgs/test_image.py | 148 - dimos/msgs/std_msgs/Bool.py | 28 - dimos/msgs/std_msgs/Header.py | 106 - dimos/msgs/std_msgs/Int32.py | 32 - dimos/msgs/std_msgs/Int8.py | 32 - dimos/msgs/std_msgs/UInt32.py | 30 - dimos/msgs/std_msgs/__init__.py | 21 - dimos/msgs/std_msgs/test_header.py | 98 - dimos/msgs/tf2_msgs/TFMessage.py | 146 - dimos/msgs/tf2_msgs/__init__.py | 17 - dimos/msgs/tf2_msgs/test_TFMessage.py | 140 - dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py | 68 - dimos/msgs/trajectory_msgs/JointTrajectory.py | 211 - dimos/msgs/trajectory_msgs/TrajectoryPoint.py | 136 - .../msgs/trajectory_msgs/TrajectoryStatus.py | 170 - dimos/msgs/trajectory_msgs/__init__.py | 30 - dimos/msgs/vision_msgs/BoundingBox2DArray.py | 21 - dimos/msgs/vision_msgs/BoundingBox3DArray.py | 21 - dimos/msgs/vision_msgs/Detection2D.py | 27 - dimos/msgs/vision_msgs/Detection2DArray.py | 29 - dimos/msgs/vision_msgs/Detection3D.py | 27 - dimos/msgs/vision_msgs/Detection3DArray.py | 27 - dimos/msgs/vision_msgs/__init__.py | 15 - dimos/navigation/base.py | 73 - dimos/navigation/bbox_navigation.py | 76 - dimos/navigation/demo_ros_navigation.py | 60 - .../frontier_exploration/__init__.py | 3 - .../test_wavefront_frontier_goal_selector.py | 368 - .../navigation/frontier_exploration/utils.py | 138 - .../wavefront_frontier_goal_selector.py | 848 -- .../replanning_a_star/controllers.py | 156 - .../replanning_a_star/global_planner.py | 348 - .../replanning_a_star/goal_validator.py | 264 - .../replanning_a_star/local_planner.py | 324 - .../replanning_a_star/min_cost_astar.py | 227 - .../replanning_a_star/min_cost_astar_cpp.cpp | 265 - .../replanning_a_star/min_cost_astar_ext.pyi | 26 - dimos/navigation/replanning_a_star/module.py | 102 - .../replanning_a_star/navigation_map.py | 66 - .../replanning_a_star/path_clearance.py | 94 - .../replanning_a_star/path_distancer.py | 89 - .../replanning_a_star/position_tracker.py | 83 - .../replanning_a_star/replan_limiter.py | 68 - .../replanning_a_star/test_goal_validator.py | 53 - .../replanning_a_star/test_min_cost_astar.py | 88 - dimos/navigation/rosnav.py | 411 - dimos/navigation/visual/query.py | 44 - .../visual_servoing/detection_navigation.py | 208 - .../visual_servoing/visual_servoing_2d.py | 166 - dimos/perception/__init__.py | 0 dimos/perception/common/__init__.py | 81 - dimos/perception/common/utils.py | 860 -- .../demo_object_scene_registration.py | 38 - dimos/perception/detection/__init__.py | 10 - dimos/perception/detection/conftest.py | 303 - .../detection/detectors/__init__.py | 8 - .../detectors/config/custom_tracker.yaml | 21 - .../detection/detectors/conftest.py | 45 - .../detectors/person/test_person_detectors.py | 160 - .../detection/detectors/person/yolo.py | 80 - .../detectors/test_bbox_detectors.py | 190 - dimos/perception/detection/detectors/types.py | 23 - dimos/perception/detection/detectors/yolo.py | 83 - dimos/perception/detection/detectors/yoloe.py | 177 - dimos/perception/detection/module2D.py | 179 - dimos/perception/detection/module3D.py | 237 - dimos/perception/detection/moduleDB.py | 314 - dimos/perception/detection/objectDB.py | 321 - dimos/perception/detection/person_tracker.py | 126 - dimos/perception/detection/reid/__init__.py | 13 - .../detection/reid/embedding_id_system.py | 266 - dimos/perception/detection/reid/module.py | 114 - .../reid/test_embedding_id_system.py | 276 - .../perception/detection/reid/test_module.py | 44 - dimos/perception/detection/reid/type.py | 54 - dimos/perception/detection/test_moduleDB.py | 59 - dimos/perception/detection/type/__init__.py | 28 - .../detection/type/detection2d/__init__.py | 30 - .../detection/type/detection2d/base.py | 49 - .../detection/type/detection2d/bbox.py | 408 - .../type/detection2d/imageDetections2D.py | 94 - .../detection/type/detection2d/person.py | 345 - .../detection/type/detection2d/point.py | 184 - .../detection/type/detection2d/seg.py | 206 - .../detection/type/detection2d/test_bbox.py | 87 - .../detection2d/test_imageDetections2D.py | 52 - .../detection/type/detection2d/test_person.py | 71 - .../detection/type/detection3d/__init__.py | 37 - .../detection/type/detection3d/base.py | 45 - .../detection/type/detection3d/bbox.py | 94 - .../type/detection3d/imageDetections3DPC.py | 45 - .../detection/type/detection3d/object.py | 367 - .../detection/type/detection3d/pointcloud.py | 336 - .../type/detection3d/pointcloud_filters.py | 82 - .../detection3d/test_imageDetections3DPC.py | 37 - .../type/detection3d/test_pointcloud.py | 134 - .../detection/type/imageDetections.py | 92 - .../detection/type/test_detection3d.py | 36 - .../detection/type/test_object3d.py | 177 - dimos/perception/detection/type/utils.py | 101 - dimos/perception/experimental/__init__.py | 15 - .../experimental/temporal_memory/README.md | 32 - .../experimental/temporal_memory/__init__.py | 24 - .../temporal_memory/clip_filter.py | 171 - .../temporal_memory/entity_graph_db.py | 1018 -- .../temporal_memory/temporal_memory.md | 39 - .../temporal_memory/temporal_memory.py | 665 - .../temporal_memory/temporal_memory_deploy.py | 64 - .../temporal_memory_example.py | 137 - .../temporal_utils/__init__.py | 60 - .../temporal_utils/graph_utils.py | 226 - .../temporal_memory/temporal_utils/helpers.py | 74 - .../temporal_memory/temporal_utils/parsers.py | 156 - .../temporal_memory/temporal_utils/prompts.py | 359 - .../temporal_memory/temporal_utils/state.py | 139 - .../test_temporal_memory_module.py | 227 - dimos/perception/object_scene_registration.py | 358 - dimos/perception/object_tracker.py | 641 - dimos/perception/object_tracker_2d.py | 301 - dimos/perception/object_tracker_3d.py | 307 - dimos/perception/spatial_perception.py | 592 - dimos/perception/test_spatial_memory.py | 202 - .../perception/test_spatial_memory_module.py | 227 - dimos/protocol/__init__.py | 0 dimos/protocol/encode/__init__.py | 89 - dimos/protocol/mcp/README.md | 35 - dimos/protocol/mcp/__init__.py | 0 dimos/protocol/mcp/__main__.py | 36 - dimos/protocol/mcp/bridge.py | 53 - dimos/protocol/mcp/mcp.py | 139 - dimos/protocol/mcp/test_mcp_module.py | 130 - dimos/protocol/pubsub/__init__.py | 9 - .../pubsub/benchmark/test_benchmark.py | 176 - dimos/protocol/pubsub/benchmark/testdata.py | 402 - dimos/protocol/pubsub/benchmark/type.py | 273 - dimos/protocol/pubsub/bridge.py | 97 - dimos/protocol/pubsub/encoders.py | 130 - dimos/protocol/pubsub/impl/__init__.py | 6 - dimos/protocol/pubsub/impl/ddspubsub.py | 161 - dimos/protocol/pubsub/impl/jpeg_shm.py | 43 - dimos/protocol/pubsub/impl/lcmpubsub.py | 171 - dimos/protocol/pubsub/impl/memory.py | 61 - dimos/protocol/pubsub/impl/redispubsub.py | 198 - dimos/protocol/pubsub/impl/rospubsub.py | 311 - .../pubsub/impl/rospubsub_conversion.py | 365 - dimos/protocol/pubsub/impl/shmpubsub.py | 345 - dimos/protocol/pubsub/impl/test_lcmpubsub.py | 196 - dimos/protocol/pubsub/impl/test_rospubsub.py | 286 - dimos/protocol/pubsub/patterns.py | 98 - dimos/protocol/pubsub/shm/ipc_factory.py | 318 - dimos/protocol/pubsub/spec.py | 191 - dimos/protocol/pubsub/test_encoder.py | 171 - dimos/protocol/pubsub/test_pattern_sub.py | 263 - dimos/protocol/pubsub/test_patterns.py | 132 - dimos/protocol/pubsub/test_spec.py | 354 - dimos/protocol/rpc/__init__.py | 18 - dimos/protocol/rpc/pubsubrpc.py | 318 - dimos/protocol/rpc/redisrpc.py | 21 - dimos/protocol/rpc/rpc_utils.py | 104 - dimos/protocol/rpc/spec.py | 104 - dimos/protocol/rpc/test_lcmrpc.py | 45 - dimos/protocol/rpc/test_rpc_utils.py | 70 - dimos/protocol/rpc/test_spec.py | 399 - dimos/protocol/service/__init__.py | 8 - dimos/protocol/service/ddsservice.py | 80 - dimos/protocol/service/lcmservice.py | 202 - dimos/protocol/service/spec.py | 38 - dimos/protocol/service/system_configurator.py | 436 - dimos/protocol/service/test_lcmservice.py | 314 - dimos/protocol/service/test_spec.py | 102 - .../service/test_system_configurator.py | 482 - dimos/protocol/tf/__init__.py | 17 - dimos/protocol/tf/test_tf.py | 685 - dimos/protocol/tf/tf.py | 344 - dimos/protocol/tf/tflcmcpp.py | 93 - dimos/robot/__init__.py | 0 dimos/robot/all_blueprints.py | 144 - dimos/robot/cli/dimos.py | 222 - dimos/robot/cli/topic.py | 135 - dimos/robot/drone/README.md | 289 - dimos/robot/drone/__init__.py | 27 - dimos/robot/drone/camera_module.py | 286 - dimos/robot/drone/connection_module.py | 488 - dimos/robot/drone/dji_video_stream.py | 219 - dimos/robot/drone/drone.py | 495 - dimos/robot/drone/drone_tracking_module.py | 402 - .../drone/drone_visual_servoing_controller.py | 103 - dimos/robot/drone/mavlink_connection.py | 1109 -- dimos/robot/drone/test_drone.py | 1035 -- dimos/robot/foxglove_bridge.py | 120 - dimos/robot/get_all_blueprints.py | 31 - dimos/robot/manipulators/__init__.py | 0 dimos/robot/manipulators/piper/__init__.py | 0 dimos/robot/manipulators/piper/blueprints.py | 97 - dimos/robot/manipulators/xarm/__init__.py | 0 dimos/robot/manipulators/xarm/blueprints.py | 121 - dimos/robot/position_stream.py | 161 - dimos/robot/robot.py | 60 - dimos/robot/ros_command_queue.py | 473 - dimos/robot/test_all_blueprints.py | 45 - dimos/robot/test_all_blueprints_generation.py | 214 - dimos/robot/unitree/__init__.py | 0 dimos/robot/unitree/b1/README.md | 219 - dimos/robot/unitree/b1/__init__.py | 8 - dimos/robot/unitree/b1/b1_command.py | 96 - dimos/robot/unitree/b1/connection.py | 422 - dimos/robot/unitree/b1/joystick_module.py | 282 - .../robot/unitree/b1/joystick_server_udp.cpp | 366 - dimos/robot/unitree/b1/test_connection.py | 431 - dimos/robot/unitree/b1/unitree_b1.py | 224 - dimos/robot/unitree/connection.py | 17 +- .../unitree/demo_error_on_name_conflicts.py | 53 - dimos/robot/unitree/depth_module.py | 243 - dimos/robot/unitree/g1/blueprints/__init__.py | 37 - .../unitree/g1/blueprints/agentic/__init__.py | 16 - .../g1/blueprints/agentic/_agentic_skills.py | 29 - .../blueprints/agentic/unitree_g1_agentic.py | 27 - .../agentic/unitree_g1_agentic_sim.py | 27 - .../g1/blueprints/agentic/unitree_g1_full.py | 29 - .../unitree/g1/blueprints/basic/__init__.py | 16 - .../g1/blueprints/basic/unitree_g1_basic.py | 31 - .../blueprints/basic/unitree_g1_basic_sim.py | 31 - .../blueprints/basic/unitree_g1_joystick.py | 27 - .../g1/blueprints/perceptive/__init__.py | 16 - .../perceptive/_perception_and_memory.py | 29 - .../g1/blueprints/perceptive/unitree_g1.py | 29 - .../perceptive/unitree_g1_detection.py | 111 - .../blueprints/perceptive/unitree_g1_shm.py | 40 - .../blueprints/perceptive/unitree_g1_sim.py | 29 - .../g1/blueprints/primitive/__init__.py | 16 - .../primitive/uintree_g1_primitive_no_nav.py | 136 - dimos/robot/unitree/g1/connection.py | 102 - dimos/robot/unitree/g1/sim.py | 177 - dimos/robot/unitree/g1/skill_container.py | 163 - .../robot/unitree/go2/blueprints/__init__.py | 37 - .../go2/blueprints/agentic/__init__.py | 16 - .../go2/blueprints/agentic/_common_agentic.py | 32 - .../blueprints/agentic/unitree_go2_agentic.py | 27 - .../unitree_go2_agentic_huggingface.py | 27 - .../agentic/unitree_go2_agentic_mcp.py | 25 - .../agentic/unitree_go2_agentic_ollama.py | 30 - .../agentic/unitree_go2_temporal_memory.py | 25 - .../unitree/go2/blueprints/basic/__init__.py | 16 - .../go2/blueprints/basic/unitree_go2_basic.py | 104 - .../unitree/go2/blueprints/smart/__init__.py | 16 - .../go2/blueprints/smart/_with_jpeg.py | 26 - .../go2/blueprints/smart/unitree_go2.py | 31 - .../blueprints/smart/unitree_go2_detection.py | 69 - .../go2/blueprints/smart/unitree_go2_ros.py | 30 - .../blueprints/smart/unitree_go2_spatial.py | 27 - .../smart/unitree_go2_vlm_stream_test.py | 27 - dimos/robot/unitree/go2/connection.py | 333 - dimos/robot/unitree/go2/go2.urdf | 22 - dimos/robot/unitree/keyboard_teleop.py | 205 - dimos/robot/unitree/modular/detect.py | 186 - dimos/robot/unitree/mujoco_connection.py | 355 - .../unitree/params/front_camera_720.yaml | 26 - dimos/robot/unitree/params/sim_camera.yaml | 26 - dimos/robot/unitree/rosnav.py | 136 - dimos/robot/unitree/testing/__init__.py | 0 dimos/robot/unitree/testing/helpers.py | 170 - dimos/robot/unitree/testing/mock.py | 92 - dimos/robot/unitree/testing/test_actors.py | 114 - dimos/robot/unitree/testing/test_tooling.py | 37 - dimos/robot/unitree/type/__init__.py | 0 dimos/robot/unitree/type/lidar.py | 74 - dimos/robot/unitree/type/lowstate.py | 93 - dimos/robot/unitree/type/map.py | 128 - dimos/robot/unitree/type/odometry.py | 102 - dimos/robot/unitree/type/test_lidar.py | 30 - dimos/robot/unitree/type/test_odometry.py | 49 - dimos/robot/unitree/type/test_timeseries.py | 44 - dimos/robot/unitree/type/timeseries.py | 149 - dimos/robot/unitree/type/vector.py | 442 - .../robot/unitree/unitree_skill_container.py | 339 - dimos/robot/unitree_webrtc/README.md | 1 - dimos/robot/unitree_webrtc/__init__.py | 32 - dimos/robot/unitree_webrtc/type/__init__.py | 33 - dimos/robot/unitree_webrtc/type/lidar.py | 18 - dimos/robot/unitree_webrtc/type/lowstate.py | 18 - dimos/robot/unitree_webrtc/type/map.py | 18 - dimos/robot/unitree_webrtc/type/odometry.py | 18 - dimos/robot/unitree_webrtc/type/timeseries.py | 18 - dimos/robot/unitree_webrtc/type/vector.py | 18 - dimos/robot/utils/README.md | 38 - dimos/robot/utils/robot_debugger.py | 59 - dimos/rxpy_backpressure/LICENSE.txt | 21 - dimos/rxpy_backpressure/__init__.py | 3 - dimos/rxpy_backpressure/backpressure.py | 29 - dimos/rxpy_backpressure/drop.py | 67 - dimos/rxpy_backpressure/function_runner.py | 6 - dimos/rxpy_backpressure/latest.py | 57 - dimos/rxpy_backpressure/locks.py | 30 - dimos/rxpy_backpressure/observer.py | 18 - dimos/simulation/__init__.py | 15 - dimos/simulation/base/__init__.py | 0 dimos/simulation/base/simulator_base.py | 47 - dimos/simulation/base/stream_base.py | 116 - dimos/simulation/engines/__init__.py | 25 - dimos/simulation/engines/base.py | 84 - dimos/simulation/engines/mujoco_engine.py | 300 - dimos/simulation/genesis/__init__.py | 4 - dimos/simulation/genesis/simulator.py | 159 - dimos/simulation/genesis/stream.py | 144 - dimos/simulation/isaac/__init__.py | 4 - dimos/simulation/isaac/simulator.py | 44 - dimos/simulation/isaac/stream.py | 137 - dimos/simulation/manipulators/__init__.py | 54 - .../manipulators/sim_manip_interface.py | 200 - dimos/simulation/manipulators/sim_module.py | 247 - .../manipulators/test_sim_module.py | 123 - dimos/simulation/mujoco/constants.py | 35 - dimos/simulation/mujoco/depth_camera.py | 88 - dimos/simulation/mujoco/input_controller.py | 26 - dimos/simulation/mujoco/model.py | 156 - dimos/simulation/mujoco/mujoco_process.py | 246 - dimos/simulation/mujoco/person_on_track.py | 160 - dimos/simulation/mujoco/policy.py | 151 - dimos/simulation/mujoco/shared_memory.py | 286 - dimos/simulation/sim_blueprints.py | 48 - dimos/simulation/utils/xml_parser.py | 277 - dimos/skills/__init__.py | 0 dimos/skills/kill_skill.py | 61 - .../abstract_manipulation_skill.py | 58 - .../manipulation/force_constraint_skill.py | 72 - dimos/skills/manipulation/manipulate_skill.py | 173 - dimos/skills/manipulation/pick_and_place.py | 444 - .../manipulation/rotation_constraint_skill.py | 111 - .../translation_constraint_skill.py | 100 - dimos/skills/rest/__init__.py | 0 dimos/skills/rest/rest.py | 101 - dimos/skills/skills.py | 343 - dimos/skills/speak.py | 168 - dimos/skills/unitree/__init__.py | 0 dimos/skills/unitree/unitree_speak.py | 280 - dimos/skills/visual_navigation_skills.py | 150 - dimos/spec/__init__.py | 14 - dimos/spec/control.py | 22 - dimos/spec/mapping.py | 27 - dimos/spec/nav.py | 31 - dimos/spec/perception.py | 51 - dimos/spec/test_utils.py | 80 - dimos/spec/utils.py | 129 - dimos/stream/__init__.py | 0 dimos/stream/audio/__init__.py | 0 dimos/stream/audio/base.py | 121 - dimos/stream/audio/node_key_recorder.py | 335 - dimos/stream/audio/node_microphone.py | 131 - dimos/stream/audio/node_normalizer.py | 220 - dimos/stream/audio/node_output.py | 188 - dimos/stream/audio/node_simulated.py | 222 - dimos/stream/audio/node_volume_monitor.py | 177 - dimos/stream/audio/pipelines.py | 52 - dimos/stream/audio/stt/node_whisper.py | 131 - dimos/stream/audio/text/base.py | 55 - dimos/stream/audio/text/node_stdout.py | 113 - dimos/stream/audio/tts/node_openai.py | 254 - dimos/stream/audio/tts/node_pytts.py | 147 - dimos/stream/audio/utils.py | 26 - dimos/stream/audio/volume.py | 109 - dimos/stream/data_provider.py | 182 - dimos/stream/frame_processor.py | 304 - dimos/stream/ros_video_provider.py | 111 - dimos/stream/rtsp_video_provider.py | 379 - dimos/stream/stream_merger.py | 45 - dimos/stream/video_operators.py | 605 - dimos/stream/video_provider.py | 234 - dimos/stream/video_providers/__init__.py | 0 dimos/teleop/README.md | 81 - dimos/teleop/__init__.py | 15 - dimos/teleop/keyboard/__init__.py | 0 .../teleop/keyboard/keyboard_teleop_module.py | 219 - dimos/teleop/phone/README.md | 70 - dimos/teleop/phone/__init__.py | 33 - dimos/teleop/phone/blueprints.py | 32 - dimos/teleop/phone/phone_extensions.py | 51 - dimos/teleop/phone/phone_teleop_module.py | 308 - dimos/teleop/phone/web/static/index.html | 393 - dimos/teleop/phone/web/teleop_server.ts | 85 - dimos/teleop/quest/__init__.py | 54 - dimos/teleop/quest/blueprints.py | 114 - dimos/teleop/quest/quest_extensions.py | 181 - dimos/teleop/quest/quest_teleop_module.py | 407 - dimos/teleop/quest/quest_types.py | 169 - dimos/teleop/quest/web/README.md | 69 - dimos/teleop/quest/web/static/index.html | 409 - dimos/teleop/quest/web/teleop_server.ts | 84 - dimos/teleop/utils/__init__.py | 15 - dimos/teleop/utils/teleop_transforms.py | 79 - dimos/teleop/utils/teleop_visualization.py | 59 - dimos/types/constants.py | 24 - dimos/types/manipulation.py | 168 - dimos/types/robot_capabilities.py | 27 - dimos/types/robot_location.py | 138 - dimos/types/ros_polyfill.py | 48 - dimos/types/sample.py | 583 - dimos/types/test_timestamped.py | 581 - dimos/types/test_vector.py | 384 - dimos/types/test_weaklist.py | 167 - dimos/types/timestamped.py | 287 - dimos/types/vector.py | 457 - dimos/types/weaklist.py | 86 - dimos/utils/actor_registry.py | 84 - dimos/utils/cli/__init__.py | 0 dimos/utils/cli/agentspy/agentspy.py | 238 - dimos/utils/cli/agentspy/demo_agentspy.py | 67 - dimos/utils/cli/dimos.tcss | 91 - .../foxglove_bridge/run_foxglove_bridge.py | 66 - dimos/utils/cli/human/humancli.py | 409 - dimos/utils/cli/human/humanclianim.py | 188 - dimos/utils/cli/lcmspy/lcmspy.py | 213 - dimos/utils/cli/lcmspy/run_lcmspy.py | 135 - dimos/utils/cli/lcmspy/test_lcmspy.py | 222 - dimos/utils/cli/plot.py | 281 - dimos/utils/cli/theme.py | 108 - dimos/utils/data.py | 336 - dimos/utils/decorators/__init__.py | 14 - dimos/utils/decorators/accumulators.py | 106 - dimos/utils/decorators/decorators.py | 222 - dimos/utils/decorators/test_decorators.py | 318 - dimos/utils/demo_image_encoding.py | 127 - dimos/utils/docs/doclinks.md | 96 - dimos/utils/docs/doclinks.py | 589 - dimos/utils/docs/test_doclinks.py | 524 - dimos/utils/extract_frames.py | 83 - dimos/utils/fast_image_generator.py | 305 - dimos/utils/generic.py | 88 - dimos/utils/gpu_utils.py | 23 - dimos/utils/llm_utils.py | 74 - dimos/utils/logging_config.py | 291 - dimos/utils/metrics.py | 90 - dimos/utils/monitoring.py | 307 - dimos/utils/path_utils.py | 22 - dimos/utils/reactive.py | 324 - dimos/utils/sequential_ids.py | 27 - dimos/utils/simple_controller.py | 172 - dimos/utils/test_data.py | 349 - dimos/utils/test_foxglove_bridge.py | 83 - dimos/utils/test_generic.py | 31 - dimos/utils/test_llm_utils.py | 123 - dimos/utils/test_reactive.py | 298 - dimos/utils/test_transform_utils.py | 678 - dimos/utils/test_trigonometry.py | 36 - dimos/utils/testing/__init__.py | 9 - dimos/utils/testing/moment.py | 100 - dimos/utils/testing/replay.py | 22 - dimos/utils/testing/test_moment.py | 75 - dimos/utils/testing/test_replay.py | 262 - dimos/utils/threadpool.py | 79 - dimos/utils/transform_utils.py | 386 - dimos/utils/trigonometry.py | 19 - dimos/utils/urdf.py | 69 - dimos/visualization/rerun/bridge.py | 346 - dimos/web/__init__.py | 0 dimos/web/command-center-extension/.gitignore | 6 - .../command-center-extension/.prettierrc.yaml | 5 - .../web/command-center-extension/CHANGELOG.md | 0 .../command-center-extension/eslint.config.js | 23 - dimos/web/command-center-extension/index.html | 18 - .../package-lock.json | 8602 ------------ .../web/command-center-extension/package.json | 48 - .../web/command-center-extension/src/App.tsx | 128 - .../command-center-extension/src/Button.tsx | 24 - .../src/Connection.ts | 110 - .../src/ExplorePanel.tsx | 41 - .../src/GpsButton.tsx | 41 - .../src/KeyboardControlPanel.tsx | 167 - .../src/components/CostmapLayer.tsx | 165 - .../src/components/GridLayer.tsx | 105 - .../src/components/LeafletMap.tsx | 150 - .../src/components/PathLayer.tsx | 57 - .../src/components/VectorLayer.tsx | 41 - .../src/components/VisualizerComponent.tsx | 102 - .../src/components/VisualizerWrapper.tsx | 86 - .../web/command-center-extension/src/index.ts | 14 - .../web/command-center-extension/src/init.ts | 9 - .../src/optimizedCostmap.ts | 120 - .../src/standalone.tsx | 20 - .../web/command-center-extension/src/types.ts | 127 - .../command-center-extension/tsconfig.json | 22 - .../command-center-extension/vite.config.ts | 21 - dimos/web/dimos_interface/.gitignore | 41 - dimos/web/dimos_interface/__init__.py | 12 - dimos/web/dimos_interface/api/README.md | 86 - dimos/web/dimos_interface/api/__init__.py | 0 .../web/dimos_interface/api/requirements.txt | 7 - dimos/web/dimos_interface/api/server.py | 373 - .../api/templates/index_fastapi.html | 541 - dimos/web/dimos_interface/index.html | 37 - dimos/web/dimos_interface/package.json | 46 - dimos/web/dimos_interface/postcss.config.js | 22 - dimos/web/dimos_interface/public/icon.png | 3 - dimos/web/dimos_interface/src/App.svelte | 53 - dimos/web/dimos_interface/src/app.css | 45 - .../src/components/History.svelte | 25 - .../src/components/Input.svelte | 109 - .../dimos_interface/src/components/Ps1.svelte | 11 - .../src/components/StreamViewer.svelte | 196 - .../src/components/VoiceButton.svelte | 262 - .../dimos_interface/src/interfaces/command.ts | 20 - .../dimos_interface/src/interfaces/theme.ts | 38 - dimos/web/dimos_interface/src/main.ts | 24 - .../web/dimos_interface/src/stores/history.ts | 26 - .../web/dimos_interface/src/stores/stream.ts | 180 - dimos/web/dimos_interface/src/stores/theme.ts | 31 - .../web/dimos_interface/src/utils/commands.ts | 374 - .../dimos_interface/src/utils/simulation.ts | 214 - .../web/dimos_interface/src/utils/tracking.ts | 31 - dimos/web/dimos_interface/src/vite-env.d.ts | 18 - dimos/web/dimos_interface/svelte.config.js | 23 - dimos/web/dimos_interface/tailwind.config.js | 22 - dimos/web/dimos_interface/themes.json | 4974 ------- dimos/web/dimos_interface/tsconfig.json | 25 - dimos/web/dimos_interface/tsconfig.node.json | 11 - dimos/web/dimos_interface/vite.config.ts | 97 - dimos/web/edge_io.py | 26 - dimos/web/fastapi_server.py | 226 - dimos/web/flask_server.py | 105 - dimos/web/robot_web_interface.py | 35 - dimos/web/templates/index_fastapi.html | 389 - dimos/web/templates/index_flask.html | 118 - dimos/web/templates/rerun_dashboard.html | 83 - dimos/web/websocket_vis/README.md | 66 - dimos/web/websocket_vis/costmap_viz.py | 65 - dimos/web/websocket_vis/optimized_costmap.py | 160 - dimos/web/websocket_vis/path_history.py | 75 - .../web/websocket_vis/websocket_vis_module.py | 402 - docker/dev/Dockerfile | 54 - docker/dev/bash.sh | 198 - docker/dev/docker-compose-cuda.yaml | 32 - docker/dev/docker-compose.yaml | 23 - docker/dev/entrypoint.sh | 8 - docker/dev/tmux.conf | 84 - docker/navigation/.env.hardware | 101 - docker/navigation/.gitignore | 20 - docker/navigation/Dockerfile | 508 - docker/navigation/README.md | 184 - docker/navigation/build.sh | 140 - docker/navigation/docker-compose.dev.yml | 23 - docker/navigation/docker-compose.yml | 353 - .../foxglove_utility/goal_autonomy_relay.py | 98 - .../foxglove_utility/twist_relay.py | 76 - docker/navigation/ros_launch_wrapper.py | 195 - docker/navigation/run_both.sh | 202 - docker/navigation/start.sh | 389 - docker/python/Dockerfile | 54 - docker/python/module-install.sh | 73 - docker/ros/Dockerfile | 85 - docker/ros/install-nix.sh | 124 - .../agents/docs/assets/codeblocks_example.svg | 47 - docs/agents/docs/assets/pikchr_basic.svg | 12 - docs/agents/docs/assets/pikchr_branch.svg | 16 - docs/agents/docs/assets/pikchr_explicit.svg | 8 - docs/agents/docs/assets/pikchr_labels.svg | 5 - docs/agents/docs/assets/pikchr_sizing.svg | 13 - docs/agents/docs/codeblocks.md | 314 - docs/agents/docs/doclinks.md | 21 - docs/agents/docs/index.md | 192 - docs/agents/index.md | 19 - docs/capabilities/agents/readme.md | 1 - docs/capabilities/manipulation/readme.md | 112 - .../navigation/native/assets/1-lidar.png | 3 - .../navigation/native/assets/2-globalmap.png | 3 - .../native/assets/3-globalcostmap.png | 3 - .../navigation/native/assets/4-navcostmap.png | 3 - .../navigation/native/assets/5-all.png | 3 - .../native/assets/go2_blueprint.svg | 188 - .../native/assets/go2nav_dataflow.svg | 22 - .../navigation/native/assets/noros_nav.gif | 3 - docs/capabilities/navigation/native/index.md | 144 - docs/capabilities/navigation/readme.md | 10 - docs/capabilities/perception/readme.md | 3 - docs/development/README.md | 273 - docs/development/adding_a_custom_arm.md | 730 - docs/development/assets/docker-hierarchy.svg | 31 - docs/development/assets/get_data_flow.svg | 25 - docs/development/depth_camera_integration.md | 147 - docs/development/dimos_run.md | 79 - docs/development/docker.md | 162 - docs/development/grid_testing.md | 116 - docs/development/large_file_management.md | 206 - docs/development/testing.md | 122 - docs/development/writing_docs.md | 7 - docs/hardware/integration_guide.md | 3 - docs/installation/nix.md | 57 - docs/installation/osx.md | 41 - docs/installation/ubuntu.md | 39 - docs/platforms/quadruped/go2/index.md | 113 - docs/todo.md | 1 - docs/usage/README.md | 12 - docs/usage/assets/abstraction_layers.svg | 20 - docs/usage/assets/camera_module.svg | 87 - docs/usage/assets/go2_agentic.svg | 260 - docs/usage/assets/go2_nav.svg | 183 - docs/usage/assets/lcmspy.png | 3 - docs/usage/assets/pubsub_benchmark.png | 3 - docs/usage/assets/transforms.png | 3 - docs/usage/assets/transforms_chain.svg | 12 - docs/usage/assets/transforms_modules.svg | 20 - docs/usage/assets/transforms_tree.svg | 26 - docs/usage/blueprints.md | 316 - docs/usage/configuration.md | 90 - docs/usage/data_streams/README.md | 41 - docs/usage/data_streams/advanced_streams.md | 295 - .../data_streams/assets/alignment_flow.svg | 22 - .../assets/alignment_overview.svg | 18 - .../assets/alignment_timeline.png | 3 - .../assets/alignment_timeline2.png | 3 - .../assets/alignment_timeline3.png | 3 - .../data_streams/assets/backpressure.svg | 15 - .../assets/backpressure_solution.svg | 21 - .../data_streams/assets/frame_mosaic.jpg | 3 - .../data_streams/assets/frame_mosaic2.jpg | 3 - .../data_streams/assets/getter_hot_cold.svg | 71 - .../data_streams/assets/observable_flow.svg | 16 - .../data_streams/assets/sharpness_graph.svg | 1414 -- .../data_streams/assets/sharpness_graph2.svg | 1429 -- docs/usage/data_streams/quality_filter.md | 316 - docs/usage/data_streams/reactivex.md | 494 - docs/usage/data_streams/storage_replay.md | 231 - docs/usage/data_streams/temporal_alignment.md | 313 - docs/usage/lcm.md | 180 - docs/usage/modules.md | 179 - docs/usage/native_modules.md | 282 - docs/usage/sensor_streams/README.md | 41 - docs/usage/sensor_streams/advanced_streams.md | 295 - .../sensor_streams/assets/alignment_flow.svg | 22 - .../assets/alignment_overview.svg | 18 - .../assets/alignment_timeline.png | 3 - .../assets/alignment_timeline2.png | 3 - .../assets/alignment_timeline3.png | 3 - .../sensor_streams/assets/backpressure.svg | 15 - .../assets/backpressure_solution.svg | 21 - .../sensor_streams/assets/frame_mosaic.jpg | 3 - .../sensor_streams/assets/frame_mosaic2.jpg | 3 - .../sensor_streams/assets/getter_hot_cold.svg | 71 - .../sensor_streams/assets/observable_flow.svg | 16 - .../sensor_streams/assets/sharpness_graph.svg | 1414 -- .../assets/sharpness_graph2.svg | 1429 -- docs/usage/sensor_streams/quality_filter.md | 316 - docs/usage/sensor_streams/reactivex.md | 494 - docs/usage/sensor_streams/storage_replay.md | 231 - .../sensor_streams/temporal_alignment.md | 313 - docs/usage/transforms.md | 469 - docs/usage/transports.md | 437 - docs/usage/transports/dds.md | 26 - docs/usage/transports/index.md | 437 - docs/usage/visualization.md | 115 - examples/camera_grayscale.py | 38 - examples/language-interop/README.md | 20 - examples/language-interop/assets/lcmspy.png | 3 - examples/language-interop/cpp/.gitignore | 1 - examples/language-interop/cpp/CMakeLists.txt | 28 - examples/language-interop/cpp/README.md | 17 - examples/language-interop/cpp/main.cpp | 68 - examples/language-interop/lua/.gitignore | 3 - examples/language-interop/lua/README.md | 38 - examples/language-interop/lua/main.lua | 59 - examples/language-interop/lua/setup.sh | 100 - examples/language-interop/ts/README.md | 34 - examples/language-interop/ts/deno.json | 9 - examples/language-interop/ts/deno.lock | 21 - examples/language-interop/ts/main.ts | 43 - examples/language-interop/ts/web/index.html | 213 - examples/language-interop/ts/web/server.ts | 69 - examples/rpc_calls.py | 56 - examples/simplerobot/README.md | 50 - examples/simplerobot/simplerobot.py | 158 - examples/simplerobot/vis.py | 95 - flake.lock | 177 - flake.nix | 315 - onnx/metric3d_vit_small.onnx | 3 - pyproject.toml | 422 - setup.py | 84 - uv.lock | 11069 ---------------- 1194 files changed, 16 insertions(+), 192581 deletions(-) delete mode 100644 .devcontainer/devcontainer.json delete mode 100644 .dockerignore delete mode 100644 .editorconfig delete mode 100644 .envrc.nix delete mode 100644 .envrc.venv delete mode 100644 .gitattributes delete mode 100644 .github/actions/docker-build/action.yml delete mode 100644 .github/pull_request_template.md delete mode 100644 .github/workflows/_docker-build-template.yml delete mode 100644 .github/workflows/code-cleanup.yml delete mode 100644 .github/workflows/docker.yml delete mode 100644 .github/workflows/readme.md delete mode 100644 .github/workflows/tests.yml delete mode 100644 .gitignore delete mode 100644 .pre-commit-config.yaml delete mode 100644 .python-version delete mode 100644 .style.yapf delete mode 100644 CLA.md delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 README.md delete mode 100644 assets/dimensional-dark.svg delete mode 100644 assets/dimensional-light.svg delete mode 100644 assets/dimensional-text.svg delete mode 100644 assets/dimensional.command-center-extension-0.0.1.foxe delete mode 100644 assets/dimensional.svg delete mode 100644 assets/dimensionalascii.txt delete mode 100644 assets/dimos_interface.gif delete mode 100644 assets/dimos_terminal.png delete mode 100644 assets/drone_foxglove_lcm_dashboard.json delete mode 100644 assets/foxglove_dashboards/go2.json delete mode 100644 assets/foxglove_dashboards/old/foxglove_g1_detections.json delete mode 100644 assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json delete mode 100644 assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json delete mode 100644 assets/foxglove_dashboards/old/foxglove_unitree_yolo.json delete mode 100644 assets/framecount.mp4 delete mode 100644 assets/license_file_header.txt delete mode 100644 assets/readme/agentic_control.gif delete mode 100644 assets/readme/agents.png delete mode 100644 assets/readme/dimos_demo.gif delete mode 100644 assets/readme/lidar.gif delete mode 100644 assets/readme/lidar.png delete mode 100644 assets/readme/navigation.gif delete mode 100644 assets/readme/navigation.png delete mode 100644 assets/readme/perception.png delete mode 100644 assets/readme/spacer.png delete mode 100644 assets/readme/spatial_memory.gif delete mode 100644 assets/simple_demo.mp4 delete mode 100644 assets/simple_demo_small.gif delete mode 100644 assets/trimmed_video_office.mov delete mode 100755 bin/agent_web delete mode 100755 bin/cuda/fix_ort.sh delete mode 100755 bin/dev delete mode 100755 bin/dockerbuild delete mode 100755 bin/doclinks delete mode 100755 bin/filter-errors-after-date delete mode 100755 bin/filter-errors-for-user delete mode 100644 bin/hooks/filter_commit_message.py delete mode 100755 bin/hooks/largefiles_check delete mode 100755 bin/hooks/lfs_check delete mode 100755 bin/lfs_push delete mode 100755 bin/mypy-ros delete mode 100755 bin/pytest-fast delete mode 100755 bin/pytest-mujoco delete mode 100755 bin/pytest-slow delete mode 100755 bin/re-ignore-mypy.py delete mode 100755 bin/robot-debugger delete mode 100755 bin/ros delete mode 100644 data/.lfs/ab_lidar_frames.tar.gz delete mode 100644 data/.lfs/apartment.tar.gz delete mode 100644 data/.lfs/assets.tar.gz delete mode 100644 data/.lfs/astar_corner_min_cost.png.tar.gz delete mode 100644 data/.lfs/astar_min_cost.png.tar.gz delete mode 100644 data/.lfs/big_office.ply.tar.gz delete mode 100644 data/.lfs/big_office_height_cost_occupancy.png.tar.gz delete mode 100644 data/.lfs/big_office_simple_occupancy.png.tar.gz delete mode 100644 data/.lfs/cafe-smol.jpg.tar.gz delete mode 100644 data/.lfs/cafe.jpg.tar.gz delete mode 100644 data/.lfs/chair-image.png.tar.gz delete mode 100644 data/.lfs/command_center.html.tar.gz delete mode 100644 data/.lfs/drone.tar.gz delete mode 100644 data/.lfs/expected_occupancy_scene.xml.tar.gz delete mode 100644 data/.lfs/g1_zed.tar.gz delete mode 100644 data/.lfs/gradient_simple.png.tar.gz delete mode 100644 data/.lfs/gradient_voronoi.png.tar.gz delete mode 100644 data/.lfs/inflation_simple.png.tar.gz delete mode 100644 data/.lfs/lcm_msgs.tar.gz delete mode 100644 data/.lfs/make_navigation_map_mixed.png.tar.gz delete mode 100644 data/.lfs/make_navigation_map_simple.png.tar.gz delete mode 100644 data/.lfs/make_path_mask_full.png.tar.gz delete mode 100644 data/.lfs/make_path_mask_two_meters.png.tar.gz delete mode 100644 data/.lfs/models_clip.tar.gz delete mode 100644 data/.lfs/models_contact_graspnet.tar.gz delete mode 100644 data/.lfs/models_edgetam.tar.gz delete mode 100644 data/.lfs/models_fastsam.tar.gz delete mode 100644 data/.lfs/models_graspgen.tar.gz delete mode 100644 data/.lfs/models_mobileclip.tar.gz delete mode 100644 data/.lfs/models_torchreid.tar.gz delete mode 100644 data/.lfs/models_yolo.tar.gz delete mode 100644 data/.lfs/models_yoloe.tar.gz delete mode 100644 data/.lfs/mujoco_sim.tar.gz delete mode 100644 data/.lfs/occupancy_general.png.tar.gz delete mode 100644 data/.lfs/occupancy_simple.npy.tar.gz delete mode 100644 data/.lfs/occupancy_simple.png.tar.gz delete mode 100644 data/.lfs/office_building_1.tar.gz delete mode 100644 data/.lfs/office_lidar.tar.gz delete mode 100644 data/.lfs/osm_map_test.tar.gz delete mode 100644 data/.lfs/overlay_occupied.png.tar.gz delete mode 100644 data/.lfs/person.tar.gz delete mode 100644 data/.lfs/piper_description.tar.gz delete mode 100644 data/.lfs/raw_odometry_rotate_walk.tar.gz delete mode 100644 data/.lfs/replay_g1.tar.gz delete mode 100644 data/.lfs/replay_g1_run.tar.gz delete mode 100644 data/.lfs/resample_path_simple.png.tar.gz delete mode 100644 data/.lfs/resample_path_smooth.png.tar.gz delete mode 100644 data/.lfs/rgbd_frames.tar.gz delete mode 100644 data/.lfs/smooth_occupied.png.tar.gz delete mode 100644 data/.lfs/three_paths.npy.tar.gz delete mode 100644 data/.lfs/three_paths.ply.tar.gz delete mode 100644 data/.lfs/three_paths.png.tar.gz delete mode 100644 data/.lfs/unitree_go2_bigoffice.tar.gz delete mode 100644 data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz delete mode 100644 data/.lfs/unitree_go2_lidar_corrected.tar.gz delete mode 100644 data/.lfs/unitree_go2_office_walk2.tar.gz delete mode 100644 data/.lfs/unitree_office_walk.tar.gz delete mode 100644 data/.lfs/unitree_raw_webrtc_replay.tar.gz delete mode 100644 data/.lfs/video.tar.gz delete mode 100644 data/.lfs/visualize_occupancy_rainbow.png.tar.gz delete mode 100644 data/.lfs/visualize_occupancy_turbo.png.tar.gz delete mode 100644 data/.lfs/xarm7.tar.gz delete mode 100644 data/.lfs/xarm_description.tar.gz delete mode 100644 default.env delete mode 100644 dimos/__init__.py delete mode 100644 dimos/agents/agent.py delete mode 100644 dimos/agents/agent_test_runner.py delete mode 100644 dimos/agents/annotation.py delete mode 100644 dimos/agents/conftest.py delete mode 100644 dimos/agents/demo_agent.py delete mode 100644 dimos/agents/fixtures/test_can_call_again_on_error[False].json delete mode 100644 dimos/agents/fixtures/test_can_call_again_on_error[True].json delete mode 100644 dimos/agents/fixtures/test_can_call_tool[False].json delete mode 100644 dimos/agents/fixtures/test_can_call_tool[True].json delete mode 100644 dimos/agents/fixtures/test_get_gps_position_for_queries.json delete mode 100644 dimos/agents/fixtures/test_go_to_semantic_location.json delete mode 100644 dimos/agents/fixtures/test_image.json delete mode 100644 dimos/agents/fixtures/test_multiple_tool_calls_with_multiple_messages.json delete mode 100644 dimos/agents/fixtures/test_pounce.json delete mode 100644 dimos/agents/fixtures/test_prompt.json delete mode 100644 dimos/agents/fixtures/test_set_gps_travel_points.json delete mode 100644 dimos/agents/fixtures/test_set_gps_travel_points_multiple.json delete mode 100644 dimos/agents/fixtures/test_start_exploration.json delete mode 100644 dimos/agents/fixtures/test_stop_movement.json delete mode 100644 dimos/agents/fixtures/test_where_am_i.json delete mode 100644 dimos/agents/ollama_agent.py delete mode 100644 dimos/agents/skills/demo_calculator_skill.py delete mode 100644 dimos/agents/skills/demo_google_maps_skill.py delete mode 100644 dimos/agents/skills/demo_gps_nav.py delete mode 100644 dimos/agents/skills/demo_robot.py delete mode 100644 dimos/agents/skills/demo_skill.py delete mode 100644 dimos/agents/skills/google_maps_skill_container.py delete mode 100644 dimos/agents/skills/gps_nav_skill.py delete mode 100644 dimos/agents/skills/navigation.py delete mode 100644 dimos/agents/skills/osm.py delete mode 100644 dimos/agents/skills/person_follow.py delete mode 100644 dimos/agents/skills/speak_skill.py delete mode 100644 dimos/agents/skills/test_google_maps_skill_container.py delete mode 100644 dimos/agents/skills/test_gps_nav_skills.py delete mode 100644 dimos/agents/skills/test_navigation.py delete mode 100644 dimos/agents/skills/test_unitree_skill_container.py delete mode 100644 dimos/agents/system_prompt.py delete mode 100644 dimos/agents/test_agent.py delete mode 100644 dimos/agents/testing.py delete mode 100644 dimos/agents/utils.py delete mode 100644 dimos/agents/vlm_agent.py delete mode 100644 dimos/agents/vlm_stream_tester.py delete mode 100644 dimos/agents/web_human_input.py delete mode 100644 dimos/agents_deprecated/__init__.py delete mode 100644 dimos/agents_deprecated/agent.py delete mode 100644 dimos/agents_deprecated/agent_config.py delete mode 100644 dimos/agents_deprecated/agent_message.py delete mode 100644 dimos/agents_deprecated/agent_types.py delete mode 100644 dimos/agents_deprecated/claude_agent.py delete mode 100644 dimos/agents_deprecated/memory/__init__.py delete mode 100644 dimos/agents_deprecated/memory/base.py delete mode 100644 dimos/agents_deprecated/memory/chroma_impl.py delete mode 100644 dimos/agents_deprecated/memory/image_embedding.py delete mode 100644 dimos/agents_deprecated/memory/spatial_vector_db.py delete mode 100644 dimos/agents_deprecated/memory/test_image_embedding.py delete mode 100644 dimos/agents_deprecated/memory/visual_memory.py delete mode 100644 dimos/agents_deprecated/modules/__init__.py delete mode 100644 dimos/agents_deprecated/modules/base.py delete mode 100644 dimos/agents_deprecated/modules/base_agent.py delete mode 100644 dimos/agents_deprecated/modules/gateway/__init__.py delete mode 100644 dimos/agents_deprecated/modules/gateway/client.py delete mode 100644 dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py delete mode 100644 dimos/agents_deprecated/modules/gateway/tensorzero_simple.py delete mode 100644 dimos/agents_deprecated/modules/gateway/utils.py delete mode 100644 dimos/agents_deprecated/prompt_builder/__init__.py delete mode 100644 dimos/agents_deprecated/prompt_builder/impl.py delete mode 100644 dimos/agents_deprecated/tokenizer/__init__.py delete mode 100644 dimos/agents_deprecated/tokenizer/base.py delete mode 100644 dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py delete mode 100644 dimos/agents_deprecated/tokenizer/openai_tokenizer.py delete mode 100644 dimos/assets/foxglove_dashboards/Overwatch.json delete mode 100644 dimos/conftest.py delete mode 100644 dimos/constants.py delete mode 100644 dimos/control/README.md delete mode 100644 dimos/control/__init__.py delete mode 100644 dimos/control/blueprints.py delete mode 100644 dimos/control/components.py delete mode 100644 dimos/control/coordinator.py delete mode 100644 dimos/control/examples/cartesian_ik_jogger.py delete mode 100644 dimos/control/hardware_interface.py delete mode 100644 dimos/control/task.py delete mode 100644 dimos/control/tasks/__init__.py delete mode 100644 dimos/control/tasks/cartesian_ik_task.py delete mode 100644 dimos/control/tasks/servo_task.py delete mode 100644 dimos/control/tasks/teleop_task.py delete mode 100644 dimos/control/tasks/trajectory_task.py delete mode 100644 dimos/control/tasks/velocity_task.py delete mode 100644 dimos/control/test_control.py delete mode 100644 dimos/control/tick_loop.py delete mode 100644 dimos/core/__init__.py delete mode 100644 dimos/core/_dask_exports.py delete mode 100644 dimos/core/_protocol_exports.py delete mode 100644 dimos/core/_test_future_annotations_helper.py delete mode 100644 dimos/core/blueprints.py delete mode 100644 dimos/core/colors.py delete mode 100644 dimos/core/core.py delete mode 100644 dimos/core/docker_build.py delete mode 100644 dimos/core/docker_runner.py delete mode 100644 dimos/core/global_config.py delete mode 100644 dimos/core/introspection/__init__.py delete mode 100644 dimos/core/introspection/blueprint/__init__.py delete mode 100644 dimos/core/introspection/blueprint/dot.py delete mode 100644 dimos/core/introspection/module/__init__.py delete mode 100644 dimos/core/introspection/module/ansi.py delete mode 100644 dimos/core/introspection/module/dot.py delete mode 100644 dimos/core/introspection/module/info.py delete mode 100644 dimos/core/introspection/module/render.py delete mode 100644 dimos/core/introspection/svg.py delete mode 100644 dimos/core/introspection/utils.py delete mode 100644 dimos/core/module.py delete mode 100644 dimos/core/module_coordinator.py delete mode 100644 dimos/core/native_module.py delete mode 100644 dimos/core/o3dpickle.py delete mode 100644 dimos/core/resource.py delete mode 100644 dimos/core/rpc_client.py delete mode 100644 dimos/core/stream.py delete mode 100644 dimos/core/test_blueprints.py delete mode 100644 dimos/core/test_core.py delete mode 100644 dimos/core/test_modules.py delete mode 100644 dimos/core/test_native_module.py delete mode 100644 dimos/core/test_rpcstress.py delete mode 100644 dimos/core/test_stream.py delete mode 100644 dimos/core/test_worker.py delete mode 100644 dimos/core/testing.py delete mode 100755 dimos/core/tests/native_echo.py delete mode 100644 dimos/core/transport.py delete mode 100644 dimos/core/worker.py delete mode 100644 dimos/core/worker_manager.py delete mode 100644 dimos/e2e_tests/conftest.py delete mode 100644 dimos/e2e_tests/dimos_cli_call.py delete mode 100644 dimos/e2e_tests/lcm_spy.py delete mode 100644 dimos/e2e_tests/test_control_coordinator.py delete mode 100644 dimos/e2e_tests/test_dimos_cli_e2e.py delete mode 100644 dimos/e2e_tests/test_person_follow.py delete mode 100644 dimos/e2e_tests/test_simulation_module.py delete mode 100644 dimos/e2e_tests/test_spatial_memory.py delete mode 100644 dimos/environment/__init__.py delete mode 100644 dimos/environment/environment.py delete mode 100644 dimos/exceptions/__init__.py delete mode 100644 dimos/exceptions/agent_memory_exceptions.py delete mode 100644 dimos/hardware/__init__.py delete mode 100644 dimos/hardware/end_effectors/__init__.py delete mode 100644 dimos/hardware/end_effectors/end_effector.py delete mode 100644 dimos/hardware/manipulators/README.md delete mode 100644 dimos/hardware/manipulators/__init__.py delete mode 100644 dimos/hardware/manipulators/mock/__init__.py delete mode 100644 dimos/hardware/manipulators/mock/adapter.py delete mode 100644 dimos/hardware/manipulators/piper/__init__.py delete mode 100644 dimos/hardware/manipulators/piper/adapter.py delete mode 100644 dimos/hardware/manipulators/registry.py delete mode 100644 dimos/hardware/manipulators/spec.py delete mode 100644 dimos/hardware/manipulators/xarm/__init__.py delete mode 100644 dimos/hardware/manipulators/xarm/adapter.py delete mode 100644 dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py delete mode 100755 dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py delete mode 100755 dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py delete mode 100644 dimos/hardware/sensors/camera/module.py delete mode 100644 dimos/hardware/sensors/camera/realsense/README.md delete mode 100644 dimos/hardware/sensors/camera/realsense/__init__.py delete mode 100644 dimos/hardware/sensors/camera/realsense/camera.py delete mode 100644 dimos/hardware/sensors/camera/realsense/handeyeout_xarm6/calibration.json delete mode 100644 dimos/hardware/sensors/camera/spec.py delete mode 100644 dimos/hardware/sensors/camera/test_webcam.py delete mode 100644 dimos/hardware/sensors/camera/webcam.py delete mode 100644 dimos/hardware/sensors/camera/zed/__init__.py delete mode 100644 dimos/hardware/sensors/camera/zed/camera.py delete mode 100644 dimos/hardware/sensors/camera/zed/single_webcam.yaml delete mode 100644 dimos/hardware/sensors/camera/zed/test_zed.py delete mode 100644 dimos/hardware/sensors/fake_zed_module.py delete mode 100644 dimos/hardware/sensors/lidar/__init__.py delete mode 100644 dimos/hardware/sensors/lidar/common/dimos_native_module.hpp delete mode 100644 dimos/hardware/sensors/lidar/common/livox_sdk_config.hpp delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/__init__.py delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/avia.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/horizon.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/marsim.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/mid360.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/ouster64.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/config/velodyne.yaml delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/CMakeLists.txt delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/README.md delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/cloud_filter.hpp delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/config/mid360.json delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/flake.lock delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/flake.nix delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/main.cpp delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/cpp/voxel_map.hpp delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py delete mode 100644 dimos/hardware/sensors/lidar/fastlio2/module.py delete mode 100644 dimos/hardware/sensors/lidar/livox/__init__.py delete mode 100644 dimos/hardware/sensors/lidar/livox/cpp/CMakeLists.txt delete mode 100644 dimos/hardware/sensors/lidar/livox/cpp/README.md delete mode 100644 dimos/hardware/sensors/lidar/livox/cpp/flake.lock delete mode 100644 dimos/hardware/sensors/lidar/livox/cpp/flake.nix delete mode 100644 dimos/hardware/sensors/lidar/livox/cpp/main.cpp delete mode 100644 dimos/hardware/sensors/lidar/livox/livox_blueprints.py delete mode 100644 dimos/hardware/sensors/lidar/livox/module.py delete mode 100644 dimos/hardware/sensors/lidar/livox/ports.py delete mode 100644 dimos/manipulation/__init__.py delete mode 100644 dimos/manipulation/control/__init__.py delete mode 100644 dimos/manipulation/control/coordinator_client.py delete mode 100644 dimos/manipulation/control/dual_trajectory_setter.py delete mode 100644 dimos/manipulation/control/servo_control/README.md delete mode 100644 dimos/manipulation/control/servo_control/__init__.py delete mode 100644 dimos/manipulation/control/servo_control/cartesian_motion_controller.py delete mode 100644 dimos/manipulation/control/target_setter.py delete mode 100644 dimos/manipulation/control/trajectory_controller/__init__.py delete mode 100644 dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py delete mode 100644 dimos/manipulation/control/trajectory_controller/spec.py delete mode 100644 dimos/manipulation/control/trajectory_setter.py delete mode 100644 dimos/manipulation/grasping/__init__.py delete mode 100644 dimos/manipulation/grasping/demo_grasping.py delete mode 100644 dimos/manipulation/grasping/docker_context/Dockerfile delete mode 100644 dimos/manipulation/grasping/graspgen_module.py delete mode 100644 dimos/manipulation/grasping/grasping.py delete mode 100644 dimos/manipulation/grasping/visualize_grasps.py delete mode 100644 dimos/manipulation/manipulation_blueprints.py delete mode 100644 dimos/manipulation/manipulation_interface.py delete mode 100644 dimos/manipulation/manipulation_module.py delete mode 100644 dimos/manipulation/planning/README.md delete mode 100644 dimos/manipulation/planning/__init__.py delete mode 100644 dimos/manipulation/planning/examples/__init__.py delete mode 100644 dimos/manipulation/planning/factory.py delete mode 100644 dimos/manipulation/planning/kinematics/__init__.py delete mode 100644 dimos/manipulation/planning/kinematics/drake_optimization_ik.py delete mode 100644 dimos/manipulation/planning/kinematics/jacobian_ik.py delete mode 100644 dimos/manipulation/planning/kinematics/pinocchio_ik.py delete mode 100644 dimos/manipulation/planning/monitor/__init__.py delete mode 100644 dimos/manipulation/planning/monitor/world_monitor.py delete mode 100644 dimos/manipulation/planning/monitor/world_obstacle_monitor.py delete mode 100644 dimos/manipulation/planning/monitor/world_state_monitor.py delete mode 100644 dimos/manipulation/planning/planners/__init__.py delete mode 100644 dimos/manipulation/planning/planners/rrt_planner.py delete mode 100644 dimos/manipulation/planning/spec/__init__.py delete mode 100644 dimos/manipulation/planning/spec/config.py delete mode 100644 dimos/manipulation/planning/spec/enums.py delete mode 100644 dimos/manipulation/planning/spec/protocols.py delete mode 100644 dimos/manipulation/planning/spec/types.py delete mode 100644 dimos/manipulation/planning/trajectory_generator/__init__.py delete mode 100644 dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py delete mode 100644 dimos/manipulation/planning/trajectory_generator/spec.py delete mode 100644 dimos/manipulation/planning/utils/__init__.py delete mode 100644 dimos/manipulation/planning/utils/kinematics_utils.py delete mode 100644 dimos/manipulation/planning/utils/mesh_utils.py delete mode 100644 dimos/manipulation/planning/utils/path_utils.py delete mode 100644 dimos/manipulation/planning/world/__init__.py delete mode 100644 dimos/manipulation/planning/world/drake_world.py delete mode 100644 dimos/manipulation/test_manipulation_module.py delete mode 100644 dimos/manipulation/test_manipulation_unit.py delete mode 100644 dimos/mapping/__init__.py delete mode 100644 dimos/mapping/costmapper.py delete mode 100644 dimos/mapping/google_maps/conftest.py delete mode 100644 dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json delete mode 100644 dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json delete mode 100644 dimos/mapping/google_maps/fixtures/get_position.json delete mode 100644 dimos/mapping/google_maps/fixtures/get_position_with_places.json delete mode 100644 dimos/mapping/google_maps/google_maps.py delete mode 100644 dimos/mapping/google_maps/test_google_maps.py delete mode 100644 dimos/mapping/google_maps/types.py delete mode 100644 dimos/mapping/occupancy/conftest.py delete mode 100644 dimos/mapping/occupancy/extrude_occupancy.py delete mode 100644 dimos/mapping/occupancy/gradient.py delete mode 100644 dimos/mapping/occupancy/inflation.py delete mode 100644 dimos/mapping/occupancy/operations.py delete mode 100644 dimos/mapping/occupancy/path_map.py delete mode 100644 dimos/mapping/occupancy/path_mask.py delete mode 100644 dimos/mapping/occupancy/path_resampling.py delete mode 100644 dimos/mapping/occupancy/test_extrude_occupancy.py delete mode 100644 dimos/mapping/occupancy/test_gradient.py delete mode 100644 dimos/mapping/occupancy/test_inflation.py delete mode 100644 dimos/mapping/occupancy/test_operations.py delete mode 100644 dimos/mapping/occupancy/test_path_map.py delete mode 100644 dimos/mapping/occupancy/test_path_mask.py delete mode 100644 dimos/mapping/occupancy/test_path_resampling.py delete mode 100644 dimos/mapping/occupancy/test_visualizations.py delete mode 100644 dimos/mapping/occupancy/visualizations.py delete mode 100644 dimos/mapping/occupancy/visualize_path.py delete mode 100644 dimos/mapping/osm/README.md delete mode 100644 dimos/mapping/osm/__init__.py delete mode 100644 dimos/mapping/osm/current_location_map.py delete mode 100644 dimos/mapping/osm/demo_osm.py delete mode 100644 dimos/mapping/osm/osm.py delete mode 100644 dimos/mapping/osm/query.py delete mode 100644 dimos/mapping/osm/test_osm.py delete mode 100644 dimos/mapping/pointclouds/accumulators/general.py delete mode 100644 dimos/mapping/pointclouds/accumulators/protocol.py delete mode 100644 dimos/mapping/pointclouds/demo.py delete mode 100644 dimos/mapping/pointclouds/occupancy.py delete mode 100644 dimos/mapping/pointclouds/test_occupancy.py delete mode 100644 dimos/mapping/pointclouds/test_occupancy_speed.py delete mode 100644 dimos/mapping/pointclouds/util.py delete mode 100644 dimos/mapping/test_voxels.py delete mode 100644 dimos/mapping/types.py delete mode 100644 dimos/mapping/utils/distance.py delete mode 100644 dimos/mapping/voxels.py delete mode 100644 dimos/memory/embedding.py delete mode 100644 dimos/memory/test_embedding.py delete mode 100644 dimos/memory/timeseries/__init__.py delete mode 100644 dimos/memory/timeseries/base.py delete mode 100644 dimos/memory/timeseries/inmemory.py delete mode 100644 dimos/memory/timeseries/legacy.py delete mode 100644 dimos/memory/timeseries/pickledir.py delete mode 100644 dimos/memory/timeseries/postgres.py delete mode 100644 dimos/memory/timeseries/sqlite.py delete mode 100644 dimos/memory/timeseries/test_base.py delete mode 100644 dimos/memory/timeseries/test_legacy.py delete mode 100644 dimos/models/__init__.py delete mode 100644 dimos/models/base.py delete mode 100644 dimos/models/depth/__init__.py delete mode 100644 dimos/models/depth/metric3d.py delete mode 100644 dimos/models/depth/test_metric3d.py delete mode 100644 dimos/models/embedding/__init__.py delete mode 100644 dimos/models/embedding/base.py delete mode 100644 dimos/models/embedding/clip.py delete mode 100644 dimos/models/embedding/mobileclip.py delete mode 100644 dimos/models/embedding/test_embedding.py delete mode 100644 dimos/models/embedding/treid.py delete mode 100644 dimos/models/qwen/video_query.py delete mode 100644 dimos/models/segmentation/configs/edgetam.yaml delete mode 100644 dimos/models/segmentation/edge_tam.py delete mode 100644 dimos/models/test_base.py delete mode 100644 dimos/models/vl/README.md delete mode 100644 dimos/models/vl/__init__.py delete mode 100644 dimos/models/vl/base.py delete mode 100644 dimos/models/vl/florence.py delete mode 100644 dimos/models/vl/moondream.py delete mode 100644 dimos/models/vl/moondream_hosted.py delete mode 100644 dimos/models/vl/openai.py delete mode 100644 dimos/models/vl/qwen.py delete mode 100644 dimos/models/vl/test_base.py delete mode 100644 dimos/models/vl/test_captioner.py delete mode 100644 dimos/models/vl/test_models.py delete mode 100644 dimos/models/vl/test_vlm.py delete mode 100644 dimos/msgs/__init__.py delete mode 100644 dimos/msgs/foxglove_msgs/Color.py delete mode 100644 dimos/msgs/foxglove_msgs/ImageAnnotations.py delete mode 100644 dimos/msgs/foxglove_msgs/__init__.py delete mode 100644 dimos/msgs/geometry_msgs/Pose.py delete mode 100644 dimos/msgs/geometry_msgs/PoseArray.py delete mode 100644 dimos/msgs/geometry_msgs/PoseStamped.py delete mode 100644 dimos/msgs/geometry_msgs/PoseWithCovariance.py delete mode 100644 dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py delete mode 100644 dimos/msgs/geometry_msgs/Quaternion.py delete mode 100644 dimos/msgs/geometry_msgs/Transform.py delete mode 100644 dimos/msgs/geometry_msgs/Twist.py delete mode 100644 dimos/msgs/geometry_msgs/TwistStamped.py delete mode 100644 dimos/msgs/geometry_msgs/TwistWithCovariance.py delete mode 100644 dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py delete mode 100644 dimos/msgs/geometry_msgs/Vector3.py delete mode 100644 dimos/msgs/geometry_msgs/Wrench.py delete mode 100644 dimos/msgs/geometry_msgs/WrenchStamped.py delete mode 100644 dimos/msgs/geometry_msgs/__init__.py delete mode 100644 dimos/msgs/geometry_msgs/test_Pose.py delete mode 100644 dimos/msgs/geometry_msgs/test_PoseStamped.py delete mode 100644 dimos/msgs/geometry_msgs/test_PoseWithCovariance.py delete mode 100644 dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py delete mode 100644 dimos/msgs/geometry_msgs/test_Quaternion.py delete mode 100644 dimos/msgs/geometry_msgs/test_Transform.py delete mode 100644 dimos/msgs/geometry_msgs/test_Twist.py delete mode 100644 dimos/msgs/geometry_msgs/test_TwistStamped.py delete mode 100644 dimos/msgs/geometry_msgs/test_TwistWithCovariance.py delete mode 100644 dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py delete mode 100644 dimos/msgs/geometry_msgs/test_Vector3.py delete mode 100644 dimos/msgs/geometry_msgs/test_publish.py delete mode 100644 dimos/msgs/helpers.py delete mode 100644 dimos/msgs/nav_msgs/OccupancyGrid.py delete mode 100644 dimos/msgs/nav_msgs/Odometry.py delete mode 100644 dimos/msgs/nav_msgs/Path.py delete mode 100644 dimos/msgs/nav_msgs/__init__.py delete mode 100644 dimos/msgs/nav_msgs/test_OccupancyGrid.py delete mode 100644 dimos/msgs/nav_msgs/test_Odometry.py delete mode 100644 dimos/msgs/nav_msgs/test_Path.py delete mode 100644 dimos/msgs/protocol.py delete mode 100644 dimos/msgs/sensor_msgs/CameraInfo.py delete mode 100644 dimos/msgs/sensor_msgs/Image.py delete mode 100644 dimos/msgs/sensor_msgs/Imu.py delete mode 100644 dimos/msgs/sensor_msgs/JointCommand.py delete mode 100644 dimos/msgs/sensor_msgs/JointState.py delete mode 100644 dimos/msgs/sensor_msgs/Joy.py delete mode 100644 dimos/msgs/sensor_msgs/PointCloud2.py delete mode 100644 dimos/msgs/sensor_msgs/RobotState.py delete mode 100644 dimos/msgs/sensor_msgs/__init__.py delete mode 100644 dimos/msgs/sensor_msgs/image_impls/AbstractImage.py delete mode 100644 dimos/msgs/sensor_msgs/test_CameraInfo.py delete mode 100644 dimos/msgs/sensor_msgs/test_Joy.py delete mode 100644 dimos/msgs/sensor_msgs/test_PointCloud2.py delete mode 100644 dimos/msgs/sensor_msgs/test_image.py delete mode 100644 dimos/msgs/std_msgs/Bool.py delete mode 100644 dimos/msgs/std_msgs/Header.py delete mode 100644 dimos/msgs/std_msgs/Int32.py delete mode 100644 dimos/msgs/std_msgs/Int8.py delete mode 100644 dimos/msgs/std_msgs/UInt32.py delete mode 100644 dimos/msgs/std_msgs/__init__.py delete mode 100644 dimos/msgs/std_msgs/test_header.py delete mode 100644 dimos/msgs/tf2_msgs/TFMessage.py delete mode 100644 dimos/msgs/tf2_msgs/__init__.py delete mode 100644 dimos/msgs/tf2_msgs/test_TFMessage.py delete mode 100644 dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py delete mode 100644 dimos/msgs/trajectory_msgs/JointTrajectory.py delete mode 100644 dimos/msgs/trajectory_msgs/TrajectoryPoint.py delete mode 100644 dimos/msgs/trajectory_msgs/TrajectoryStatus.py delete mode 100644 dimos/msgs/trajectory_msgs/__init__.py delete mode 100644 dimos/msgs/vision_msgs/BoundingBox2DArray.py delete mode 100644 dimos/msgs/vision_msgs/BoundingBox3DArray.py delete mode 100644 dimos/msgs/vision_msgs/Detection2D.py delete mode 100644 dimos/msgs/vision_msgs/Detection2DArray.py delete mode 100644 dimos/msgs/vision_msgs/Detection3D.py delete mode 100644 dimos/msgs/vision_msgs/Detection3DArray.py delete mode 100644 dimos/msgs/vision_msgs/__init__.py delete mode 100644 dimos/navigation/base.py delete mode 100644 dimos/navigation/bbox_navigation.py delete mode 100644 dimos/navigation/demo_ros_navigation.py delete mode 100644 dimos/navigation/frontier_exploration/__init__.py delete mode 100644 dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py delete mode 100644 dimos/navigation/frontier_exploration/utils.py delete mode 100644 dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py delete mode 100644 dimos/navigation/replanning_a_star/controllers.py delete mode 100644 dimos/navigation/replanning_a_star/global_planner.py delete mode 100644 dimos/navigation/replanning_a_star/goal_validator.py delete mode 100644 dimos/navigation/replanning_a_star/local_planner.py delete mode 100644 dimos/navigation/replanning_a_star/min_cost_astar.py delete mode 100644 dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp delete mode 100644 dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi delete mode 100644 dimos/navigation/replanning_a_star/module.py delete mode 100644 dimos/navigation/replanning_a_star/navigation_map.py delete mode 100644 dimos/navigation/replanning_a_star/path_clearance.py delete mode 100644 dimos/navigation/replanning_a_star/path_distancer.py delete mode 100644 dimos/navigation/replanning_a_star/position_tracker.py delete mode 100644 dimos/navigation/replanning_a_star/replan_limiter.py delete mode 100644 dimos/navigation/replanning_a_star/test_goal_validator.py delete mode 100644 dimos/navigation/replanning_a_star/test_min_cost_astar.py delete mode 100644 dimos/navigation/rosnav.py delete mode 100644 dimos/navigation/visual/query.py delete mode 100644 dimos/navigation/visual_servoing/detection_navigation.py delete mode 100644 dimos/navigation/visual_servoing/visual_servoing_2d.py delete mode 100644 dimos/perception/__init__.py delete mode 100644 dimos/perception/common/__init__.py delete mode 100644 dimos/perception/common/utils.py delete mode 100644 dimos/perception/demo_object_scene_registration.py delete mode 100644 dimos/perception/detection/__init__.py delete mode 100644 dimos/perception/detection/conftest.py delete mode 100644 dimos/perception/detection/detectors/__init__.py delete mode 100644 dimos/perception/detection/detectors/config/custom_tracker.yaml delete mode 100644 dimos/perception/detection/detectors/conftest.py delete mode 100644 dimos/perception/detection/detectors/person/test_person_detectors.py delete mode 100644 dimos/perception/detection/detectors/person/yolo.py delete mode 100644 dimos/perception/detection/detectors/test_bbox_detectors.py delete mode 100644 dimos/perception/detection/detectors/types.py delete mode 100644 dimos/perception/detection/detectors/yolo.py delete mode 100644 dimos/perception/detection/detectors/yoloe.py delete mode 100644 dimos/perception/detection/module2D.py delete mode 100644 dimos/perception/detection/module3D.py delete mode 100644 dimos/perception/detection/moduleDB.py delete mode 100644 dimos/perception/detection/objectDB.py delete mode 100644 dimos/perception/detection/person_tracker.py delete mode 100644 dimos/perception/detection/reid/__init__.py delete mode 100644 dimos/perception/detection/reid/embedding_id_system.py delete mode 100644 dimos/perception/detection/reid/module.py delete mode 100644 dimos/perception/detection/reid/test_embedding_id_system.py delete mode 100644 dimos/perception/detection/reid/test_module.py delete mode 100644 dimos/perception/detection/reid/type.py delete mode 100644 dimos/perception/detection/test_moduleDB.py delete mode 100644 dimos/perception/detection/type/__init__.py delete mode 100644 dimos/perception/detection/type/detection2d/__init__.py delete mode 100644 dimos/perception/detection/type/detection2d/base.py delete mode 100644 dimos/perception/detection/type/detection2d/bbox.py delete mode 100644 dimos/perception/detection/type/detection2d/imageDetections2D.py delete mode 100644 dimos/perception/detection/type/detection2d/person.py delete mode 100644 dimos/perception/detection/type/detection2d/point.py delete mode 100644 dimos/perception/detection/type/detection2d/seg.py delete mode 100644 dimos/perception/detection/type/detection2d/test_bbox.py delete mode 100644 dimos/perception/detection/type/detection2d/test_imageDetections2D.py delete mode 100644 dimos/perception/detection/type/detection2d/test_person.py delete mode 100644 dimos/perception/detection/type/detection3d/__init__.py delete mode 100644 dimos/perception/detection/type/detection3d/base.py delete mode 100644 dimos/perception/detection/type/detection3d/bbox.py delete mode 100644 dimos/perception/detection/type/detection3d/imageDetections3DPC.py delete mode 100644 dimos/perception/detection/type/detection3d/object.py delete mode 100644 dimos/perception/detection/type/detection3d/pointcloud.py delete mode 100644 dimos/perception/detection/type/detection3d/pointcloud_filters.py delete mode 100644 dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py delete mode 100644 dimos/perception/detection/type/detection3d/test_pointcloud.py delete mode 100644 dimos/perception/detection/type/imageDetections.py delete mode 100644 dimos/perception/detection/type/test_detection3d.py delete mode 100644 dimos/perception/detection/type/test_object3d.py delete mode 100644 dimos/perception/detection/type/utils.py delete mode 100644 dimos/perception/experimental/__init__.py delete mode 100644 dimos/perception/experimental/temporal_memory/README.md delete mode 100644 dimos/perception/experimental/temporal_memory/__init__.py delete mode 100644 dimos/perception/experimental/temporal_memory/clip_filter.py delete mode 100644 dimos/perception/experimental/temporal_memory/entity_graph_db.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_memory.md delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_memory.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_memory_deploy.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_memory_example.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/__init__.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/graph_utils.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/helpers.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/parsers.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/prompts.py delete mode 100644 dimos/perception/experimental/temporal_memory/temporal_utils/state.py delete mode 100644 dimos/perception/experimental/temporal_memory/test_temporal_memory_module.py delete mode 100644 dimos/perception/object_scene_registration.py delete mode 100644 dimos/perception/object_tracker.py delete mode 100644 dimos/perception/object_tracker_2d.py delete mode 100644 dimos/perception/object_tracker_3d.py delete mode 100644 dimos/perception/spatial_perception.py delete mode 100644 dimos/perception/test_spatial_memory.py delete mode 100644 dimos/perception/test_spatial_memory_module.py delete mode 100644 dimos/protocol/__init__.py delete mode 100644 dimos/protocol/encode/__init__.py delete mode 100644 dimos/protocol/mcp/README.md delete mode 100644 dimos/protocol/mcp/__init__.py delete mode 100644 dimos/protocol/mcp/__main__.py delete mode 100644 dimos/protocol/mcp/bridge.py delete mode 100644 dimos/protocol/mcp/mcp.py delete mode 100644 dimos/protocol/mcp/test_mcp_module.py delete mode 100644 dimos/protocol/pubsub/__init__.py delete mode 100644 dimos/protocol/pubsub/benchmark/test_benchmark.py delete mode 100644 dimos/protocol/pubsub/benchmark/testdata.py delete mode 100644 dimos/protocol/pubsub/benchmark/type.py delete mode 100644 dimos/protocol/pubsub/bridge.py delete mode 100644 dimos/protocol/pubsub/encoders.py delete mode 100644 dimos/protocol/pubsub/impl/__init__.py delete mode 100644 dimos/protocol/pubsub/impl/ddspubsub.py delete mode 100644 dimos/protocol/pubsub/impl/jpeg_shm.py delete mode 100644 dimos/protocol/pubsub/impl/lcmpubsub.py delete mode 100644 dimos/protocol/pubsub/impl/memory.py delete mode 100644 dimos/protocol/pubsub/impl/redispubsub.py delete mode 100644 dimos/protocol/pubsub/impl/rospubsub.py delete mode 100644 dimos/protocol/pubsub/impl/rospubsub_conversion.py delete mode 100644 dimos/protocol/pubsub/impl/shmpubsub.py delete mode 100644 dimos/protocol/pubsub/impl/test_lcmpubsub.py delete mode 100644 dimos/protocol/pubsub/impl/test_rospubsub.py delete mode 100644 dimos/protocol/pubsub/patterns.py delete mode 100644 dimos/protocol/pubsub/shm/ipc_factory.py delete mode 100644 dimos/protocol/pubsub/spec.py delete mode 100644 dimos/protocol/pubsub/test_encoder.py delete mode 100644 dimos/protocol/pubsub/test_pattern_sub.py delete mode 100644 dimos/protocol/pubsub/test_patterns.py delete mode 100644 dimos/protocol/pubsub/test_spec.py delete mode 100644 dimos/protocol/rpc/__init__.py delete mode 100644 dimos/protocol/rpc/pubsubrpc.py delete mode 100644 dimos/protocol/rpc/redisrpc.py delete mode 100644 dimos/protocol/rpc/rpc_utils.py delete mode 100644 dimos/protocol/rpc/spec.py delete mode 100644 dimos/protocol/rpc/test_lcmrpc.py delete mode 100644 dimos/protocol/rpc/test_rpc_utils.py delete mode 100644 dimos/protocol/rpc/test_spec.py delete mode 100644 dimos/protocol/service/__init__.py delete mode 100644 dimos/protocol/service/ddsservice.py delete mode 100644 dimos/protocol/service/lcmservice.py delete mode 100644 dimos/protocol/service/spec.py delete mode 100644 dimos/protocol/service/system_configurator.py delete mode 100644 dimos/protocol/service/test_lcmservice.py delete mode 100644 dimos/protocol/service/test_spec.py delete mode 100644 dimos/protocol/service/test_system_configurator.py delete mode 100644 dimos/protocol/tf/__init__.py delete mode 100644 dimos/protocol/tf/test_tf.py delete mode 100644 dimos/protocol/tf/tf.py delete mode 100644 dimos/protocol/tf/tflcmcpp.py delete mode 100644 dimos/robot/__init__.py delete mode 100644 dimos/robot/all_blueprints.py delete mode 100644 dimos/robot/cli/dimos.py delete mode 100644 dimos/robot/cli/topic.py delete mode 100644 dimos/robot/drone/README.md delete mode 100644 dimos/robot/drone/__init__.py delete mode 100644 dimos/robot/drone/camera_module.py delete mode 100644 dimos/robot/drone/connection_module.py delete mode 100644 dimos/robot/drone/dji_video_stream.py delete mode 100644 dimos/robot/drone/drone.py delete mode 100644 dimos/robot/drone/drone_tracking_module.py delete mode 100644 dimos/robot/drone/drone_visual_servoing_controller.py delete mode 100644 dimos/robot/drone/mavlink_connection.py delete mode 100644 dimos/robot/drone/test_drone.py delete mode 100644 dimos/robot/foxglove_bridge.py delete mode 100644 dimos/robot/get_all_blueprints.py delete mode 100644 dimos/robot/manipulators/__init__.py delete mode 100644 dimos/robot/manipulators/piper/__init__.py delete mode 100644 dimos/robot/manipulators/piper/blueprints.py delete mode 100644 dimos/robot/manipulators/xarm/__init__.py delete mode 100644 dimos/robot/manipulators/xarm/blueprints.py delete mode 100644 dimos/robot/position_stream.py delete mode 100644 dimos/robot/robot.py delete mode 100644 dimos/robot/ros_command_queue.py delete mode 100644 dimos/robot/test_all_blueprints.py delete mode 100644 dimos/robot/test_all_blueprints_generation.py delete mode 100644 dimos/robot/unitree/__init__.py delete mode 100644 dimos/robot/unitree/b1/README.md delete mode 100644 dimos/robot/unitree/b1/__init__.py delete mode 100644 dimos/robot/unitree/b1/b1_command.py delete mode 100644 dimos/robot/unitree/b1/connection.py delete mode 100644 dimos/robot/unitree/b1/joystick_module.py delete mode 100644 dimos/robot/unitree/b1/joystick_server_udp.cpp delete mode 100644 dimos/robot/unitree/b1/test_connection.py delete mode 100644 dimos/robot/unitree/b1/unitree_b1.py delete mode 100644 dimos/robot/unitree/demo_error_on_name_conflicts.py delete mode 100644 dimos/robot/unitree/depth_module.py delete mode 100644 dimos/robot/unitree/g1/blueprints/__init__.py delete mode 100644 dimos/robot/unitree/g1/blueprints/agentic/__init__.py delete mode 100644 dimos/robot/unitree/g1/blueprints/agentic/_agentic_skills.py delete mode 100644 dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic.py delete mode 100644 dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic_sim.py delete mode 100644 dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_full.py delete mode 100644 dimos/robot/unitree/g1/blueprints/basic/__init__.py delete mode 100644 dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic.py delete mode 100644 dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic_sim.py delete mode 100644 dimos/robot/unitree/g1/blueprints/basic/unitree_g1_joystick.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/__init__.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/_perception_and_memory.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_detection.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_shm.py delete mode 100644 dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_sim.py delete mode 100644 dimos/robot/unitree/g1/blueprints/primitive/__init__.py delete mode 100644 dimos/robot/unitree/g1/blueprints/primitive/uintree_g1_primitive_no_nav.py delete mode 100644 dimos/robot/unitree/g1/connection.py delete mode 100644 dimos/robot/unitree/g1/sim.py delete mode 100644 dimos/robot/unitree/g1/skill_container.py delete mode 100644 dimos/robot/unitree/go2/blueprints/__init__.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/__init__.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/_common_agentic.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_huggingface.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_mcp.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_ollama.py delete mode 100644 dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_temporal_memory.py delete mode 100644 dimos/robot/unitree/go2/blueprints/basic/__init__.py delete mode 100644 dimos/robot/unitree/go2/blueprints/basic/unitree_go2_basic.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/__init__.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/_with_jpeg.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/unitree_go2.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/unitree_go2_ros.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/unitree_go2_spatial.py delete mode 100644 dimos/robot/unitree/go2/blueprints/smart/unitree_go2_vlm_stream_test.py delete mode 100644 dimos/robot/unitree/go2/connection.py delete mode 100644 dimos/robot/unitree/go2/go2.urdf delete mode 100644 dimos/robot/unitree/keyboard_teleop.py delete mode 100644 dimos/robot/unitree/modular/detect.py delete mode 100644 dimos/robot/unitree/mujoco_connection.py delete mode 100644 dimos/robot/unitree/params/front_camera_720.yaml delete mode 100644 dimos/robot/unitree/params/sim_camera.yaml delete mode 100644 dimos/robot/unitree/rosnav.py delete mode 100644 dimos/robot/unitree/testing/__init__.py delete mode 100644 dimos/robot/unitree/testing/helpers.py delete mode 100644 dimos/robot/unitree/testing/mock.py delete mode 100644 dimos/robot/unitree/testing/test_actors.py delete mode 100644 dimos/robot/unitree/testing/test_tooling.py delete mode 100644 dimos/robot/unitree/type/__init__.py delete mode 100644 dimos/robot/unitree/type/lidar.py delete mode 100644 dimos/robot/unitree/type/lowstate.py delete mode 100644 dimos/robot/unitree/type/map.py delete mode 100644 dimos/robot/unitree/type/odometry.py delete mode 100644 dimos/robot/unitree/type/test_lidar.py delete mode 100644 dimos/robot/unitree/type/test_odometry.py delete mode 100644 dimos/robot/unitree/type/test_timeseries.py delete mode 100644 dimos/robot/unitree/type/timeseries.py delete mode 100644 dimos/robot/unitree/type/vector.py delete mode 100644 dimos/robot/unitree/unitree_skill_container.py delete mode 100644 dimos/robot/unitree_webrtc/README.md delete mode 100644 dimos/robot/unitree_webrtc/__init__.py delete mode 100644 dimos/robot/unitree_webrtc/type/__init__.py delete mode 100644 dimos/robot/unitree_webrtc/type/lidar.py delete mode 100644 dimos/robot/unitree_webrtc/type/lowstate.py delete mode 100644 dimos/robot/unitree_webrtc/type/map.py delete mode 100644 dimos/robot/unitree_webrtc/type/odometry.py delete mode 100644 dimos/robot/unitree_webrtc/type/timeseries.py delete mode 100644 dimos/robot/unitree_webrtc/type/vector.py delete mode 100644 dimos/robot/utils/README.md delete mode 100644 dimos/robot/utils/robot_debugger.py delete mode 100644 dimos/rxpy_backpressure/LICENSE.txt delete mode 100644 dimos/rxpy_backpressure/__init__.py delete mode 100644 dimos/rxpy_backpressure/backpressure.py delete mode 100644 dimos/rxpy_backpressure/drop.py delete mode 100644 dimos/rxpy_backpressure/function_runner.py delete mode 100644 dimos/rxpy_backpressure/latest.py delete mode 100644 dimos/rxpy_backpressure/locks.py delete mode 100644 dimos/rxpy_backpressure/observer.py delete mode 100644 dimos/simulation/__init__.py delete mode 100644 dimos/simulation/base/__init__.py delete mode 100644 dimos/simulation/base/simulator_base.py delete mode 100644 dimos/simulation/base/stream_base.py delete mode 100644 dimos/simulation/engines/__init__.py delete mode 100644 dimos/simulation/engines/base.py delete mode 100644 dimos/simulation/engines/mujoco_engine.py delete mode 100644 dimos/simulation/genesis/__init__.py delete mode 100644 dimos/simulation/genesis/simulator.py delete mode 100644 dimos/simulation/genesis/stream.py delete mode 100644 dimos/simulation/isaac/__init__.py delete mode 100644 dimos/simulation/isaac/simulator.py delete mode 100644 dimos/simulation/isaac/stream.py delete mode 100644 dimos/simulation/manipulators/__init__.py delete mode 100644 dimos/simulation/manipulators/sim_manip_interface.py delete mode 100644 dimos/simulation/manipulators/sim_module.py delete mode 100644 dimos/simulation/manipulators/test_sim_module.py delete mode 100644 dimos/simulation/mujoco/constants.py delete mode 100644 dimos/simulation/mujoco/depth_camera.py delete mode 100644 dimos/simulation/mujoco/input_controller.py delete mode 100644 dimos/simulation/mujoco/model.py delete mode 100755 dimos/simulation/mujoco/mujoco_process.py delete mode 100644 dimos/simulation/mujoco/person_on_track.py delete mode 100644 dimos/simulation/mujoco/policy.py delete mode 100644 dimos/simulation/mujoco/shared_memory.py delete mode 100644 dimos/simulation/sim_blueprints.py delete mode 100644 dimos/simulation/utils/xml_parser.py delete mode 100644 dimos/skills/__init__.py delete mode 100644 dimos/skills/kill_skill.py delete mode 100644 dimos/skills/manipulation/abstract_manipulation_skill.py delete mode 100644 dimos/skills/manipulation/force_constraint_skill.py delete mode 100644 dimos/skills/manipulation/manipulate_skill.py delete mode 100644 dimos/skills/manipulation/pick_and_place.py delete mode 100644 dimos/skills/manipulation/rotation_constraint_skill.py delete mode 100644 dimos/skills/manipulation/translation_constraint_skill.py delete mode 100644 dimos/skills/rest/__init__.py delete mode 100644 dimos/skills/rest/rest.py delete mode 100644 dimos/skills/skills.py delete mode 100644 dimos/skills/speak.py delete mode 100644 dimos/skills/unitree/__init__.py delete mode 100644 dimos/skills/unitree/unitree_speak.py delete mode 100644 dimos/skills/visual_navigation_skills.py delete mode 100644 dimos/spec/__init__.py delete mode 100644 dimos/spec/control.py delete mode 100644 dimos/spec/mapping.py delete mode 100644 dimos/spec/nav.py delete mode 100644 dimos/spec/perception.py delete mode 100644 dimos/spec/test_utils.py delete mode 100644 dimos/spec/utils.py delete mode 100644 dimos/stream/__init__.py delete mode 100644 dimos/stream/audio/__init__.py delete mode 100644 dimos/stream/audio/base.py delete mode 100644 dimos/stream/audio/node_key_recorder.py delete mode 100644 dimos/stream/audio/node_microphone.py delete mode 100644 dimos/stream/audio/node_normalizer.py delete mode 100644 dimos/stream/audio/node_output.py delete mode 100644 dimos/stream/audio/node_simulated.py delete mode 100644 dimos/stream/audio/node_volume_monitor.py delete mode 100644 dimos/stream/audio/pipelines.py delete mode 100644 dimos/stream/audio/stt/node_whisper.py delete mode 100644 dimos/stream/audio/text/base.py delete mode 100644 dimos/stream/audio/text/node_stdout.py delete mode 100644 dimos/stream/audio/tts/node_openai.py delete mode 100644 dimos/stream/audio/tts/node_pytts.py delete mode 100644 dimos/stream/audio/utils.py delete mode 100644 dimos/stream/audio/volume.py delete mode 100644 dimos/stream/data_provider.py delete mode 100644 dimos/stream/frame_processor.py delete mode 100644 dimos/stream/ros_video_provider.py delete mode 100644 dimos/stream/rtsp_video_provider.py delete mode 100644 dimos/stream/stream_merger.py delete mode 100644 dimos/stream/video_operators.py delete mode 100644 dimos/stream/video_provider.py delete mode 100644 dimos/stream/video_providers/__init__.py delete mode 100644 dimos/teleop/README.md delete mode 100644 dimos/teleop/__init__.py delete mode 100644 dimos/teleop/keyboard/__init__.py delete mode 100644 dimos/teleop/keyboard/keyboard_teleop_module.py delete mode 100644 dimos/teleop/phone/README.md delete mode 100644 dimos/teleop/phone/__init__.py delete mode 100644 dimos/teleop/phone/blueprints.py delete mode 100644 dimos/teleop/phone/phone_extensions.py delete mode 100644 dimos/teleop/phone/phone_teleop_module.py delete mode 100644 dimos/teleop/phone/web/static/index.html delete mode 100755 dimos/teleop/phone/web/teleop_server.ts delete mode 100644 dimos/teleop/quest/__init__.py delete mode 100644 dimos/teleop/quest/blueprints.py delete mode 100644 dimos/teleop/quest/quest_extensions.py delete mode 100644 dimos/teleop/quest/quest_teleop_module.py delete mode 100644 dimos/teleop/quest/quest_types.py delete mode 100644 dimos/teleop/quest/web/README.md delete mode 100644 dimos/teleop/quest/web/static/index.html delete mode 100755 dimos/teleop/quest/web/teleop_server.ts delete mode 100644 dimos/teleop/utils/__init__.py delete mode 100644 dimos/teleop/utils/teleop_transforms.py delete mode 100644 dimos/teleop/utils/teleop_visualization.py delete mode 100644 dimos/types/constants.py delete mode 100644 dimos/types/manipulation.py delete mode 100644 dimos/types/robot_capabilities.py delete mode 100644 dimos/types/robot_location.py delete mode 100644 dimos/types/ros_polyfill.py delete mode 100644 dimos/types/sample.py delete mode 100644 dimos/types/test_timestamped.py delete mode 100644 dimos/types/test_vector.py delete mode 100644 dimos/types/test_weaklist.py delete mode 100644 dimos/types/timestamped.py delete mode 100644 dimos/types/vector.py delete mode 100644 dimos/types/weaklist.py delete mode 100644 dimos/utils/actor_registry.py delete mode 100644 dimos/utils/cli/__init__.py delete mode 100644 dimos/utils/cli/agentspy/agentspy.py delete mode 100755 dimos/utils/cli/agentspy/demo_agentspy.py delete mode 100644 dimos/utils/cli/dimos.tcss delete mode 100644 dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py delete mode 100644 dimos/utils/cli/human/humancli.py delete mode 100644 dimos/utils/cli/human/humanclianim.py delete mode 100755 dimos/utils/cli/lcmspy/lcmspy.py delete mode 100644 dimos/utils/cli/lcmspy/run_lcmspy.py delete mode 100644 dimos/utils/cli/lcmspy/test_lcmspy.py delete mode 100644 dimos/utils/cli/plot.py delete mode 100644 dimos/utils/cli/theme.py delete mode 100644 dimos/utils/data.py delete mode 100644 dimos/utils/decorators/__init__.py delete mode 100644 dimos/utils/decorators/accumulators.py delete mode 100644 dimos/utils/decorators/decorators.py delete mode 100644 dimos/utils/decorators/test_decorators.py delete mode 100644 dimos/utils/demo_image_encoding.py delete mode 100644 dimos/utils/docs/doclinks.md delete mode 100644 dimos/utils/docs/doclinks.py delete mode 100644 dimos/utils/docs/test_doclinks.py delete mode 100644 dimos/utils/extract_frames.py delete mode 100644 dimos/utils/fast_image_generator.py delete mode 100644 dimos/utils/generic.py delete mode 100644 dimos/utils/gpu_utils.py delete mode 100644 dimos/utils/llm_utils.py delete mode 100644 dimos/utils/logging_config.py delete mode 100644 dimos/utils/metrics.py delete mode 100644 dimos/utils/monitoring.py delete mode 100644 dimos/utils/path_utils.py delete mode 100644 dimos/utils/reactive.py delete mode 100644 dimos/utils/sequential_ids.py delete mode 100644 dimos/utils/simple_controller.py delete mode 100644 dimos/utils/test_data.py delete mode 100644 dimos/utils/test_foxglove_bridge.py delete mode 100644 dimos/utils/test_generic.py delete mode 100644 dimos/utils/test_llm_utils.py delete mode 100644 dimos/utils/test_reactive.py delete mode 100644 dimos/utils/test_transform_utils.py delete mode 100644 dimos/utils/test_trigonometry.py delete mode 100644 dimos/utils/testing/__init__.py delete mode 100644 dimos/utils/testing/moment.py delete mode 100644 dimos/utils/testing/replay.py delete mode 100644 dimos/utils/testing/test_moment.py delete mode 100644 dimos/utils/testing/test_replay.py delete mode 100644 dimos/utils/threadpool.py delete mode 100644 dimos/utils/transform_utils.py delete mode 100644 dimos/utils/trigonometry.py delete mode 100644 dimos/utils/urdf.py delete mode 100644 dimos/visualization/rerun/bridge.py delete mode 100644 dimos/web/__init__.py delete mode 100644 dimos/web/command-center-extension/.gitignore delete mode 100644 dimos/web/command-center-extension/.prettierrc.yaml delete mode 100644 dimos/web/command-center-extension/CHANGELOG.md delete mode 100644 dimos/web/command-center-extension/eslint.config.js delete mode 100644 dimos/web/command-center-extension/index.html delete mode 100644 dimos/web/command-center-extension/package-lock.json delete mode 100644 dimos/web/command-center-extension/package.json delete mode 100644 dimos/web/command-center-extension/src/App.tsx delete mode 100644 dimos/web/command-center-extension/src/Button.tsx delete mode 100644 dimos/web/command-center-extension/src/Connection.ts delete mode 100644 dimos/web/command-center-extension/src/ExplorePanel.tsx delete mode 100644 dimos/web/command-center-extension/src/GpsButton.tsx delete mode 100644 dimos/web/command-center-extension/src/KeyboardControlPanel.tsx delete mode 100644 dimos/web/command-center-extension/src/components/CostmapLayer.tsx delete mode 100644 dimos/web/command-center-extension/src/components/GridLayer.tsx delete mode 100644 dimos/web/command-center-extension/src/components/LeafletMap.tsx delete mode 100644 dimos/web/command-center-extension/src/components/PathLayer.tsx delete mode 100644 dimos/web/command-center-extension/src/components/VectorLayer.tsx delete mode 100644 dimos/web/command-center-extension/src/components/VisualizerComponent.tsx delete mode 100644 dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx delete mode 100644 dimos/web/command-center-extension/src/index.ts delete mode 100644 dimos/web/command-center-extension/src/init.ts delete mode 100644 dimos/web/command-center-extension/src/optimizedCostmap.ts delete mode 100644 dimos/web/command-center-extension/src/standalone.tsx delete mode 100644 dimos/web/command-center-extension/src/types.ts delete mode 100644 dimos/web/command-center-extension/tsconfig.json delete mode 100644 dimos/web/command-center-extension/vite.config.ts delete mode 100644 dimos/web/dimos_interface/.gitignore delete mode 100644 dimos/web/dimos_interface/__init__.py delete mode 100644 dimos/web/dimos_interface/api/README.md delete mode 100644 dimos/web/dimos_interface/api/__init__.py delete mode 100644 dimos/web/dimos_interface/api/requirements.txt delete mode 100644 dimos/web/dimos_interface/api/server.py delete mode 100644 dimos/web/dimos_interface/api/templates/index_fastapi.html delete mode 100644 dimos/web/dimos_interface/index.html delete mode 100644 dimos/web/dimos_interface/package.json delete mode 100644 dimos/web/dimos_interface/postcss.config.js delete mode 100644 dimos/web/dimos_interface/public/icon.png delete mode 100644 dimos/web/dimos_interface/src/App.svelte delete mode 100644 dimos/web/dimos_interface/src/app.css delete mode 100644 dimos/web/dimos_interface/src/components/History.svelte delete mode 100644 dimos/web/dimos_interface/src/components/Input.svelte delete mode 100644 dimos/web/dimos_interface/src/components/Ps1.svelte delete mode 100644 dimos/web/dimos_interface/src/components/StreamViewer.svelte delete mode 100644 dimos/web/dimos_interface/src/components/VoiceButton.svelte delete mode 100644 dimos/web/dimos_interface/src/interfaces/command.ts delete mode 100644 dimos/web/dimos_interface/src/interfaces/theme.ts delete mode 100644 dimos/web/dimos_interface/src/main.ts delete mode 100644 dimos/web/dimos_interface/src/stores/history.ts delete mode 100644 dimos/web/dimos_interface/src/stores/stream.ts delete mode 100644 dimos/web/dimos_interface/src/stores/theme.ts delete mode 100644 dimos/web/dimos_interface/src/utils/commands.ts delete mode 100644 dimos/web/dimos_interface/src/utils/simulation.ts delete mode 100644 dimos/web/dimos_interface/src/utils/tracking.ts delete mode 100644 dimos/web/dimos_interface/src/vite-env.d.ts delete mode 100644 dimos/web/dimos_interface/svelte.config.js delete mode 100644 dimos/web/dimos_interface/tailwind.config.js delete mode 100644 dimos/web/dimos_interface/themes.json delete mode 100644 dimos/web/dimos_interface/tsconfig.json delete mode 100644 dimos/web/dimos_interface/tsconfig.node.json delete mode 100644 dimos/web/dimos_interface/vite.config.ts delete mode 100644 dimos/web/edge_io.py delete mode 100644 dimos/web/fastapi_server.py delete mode 100644 dimos/web/flask_server.py delete mode 100644 dimos/web/robot_web_interface.py delete mode 100644 dimos/web/templates/index_fastapi.html delete mode 100644 dimos/web/templates/index_flask.html delete mode 100644 dimos/web/templates/rerun_dashboard.html delete mode 100644 dimos/web/websocket_vis/README.md delete mode 100644 dimos/web/websocket_vis/costmap_viz.py delete mode 100644 dimos/web/websocket_vis/optimized_costmap.py delete mode 100644 dimos/web/websocket_vis/path_history.py delete mode 100644 dimos/web/websocket_vis/websocket_vis_module.py delete mode 100644 docker/dev/Dockerfile delete mode 100755 docker/dev/bash.sh delete mode 100644 docker/dev/docker-compose-cuda.yaml delete mode 100644 docker/dev/docker-compose.yaml delete mode 100644 docker/dev/entrypoint.sh delete mode 100644 docker/dev/tmux.conf delete mode 100644 docker/navigation/.env.hardware delete mode 100644 docker/navigation/.gitignore delete mode 100644 docker/navigation/Dockerfile delete mode 100644 docker/navigation/README.md delete mode 100755 docker/navigation/build.sh delete mode 100644 docker/navigation/docker-compose.dev.yml delete mode 100644 docker/navigation/docker-compose.yml delete mode 100755 docker/navigation/foxglove_utility/goal_autonomy_relay.py delete mode 100644 docker/navigation/foxglove_utility/twist_relay.py delete mode 100755 docker/navigation/ros_launch_wrapper.py delete mode 100755 docker/navigation/run_both.sh delete mode 100755 docker/navigation/start.sh delete mode 100644 docker/python/Dockerfile delete mode 100644 docker/python/module-install.sh delete mode 100644 docker/ros/Dockerfile delete mode 100644 docker/ros/install-nix.sh delete mode 100644 docs/agents/docs/assets/codeblocks_example.svg delete mode 100644 docs/agents/docs/assets/pikchr_basic.svg delete mode 100644 docs/agents/docs/assets/pikchr_branch.svg delete mode 100644 docs/agents/docs/assets/pikchr_explicit.svg delete mode 100644 docs/agents/docs/assets/pikchr_labels.svg delete mode 100644 docs/agents/docs/assets/pikchr_sizing.svg delete mode 100644 docs/agents/docs/codeblocks.md delete mode 100644 docs/agents/docs/doclinks.md delete mode 100644 docs/agents/docs/index.md delete mode 100644 docs/agents/index.md delete mode 100644 docs/capabilities/agents/readme.md delete mode 100644 docs/capabilities/manipulation/readme.md delete mode 100644 docs/capabilities/navigation/native/assets/1-lidar.png delete mode 100644 docs/capabilities/navigation/native/assets/2-globalmap.png delete mode 100644 docs/capabilities/navigation/native/assets/3-globalcostmap.png delete mode 100644 docs/capabilities/navigation/native/assets/4-navcostmap.png delete mode 100644 docs/capabilities/navigation/native/assets/5-all.png delete mode 100644 docs/capabilities/navigation/native/assets/go2_blueprint.svg delete mode 100644 docs/capabilities/navigation/native/assets/go2nav_dataflow.svg delete mode 100644 docs/capabilities/navigation/native/assets/noros_nav.gif delete mode 100644 docs/capabilities/navigation/native/index.md delete mode 100644 docs/capabilities/navigation/readme.md delete mode 100644 docs/capabilities/perception/readme.md delete mode 100644 docs/development/README.md delete mode 100644 docs/development/adding_a_custom_arm.md delete mode 100644 docs/development/assets/docker-hierarchy.svg delete mode 100644 docs/development/assets/get_data_flow.svg delete mode 100644 docs/development/depth_camera_integration.md delete mode 100644 docs/development/dimos_run.md delete mode 100644 docs/development/docker.md delete mode 100644 docs/development/grid_testing.md delete mode 100644 docs/development/large_file_management.md delete mode 100644 docs/development/testing.md delete mode 100644 docs/development/writing_docs.md delete mode 100644 docs/hardware/integration_guide.md delete mode 100644 docs/installation/nix.md delete mode 100644 docs/installation/osx.md delete mode 100644 docs/installation/ubuntu.md delete mode 100644 docs/platforms/quadruped/go2/index.md delete mode 100644 docs/todo.md delete mode 100644 docs/usage/README.md delete mode 100644 docs/usage/assets/abstraction_layers.svg delete mode 100644 docs/usage/assets/camera_module.svg delete mode 100644 docs/usage/assets/go2_agentic.svg delete mode 100644 docs/usage/assets/go2_nav.svg delete mode 100644 docs/usage/assets/lcmspy.png delete mode 100644 docs/usage/assets/pubsub_benchmark.png delete mode 100644 docs/usage/assets/transforms.png delete mode 100644 docs/usage/assets/transforms_chain.svg delete mode 100644 docs/usage/assets/transforms_modules.svg delete mode 100644 docs/usage/assets/transforms_tree.svg delete mode 100644 docs/usage/blueprints.md delete mode 100644 docs/usage/configuration.md delete mode 100644 docs/usage/data_streams/README.md delete mode 100644 docs/usage/data_streams/advanced_streams.md delete mode 100644 docs/usage/data_streams/assets/alignment_flow.svg delete mode 100644 docs/usage/data_streams/assets/alignment_overview.svg delete mode 100644 docs/usage/data_streams/assets/alignment_timeline.png delete mode 100644 docs/usage/data_streams/assets/alignment_timeline2.png delete mode 100644 docs/usage/data_streams/assets/alignment_timeline3.png delete mode 100644 docs/usage/data_streams/assets/backpressure.svg delete mode 100644 docs/usage/data_streams/assets/backpressure_solution.svg delete mode 100644 docs/usage/data_streams/assets/frame_mosaic.jpg delete mode 100644 docs/usage/data_streams/assets/frame_mosaic2.jpg delete mode 100644 docs/usage/data_streams/assets/getter_hot_cold.svg delete mode 100644 docs/usage/data_streams/assets/observable_flow.svg delete mode 100644 docs/usage/data_streams/assets/sharpness_graph.svg delete mode 100644 docs/usage/data_streams/assets/sharpness_graph2.svg delete mode 100644 docs/usage/data_streams/quality_filter.md delete mode 100644 docs/usage/data_streams/reactivex.md delete mode 100644 docs/usage/data_streams/storage_replay.md delete mode 100644 docs/usage/data_streams/temporal_alignment.md delete mode 100644 docs/usage/lcm.md delete mode 100644 docs/usage/modules.md delete mode 100644 docs/usage/native_modules.md delete mode 100644 docs/usage/sensor_streams/README.md delete mode 100644 docs/usage/sensor_streams/advanced_streams.md delete mode 100644 docs/usage/sensor_streams/assets/alignment_flow.svg delete mode 100644 docs/usage/sensor_streams/assets/alignment_overview.svg delete mode 100644 docs/usage/sensor_streams/assets/alignment_timeline.png delete mode 100644 docs/usage/sensor_streams/assets/alignment_timeline2.png delete mode 100644 docs/usage/sensor_streams/assets/alignment_timeline3.png delete mode 100644 docs/usage/sensor_streams/assets/backpressure.svg delete mode 100644 docs/usage/sensor_streams/assets/backpressure_solution.svg delete mode 100644 docs/usage/sensor_streams/assets/frame_mosaic.jpg delete mode 100644 docs/usage/sensor_streams/assets/frame_mosaic2.jpg delete mode 100644 docs/usage/sensor_streams/assets/getter_hot_cold.svg delete mode 100644 docs/usage/sensor_streams/assets/observable_flow.svg delete mode 100644 docs/usage/sensor_streams/assets/sharpness_graph.svg delete mode 100644 docs/usage/sensor_streams/assets/sharpness_graph2.svg delete mode 100644 docs/usage/sensor_streams/quality_filter.md delete mode 100644 docs/usage/sensor_streams/reactivex.md delete mode 100644 docs/usage/sensor_streams/storage_replay.md delete mode 100644 docs/usage/sensor_streams/temporal_alignment.md delete mode 100644 docs/usage/transforms.md delete mode 100644 docs/usage/transports.md delete mode 100644 docs/usage/transports/dds.md delete mode 100644 docs/usage/transports/index.md delete mode 100644 docs/usage/visualization.md delete mode 100644 examples/camera_grayscale.py delete mode 100644 examples/language-interop/README.md delete mode 100644 examples/language-interop/assets/lcmspy.png delete mode 100644 examples/language-interop/cpp/.gitignore delete mode 100644 examples/language-interop/cpp/CMakeLists.txt delete mode 100644 examples/language-interop/cpp/README.md delete mode 100644 examples/language-interop/cpp/main.cpp delete mode 100644 examples/language-interop/lua/.gitignore delete mode 100644 examples/language-interop/lua/README.md delete mode 100644 examples/language-interop/lua/main.lua delete mode 100755 examples/language-interop/lua/setup.sh delete mode 100644 examples/language-interop/ts/README.md delete mode 100644 examples/language-interop/ts/deno.json delete mode 100644 examples/language-interop/ts/deno.lock delete mode 100644 examples/language-interop/ts/main.ts delete mode 100644 examples/language-interop/ts/web/index.html delete mode 100644 examples/language-interop/ts/web/server.ts delete mode 100644 examples/rpc_calls.py delete mode 100644 examples/simplerobot/README.md delete mode 100644 examples/simplerobot/simplerobot.py delete mode 100644 examples/simplerobot/vis.py delete mode 100644 flake.lock delete mode 100644 flake.nix delete mode 100644 onnx/metric3d_vit_small.onnx delete mode 100644 pyproject.toml delete mode 100644 setup.py delete mode 100644 uv.lock diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 3196c823e5..0000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "dimos-dev", - "image": "ghcr.io/dimensionalos/dev:dev", - "customizations": { - "vscode": { - "extensions": [ - "charliermarsh.ruff", - "ms-python.vscode-pylance" - ] - } - }, - "containerEnv": { - "PYTHONPATH": "${localEnv:PYTHONPATH}:/workspaces/dimos", - "DISPLAY": "${localEnv:DISPLAY}", - "WAYLAND_DISPLAY": "${localEnv:WAYLAND_DISPLAY}", - "XDG_RUNTIME_DIR": "${localEnv:XDG_RUNTIME_DIR}" - }, - "mounts": [ - "source=/tmp/.X11-unix,target=/tmp/.X11-unix,type=bind", - "source=${localEnv:XDG_RUNTIME_DIR},target=${localEnv:XDG_RUNTIME_DIR},type=bind" - ], - "postCreateCommand": "git config --global --add safe.directory /workspaces/dimos && cd /workspaces/dimos && pre-commit install", - "settings": { - "notebook.formatOnSave.enabled": true, - "notebook.codeActionsOnSave": { - "notebook.source.fixAll": "explicit", - "notebook.source.organizeImports": "explicit" - }, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", - "source.organizeImports": "explicit" - }, - "editor.defaultFormatter": "charliermarsh.ruff", - "editor.formatOnSave": true - }, - "runArgs": [ - "--cap-add=NET_ADMIN" - ] -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index ed33d0f4c8..0000000000 --- a/.dockerignore +++ /dev/null @@ -1,109 +0,0 @@ -# Version control -.git -.gitignore -.github/ - -# Editor and IDE files -.vscode -.idea -*.swp -*.swo -.cursor/ -.cursorignore - -# Shell history -.bash_history -.zsh_history -.history - -# Python virtual environments -**/venv/ -**/.venv/ -**/env/ -**/.env/ -**/*-venv/ -**/*_venv/ -**/ENV/ - - -# Python build artifacts -__pycache__/ -*.pyc -*.pyo -*.pyd -.Python -*.egg-info/ -dist/ -build/ -*.so -*.dylib - -# Environment file -.env -.env.local -.env.*.local - -# Large data files (LFS archives only) -data/* -!data/.lfs/ - -# Model files (can be downloaded at runtime) -*.pt -*.pth -*.onnx -*.pb -*.h5 -*.ckpt -*.safetensors -checkpoints/ -assets/model-cache - -# Logs -*.log - -# Large media files (not needed for functionality) -*.png -*.jpg -*.jpeg -*.gif -*.mp4 -*.mov -*.avi -*.mkv -*.webm -*.MOV - -# Large font files -*.ttf -*.otf - -# Node modules (for dev tools, not needed in container) -node_modules/ -package-lock.json -package.json -bin/node_modules/ - -# Database files -*.db -*.sqlite -*.sqlite3 - -# OS generated files -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# Temporary files -tmp/ -temp/ -*.tmp -.python-version - -# Exclude all assets subdirectories -assets/*/* -!assets/agent/prompt.txt -!assets/* diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 6644370b86..0000000000 --- a/.editorconfig +++ /dev/null @@ -1,27 +0,0 @@ -# top-most EditorConfig file -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = lf -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true - -[*.md] -indent_size = 4 - -[*.nix] -indent_size = 2 - -[*.{py,ipynb}] -indent_size = 4 -max_line_length = 100 - -[*.rs] -indent_style = space -indent_size = 4 - -[*.{ts,svelte}] -indent_size = 2 diff --git a/.envrc.nix b/.envrc.nix deleted file mode 100644 index a3f663db80..0000000000 --- a/.envrc.nix +++ /dev/null @@ -1,11 +0,0 @@ -if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" -fi -use flake . -for venv in venv .venv env; do - if [[ -f "$venv/bin/activate" ]]; then - source "$venv/bin/activate" - break - fi -done -dotenv_if_exists diff --git a/.envrc.venv b/.envrc.venv deleted file mode 100644 index e315a030c7..0000000000 --- a/.envrc.venv +++ /dev/null @@ -1,7 +0,0 @@ -for venv in venv .venv env; do - if [[ -f "$venv/bin/activate" ]]; then - source "$venv/bin/activate" - break - fi -done -dotenv_if_exists diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 8f05eb707f..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,18 +0,0 @@ -# Handle line endings automatically for files Git considers text, -# converting them to LF on checkout. -* text=auto eol=lf -# Ensure Python files always use LF for line endings. -*.py text eol=lf -# Treat designated file types as binary and do not alter their contents or line endings. -*.png filter=lfs diff=lfs merge=lfs -text binary -*.jpg filter=lfs diff=lfs merge=lfs -text binary -*.jpeg filter=lfs diff=lfs merge=lfs -text binary -*.ico binary -*.pdf binary -# Explicit LFS tracking for test files -/data/.lfs/*.tar.gz filter=lfs diff=lfs merge=lfs -text -*.onnx filter=lfs diff=lfs merge=lfs -text binary -*.mp4 filter=lfs diff=lfs merge=lfs -text binary -*.mov filter=lfs diff=lfs merge=lfs -text binary -*.gif filter=lfs diff=lfs merge=lfs -text binary -*.foxe filter=lfs diff=lfs merge=lfs -text binary diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml deleted file mode 100644 index a538ad35fd..0000000000 --- a/.github/actions/docker-build/action.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: docker-build -description: "Composite action to build and push a Docker target to GHCR" -inputs: - target: - description: "Dockerfile target stage to build" - required: true - tag: - description: "Image tag to push" - required: true - freespace: - description: "Remove large pre‑installed SDKs before building to free space" - required: false - default: "false" - context: - description: "Docker build context" - required: false - default: "." - -runs: - using: "composite" - steps: - - name: Free up disk space - if: ${{ inputs.freespace == 'true' }} - shell: bash - run: | - echo -e "pre cleanup space:\n $(df -h)" - sudo rm -rf /opt/ghc - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/share/boost - sudo rm -rf /usr/local/lib/android - echo -e "post cleanup space:\n $(df -h)" - - - uses: actions/checkout@v4 - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - uses: crazy-max/ghaction-github-runtime@v3 - - - uses: docker/setup-buildx-action@v3 - with: - driver: docker-container - install: true - use: true - - - name: Build & Push ${{ inputs.target }} - uses: docker/build-push-action@v6 - with: - push: true - context: ${{ inputs.context }} - file: docker/${{ inputs.target }}/Dockerfile - tags: ghcr.io/dimensionalos/${{ inputs.target }}:${{ inputs.tag }} - cache-from: type=gha,scope=${{ inputs.target }} - cache-to: type=gha,mode=max,scope=${{ inputs.target }} - build-args: | - FROM_TAG=${{ inputs.tag }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index b6a3420c77..0000000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,30 +0,0 @@ -## Problem - - - - -Closes DIM-XXX - -## Solution - - - - - -## Breaking Changes - - - - - -## How to Test - - - -## Contributor License Agreement - -- [ ] I have read and approved the [CLA](https://github.com/dimensionalOS/dimos/blob/main/CLA.md). diff --git a/.github/workflows/_docker-build-template.yml b/.github/workflows/_docker-build-template.yml deleted file mode 100644 index 478a9bec84..0000000000 --- a/.github/workflows/_docker-build-template.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: docker-build-template -on: - workflow_call: - inputs: - from-image: { type: string, required: true } - to-image: { type: string, required: true } - dockerfile: { type: string, required: true } - freespace: { type: boolean, default: true } - should-run: { type: boolean, default: false } - context: { type: string, default: '.' } - -# you can run this locally as well via -# ./bin/dockerbuild [image-name] -jobs: - build: - runs-on: [self-hosted, Linux] - permissions: - contents: read - packages: write - - steps: - - name: Fix permissions - if: ${{ inputs.should-run }} - run: | - sudo chown -R $USER:$USER ${{ github.workspace }} || true - - - uses: actions/checkout@v4 - if: ${{ inputs.should-run }} - with: - fetch-depth: 0 - - - name: free up disk space - # explicitly enable this for large builds - if: ${{ inputs.should-run && inputs.freespace }} - run: | - echo -e "pre cleanup space:\n $(df -h)" - sudo rm -rf /opt/ghc - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/share/boost - sudo rm -rf /usr/local/lib/android - - echo "=== Cleaning images from deleted branches ===" - - # Get list of all remote branches - git ls-remote --heads origin | awk '{print $2}' | sed 's|refs/heads/||' > /tmp/active_branches.txt - - # Check each docker image tag against branch list - docker images --format "{{.Repository}}:{{.Tag}}|{{.ID}}" | \ - grep "ghcr.io/dimensionalos" | \ - grep -v ":" | \ - while IFS='|' read image_ref id; do - tag=$(echo "$image_ref" | cut -d: -f2) - - # Skip if tag matches an active branch - if grep -qx "$tag" /tmp/active_branches.txt; then - echo "Branch exists: $tag - keeping $image_ref" - else - echo "Branch deleted: $tag - removing $image_ref" - docker rmi "$id" 2>/dev/null || true - fi - done - - rm -f /tmp/active_branches.txt - - USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') - echo "Pre-docker-cleanup disk usage: ${USAGE}%" - - if [ $USAGE -gt 60 ]; then - echo "=== Running quick cleanup (usage > 60%) ===" - - # Keep newest image per tag - docker images --format "{{.Repository}}|{{.Tag}}|{{.ID}}" | \ - grep "ghcr.io/dimensionalos" | \ - grep -v "" | \ - while IFS='|' read repo tag id; do - created_ts=$(docker inspect -f '{{.Created}}' "$id" 2>/dev/null) - created_unix=$(date -d "$created_ts" +%s 2>/dev/null || echo "0") - echo "${repo}|${tag}|${id}|${created_unix}" - done | sort -t'|' -k1,1 -k2,2 -k4,4nr | \ - awk -F'|' ' - { - repo=$1; tag=$2; id=$3 - repo_tag = repo ":" tag - - # Skip protected tags - if (tag ~ /^(main|dev|latest)$/) next - - # Keep newest per tag, remove older duplicates - if (!(repo_tag in seen_combos)) { - seen_combos[repo_tag] = 1 - } else { - system("docker rmi " id " 2>/dev/null || true") - } - }' - - docker image prune -f - docker volume prune -f - fi - - # Aggressive cleanup if still above 85% - USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//') - if [ $USAGE -gt 85 ]; then - echo "=== AGGRESSIVE cleanup (usage > 85%) - removing all except main/dev ===" - - # Remove ALL images except main and dev tags - docker images --format "{{.Repository}}:{{.Tag}} {{.ID}}" | \ - grep -E "ghcr.io/dimensionalos" | \ - grep -vE ":(main|dev)$" | \ - awk '{print $2}' | xargs -r docker rmi -f || true - - docker container prune -f - docker volume prune -a -f - docker network prune -f - docker image prune -f - fi - - echo -e "post cleanup space:\n $(df -h)" - - - uses: docker/login-action@v3 - if: ${{ inputs.should-run }} - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # required for github cache of docker layers - - uses: crazy-max/ghaction-github-runtime@v3 - if: ${{ inputs.should-run }} - - # required for github cache of docker layers - - uses: docker/setup-buildx-action@v3 - if: ${{ inputs.should-run }} - with: - driver: docker-container - install: true - use: true - - - uses: docker/build-push-action@v6 - if: ${{ inputs.should-run }} - with: - push: true - context: ${{ inputs.context }} - file: docker/${{ inputs.dockerfile }}/Dockerfile - tags: ${{ inputs.to-image }} - cache-from: type=gha,scope=${{ inputs.dockerfile }} - cache-to: type=gha,mode=max,scope=${{ inputs.dockerfile }} - #cache-from: type=gha,scope=${{ inputs.dockerfile }}-${{ inputs.from-image }} - #cache-to: type=gha,mode=max,scope=${{ inputs.dockerfile }}-${{ inputs.from-image }} - build-args: FROM_IMAGE=${{ inputs.from-image }} diff --git a/.github/workflows/code-cleanup.yml b/.github/workflows/code-cleanup.yml deleted file mode 100644 index 745f3852c1..0000000000 --- a/.github/workflows/code-cleanup.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: code-cleanup -on: - pull_request: - paths-ignore: - - '**.md' - -permissions: - contents: write - packages: write - pull-requests: read - -jobs: - pre-commit: - runs-on: self-hosted - steps: - - name: Fix permissions - run: | - sudo chown -R $USER:$USER ${{ github.workspace }} || true - - - uses: actions/checkout@v3 - - uses: actions/setup-python@v3 - - uses: astral-sh/setup-uv@v4 - - name: Run pre-commit - id: pre-commit-first - uses: pre-commit/action@v3.0.1 - continue-on-error: true - - - name: Re-run pre-commit if failed initially - id: pre-commit-retry - if: steps.pre-commit-first.outcome == 'failure' - uses: pre-commit/action@v3.0.1 - continue-on-error: false - - - name: Commit code changes - uses: stefanzweifel/git-auto-commit-action@v5 - with: - commit_message: "CI code cleanup" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 03de5c3d15..0000000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,341 +0,0 @@ -name: docker -on: - push: - branches: - - main - - dev - paths-ignore: - - '**.md' - pull_request: - -permissions: - contents: read - packages: write - pull-requests: read - -jobs: - check-changes: - runs-on: [self-hosted, Linux] - outputs: - ros: ${{ steps.filter.outputs.ros }} - python: ${{ steps.filter.outputs.python }} - dev: ${{ steps.filter.outputs.dev }} - navigation: ${{ steps.filter.outputs.navigation }} - tests: ${{ steps.filter.outputs.tests }} - branch-tag: ${{ steps.set-tag.outputs.branch_tag }} - steps: - - name: Fix permissions - run: | - sudo chown -R $USER:$USER ${{ github.workspace }} || true - - - uses: actions/checkout@v4 - - id: filter - uses: dorny/paths-filter@v3 - with: - base: ${{ github.event.before }} - filters: | - # ros and python are (alternative) root images - # change to root stuff like docker.yml etc triggers rebuild of those - # which cascades into a full rebuild - ros: - - .github/workflows/_docker-build-template.yml - - .github/workflows/docker.yml - - docker/ros/** - - python: - - .github/workflows/_docker-build-template.yml - - .github/workflows/docker.yml - - docker/python/** - - pyproject.toml - - dev: - - docker/dev/** - - navigation: - - .github/workflows/_docker-build-template.yml - - .github/workflows/docker.yml - - docker/navigation/** - - tests: - - dimos/** - - - name: Determine Branch Tag - id: set-tag - run: | - case "${GITHUB_REF_NAME}" in - main) branch_tag="latest" ;; - dev) branch_tag="dev" ;; - *) - branch_tag=$(echo "${GITHUB_REF_NAME}" \ - | tr '[:upper:]' '[:lower:]' \ - | sed -E 's#[^a-z0-9_.-]+#_#g' \ - | sed -E 's#^-+|-+$##g') - ;; - esac - echo "branch tag determined: ${branch_tag}" - echo branch_tag="${branch_tag}" >> "$GITHUB_OUTPUT" - - # just a debugger - inspect-needs: - needs: [check-changes, ros] - runs-on: dimos-runner-ubuntu-2204 - if: always() - steps: - - run: | - echo '${{ toJSON(needs) }}' - - ros: - needs: [check-changes] - if: needs.check-changes.outputs.ros == 'true' - uses: ./.github/workflows/_docker-build-template.yml - with: - should-run: true - from-image: ubuntu:22.04 - to-image: ghcr.io/dimensionalos/ros:${{ needs.check-changes.outputs.branch-tag }} - dockerfile: ros - - ros-python: - needs: [check-changes, ros] - if: always() - uses: ./.github/workflows/_docker-build-template.yml - with: - should-run: ${{ - needs.check-changes.outputs.python == 'true' && - needs.check-changes.result != 'error' && - needs.ros.result != 'error' - }} - - from-image: ghcr.io/dimensionalos/ros:${{ needs.ros.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - to-image: ghcr.io/dimensionalos/ros-python:${{ needs.check-changes.outputs.branch-tag }} - dockerfile: python - - python: - needs: [check-changes] - if: needs.check-changes.outputs.python == 'true' - uses: ./.github/workflows/_docker-build-template.yml - with: - should-run: true - dockerfile: python - from-image: ubuntu:22.04 - to-image: ghcr.io/dimensionalos/python:${{ needs.check-changes.outputs.branch-tag }} - - dev: - needs: [check-changes, python] - if: always() - - uses: ./.github/workflows/_docker-build-template.yml - with: - should-run: ${{ - needs.check-changes.result == 'success' && - ((needs.python.result == 'success') || - (needs.python.result == 'skipped' && - needs.check-changes.outputs.dev == 'true')) }} - from-image: ghcr.io/dimensionalos/python:${{ needs.python.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - to-image: ghcr.io/dimensionalos/dev:${{ needs.check-changes.outputs.branch-tag }} - dockerfile: dev - - navigation: - needs: [check-changes] - if: needs.check-changes.outputs.navigation == 'true' - runs-on: [self-hosted, Linux] - permissions: - contents: read - packages: write - steps: - - name: Fix permissions - run: | - sudo chown -R $USER:$USER ${{ github.workspace }} || true - - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Checkout ros-navigation-autonomy-stack - uses: actions/checkout@v4 - with: - repository: dimensionalOS/ros-navigation-autonomy-stack - ref: fastlio2 - path: docker/navigation/ros-navigation-autonomy-stack - fetch-depth: 1 - lfs: false - token: ${{ secrets.NAV_REPO_READ_TOKEN }} - - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - uses: crazy-max/ghaction-github-runtime@v3 - - - uses: docker/setup-buildx-action@v3 - with: - driver: docker-container - install: true - use: true - - - uses: docker/build-push-action@v6 - with: - push: true - context: . - file: docker/navigation/Dockerfile - tags: ghcr.io/dimensionalos/navigation:${{ needs.check-changes.outputs.branch-tag }} - cache-from: type=gha,scope=navigation - cache-to: type=gha,mode=max,scope=navigation - build-args: | - ROS_DISTRO=humble - - ros-dev: - needs: [check-changes, ros-python] - if: always() - uses: ./.github/workflows/_docker-build-template.yml - with: - should-run: ${{ - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.dev == 'true' || - (needs.ros-python.result == 'success' && (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.ros == 'true'))) - }} - from-image: ghcr.io/dimensionalos/ros-python:${{ needs.ros-python.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - to-image: ghcr.io/dimensionalos/ros-dev:${{ needs.check-changes.outputs.branch-tag }} - dockerfile: dev - - run-ros-tests: - needs: [check-changes, ros-dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.ros == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "pytest && pytest -m ros" # run tests that depend on ros as well - dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - run-tests: - needs: [check-changes, dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "pytest" - dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - # we run in parallel with normal tests for speed - run-heavy-tests: - needs: [check-changes, dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "pytest -m heavy" - dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - run-lcm-tests: - needs: [check-changes, dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "pytest -m lcm" - dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - run-integration-tests: - needs: [check-changes, dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "pytest -m integration" - dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - run-mypy: - needs: [check-changes, ros-dev] - if: ${{ - always() && - needs.check-changes.result == 'success' && - (needs.check-changes.outputs.tests == 'true' || - needs.check-changes.outputs.ros == 'true' || - needs.check-changes.outputs.python == 'true' || - needs.check-changes.outputs.dev == 'true') - }} - uses: ./.github/workflows/tests.yml - secrets: inherit - with: - cmd: "MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy dimos" - dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - - # Run module tests directly to avoid pytest forking issues - # run-module-tests: - # needs: [check-changes, dev] - # if: ${{ - # always() && - # needs.check-changes.result == 'success' && - # ((needs.dev.result == 'success') || - # (needs.dev.result == 'skipped' && - # needs.check-changes.outputs.tests == 'true')) - # }} - # runs-on: [self-hosted, x64, 16gb] - # container: - # image: ghcr.io/dimensionalos/dev:${{ needs.check-changes.outputs.dev == 'true' && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }} - # steps: - # - name: Fix permissions - # run: | - # sudo chown -R $USER:$USER ${{ github.workspace }} || true - # - # - uses: actions/checkout@v4 - # with: - # lfs: true - # - # - name: Configure Git LFS - # run: | - # git config --global --add safe.directory '*' - # git lfs install - # git lfs fetch - # git lfs checkout - # - # - name: Run module tests - # env: - # CI: "true" - # run: | - # /entrypoint.sh bash -c "pytest -m module" - - ci-complete: - needs: [check-changes, ros, python, ros-python, dev, ros-dev, run-tests, run-heavy-tests, run-lcm-tests, run-integration-tests, run-ros-tests, run-mypy] - runs-on: [self-hosted, Linux] - if: always() - steps: - - name: CI gate - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} - run: | - echo "❌ One or more CI jobs failed or were cancelled" - exit 1 - - name: CI passed - run: echo "✅ All CI checks passed or were intentionally skipped" diff --git a/.github/workflows/readme.md b/.github/workflows/readme.md deleted file mode 100644 index f82ba479bb..0000000000 --- a/.github/workflows/readme.md +++ /dev/null @@ -1,51 +0,0 @@ -# general structure of workflows - -Docker.yml checks for releavant file changes and re-builds required images -Currently images have a dependancy chain of ros -> python -> dev (in the future this might be a tree and can fork) - -On top of the dev image then tests are run. -Dev image is also what developers use in their own IDE via devcontainers -https://code.visualstudio.com/docs/devcontainers/containers - -# login to github docker repo - -create personal access token (classic, not fine grained) -https://github.com/settings/tokens - -add permissions -- read:packages scope to download container images and read their metadata. - - and optionally, - -- write:packages scope to download and upload container images and read and write their metadata. -- delete:packages scope to delete container images. - -more info @ https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry - -login to docker via - -`sh -echo TOKEN | docker login ghcr.io -u GITHUB_USER --password-stdin -` - -pull dev image (dev branch) -`sh -docker pull ghcr.io/dimensionalos/dev:dev -` - -pull dev image (master) -`sh -docker pull ghcr.io/dimensionalos/dev:latest -` - -# todo - -Currently there is an issue with ensuring both correct docker image build ordering, and skipping unneccessary re-builds. - -(we need job dependancies for builds to wait to their images underneath to be built (for example py waits for ros)) -by default if a parent is skipped, it's children get skipped as well, unless they have always() in their conditional. - -Issue is once we put always() in the conditional, it seems that no matter what other check we put in the same conditional, job will always run. -for this reason we cannot skip python (and above) builds for now. Needs review. - -I think we will need to write our own build dispatcher in python that calls github workflows that build images. diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 25273238dc..0000000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: tests - -on: - workflow_call: - inputs: - dev-image: - required: true - type: string - default: "dev:dev" - cmd: - required: true - type: string - -permissions: - contents: read - packages: read - -jobs: - run-tests: - runs-on: [self-hosted, Linux] - container: - image: ghcr.io/dimensionalos/${{ inputs.dev-image }} - env: - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - ALIBABA_API_KEY: ${{ secrets.ALIBABA_API_KEY }} - - steps: - - uses: actions/checkout@v4 - - - name: Fix permissions - run: | - git config --global --add safe.directory '*' - - - name: Run tests - run: | - /entrypoint.sh bash -c "${{ inputs.cmd }}" - - - name: check disk space - if: failure() - run: | - df -h diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 24a3dd8919..0000000000 --- a/.gitignore +++ /dev/null @@ -1,71 +0,0 @@ -# generic ignore pattern -**/*.ignore -**/*.ignore.* - -.vscode/ - -# Ignore Python cache files -__pycache__/ -*.pyc - -# Ignore virtual environment directories -*venv*/ -.venv*/ -.ssh/ -.direnv/ - -# Ignore python tooling dirs -*.egg-info/ -__pycache__ - -.env -**/.DS_Store - -# Ignore default runtime output folder -/assets/output/ -/assets/rgbd_data/ -/assets/saved_maps/ -/assets/model-cache/ -/assets/agent/memory.txt - -.bash_history - -# Ignore all test data directories but allow compressed files -/data/* -!/data/.lfs/ - -# node env (used by devcontainers cli) -node_modules -package.json -package-lock.json - -# Ignore build artifacts -dist/ -build/ - -# Ignore data directory but keep .lfs subdirectory -data/* -!data/.lfs/ -FastSAM-x.pt -yolo11n.pt - -/thread_monitor_report.csv - -# symlink one of .envrc.* if you'd like to use -.envrc -.claude -**/CLAUDE.md -.direnv/ - -/logs - -*.so - -/.mypy_cache* - -*mobileclip* -/results -**/cpp/result - -CLAUDE.MD -/assets/teleop_certs/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 29d068dccf..0000000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,101 +0,0 @@ -default_stages: [pre-commit] -default_install_hook_types: [pre-commit, commit-msg] -exclude: (dimos/models/.*)|(deprecated) -repos: - - - repo: https://github.com/Lucas-C/pre-commit-hooks - rev: v1.5.5 - hooks: - - id: forbid-crlf - - id: remove-crlf - - id: insert-license - files: \.py$ - exclude: (__init__\.py$)|(dimos/rxpy_backpressure/) - args: - # use if you want to remove licences from all files - # (for globally changing wording or something) - #- --remove-header - - --license-filepath - - assets/license_file_header.txt - - --use-current-year - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.3 - hooks: - - id: ruff-format - stages: [pre-commit] - - id: ruff-check - args: [--fix, --unsafe-fixes] - - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 - hooks: - - id: check-case-conflict - - id: trailing-whitespace - language: python - types: [text] - - id: end-of-file-fixer - - id: mixed-line-ending - args: [--fix=lf] - - id: check-json - - id: check-toml - - id: check-yaml - - id: pretty-format-json - name: format json - args: [ --autofix, --no-sort-keys ] - - - repo: https://github.com/editorconfig-checker/editorconfig-checker.python - rev: 3.4.1 - hooks: - - id: editorconfig-checker - alias: ec - args: [-disable-max-line-length, -disable-indentation] - - # - repo: local - # hooks: - # - id: mypy - # name: Type check - # # possible to also run within the dev image - # #entry: "./bin/dev mypy" - # entry: "./bin/mypy" - # language: python - # additional_dependencies: ["mypy==1.15.0", "numpy>=1.26.4,<2.0.0"] - # types: [python] - - - repo: local - hooks: - - id: uv-lock-check - name: Check uv.lock is up-to-date - entry: uv lock --check - language: system - files: ^pyproject\.toml$ - pass_filenames: false - - - id: lfs_check - name: LFS data - always_run: true - pass_filenames: false - entry: bin/hooks/lfs_check - language: script - - - id: largefiles-check - name: Large files check - always_run: true - pass_filenames: false - entry: python bin/hooks/largefiles_check - language: python - additional_dependencies: ['tomli'] - - - id: doclinks - name: Doclinks - always_run: true - pass_filenames: false - entry: python -m dimos.utils.docs.doclinks docs/ - language: system - files: ^docs/.*\.md$ - - - id: filter-commit-message - name: Filter generated signatures from commit message - entry: python bin/hooks/filter_commit_message.py - language: python - stages: [commit-msg] diff --git a/.python-version b/.python-version deleted file mode 100644 index e4fba21835..0000000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.12 diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index b8d6fb374a..0000000000 --- a/.style.yapf +++ /dev/null @@ -1,3 +0,0 @@ - [style] - based_on_style = google - column_limit = 80 diff --git a/CLA.md b/CLA.md deleted file mode 100644 index 507849309c..0000000000 --- a/CLA.md +++ /dev/null @@ -1,24 +0,0 @@ -## Dimensional OS Individual Contributor License Agreement - -In order to clarify the intellectual property license granted with Contributions from any person or entity, **Dimensional Inc.** ("**Dimensional**") must have a Contributor License Agreement ("CLA") on file that has been signed by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Dimensional; it does not change your rights to use your own Contributions for any other purpose. - -You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Dimensional. Except for the license granted herein to Dimensional and recipients of software distributed by Dimensional, You reserve all right, title, and interest in and to Your Contributions. - -1. Definitions. - "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Dimensional. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - - "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to Dimensional for inclusion in, or documentation of, any of the products owned or managed by Dimensional (the "Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Dimensional or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Dimensional for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Dimensional and to recipients of software distributed by Dimensional a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. - -3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Dimensional and to recipients of software distributed by Dimensional a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. - -4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your Contributions, you represent that you have received permission to make Contributions on behalf of that employer, that your employer has waived such rights for your Contributions to Dimensional, or that your employer has executed a separate Corporate CLA with Dimensional. - -5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others). You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your Contributions. - -6. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - -7. Should You wish to submit work that is not Your original creation, You may submit it to Dimensional separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]." - -8. You agree to notify Dimensional of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 5e2927e3ad..0000000000 --- a/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - Copyright 2025 Dimensional Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 59df25c071..0000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,21 +0,0 @@ -global-exclude *.pyc -global-exclude __pycache__ -global-exclude .DS_Store - -# Exclude web development directories -recursive-exclude dimos/web/command-center-extension * -recursive-exclude dimos/web/websocket_vis/node_modules * -recursive-exclude dimos/agents/fixtures * -recursive-exclude dimos/mapping/google_maps/fixtures * -recursive-exclude dimos/web/dimos_interface * - -# Exclude development files -exclude .gitignore -exclude .gitattributes -prune .git -prune .github -prune .mypy_cache -prune .pytest_cache -prune .ruff_cache -prune .vscode -prune dimos/web/command-center-extension diff --git a/README.md b/README.md deleted file mode 100644 index a84fe11b0e..0000000000 --- a/README.md +++ /dev/null @@ -1,280 +0,0 @@ - -
- -banner_bordered_trimmed - -

The Agentive Operating System for Generalist Robotics

- -[![Discord](https://img.shields.io/discord/1341146487186391173?style=flat-square&logo=discord&logoColor=white&label=Discord&color=5865F2)](https://discord.gg/dimos) -[![Stars](https://img.shields.io/github/stars/dimensionalOS/dimos?style=flat-square)](https://github.com/dimensionalOS/dimos/stargazers) -[![Forks](https://img.shields.io/github/forks/dimensionalOS/dimos?style=flat-square)](https://github.com/dimensionalOS/dimos/fork) -[![Contributors](https://img.shields.io/github/contributors/dimensionalOS/dimos?style=flat-square)](https://github.com/dimensionalOS/dimos/graphs/contributors) -![Nix](https://img.shields.io/badge/Nix-flakes-5277C3?style=flat-square&logo=NixOS&logoColor=white) -![NixOS](https://img.shields.io/badge/NixOS-supported-5277C3?style=flat-square&logo=NixOS&logoColor=white) -![CUDA](https://img.shields.io/badge/CUDA-supported-76B900?style=flat-square&logo=nvidia&logoColor=white) -[![Docker](https://img.shields.io/badge/Docker-ready-2496ED?style=flat-square&logo=docker&logoColor=white)](https://www.docker.com/) - - - -[Hardware](#hardware) • -[Installation](#installation) • -[Development](#development) • -[Multi Language](#multi-language-support) • -[ROS](#ros-interop) - -⚠️ **Alpha Pre-Release: Expect Breaking Changes** ⚠️ - - - -
- -# Intro - -Dimensional is the modern operating system for generalist robotics. We are setting the next-generation SDK standard, integrating with the majority of robot manufacturers. - -With a simple install and no ROS required, build physical applications entirely in python that run on any humanoid, quadruped, or drone. - -Dimensional is agent native -- "vibecode" your robots in natural language and build (local & hosted) multi-agent systems that work seamlessly with your hardware. Agents run as native modules — subscribing to any embedded stream, from perception (lidar, camera) and spatial memory down to control loops and motor drivers. - - - - - - - - - - - - - - - - - -
- Navigation - - Perception -
-

Navigation and Mapping

- SLAM, dynamic obstacle avoidance, route planning, and autonomous exploration — via both DimOS native and ROS
Watch video -
-

Perception

- Detectors, 3d projections, VLMs, Audio processing -
- Agents - - Spatial Memory -
-

Agentive Control, MCP

- "hey Robot, go find the kitchen"
Watch video -
-

Spatial Memory

- Spatio-temporal RAG, Dynamic memory, Object localization and permanence
Watch video -
- - -# Hardware - - - - - - - - - - - - - - - - - -
-

Quadruped

- -
-

Humanoid

- -
-

Arm

- -
-

Drone

- -
-

Misc

- -
- 🟩 Unitree Go2 pro/air
- 🟥 Unitree B1
-
- 🟨 Unitree G1
-
- 🟥 Xarm
- 🟥 AgileX Piper
-
- 🟥 Mavlink
- 🟥 DJI SDK
-
- 🟥 Force Torque Sensor
-
-
-
-🟩 stable 🟨 beta 🟧 alpha 🟥 experimental - -
- -# Installation - -## System Install - -To set up your system dependencies, follow one of these guides: - -- 🟩 [Ubuntu 22.04 / 24.04](docs/installation/ubuntu.md) -- 🟩 [NixOS / General Linux](docs/installation/nix.md) -- 🟧 [macOS](docs/installation/osx.md) - -## Python Install - -### Quickstart - -```bash -uv venv --python "3.12" -source .venv/bin/activate -uv pip install dimos[base,unitree] - -# Replay a recorded Go2 session (no hardware needed) -# NOTE: First run will show a black rerun window while ~2.4 GB downloads from LFS -dimos --replay run unitree-go2 -``` - -```bash -# Install with simulation support -uv pip install dimos[base,unitree,sim] - -# Run Go2 in MuJoCo simulation -dimos --simulation run unitree-go2 - -# Run G1 humanoid in simulation -dimos --simulation run unitree-g1-sim -``` - -```bash -# Control a real robot (Unitree Go2 over WebRTC) -export ROBOT_IP= -dimos run unitree-go2 -``` - -# Usage - -## Use DimOS as a Library - -See below a simple robot connection module that sends streams of continuous `cmd_vel` to the robot and receives `color_image` to a simple `Listener` module. DimOS Modules are subsystems on a robot that communicate with other modules using standardized messages. - -```py -import threading, time, numpy as np -from dimos.core import In, Module, Out, rpc, autoconnect -from dimos.msgs.geometry_msgs import Twist -from dimos.msgs.sensor_msgs import Image, ImageFormat - -class RobotConnection(Module): - cmd_vel: In[Twist] - color_image: Out[Image] - - @rpc - def start(self): - threading.Thread(target=self._image_loop, daemon=True).start() - - def _image_loop(self): - while True: - img = Image.from_numpy( - np.zeros((120, 160, 3), np.uint8), - format=ImageFormat.RGB, - frame_id="camera_optical", - ) - self.color_image.publish(img) - time.sleep(0.2) - -class Listener(Module): - color_image: In[Image] - - @rpc - def start(self): - self.color_image.subscribe(lambda img: print(f"image {img.width}x{img.height}")) - -if __name__ == "__main__": - autoconnect( - RobotConnection.blueprint(), - Listener.blueprint(), - ).build().loop() -``` - -## Blueprints - -Blueprints are instructions for how to construct and wire modules. We compose them with -`autoconnect(...)`, which connects streams by `(name, type)` and returns a `Blueprint`. - -Blueprints can be composed, remapped, and have transports overridden if `autoconnect()` fails due to conflicting variable names or `In[]` and `Out[]` message types. - -A blueprint example that connects the image stream from a robot to an LLM Agent for reasoning and action execution. -```py -from dimos.core import autoconnect, LCMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.robot.unitree.go2.connection import go2_connection -from dimos.agents.agent import agent - -blueprint = autoconnect( - go2_connection(), - agent(), -).transports({("color_image", Image): LCMTransport("/color_image", Image)}) - -# Run the blueprint -if __name__ == "__main__": - blueprint.build().loop() -``` - -## Library API - -- [Modules](docs/usage/modules.md) -- [LCM](docs/usage/lcm.md) -- [Blueprints](docs/usage/blueprints.md) -- [Transports](docs/usage/transports/index.md) -- [Data Streams](docs/usage/data_streams/README.md) -- [Configuration](docs/usage/configuration.md) -- [Visualization](docs/usage/visualization.md) - -## Demos - -DimOS Demo - -# Development - -## Develop on DimOS - -```sh -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -uv sync --all-extras --no-extra dds - -# Run fast test suite -uv run pytest dimos -``` - -## Multi Language Support - -Python is our glue and prototyping language, but we support many languages via LCM interop. - -Check our language interop examples: -- [C++](examples/language-interop/cpp/) -- [Lua](examples/language-interop/lua/) -- [TypeScript](examples/language-interop/ts/) - -## ROS interop - -For researchers, we can talk to ROS directly via [ROS Transports](docs/usage/transports/index.md), or host dockerized ROS deployments as first-class DimOS modules, allowing you easy installation and portability diff --git a/assets/dimensional-dark.svg b/assets/dimensional-dark.svg deleted file mode 100644 index 95edaadc0e..0000000000 --- a/assets/dimensional-dark.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/assets/dimensional-light.svg b/assets/dimensional-light.svg deleted file mode 100644 index f0a107bd10..0000000000 --- a/assets/dimensional-light.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/assets/dimensional-text.svg b/assets/dimensional-text.svg deleted file mode 100644 index 0cf32b3d19..0000000000 --- a/assets/dimensional-text.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - diff --git a/assets/dimensional.command-center-extension-0.0.1.foxe b/assets/dimensional.command-center-extension-0.0.1.foxe deleted file mode 100644 index 163f1ef36b..0000000000 --- a/assets/dimensional.command-center-extension-0.0.1.foxe +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:98a2a2154b102e8d889bb83305163ead388016377b8e8a56c8f42034443f9be4 -size 1229315 diff --git a/assets/dimensional.svg b/assets/dimensional.svg deleted file mode 100644 index f36a0dea40..0000000000 --- a/assets/dimensional.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - diff --git a/assets/dimensionalascii.txt b/assets/dimensionalascii.txt deleted file mode 100644 index 9b35fb8778..0000000000 --- a/assets/dimensionalascii.txt +++ /dev/null @@ -1,7 +0,0 @@ - - ██████╗ ██╗███╗ ███╗███████╗███╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗ - ██╔══██╗██║████╗ ████║██╔════╝████╗ ██║██╔════╝██║██╔═══██╗████╗ ██║██╔══██╗██║ - ██║ ██║██║██╔████╔██║█████╗ ██╔██╗ ██║███████╗██║██║ ██║██╔██╗ ██║███████║██║ - ██║ ██║██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║ - ██████╔╝██║██║ ╚═╝ ██║███████╗██║ ╚████║███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗ - ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ diff --git a/assets/dimos_interface.gif b/assets/dimos_interface.gif deleted file mode 100644 index e610a2b390..0000000000 --- a/assets/dimos_interface.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:13a5348ec51bef34d8cc3aa4afc99975befb7f118826df571130b1a2fa1b59e9 -size 13361230 diff --git a/assets/dimos_terminal.png b/assets/dimos_terminal.png deleted file mode 100644 index a71b06e1cc..0000000000 --- a/assets/dimos_terminal.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e45d7f700813e8aa042cc76f7fcf4ef7836f5f1a46708275d9cc11fd6559ba9 -size 25557 diff --git a/assets/drone_foxglove_lcm_dashboard.json b/assets/drone_foxglove_lcm_dashboard.json deleted file mode 100644 index cfcd8afb47..0000000000 --- a/assets/drone_foxglove_lcm_dashboard.json +++ /dev/null @@ -1,381 +0,0 @@ -{ - "configById": { - "RawMessages!3zk027p": { - "diffEnabled": false, - "diffMethod": "custom", - "diffTopicPath": "", - "showFullMessageForDiff": false, - "topicPath": "/drone/telemetry", - "fontSize": 12 - }, - "RawMessages!ra9m3n": { - "diffEnabled": false, - "diffMethod": "custom", - "diffTopicPath": "", - "showFullMessageForDiff": false, - "topicPath": "/drone/status", - "fontSize": 12 - }, - "RawMessages!2rdgzs9": { - "diffEnabled": false, - "diffMethod": "custom", - "diffTopicPath": "", - "showFullMessageForDiff": false, - "topicPath": "/drone/odom", - "fontSize": 12 - }, - "3D!18i6zy7": { - "layers": { - "845139cb-26bc-40b3-8161-8ab60af4baf5": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", - "layerId": "foxglove.Grid", - "lineWidth": 0.5, - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 1, - "size": 30, - "divisions": 30, - "color": "#248eff57" - }, - "ff758451-8c06-4419-a995-e93c825eb8be": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", - "layerId": "foxglove.Grid", - "frameId": "base_link", - "size": 3, - "divisions": 3, - "lineWidth": 1.5, - "color": "#24fff4ff", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 2 - } - }, - "cameraState": { - "perspective": true, - "distance": 35.161738318180966, - "phi": 54.90139603020621, - "thetaOffset": -55.91718358847429, - "targetOffset": [ - -1.0714086708240587, - -1.3106525624032879, - 2.481084387307447e-16 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": true, - "ignoreColladaUpAxis": false, - "syncCamera": false, - "transforms": { - "visible": true - } - }, - "transforms": {}, - "topics": { - "/lidar": { - "stixelsEnabled": false, - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 10, - "explicitAlpha": 1, - "decayTime": 0, - "cubeSize": 0.1, - "minValue": -0.3, - "cubeOutline": false - }, - "/odom": { - "visible": true, - "axisScale": 1 - }, - "/video": { - "visible": false - }, - "/global_map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 10, - "decayTime": 0, - "pointShape": "cube", - "cubeOutline": false, - "cubeSize": 0.08, - "gradient": [ - "#06011dff", - "#d1e2e2ff" - ], - "stixelsEnabled": false, - "explicitAlpha": 1, - "minValue": -0.2 - }, - "/global_path": { - "visible": true, - "type": "line", - "arrowScale": [ - 1, - 0.15, - 0.15 - ], - "lineWidth": 0.132, - "gradient": [ - "#6bff7cff", - "#0081ffff" - ] - }, - "/global_target": { - "visible": true - }, - "/pt": { - "visible": false - }, - "/global_costmap": { - "visible": true, - "maxColor": "#8d3939ff", - "frameLocked": false, - "unknownColor": "#80808000", - "colorMode": "custom", - "alpha": 0.517, - "minColor": "#1e00ff00" - }, - "/global_gradient": { - "visible": true, - "maxColor": "#690066ff", - "unknownColor": "#30b89a00", - "minColor": "#00000000", - "colorMode": "custom", - "alpha": 0.3662, - "frameLocked": false, - "drawBehind": false - }, - "/global_cost_field": { - "visible": false, - "maxColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/global_passable": { - "visible": false, - "maxColor": "#ffffff00", - "minColor": "#ff0000ff", - "unknownColor": "#80808000" - } - }, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/estimate", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {}, - "foxglovePanelTitle": "test", - "followTf": "world" - }, - "Image!3mnp456": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": true - }, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/drone/color_image", - "colorMode": "gradient", - "calibrationTopic": "/drone/camera_info" - }, - "foxglovePanelTitle": "/video" - }, - "Image!1gtgk2x": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": true - }, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/drone/depth_colorized", - "colorMode": "gradient", - "calibrationTopic": "/drone/camera_info" - }, - "foxglovePanelTitle": "/video" - }, - "Plot!a1gj37": { - "paths": [ - { - "timestampMethod": "receiveTime", - "value": "/drone/odom.pose.position.x", - "enabled": true, - "color": "#4e98e2" - }, - { - "timestampMethod": "receiveTime", - "value": "/drone/odom.pose.orientation.y", - "enabled": true, - "color": "#f5774d" - }, - { - "timestampMethod": "receiveTime", - "value": "/drone/odom.pose.position.z", - "enabled": true, - "color": "#f7df71" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "drawerConfig": { - "tracks": [] - }, - "layout": { - "direction": "row", - "first": { - "first": { - "first": "RawMessages!3zk027p", - "second": "RawMessages!ra9m3n", - "direction": "column", - "splitPercentage": 69.92084432717678 - }, - "second": "RawMessages!2rdgzs9", - "direction": "column", - "splitPercentage": 70.97625329815304 - }, - "second": { - "first": "3D!18i6zy7", - "second": { - "first": "Image!3mnp456", - "second": { - "first": "Image!1gtgk2x", - "second": "Plot!a1gj37", - "direction": "column" - }, - "direction": "column", - "splitPercentage": 36.93931398416886 - }, - "direction": "row", - "splitPercentage": 52.45307143723201 - }, - "splitPercentage": 39.13203076769059 - } -} diff --git a/assets/foxglove_dashboards/go2.json b/assets/foxglove_dashboards/go2.json deleted file mode 100644 index fb9df219c2..0000000000 --- a/assets/foxglove_dashboards/go2.json +++ /dev/null @@ -1,603 +0,0 @@ -{ - "configById": { - "3D!3ezwzdr": { - "cameraState": { - "perspective": false, - "distance": 10.26684166532264, - "phi": 29.073691502600532, - "thetaOffset": 93.32472375597958, - "targetOffset": [ - 3.280168913303102, - -1.418093876569801, - -2.6619087209849424e-16 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "transforms": { - "labelSize": 0.1, - "axisSize": 0.51 - } - }, - "transforms": { - "frame:sensor_at_scan": { - "visible": false - }, - "frame:camera_optical": { - "visible": false - }, - "frame:camera_link": { - "visible": false - }, - "frame:base_link": { - "visible": true - }, - "frame:sensor": { - "visible": false - }, - "frame:map": { - "visible": false - }, - "frame:world": { - "visible": false - } - }, - "topics": { - "/lidar": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 2.85, - "decayTime": 6, - "pointShape": "circle" - }, - "/detectorDB/scene_update": { - "visible": true - }, - "/path_active": { - "visible": true, - "lineWidth": 0.05, - "gradient": [ - "#00ff1eff", - "#6bff6e80" - ] - }, - "/map": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/image": { - "visible": false - }, - "/camera_info": { - "visible": true, - "distance": 1, - "color": "#c4bcffff" - }, - "/detectorDB/pointcloud/0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "pointSize": 2, - "flatColor": "#00ff00ff", - "cubeSize": 0.03 - }, - "/detectorDB/pointcloud/1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.03, - "flatColor": "#ff0000ff" - }, - "/detectorDB/pointcloud/2": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.03, - "flatColor": "#00aaffff" - }, - "/global_map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 4, - "pointShape": "circle", - "explicitAlpha": 1, - "cubeSize": 0.05, - "cubeOutline": false, - "flatColor": "#ed8080ff", - "minValue": -0.1, - "decayTime": 0 - }, - "/global_costmap": { - "visible": true, - "colorMode": "custom", - "unknownColor": "#ff000000", - "minColor": "#484981ff", - "maxColor": "#000000ff", - "frameLocked": false, - "drawBehind": false - }, - "/go2/color_image": { - "visible": false, - "cameraInfoTopic": "/go2/camera_info" - }, - "/go2/camera_info": { - "visible": true - }, - "/color_image": { - "visible": false, - "cameraInfoTopic": "/camera_info" - }, - "color_image": { - "visible": false, - "cameraInfoTopic": "/camera_info" - }, - "lidar": { - "visible": false, - "colorField": "z", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 2.76, - "pointShape": "cube" - }, - "odom": { - "visible": false - }, - "global_map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "cube" - }, - "prev_lidar": { - "visible": false, - "pointShape": "cube", - "colorField": "z", - "colorMode": "flat", - "colorMap": "turbo", - "gradient": [ - "#b70000ff", - "#ff0000ff" - ], - "flatColor": "#80eda2ff" - }, - "additive_global_map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "cube" - }, - "height_costmap": { - "visible": false - }, - "/odom": { - "visible": false - }, - "/costmap": { - "visible": false, - "colorMode": "custom", - "alpha": 1, - "frameLocked": false, - "maxColor": "#ff2222ff", - "minColor": "#00006bff", - "unknownColor": "#80808000" - }, - "/debug_navigation": { - "visible": false, - "cameraInfoTopic": "/camera_info" - }, - "/path": { - "visible": true, - "lineWidth": 0.03, - "gradient": [ - "#ff6b6bff", - "#ff0000ff" - ] - } - }, - "layers": { - "grid": { - "visible": true, - "drawBehind": false, - "frameLocked": true, - "label": "Grid", - "instanceId": "8cb9fe46-7478-4aa6-95c5-75c511fee62d", - "layerId": "foxglove.Grid", - "size": 50, - "color": "#24b6ffff", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "frameId": "world", - "divisions": 25, - "lineWidth": 1 - }, - "aac2d29a-9580-442f-8067-104830c336c8": { - "displayMode": "auto", - "fallbackColor": "#ffffff", - "showAxis": false, - "axisScale": 1, - "showOutlines": true, - "opacity": 1, - "visible": true, - "frameLocked": true, - "instanceId": "aac2d29a-9580-442f-8067-104830c336c8", - "label": "URDF", - "layerId": "foxglove.Urdf", - "sourceType": "filePath", - "url": "", - "filePath": "/home/lesh/coding/dimos/dimos/robot/unitree/go2/go2.urdf", - "parameter": "", - "topic": "", - "framePrefix": "", - "order": 2, - "links": { - "base_link": { - "visible": true - } - } - } - }, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {}, - "followTf": "map" - }, - "command-center-extension.command-center!3xr2po0": {}, - "Plot!3cog9zw": { - "paths": [ - { - "timestampMethod": "receiveTime", - "value": "/metrics/_calculate_costmap.data", - "enabled": true, - "color": "#4e98e2", - "id": "a1ff9a80-7a45-48ff-bdb1-232bda7bd492" - }, - { - "timestampMethod": "receiveTime", - "value": "/metrics/get_global_pointcloud.data", - "enabled": true, - "color": "#f5774d", - "id": "5fe70fbd-33f9-4b15-849f-c7c49988af95" - }, - { - "timestampMethod": "receiveTime", - "value": "/metrics/add_frame.data", - "enabled": true, - "color": "#f7df71", - "id": "bb4a56f8-78ae-45cb-850e-48c462dab40f" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - }, - "Plot!47kna9v": { - "paths": [ - { - "timestampMethod": "publishTime", - "value": "/global_map.header.stamp.sec", - "enabled": true, - "color": "#4e98e2", - "id": "19f95865-4d9e-4d38-b9d7-d227319d8ebd" - }, - { - "timestampMethod": "publishTime", - "value": "/global_costmap.header.stamp.sec", - "enabled": true, - "color": "#f5774d", - "id": "86ddc0e2-8e9c-4d52-bd5a-d02cb0357efe" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - }, - "Image!3mnp456": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": false, - "transforms": { - "showLabel": false, - "visible": false - } - }, - "transforms": { - "frame:world": { - "visible": true - }, - "frame:camera_optical": { - "visible": false - }, - "frame:camera_link": { - "visible": false - }, - "frame:base_link": { - "visible": false - } - }, - "topics": { - "/lidar": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 6, - "explicitAlpha": 0.6, - "pointShape": "circle", - "cubeSize": 0.016 - }, - "/odom": { - "visible": false - }, - "/local_costmap": { - "visible": false - }, - "/global_costmap": { - "visible": false, - "minColor": "#ffffff00", - "maxColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/detected_0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 23, - "pointShape": "cube", - "cubeSize": 0.04, - "flatColor": "#ff0000ff", - "stixelsEnabled": false - }, - "/detected_1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 20.51, - "flatColor": "#34ff00ff", - "pointShape": "cube", - "cubeSize": 0.04, - "cubeOutline": false - }, - "/filtered_pointcloud": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "rainbow", - "pointSize": 1.5, - "pointShape": "cube", - "flatColor": "#ff0000ff", - "cubeSize": 0.1 - }, - "/global_map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "cube", - "pointSize": 5, - "cubeSize": 0.03 - }, - "/detected/pointcloud/1": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.01, - "flatColor": "#00ff1eff", - "pointSize": 15, - "decayTime": 0, - "cubeOutline": true - }, - "/detected/pointcloud/2": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "circle", - "cubeSize": 0.1, - "flatColor": "#00fbffff", - "pointSize": 0.01 - }, - "/detected/pointcloud/0": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0000ff", - "pointSize": 15, - "cubeOutline": true, - "cubeSize": 0.03 - }, - "/registered_scan": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 6.49 - }, - "/detection3d/markers": { - "visible": false - }, - "/foxglove/scene_update": { - "visible": true - }, - "/scene_update": { - "visible": false - }, - "/map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 8 - }, - "/detection3d/scene_update": { - "visible": true - }, - "/detectorDB/scene_update": { - "visible": false - }, - "/detectorDB/pointcloud/0": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/detectorDB/pointcloud/1": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - } - }, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/color_image", - "colorMode": "gradient", - "annotations": { - "/detections": { - "visible": true - }, - "/annotations": { - "visible": true - }, - "/reid/annotations": { - "visible": true - }, - "/objectdb/annotations": { - "visible": true - }, - "/detector3d/annotations": { - "visible": true - }, - "/detectorDB/annotations": { - "visible": true - } - }, - "synchronize": false, - "rotation": 0, - "calibrationTopic": "/camera_info" - }, - "foxglovePanelTitle": "" - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "drawerConfig": { - "tracks": [] - }, - "layout": { - "direction": "row", - "first": "3D!3ezwzdr", - "second": { - "first": "command-center-extension.command-center!3xr2po0", - "second": { - "first": { - "first": "Plot!3cog9zw", - "second": "Plot!47kna9v", - "direction": "row" - }, - "second": "Image!3mnp456", - "direction": "column", - "splitPercentage": 38.08411214953271 - }, - "direction": "column", - "splitPercentage": 50.116550116550115 - }, - "splitPercentage": 63.706720977596746 - } -} diff --git a/assets/foxglove_dashboards/old/foxglove_g1_detections.json b/assets/foxglove_dashboards/old/foxglove_g1_detections.json deleted file mode 100644 index 7def24fdaa..0000000000 --- a/assets/foxglove_dashboards/old/foxglove_g1_detections.json +++ /dev/null @@ -1,915 +0,0 @@ -{ - "configById": { - "3D!18i6zy7": { - "layers": { - "845139cb-26bc-40b3-8161-8ab60af4baf5": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", - "layerId": "foxglove.Grid", - "lineWidth": 0.5, - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 1, - "size": 30, - "divisions": 30, - "color": "#248eff57" - }, - "ff758451-8c06-4419-a995-e93c825eb8be": { - "visible": false, - "frameLocked": true, - "label": "Grid", - "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", - "layerId": "foxglove.Grid", - "frameId": "base_link", - "divisions": 6, - "lineWidth": 1.5, - "color": "#24fff4ff", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 2, - "size": 6 - } - }, - "cameraState": { - "perspective": true, - "distance": 17.147499997813583, - "phi": 41.70966129676441, - "thetaOffset": 46.32247127821147, - "targetOffset": [ - 1.489416869802203, - 3.0285403495275056, - -1.5060700211359088 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": false, - "ignoreColladaUpAxis": false, - "syncCamera": true, - "transforms": { - "visible": true, - "showLabel": true, - "editable": true, - "enablePreloading": false, - "labelSize": 0.07 - } - }, - "transforms": { - "frame:camera_link": { - "visible": false - }, - "frame:sensor": { - "visible": false - }, - "frame:sensor_at_scan": { - "visible": false - }, - "frame:map": { - "visible": true - }, - "frame:world": { - "visible": true - } - }, - "topics": { - "/lidar": { - "stixelsEnabled": false, - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 2, - "explicitAlpha": 0.8, - "decayTime": 0, - "cubeSize": 0.05, - "cubeOutline": false, - "minValue": -2 - }, - "/odom": { - "visible": true, - "axisScale": 1 - }, - "/video": { - "visible": false - }, - "/global_map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "decayTime": 0, - "pointShape": "square", - "cubeOutline": false, - "cubeSize": 0.08, - "gradient": [ - "#06011dff", - "#d1e2e2ff" - ], - "stixelsEnabled": false, - "explicitAlpha": 0.339, - "minValue": -0.2, - "pointSize": 5 - }, - "/global_path": { - "visible": true, - "type": "line", - "arrowScale": [ - 1, - 0.15, - 0.15 - ], - "lineWidth": 0.05, - "gradient": [ - "#6bff7cff", - "#0081ffff" - ] - }, - "/global_target": { - "visible": true - }, - "/pt": { - "visible": false - }, - "/global_costmap": { - "visible": true, - "maxColor": "#6b2b2bff", - "frameLocked": false, - "unknownColor": "#80808000", - "colorMode": "custom", - "alpha": 0.517, - "minColor": "#1e00ff00", - "drawBehind": false - }, - "/global_gradient": { - "visible": true, - "maxColor": "#690066ff", - "unknownColor": "#30b89a00", - "minColor": "#00000000", - "colorMode": "custom", - "alpha": 0.3662, - "frameLocked": false, - "drawBehind": false - }, - "/global_cost_field": { - "visible": false, - "maxColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/global_passable": { - "visible": false, - "maxColor": "#ffffff00", - "minColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/image": { - "visible": true, - "cameraInfoTopic": "/camera_info", - "distance": 1.5, - "planarProjectionFactor": 0, - "color": "#e7e1ffff" - }, - "/camera_info": { - "visible": true, - "distance": 1.5, - "planarProjectionFactor": 0 - }, - "/local_costmap": { - "visible": false - }, - "/navigation_goal": { - "visible": true - }, - "/debug_camera_optical_points": { - "stixelsEnabled": false, - "visible": false, - "pointSize": 0.07, - "pointShape": "cube", - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/debug_world_points": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "rainbow", - "pointShape": "cube" - }, - "/filtered_points_suitcase_0": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0808ff", - "cubeSize": 0.149, - "pointSize": 28.57 - }, - "/filtered_points_combined": { - "visible": true, - "flatColor": "#ff0000ff", - "pointShape": "cube", - "pointSize": 6.63, - "colorField": "z", - "colorMode": "gradient", - "colorMap": "rainbow", - "cubeSize": 0.35, - "gradient": [ - "#d100caff", - "#ff0000ff" - ] - }, - "/filtered_points_box_7": { - "visible": true, - "flatColor": "#fbfaffff", - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/filtered_pointcloud": { - "visible": true, - "colorField": "z", - "colorMode": "flat", - "colorMap": "turbo", - "flatColor": "#ff0000ff", - "pointSize": 40.21, - "pointShape": "cube", - "cubeSize": 0.1, - "cubeOutline": true - }, - "/detected": { - "visible": false, - "pointSize": 1.5, - "pointShape": "cube", - "cubeSize": 0.118, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "flatColor": "#d70000ff", - "cubeOutline": true - }, - "/detected_0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 1.6, - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#e00000ff", - "stixelsEnabled": false, - "decayTime": 0, - "cubeOutline": true - }, - "/detected_1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#00ff15ff", - "cubeOutline": true - }, - "/image_detected_0": { - "visible": false - }, - "/detected/pointcloud/1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#15ff00ff", - "pointSize": 0.1, - "cubeSize": 0.05, - "cubeOutline": true - }, - "/detected/pointcloud/2": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#00ffe1ff", - "pointSize": 10, - "cubeOutline": true, - "cubeSize": 0.05 - }, - "/detected/pointcloud/0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0000ff", - "cubeOutline": true, - "cubeSize": 0.04 - }, - "/detected/image/0": { - "visible": false - }, - "/detected/image/3": { - "visible": false - }, - "/detected/pointcloud/3": { - "visible": true, - "pointSize": 1.5, - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#00fffaff", - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo" - }, - "/detected/image/1": { - "visible": false - }, - "/registered_scan": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 2 - }, - "/image/camera_info": { - "visible": true, - "distance": 2 - }, - "/map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "square", - "cubeSize": 0.13, - "explicitAlpha": 1, - "pointSize": 1, - "decayTime": 2 - }, - "/detection3d/markers": { - "visible": true, - "color": "#88ff00ff", - "showOutlines": true, - "selectedIdVariable": "" - }, - "/foxglove/scene_update": { - "visible": true - }, - "/scene_update": { - "visible": true, - "showOutlines": true, - "computeVertexNormals": true - }, - "/target": { - "visible": true, - "axisScale": 1 - }, - "/goal_pose": { - "visible": true, - "axisScale": 0.5 - }, - "/global_pointcloud": { - "visible": true, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/pointcloud_map": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/detectorDB/pointcloud/0": { - "visible": true, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/path_active": { - "visible": true - }, - "/detector3d/image/0": { - "visible": true - }, - "/detector3d/pointcloud/0": { - "visible": true, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/detectorDB/image/0": { - "visible": true - }, - "/detectorDB/scene_update": { - "visible": true - }, - "/detector3d/scene_update": { - "visible": true - }, - "/detector3d/image/1": { - "visible": true - }, - "/g1/camera_info": { - "visible": false - }, - "/detectorDB/image/1": { - "visible": true - } - }, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/estimate", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {}, - "foxglovePanelTitle": "", - "followTf": "camera_link" - }, - "Image!3mnp456": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": false, - "transforms": { - "showLabel": false, - "visible": true - } - }, - "transforms": { - "frame:world": { - "visible": false - }, - "frame:camera_optical": { - "visible": false - }, - "frame:camera_link": { - "visible": false - }, - "frame:base_link": { - "visible": false - }, - "frame:sensor": { - "visible": false - } - }, - "topics": { - "/lidar": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 6, - "explicitAlpha": 0.6, - "pointShape": "circle", - "cubeSize": 0.016 - }, - "/odom": { - "visible": false - }, - "/local_costmap": { - "visible": false - }, - "/global_costmap": { - "visible": false, - "minColor": "#ffffff00" - }, - "/detected_0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 23, - "pointShape": "cube", - "cubeSize": 0.04, - "flatColor": "#ff0000ff", - "stixelsEnabled": false - }, - "/detected_1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 20.51, - "flatColor": "#34ff00ff", - "pointShape": "cube", - "cubeSize": 0.04, - "cubeOutline": false - }, - "/filtered_pointcloud": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "rainbow", - "pointSize": 1.5, - "pointShape": "cube", - "flatColor": "#ff0000ff", - "cubeSize": 0.1 - }, - "/global_map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "cube", - "pointSize": 5 - }, - "/detected/pointcloud/1": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.01, - "flatColor": "#00ff1eff", - "pointSize": 15, - "decayTime": 0, - "cubeOutline": true - }, - "/detected/pointcloud/2": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "circle", - "cubeSize": 0.1, - "flatColor": "#00fbffff", - "pointSize": 0.01 - }, - "/detected/pointcloud/0": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0000ff", - "pointSize": 15, - "cubeOutline": true, - "cubeSize": 0.03 - }, - "/registered_scan": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 6.49 - }, - "/detection3d/markers": { - "visible": false - }, - "/foxglove/scene_update": { - "visible": true - }, - "/scene_update": { - "visible": false - }, - "/map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 8 - } - }, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/image", - "colorMode": "gradient", - "annotations": { - "/detections": { - "visible": true - }, - "/annotations": { - "visible": true - }, - "/detector3d/annotations": { - "visible": true - }, - "/detectorDB/annotations": { - "visible": true - } - }, - "synchronize": false, - "rotation": 0, - "calibrationTopic": "/camera_info" - }, - "foxglovePanelTitle": "" - }, - "Plot!3heo336": { - "paths": [ - { - "timestampMethod": "publishTime", - "value": "/image.header.stamp.nsec", - "enabled": true, - "color": "#4e98e2", - "label": "image", - "showLine": true - }, - { - "timestampMethod": "publishTime", - "value": "/map.header.stamp.nsec", - "enabled": true, - "color": "#f5774d", - "label": "lidar", - "showLine": true - }, - { - "timestampMethod": "publishTime", - "value": "/tf.transforms[0].header.stamp.nsec", - "enabled": true, - "color": "#f7df71", - "label": "tf", - "showLine": true - }, - { - "timestampMethod": "publishTime", - "value": "/odom.header.stamp.nsec", - "enabled": true, - "color": "#5cd6a9", - "label": "odom", - "showLine": true - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - }, - "StateTransitions!2wj5twf": { - "paths": [ - { - "value": "/detectorDB/annotations.texts[0].text", - "timestampMethod": "receiveTime", - "customStates": { - "type": "discrete", - "states": [] - } - }, - { - "value": "/detectorDB/annotations.texts[1].text", - "timestampMethod": "receiveTime", - "customStates": { - "type": "discrete", - "states": [] - } - }, - { - "value": "/detectorDB/annotations.texts[2].text", - "timestampMethod": "receiveTime", - "customStates": { - "type": "discrete", - "states": [] - } - } - ], - "isSynced": true - }, - "Image!47pi3ov": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detector3d/image/0" - } - }, - "Image!4kk50gw": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detectorDB/image/1" - } - }, - "Image!2348e0b": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detectorDB/image/2", - "synchronize": false - } - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "drawerConfig": { - "tracks": [] - }, - "layout": { - "first": { - "first": "3D!18i6zy7", - "second": "Image!3mnp456", - "direction": "row", - "splitPercentage": 44.31249231586115 - }, - "second": { - "first": { - "first": "Plot!3heo336", - "second": "StateTransitions!2wj5twf", - "direction": "column" - }, - "second": { - "first": "Image!47pi3ov", - "second": { - "first": "Image!4kk50gw", - "second": "Image!2348e0b", - "direction": "row" - }, - "direction": "row", - "splitPercentage": 33.06523681858802 - }, - "direction": "row", - "splitPercentage": 46.39139486467731 - }, - "direction": "column", - "splitPercentage": 65.20874751491054 - } -} diff --git a/assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json b/assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json deleted file mode 100644 index e68b79a7e4..0000000000 --- a/assets/foxglove_dashboards/old/foxglove_image_sharpness_test.json +++ /dev/null @@ -1,140 +0,0 @@ -{ - "configById": { - "Image!1dpphsz": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/all" - } - }, - "Image!2xvd0hl": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/sharp" - } - }, - "Gauge!1iofczz": { - "path": "/sharpness.x", - "minValue": 0, - "maxValue": 1, - "colorMap": "red-yellow-green", - "colorMode": "colormap", - "gradient": [ - "#0000ff", - "#ff00ff" - ], - "reverse": false - }, - "Plot!1gy7vh9": { - "paths": [ - { - "timestampMethod": "receiveTime", - "value": "/sharpness.x", - "enabled": true, - "color": "#4e98e2" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "layout": { - "first": { - "first": "Image!1dpphsz", - "second": "Image!2xvd0hl", - "direction": "row" - }, - "second": { - "first": "Gauge!1iofczz", - "second": "Plot!1gy7vh9", - "direction": "row" - }, - "direction": "column" - } -} diff --git a/assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json b/assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json deleted file mode 100644 index df4e2715bc..0000000000 --- a/assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json +++ /dev/null @@ -1,288 +0,0 @@ -{ - "configById": { - "3D!18i6zy7": { - "layers": { - "845139cb-26bc-40b3-8161-8ab60af4baf5": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", - "layerId": "foxglove.Grid", - "lineWidth": 0.5, - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 1, - "size": 30, - "divisions": 30, - "color": "#248eff57" - }, - "ff758451-8c06-4419-a995-e93c825eb8be": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", - "layerId": "foxglove.Grid", - "frameId": "base_link", - "size": 3, - "divisions": 3, - "lineWidth": 1.5, - "color": "#24fff4ff", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 2 - } - }, - "cameraState": { - "perspective": false, - "distance": 25.847108697365048, - "phi": 32.532756465990374, - "thetaOffset": -179.288640038416, - "targetOffset": [ - 1.620731759058286, - -2.9069622235988986, - -0.09942375087215619 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": true, - "ignoreColladaUpAxis": false, - "syncCamera": false, - "transforms": { - "visible": true - } - }, - "transforms": {}, - "topics": { - "/lidar": { - "stixelsEnabled": false, - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 10, - "explicitAlpha": 1, - "decayTime": 0, - "cubeSize": 0.1, - "minValue": -0.3, - "cubeOutline": false - }, - "/odom": { - "visible": true, - "axisScale": 1 - }, - "/video": { - "visible": false - }, - "/global_map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 10, - "decayTime": 0, - "pointShape": "cube", - "cubeOutline": false, - "cubeSize": 0.08, - "gradient": [ - "#06011dff", - "#d1e2e2ff" - ], - "stixelsEnabled": false, - "explicitAlpha": 1, - "minValue": -0.2 - }, - "/global_path": { - "visible": true, - "type": "line", - "arrowScale": [ - 1, - 0.15, - 0.15 - ], - "lineWidth": 0.132, - "gradient": [ - "#6bff7cff", - "#0081ffff" - ] - }, - "/global_target": { - "visible": true - }, - "/pt": { - "visible": false - }, - "/global_costmap": { - "visible": true, - "maxColor": "#8d3939ff", - "frameLocked": false, - "unknownColor": "#80808000", - "colorMode": "custom", - "alpha": 0.517, - "minColor": "#1e00ff00" - }, - "/global_gradient": { - "visible": true, - "maxColor": "#690066ff", - "unknownColor": "#30b89a00", - "minColor": "#00000000", - "colorMode": "custom", - "alpha": 0.3662, - "frameLocked": false, - "drawBehind": false - }, - "/global_cost_field": { - "visible": false, - "maxColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/global_passable": { - "visible": false, - "maxColor": "#ffffff00", - "minColor": "#ff0000ff", - "unknownColor": "#80808000" - } - }, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/estimate", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {}, - "foxglovePanelTitle": "test", - "followTf": "world" - }, - "Image!3mnp456": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": true - }, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/video", - "colorMode": "gradient" - }, - "foxglovePanelTitle": "/video" - }, - "Plot!a1gj37": { - "paths": [ - { - "timestampMethod": "receiveTime", - "value": "/odom.pose.position.y", - "enabled": true, - "color": "#4e98e2" - }, - { - "timestampMethod": "receiveTime", - "value": "/odom.pose.position.x", - "enabled": true, - "color": "#f5774d" - }, - { - "timestampMethod": "receiveTime", - "value": "/odom.pose.position.z", - "enabled": true, - "color": "#f7df71" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "drawerConfig": { - "tracks": [] - }, - "layout": { - "first": "3D!18i6zy7", - "second": { - "first": "Image!3mnp456", - "second": "Plot!a1gj37", - "direction": "column", - "splitPercentage": 28.030303030303028 - }, - "direction": "row", - "splitPercentage": 69.43271928754422 - } -} diff --git a/assets/foxglove_dashboards/old/foxglove_unitree_yolo.json b/assets/foxglove_dashboards/old/foxglove_unitree_yolo.json deleted file mode 100644 index ab53e4a71e..0000000000 --- a/assets/foxglove_dashboards/old/foxglove_unitree_yolo.json +++ /dev/null @@ -1,849 +0,0 @@ -{ - "configById": { - "3D!18i6zy7": { - "layers": { - "845139cb-26bc-40b3-8161-8ab60af4baf5": { - "visible": true, - "frameLocked": true, - "label": "Grid", - "instanceId": "845139cb-26bc-40b3-8161-8ab60af4baf5", - "layerId": "foxglove.Grid", - "lineWidth": 0.5, - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 1, - "size": 30, - "divisions": 30, - "color": "#248eff57" - }, - "ff758451-8c06-4419-a995-e93c825eb8be": { - "visible": false, - "frameLocked": true, - "label": "Grid", - "instanceId": "ff758451-8c06-4419-a995-e93c825eb8be", - "layerId": "foxglove.Grid", - "frameId": "base_link", - "divisions": 6, - "lineWidth": 1.5, - "color": "#24fff4ff", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "order": 2, - "size": 6 - } - }, - "cameraState": { - "perspective": true, - "distance": 13.268408624096915, - "phi": 26.658696672199024, - "thetaOffset": 99.69918626426482, - "targetOffset": [ - 1.740213570345715, - 0.7318803628974015, - -1.5060700211358968 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": false, - "ignoreColladaUpAxis": false, - "syncCamera": true, - "transforms": { - "visible": true, - "showLabel": true, - "editable": true, - "enablePreloading": false, - "labelSize": 0.07 - } - }, - "transforms": { - "frame:camera_link": { - "visible": false - }, - "frame:sensor": { - "visible": false - }, - "frame:sensor_at_scan": { - "visible": false - }, - "frame:map": { - "visible": true - } - }, - "topics": { - "/lidar": { - "stixelsEnabled": false, - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 2, - "explicitAlpha": 0.8, - "decayTime": 0, - "cubeSize": 0.05, - "cubeOutline": false, - "minValue": -2 - }, - "/odom": { - "visible": true, - "axisScale": 1 - }, - "/video": { - "visible": false - }, - "/global_map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "decayTime": 0, - "pointShape": "square", - "cubeOutline": false, - "cubeSize": 0.08, - "gradient": [ - "#06011dff", - "#d1e2e2ff" - ], - "stixelsEnabled": false, - "explicitAlpha": 0.339, - "minValue": -0.2, - "pointSize": 5 - }, - "/global_path": { - "visible": true, - "type": "line", - "arrowScale": [ - 1, - 0.15, - 0.15 - ], - "lineWidth": 0.05, - "gradient": [ - "#6bff7cff", - "#0081ffff" - ] - }, - "/global_target": { - "visible": true - }, - "/pt": { - "visible": false - }, - "/global_costmap": { - "visible": false, - "maxColor": "#6b2b2bff", - "frameLocked": false, - "unknownColor": "#80808000", - "colorMode": "custom", - "alpha": 0.517, - "minColor": "#1e00ff00", - "drawBehind": false - }, - "/global_gradient": { - "visible": true, - "maxColor": "#690066ff", - "unknownColor": "#30b89a00", - "minColor": "#00000000", - "colorMode": "custom", - "alpha": 0.3662, - "frameLocked": false, - "drawBehind": false - }, - "/global_cost_field": { - "visible": false, - "maxColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/global_passable": { - "visible": false, - "maxColor": "#ffffff00", - "minColor": "#ff0000ff", - "unknownColor": "#80808000" - }, - "/image": { - "visible": true, - "cameraInfoTopic": "/camera_info", - "distance": 1.5, - "planarProjectionFactor": 0, - "color": "#e7e1ffff" - }, - "/camera_info": { - "visible": true, - "distance": 1.5, - "planarProjectionFactor": 0 - }, - "/local_costmap": { - "visible": false - }, - "/navigation_goal": { - "visible": true - }, - "/debug_camera_optical_points": { - "stixelsEnabled": false, - "visible": false, - "pointSize": 0.07, - "pointShape": "cube", - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/debug_world_points": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "rainbow", - "pointShape": "cube" - }, - "/filtered_points_suitcase_0": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0808ff", - "cubeSize": 0.149, - "pointSize": 28.57 - }, - "/filtered_points_combined": { - "visible": true, - "flatColor": "#ff0000ff", - "pointShape": "cube", - "pointSize": 6.63, - "colorField": "z", - "colorMode": "gradient", - "colorMap": "rainbow", - "cubeSize": 0.35, - "gradient": [ - "#d100caff", - "#ff0000ff" - ] - }, - "/filtered_points_box_7": { - "visible": true, - "flatColor": "#fbfaffff", - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo" - }, - "/filtered_pointcloud": { - "visible": true, - "colorField": "z", - "colorMode": "flat", - "colorMap": "turbo", - "flatColor": "#ff0000ff", - "pointSize": 40.21, - "pointShape": "cube", - "cubeSize": 0.1, - "cubeOutline": true - }, - "/detected": { - "visible": false, - "pointSize": 1.5, - "pointShape": "cube", - "cubeSize": 0.118, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "flatColor": "#d70000ff", - "cubeOutline": true - }, - "/detected_0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 1.6, - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#e00000ff", - "stixelsEnabled": false, - "decayTime": 0, - "cubeOutline": true - }, - "/detected_1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#00ff15ff", - "cubeOutline": true - }, - "/image_detected_0": { - "visible": false - }, - "/detected/pointcloud/1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#15ff00ff", - "pointSize": 0.1, - "cubeSize": 0.05, - "cubeOutline": true - }, - "/detected/pointcloud/2": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#00ffe1ff", - "pointSize": 10, - "cubeOutline": true, - "cubeSize": 0.05 - }, - "/detected/pointcloud/0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0000ff", - "cubeOutline": true, - "cubeSize": 0.04 - }, - "/detected/image/0": { - "visible": false - }, - "/detected/image/3": { - "visible": false - }, - "/detected/pointcloud/3": { - "visible": true, - "pointSize": 1.5, - "pointShape": "cube", - "cubeSize": 0.1, - "flatColor": "#00fffaff", - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo" - }, - "/detected/image/1": { - "visible": false - }, - "/registered_scan": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 2 - }, - "/image/camera_info": { - "visible": true, - "distance": 2 - }, - "/map": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "square", - "cubeSize": 0.13, - "explicitAlpha": 1, - "pointSize": 1, - "decayTime": 2 - }, - "/detection3d/markers": { - "visible": true, - "color": "#88ff00ff", - "showOutlines": true, - "selectedIdVariable": "" - }, - "/foxglove/scene_update": { - "visible": true - }, - "/scene_update": { - "visible": true, - "showOutlines": true, - "computeVertexNormals": true - }, - "/target": { - "visible": true, - "axisScale": 1 - }, - "/goal_pose": { - "visible": true, - "axisScale": 0.5 - } - }, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/estimate", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {}, - "foxglovePanelTitle": "", - "followTf": "map" - }, - "Image!3mnp456": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": { - "enableStats": false, - "transforms": { - "showLabel": false, - "visible": true - } - }, - "transforms": { - "frame:world": { - "visible": true - }, - "frame:camera_optical": { - "visible": false - }, - "frame:camera_link": { - "visible": false - }, - "frame:base_link": { - "visible": false - } - }, - "topics": { - "/lidar": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 6, - "explicitAlpha": 0.6, - "pointShape": "circle", - "cubeSize": 0.016 - }, - "/odom": { - "visible": false - }, - "/local_costmap": { - "visible": false - }, - "/global_costmap": { - "visible": false, - "minColor": "#ffffff00" - }, - "/detected_0": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 23, - "pointShape": "cube", - "cubeSize": 0.04, - "flatColor": "#ff0000ff", - "stixelsEnabled": false - }, - "/detected_1": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointSize": 20.51, - "flatColor": "#34ff00ff", - "pointShape": "cube", - "cubeSize": 0.04, - "cubeOutline": false - }, - "/filtered_pointcloud": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "rainbow", - "pointSize": 1.5, - "pointShape": "cube", - "flatColor": "#ff0000ff", - "cubeSize": 0.1 - }, - "/global_map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "cube", - "pointSize": 5 - }, - "/detected/pointcloud/1": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "cubeSize": 0.01, - "flatColor": "#00ff1eff", - "pointSize": 15, - "decayTime": 0, - "cubeOutline": true - }, - "/detected/pointcloud/2": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "circle", - "cubeSize": 0.1, - "flatColor": "#00fbffff", - "pointSize": 0.01 - }, - "/detected/pointcloud/0": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "colorMap": "turbo", - "pointShape": "cube", - "flatColor": "#ff0000ff", - "pointSize": 15, - "cubeOutline": true, - "cubeSize": 0.03 - }, - "/registered_scan": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointShape": "circle", - "pointSize": 6.49 - }, - "/detection3d/markers": { - "visible": false - }, - "/foxglove/scene_update": { - "visible": true - }, - "/scene_update": { - "visible": false - }, - "/map": { - "visible": false, - "colorField": "z", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 8 - } - }, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/image", - "colorMode": "gradient", - "annotations": { - "/detections": { - "visible": true - }, - "/annotations": { - "visible": true - } - }, - "synchronize": false, - "rotation": 0, - "calibrationTopic": "/camera_info" - }, - "foxglovePanelTitle": "" - }, - "Plot!3heo336": { - "paths": [ - { - "timestampMethod": "publishTime", - "value": "/image.header.stamp.sec", - "enabled": true, - "color": "#4e98e2", - "label": "image", - "showLine": false - }, - { - "timestampMethod": "publishTime", - "value": "/map.header.stamp.sec", - "enabled": true, - "color": "#f5774d", - "label": "lidar", - "showLine": false - }, - { - "timestampMethod": "publishTime", - "value": "/tf.transforms[0].header.stamp.sec", - "enabled": true, - "color": "#f7df71", - "label": "tf", - "showLine": false - }, - { - "timestampMethod": "publishTime", - "value": "/odom.header.stamp.sec", - "enabled": true, - "color": "#5cd6a9", - "label": "odom", - "showLine": false - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": false, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240 - }, - "Image!47pi3ov": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detected/image/0" - } - }, - "Image!4kk50gw": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detected/image/1" - } - }, - "Image!2348e0b": { - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": { - "imageTopic": "/detected/image/2", - "synchronize": false - } - }, - "StateTransitions!pu21x4": { - "paths": [ - { - "value": "/annotations.texts[1].text", - "timestampMethod": "receiveTime", - "label": "detection1" - }, - { - "value": "/annotations.texts[3].text", - "timestampMethod": "receiveTime", - "label": "detection2" - }, - { - "value": "/annotations.texts[5].text", - "timestampMethod": "receiveTime", - "label": "detection3" - } - ], - "isSynced": true, - "showPoints": true, - "timeWindowMode": "automatic" - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "drawerConfig": { - "tracks": [] - }, - "layout": { - "first": { - "first": "3D!18i6zy7", - "second": "Image!3mnp456", - "direction": "row", - "splitPercentage": 47.265625 - }, - "second": { - "first": "Plot!3heo336", - "second": { - "first": { - "first": "Image!47pi3ov", - "second": { - "first": "Image!4kk50gw", - "second": "Image!2348e0b", - "direction": "row" - }, - "direction": "row", - "splitPercentage": 33.06523681858802 - }, - "second": "StateTransitions!pu21x4", - "direction": "column", - "splitPercentage": 86.63101604278076 - }, - "direction": "row", - "splitPercentage": 46.39139486467731 - }, - "direction": "column", - "splitPercentage": 81.62970106075217 - } -} diff --git a/assets/framecount.mp4 b/assets/framecount.mp4 deleted file mode 100644 index 759ee6ab27..0000000000 --- a/assets/framecount.mp4 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92256a9cceda2410ec26d58b92f457070e54deb39bf3e6e5aca174e2c7cff216 -size 34548239 diff --git a/assets/license_file_header.txt b/assets/license_file_header.txt deleted file mode 100644 index a02322f92f..0000000000 --- a/assets/license_file_header.txt +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2025 Dimensional Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/assets/readme/agentic_control.gif b/assets/readme/agentic_control.gif deleted file mode 100644 index f9f5970441..0000000000 --- a/assets/readme/agentic_control.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb0411de5e5967be8773d5d95e692a6a5859f75bb400164451a3b383b1025fb4 -size 2416274 diff --git a/assets/readme/agents.png b/assets/readme/agents.png deleted file mode 100644 index b05bee0b03..0000000000 --- a/assets/readme/agents.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a255d32f9a0ecff12d99dda9b8a51e0958ac282d7a0f814f93fd39261afaf84d -size 477123 diff --git a/assets/readme/dimos_demo.gif b/assets/readme/dimos_demo.gif deleted file mode 100644 index 5a68bd72ac..0000000000 --- a/assets/readme/dimos_demo.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fda7f7a859ce98002e0faef88fb2942f395e19995b36b585c48447ec5a9435ee -size 24011189 diff --git a/assets/readme/lidar.gif b/assets/readme/lidar.gif deleted file mode 100644 index 8302c2957d..0000000000 --- a/assets/readme/lidar.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d47badc970572aa7badf98c908490c8b86ea9f1cafbb18507cfdb5d08655cdfb -size 5900150 diff --git a/assets/readme/lidar.png b/assets/readme/lidar.png deleted file mode 100644 index 1b499de10f..0000000000 --- a/assets/readme/lidar.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65b1797fd9ac8edae5dce0691397b6aca2e975badfd58462ed8e20a4dace655e -size 927067 diff --git a/assets/readme/navigation.gif b/assets/readme/navigation.gif deleted file mode 100644 index 1402b1e85a..0000000000 --- a/assets/readme/navigation.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:64e7965f421916cdb71667a9ed99eab96c14c64bd195bd483628d1b9b9a4e95c -size 4395592 diff --git a/assets/readme/navigation.png b/assets/readme/navigation.png deleted file mode 100644 index 16819a5007..0000000000 --- a/assets/readme/navigation.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:472cabca4b0d661658bf9ffbde78e636668e9ef6499dc38ea0f552557d735bd9 -size 617989 diff --git a/assets/readme/perception.png b/assets/readme/perception.png deleted file mode 100644 index 7ec15aabbf..0000000000 --- a/assets/readme/perception.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:48e4c61c1ec588d56d61a74fd9f0d9251eadc042e7a514fb1896826d52a32988 -size 797817 diff --git a/assets/readme/spacer.png b/assets/readme/spacer.png deleted file mode 100644 index 8745fc9687..0000000000 --- a/assets/readme/spacer.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a16ec40112698cf02b9abd3d18c8db65ce40f48f2c61076b45de58695f16532 -size 66 diff --git a/assets/readme/spatial_memory.gif b/assets/readme/spatial_memory.gif deleted file mode 100644 index 070c65270b..0000000000 --- a/assets/readme/spatial_memory.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:50b9cb7898ae8d238a088252fd96d2278a1be96a0dbb761839bc58c99c17f7a7 -size 4655580 diff --git a/assets/simple_demo.mp4 b/assets/simple_demo.mp4 deleted file mode 100644 index cb8a635e78..0000000000 --- a/assets/simple_demo.mp4 +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ff2459b880baaa509e8e0de8a45e8da48ebf7cb28d4927c62b10906baa83bda0 -size 50951922 diff --git a/assets/simple_demo_small.gif b/assets/simple_demo_small.gif deleted file mode 100644 index 3c2cf54ef4..0000000000 --- a/assets/simple_demo_small.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a2b9a95d5b27cbc135cb84f6c6bc2131fa234403466befd2ee8ea81e2b2de45 -size 33374003 diff --git a/assets/trimmed_video_office.mov b/assets/trimmed_video_office.mov deleted file mode 100644 index a3072be8fc..0000000000 --- a/assets/trimmed_video_office.mov +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d72f0cf95ce1728b4a0855d6b3fe4573f5e2e86fae718720c19a84198bdcbf9d -size 2311156 diff --git a/bin/agent_web b/bin/agent_web deleted file mode 100755 index 210bf7dd3d..0000000000 --- a/bin/agent_web +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -python3 /app/tests/test_planning_agent_web_interface.py diff --git a/bin/cuda/fix_ort.sh b/bin/cuda/fix_ort.sh deleted file mode 100755 index 182f387364..0000000000 --- a/bin/cuda/fix_ort.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash -# This script fixes the onnxruntime <--> onnxruntime-gpu package clash -# that occurs when chromadb and other dependencies require the CPU-only -# onnxruntime package. It removes onnxruntime and reinstalls the GPU version. -set -euo pipefail - -: "${GPU_VER:=1.18.1}" - -python - </dev/null -} - -image_pull() { - docker pull "$IMAGE_TAG" -} - -ensure_image_downloaded() { - if ! image_exists "$1"; then - echo "Image ${IMAGE_TAG} not found. Pulling..." - image_pull "$1" - fi -} - -check_image_running() { - if docker ps -q --filter "ancestor=${IMAGE_TAG}" | grep -q .; then - return 0 - else - return 1 - fi -} - -stop_image() { - if check_image_running ${IMAGE_TAG}; then - echo "Stopping containers from image ${IMAGE_TAG}..." - docker stop $(docker ps -q --filter "ancestor=${IMAGE_TAG}") - else - echo "No containers from image ${IMAGE_TAG} are running." - fi -} - - -get_branch_tag() { - local branch_name - branch_name=$(git rev-parse --abbrev-ref HEAD) - - case "${branch_name}" in - master) image_tag="latest" ;; - main) image_tag="latest" ;; - dev) image_tag="dev" ;; - *) - image_tag=$(echo "${branch_name}" \ - | tr '[:upper:]' '[:lower:]' \ - | sed -E 's#[^a-z0-9_.-]+#_#g' \ - | sed -E 's#^-+|-+$##g') - ;; - esac - echo "${image_tag}" -} - - -build_image() { - local image_tag - image_tag=$(get_branch_tag) - - docker build \ - --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ - --build-arg GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD) \ - -t "ghcr.io/dimensionalos/ros-dev:${image_tag}" -f docker/dev/Dockerfile . -} - -remove_image() { - local tag=$(get_branch_tag) - docker rm -f "dimos-dev-${tag}" 2>/dev/null || true -} - -devcontainer_install() { - # prompt user if we should install devcontainer - read -p "devcontainer CLI (https://github.com/devcontainers/cli) not found. Install into repo root? (y/n): " install_choice - if [[ "$install_choice" != "y" && "$install_choice" != "Y" ]]; then - echo "Devcontainer CLI installation aborted. Please install manually" - exit 1 - fi - - cd "$REPO_ROOT/bin/" - if [[ ! -d "$REPO_ROOT/bin/node_modules" ]]; then - npm init -y 1>/dev/null - fi - npm install @devcontainers/cli 1>&2 - if [[ $? -ne 0 ]]; then - echo "Failed to install devcontainer CLI. Please install it manually." - exit 1 - fi - echo $REPO_ROOT/bin/node_modules/.bin/devcontainer -} - - - -find_devcontainer_bin() { - local bin_path - bin_path=$(command -v devcontainer) - - if [[ -z "$bin_path" ]]; then - bin_path="$REPO_ROOT/bin/node_modules/.bin/devcontainer" - fi - - if [[ -x "$bin_path" ]]; then - echo "$bin_path" - else - devcontainer_install - fi -} - -# Passes all arguments to devcontainer command, ensuring: -# - devcontainer CLI is installed -# - docker image is running -# - the workspace folder is set to the repository root -run_devcontainer() { - local devcontainer_bin - devcontainer_bin=$(find_devcontainer_bin) - - if ! check_image_running; then - ensure_image_downloaded - $devcontainer_bin up --workspace-folder="$REPO_ROOT" --gpu-availability="detect" - fi - - exec $devcontainer_bin $1 --workspace-folder="$REPO_ROOT" "${@:2}" -} - -if [[ $# -eq 0 ]]; then - run_devcontainer exec bash -else - case "$1" in - build) - build_image - shift - ;; - stop) - stop_image - shift - ;; - down) - stop_image - shift - ;; - pull) - docker pull "${IMAGE_TAG}" - shift - ;; - *) - run_devcontainer exec "$@" - shift - ;; - esac -fi diff --git a/bin/dockerbuild b/bin/dockerbuild deleted file mode 100755 index b02e10d5ca..0000000000 --- a/bin/dockerbuild +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Exit on error -set -e - -# Check for directory argument -if [ $# -lt 1 ]; then - echo "Usage: $0 [additional-docker-build-args]" - echo "Example: $0 base-ros-python --no-cache" - exit 1 -fi - -# Get the docker directory name -DOCKER_DIR=$1 -shift # Remove the first argument, leaving any additional args - -# Check if directory exists -if [ ! -d "docker/$DOCKER_DIR" ]; then - echo "Error: Directory docker/$DOCKER_DIR does not exist" - exit 1 -fi - -# Set image name based on directory -IMAGE_NAME="ghcr.io/dimensionalos/$DOCKER_DIR" - -echo "Building image $IMAGE_NAME from docker/$DOCKER_DIR..." -echo "Build context: $(pwd)" - -# Build the docker image with the current directory as context -docker build -t "$IMAGE_NAME" -f "docker/$DOCKER_DIR/Dockerfile" "$@" . - -echo "Successfully built $IMAGE_NAME" diff --git a/bin/doclinks b/bin/doclinks deleted file mode 100755 index 5dee1c69b0..0000000000 --- a/bin/doclinks +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -python "$REPO_ROOT/dimos/utils/docs/doclinks.py" "$@" diff --git a/bin/filter-errors-after-date b/bin/filter-errors-after-date deleted file mode 100755 index 03c7de0ca7..0000000000 --- a/bin/filter-errors-after-date +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -# Used to filter errors to only show lines committed on or after a specific date -# Can be chained with filter-errors-for-user - -from datetime import datetime -import re -import subprocess -import sys - -_blame = {} - - -def _is_after_date(file, line_no, cutoff_date): - if file not in _blame: - _blame[file] = _get_git_blame_dates_for_file(file) - line_date = _blame[file].get(line_no) - if not line_date: - return False - return line_date >= cutoff_date - - -def _get_git_blame_dates_for_file(file_name): - try: - result = subprocess.run( - ["git", "blame", "--date=short", file_name], - capture_output=True, - text=True, - check=True, - ) - - blame_map = {} - # Each line looks like: ^abc123 (Author Name 2024-01-01 1) code - blame_pattern = re.compile(r"^[^\(]+\([^\)]+(\d{4}-\d{2}-\d{2})") - - for i, line in enumerate(result.stdout.split("\n")): - if not line: - continue - match = blame_pattern.match(line) - if match: - date_str = match.group(1) - blame_map[str(i + 1)] = date_str - - return blame_map - except subprocess.CalledProcessError: - return {} - - -def main(): - if len(sys.argv) != 2: - print("Usage: filter-errors-after-date ", file=sys.stderr) - print(" Example: filter-errors-after-date 2025-10-04", file=sys.stderr) - sys.exit(1) - - cutoff_date = sys.argv[1] - - try: - datetime.strptime(cutoff_date, "%Y-%m-%d") - except ValueError: - print(f"Error: Invalid date format '{cutoff_date}'. Use YYYY-MM-DD", file=sys.stderr) - sys.exit(1) - - for line in sys.stdin.readlines(): - split = re.findall(r"^([^:]+):(\d+):(.*)", line) - if not split or len(split[0]) != 3: - continue - - file, line_no = split[0][:2] - if not file.startswith("dimos/"): - continue - - if _is_after_date(file, line_no, cutoff_date): - print(":".join(split[0])) - - -if __name__ == "__main__": - main() diff --git a/bin/filter-errors-for-user b/bin/filter-errors-for-user deleted file mode 100755 index 045b30b293..0000000000 --- a/bin/filter-errors-for-user +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 - -# Used when running `./bin/mypy-strict --for-me` - -import re -import subprocess -import sys - -_blame = {} - - -def _is_for_user(file, line_no, user_email): - if file not in _blame: - _blame[file] = _get_git_blame_for_file(file) - return _blame[file][line_no] == user_email - - -def _get_git_blame_for_file(file_name): - try: - result = subprocess.run( - ["git", "blame", "--show-email", "-e", file_name], - capture_output=True, - text=True, - check=True, - ) - - blame_map = {} - # Each line looks like: ^abc123 ( 2024-01-01 12:00:00 +0000 1) code - blame_pattern = re.compile(r"^[^\(]+\(<([^>]+)>") - - for i, line in enumerate(result.stdout.split("\n")): - if not line: - continue - match = blame_pattern.match(line) - if match: - email = match.group(1) - blame_map[str(i + 1)] = email - - return blame_map - except subprocess.CalledProcessError: - return {} - - -def main(): - if len(sys.argv) != 2: - print("Usage: filter-errors-for-user ", file=sys.stderr) - sys.exit(1) - - user_email = sys.argv[1] - - for line in sys.stdin.readlines(): - split = re.findall(r"^([^:]+):(\d+):(.*)", line) - if not split or len(split[0]) != 3: - continue - file, line_no = split[0][:2] - if not file.startswith("dimos/"): - continue - if _is_for_user(file, line_no, user_email): - print(":".join(split[0])) - - -if __name__ == "__main__": - main() diff --git a/bin/hooks/filter_commit_message.py b/bin/hooks/filter_commit_message.py deleted file mode 100644 index d22eaf9484..0000000000 --- a/bin/hooks/filter_commit_message.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path -import sys - - -def main() -> int: - if len(sys.argv) < 2: - print("Usage: filter_commit_message.py ", file=sys.stderr) - return 1 - - commit_msg_file = Path(sys.argv[1]) - if not commit_msg_file.exists(): - return 0 - - lines = commit_msg_file.read_text().splitlines(keepends=True) - - # Patterns that trigger truncation (everything from this line onwards is removed) - truncate_patterns = [ - "Generated with", - "Co-Authored-By", - ] - - # Find the first line containing any truncate pattern and truncate there - filtered_lines = [] - for line in lines: - if any(pattern in line for pattern in truncate_patterns): - break - filtered_lines.append(line) - - commit_msg_file.write_text("".join(filtered_lines)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/bin/hooks/largefiles_check b/bin/hooks/largefiles_check deleted file mode 100755 index 190183ecc6..0000000000 --- a/bin/hooks/largefiles_check +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env python3 -"""Pre-commit hook to detect large files that should be in LFS.""" - -import argparse -import fnmatch -import os -import shutil -import subprocess -import sys - -import tomli - -parser = argparse.ArgumentParser() -parser.add_argument("--all", action="store_true", help="Check all files in repo, not just staged") -args = parser.parse_args() - -# Check git-lfs is installed -if not shutil.which("git-lfs"): - print("git-lfs is not installed.") - print("\nInstall with:") - print(" Arch: pacman -S git-lfs") - print(" Ubuntu: apt install git-lfs") - print(" macOS: brew install git-lfs") - print("\nThen run: git lfs install") - sys.exit(1) - -# Load config -with open("pyproject.toml", "rb") as f: - config = tomli.load(f).get("tool", {}).get("largefiles", {}) - -max_size_kb = config.get("max_size_kb", 50) -max_bytes = max_size_kb * 1024 -ignore_patterns = config.get("ignore", []) - -# Get LFS files to exclude -result = subprocess.run( - ["git", "lfs", "ls-files", "-n"], capture_output=True, text=True, check=True -) -lfs_files = set(result.stdout.splitlines()) - -# Get files to check -if args.all: - files_cmd = ["git", "ls-files"] -else: - files_cmd = ["git", "diff", "--cached", "--name-only"] - -violations = [] -result = subprocess.run(files_cmd, capture_output=True, text=True, check=True) -for file in result.stdout.splitlines(): - if file in lfs_files: - continue - if any(fnmatch.fnmatch(file, p) for p in ignore_patterns): - continue - if os.path.isfile(file) and os.path.getsize(file) > max_bytes: - violations.append((file, os.path.getsize(file))) - -if violations: - print(f"Large files detected (limit: {max_size_kb}KB):") - for f, size in sorted(violations, key=lambda x: -x[1]): - print(f" {size // 1024}KB {f}") - print("\nEither add to LFS or to [tool.largefiles].ignore in pyproject.toml") - sys.exit(1) diff --git a/bin/hooks/lfs_check b/bin/hooks/lfs_check deleted file mode 100755 index 0ddb847d56..0000000000 --- a/bin/hooks/lfs_check +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -ROOT=$(git rev-parse --show-toplevel) -cd $ROOT - -new_data=() - -# Enable nullglob to make globs expand to nothing when not matching -shopt -s nullglob - -# Iterate through all directories in data/ -for dir_path in data/*; do - - # Extract directory name - dir_name=$(basename "$dir_path") - - # Skip .lfs directory if it exists - [ "$dir_name" = ".lfs" ] && continue - - # Define compressed file path - compressed_file="data/.lfs/${dir_name}.tar.gz" - - # Check if compressed file already exists - if [ -f "$compressed_file" ]; then - continue - fi - - new_data+=("$dir_name") -done - -if [ ${#new_data[@]} -gt 0 ]; then - echo -e "${RED}✗${NC} New test data detected at /data:" - echo -e " ${GREEN}${new_data[@]}${NC}" - echo -e "\nEither delete or run ${GREEN}./bin/lfs_push${NC}" - echo -e "(lfs_push will compress the files into /data/.lfs/, upload to LFS, and add them to your commit)" - exit 1 -fi diff --git a/bin/lfs_push b/bin/lfs_push deleted file mode 100755 index 0d9e01d743..0000000000 --- a/bin/lfs_push +++ /dev/null @@ -1,97 +0,0 @@ -#!/bin/bash -# Compresses directories in data/* into data/.lfs/dirname.tar.gz -# Pushes to LFS - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -#echo -e "${GREEN}Running test data compression check...${NC}" - -ROOT=$(git rev-parse --show-toplevel) -cd $ROOT - -# Check if data/ exists -if [ ! -d "data/" ]; then - echo -e "${YELLOW}No data directory found, skipping compression.${NC}" - exit 0 -fi - -# Track if any compression was performed -compressed_dirs=() - -# Iterate through all directories in data/ -for dir_path in data/*; do - # Skip if no directories found (glob didn't match) - [ ! "$dir_path" ] && continue - - # Extract directory name - dir_name=$(basename "$dir_path") - - # Skip .lfs directory if it exists - [ "$dir_name" = ".lfs" ] && continue - - # Define compressed file path - compressed_file="data/.lfs/${dir_name}.tar.gz" - - # Check if compressed file already exists - if [ -f "$compressed_file" ]; then - continue - fi - - echo -e " ${YELLOW}Compressing${NC} $dir_path -> $compressed_file" - - # Show directory size before compression - dir_size=$(du -sh "$dir_path" | cut -f1) - echo -e " Data size: ${YELLOW}$dir_size${NC}" - - # Create compressed archive with progress bar - # Use tar with gzip compression, excluding hidden files and common temp files - tar -czf "$compressed_file" \ - --exclude='*.tmp' \ - --exclude='*.temp' \ - --exclude='.DS_Store' \ - --exclude='Thumbs.db' \ - --checkpoint=1000 \ - --checkpoint-action=dot \ - -C "data/" \ - "$dir_name" - - if [ $? -eq 0 ]; then - # Show compressed file size - compressed_size=$(du -sh "$compressed_file" | cut -f1) - echo -e " ${GREEN}✓${NC} Successfully compressed $dir_name (${GREEN}$dir_size${NC} → ${GREEN}$compressed_size${NC})" - compressed_dirs+=("$dir_name") - - # Add the compressed file to git LFS tracking - git add -f "$compressed_file" - - echo -e " ${GREEN}✓${NC} git-add $compressed_file" - - else - echo -e " ${RED}✗${NC} Failed to compress $dir_name" - exit 1 - fi -done - -if [ ${#compressed_dirs[@]} -gt 0 ]; then - # Create commit message with compressed directory names - if [ ${#compressed_dirs[@]} -eq 1 ]; then - commit_msg="Auto-compress test data: ${compressed_dirs[0]}" - else - # Join array elements with commas - dirs_list=$(IFS=', '; echo "${compressed_dirs[*]}") - commit_msg="Auto-compress test data: ${dirs_list}" - fi - - #git commit -m "$commit_msg" - echo -e "${GREEN}✓${NC} Compressed file references added. Uploading..." - git lfs push origin $(git branch --show-current) - echo -e "${GREEN}✓${NC} Uploaded to LFS" -else - echo -e "${GREEN}✓${NC} No test data to compress" -fi diff --git a/bin/mypy-ros b/bin/mypy-ros deleted file mode 100755 index d46d6a542e..0000000000 --- a/bin/mypy-ros +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" - -mypy_args=(--show-error-codes --hide-error-context --no-pretty) - -main() { - cd "$ROOT" - - if [ -z "$(docker images -q dimos-ros-dev)" ]; then - (cd docker/ros; docker build -t dimos-ros .) - docker build -t dimos-ros-python --build-arg FROM_IMAGE=dimos-ros -f docker/python/Dockerfile . - docker build -t dimos-ros-dev --build-arg FROM_IMAGE=dimos-ros-python -f docker/dev/Dockerfile . - fi - - sudo rm -fr .mypy_cache_docker - rm -fr .mypy_cache_local - - { - mypy_docker & - mypy_local & - wait - } | sort -u -} - -cleaned() { - grep ': error: ' | sort -} - -mypy_docker() { - docker run --rm -v $(pwd):/app -w /app dimos-ros-dev bash -c " - source /opt/ros/humble/setup.bash && - MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy ${mypy_args[*]} --cache-dir .mypy_cache_docker dimos - " | cleaned -} - -mypy_local() { - MYPYPATH=/opt/ros/jazzy/lib/python3.12/site-packages \ - mypy "${mypy_args[@]}" --cache-dir .mypy_cache_local dimos | cleaned -} - -main "$@" diff --git a/bin/pytest-fast b/bin/pytest-fast deleted file mode 100755 index cb25f93288..0000000000 --- a/bin/pytest-fast +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -. .venv/bin/activate -exec pytest "$@" dimos diff --git a/bin/pytest-mujoco b/bin/pytest-mujoco deleted file mode 100755 index 07e7ed90bc..0000000000 --- a/bin/pytest-mujoco +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -. .venv/bin/activate -exec pytest "$@" -m mujoco dimos diff --git a/bin/pytest-slow b/bin/pytest-slow deleted file mode 100755 index 85643d4413..0000000000 --- a/bin/pytest-slow +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -. .venv/bin/activate -exec pytest "$@" -m 'not (tool or module or neverending or mujoco)' dimos diff --git a/bin/re-ignore-mypy.py b/bin/re-ignore-mypy.py deleted file mode 100755 index 7d71bcd986..0000000000 --- a/bin/re-ignore-mypy.py +++ /dev/null @@ -1,150 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import defaultdict -from pathlib import Path -import re -import subprocess - - -def remove_type_ignore_comments(directory: Path) -> None: - # Pattern matches "# type: ignore" with optional error codes in brackets. - # Captures any trailing comment after `type: ignore`. - type_ignore_pattern = re.compile(r"(\s*)#\s*type:\s*ignore(?:\[[^\]]*\])?(\s*#.*)?") - - for py_file in directory.rglob("*.py"): - try: - content = py_file.read_text() - except Exception: - continue - - new_lines = [] - modified = False - - for line in content.splitlines(keepends=True): - match = type_ignore_pattern.search(line) - if match: - before = line[: match.start()] - trailing_comment = match.group(2) - - if trailing_comment: - new_line = before + match.group(1) + trailing_comment.lstrip() - else: - new_line = before - - if line.endswith("\n"): - new_line = new_line.rstrip() + "\n" - else: - new_line = new_line.rstrip() - new_lines.append(new_line) - modified = True - else: - new_lines.append(line) - - if modified: - try: - py_file.write_text("".join(new_lines)) - except Exception: - pass - - -def run_mypy(root: Path) -> str: - result = subprocess.run( - [str(root / "bin" / "mypy-ros")], - capture_output=True, - text=True, - cwd=root, - ) - return result.stdout + result.stderr - - -def parse_mypy_errors(output: str) -> dict[Path, dict[int, list[str]]]: - error_pattern = re.compile(r"^(.+):(\d+): error: .+\[([^\]]+)\]\s*$") - errors: dict[Path, dict[int, list[str]]] = defaultdict(lambda: defaultdict(list)) - - for line in output.splitlines(): - match = error_pattern.match(line) - if match: - file_path = Path(match.group(1)) - line_num = int(match.group(2)) - error_code = match.group(3) - if error_code not in errors[file_path][line_num]: - errors[file_path][line_num].append(error_code) - - return errors - - -def add_type_ignore_comments(root: Path, errors: dict[Path, dict[int, list[str]]]) -> None: - comment_pattern = re.compile(r"^([^#]*?)( #.*)$") - - for file_path, line_errors in errors.items(): - full_path = root / file_path - if not full_path.exists(): - continue - - try: - content = full_path.read_text() - except Exception: - continue - - lines = content.splitlines(keepends=True) - modified = False - - for line_num, error_codes in line_errors.items(): - if line_num < 1 or line_num > len(lines): - continue - - idx = line_num - 1 - line = lines[idx] - codes_str = ", ".join(sorted(error_codes)) - ignore_comment = f" # type: ignore[{codes_str}]" - - has_newline = line.endswith("\n") - line_content = line.rstrip("\n") - - comment_match = comment_pattern.match(line_content) - if comment_match: - code_part = comment_match.group(1) - existing_comment = comment_match.group(2) - new_line = code_part + ignore_comment + existing_comment - else: - new_line = line_content + ignore_comment - - if has_newline: - new_line += "\n" - - lines[idx] = new_line - modified = True - - if modified: - try: - full_path.write_text("".join(lines)) - except Exception: - pass - - -def main() -> None: - root = Path(__file__).parent.parent - dimos_dir = root / "dimos" - - remove_type_ignore_comments(dimos_dir) - mypy_output = run_mypy(root) - errors = parse_mypy_errors(mypy_output) - add_type_ignore_comments(root, errors) - - -if __name__ == "__main__": - main() diff --git a/bin/robot-debugger b/bin/robot-debugger deleted file mode 100755 index 165a546a0c..0000000000 --- a/bin/robot-debugger +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Control the robot with a python shell (for debugging). -# -# You have to start the robot run file with: -# -# ROBOT_DEBUGGER=true python -# -# And now start this script -# -# $ ./bin/robot-debugger -# >>> robot.explore() -# True -# >>> - - -exec python -i <(cat < 0: - print("\nConnected.") - break - except ConnectionRefusedError: - print("Not started yet. Trying again...") - time.sleep(2) -else: - print("Failed to connect. Is it started?") - exit(1) - -robot = c.root.robot() -EOF -) diff --git a/bin/ros b/bin/ros deleted file mode 100755 index d0349a9d2c..0000000000 --- a/bin/ros +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env bash -ros2 launch go2_robot_sdk robot.launch.py diff --git a/data/.lfs/ab_lidar_frames.tar.gz b/data/.lfs/ab_lidar_frames.tar.gz deleted file mode 100644 index 38c61cd506..0000000000 --- a/data/.lfs/ab_lidar_frames.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ab4efaf5d7d4303424868fecaf10083378007adf20244fd17ed934e37f2996da -size 116271 diff --git a/data/.lfs/apartment.tar.gz b/data/.lfs/apartment.tar.gz deleted file mode 100644 index c8e6cf0331..0000000000 --- a/data/.lfs/apartment.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d2c44f39573a80a65aeb6ccd3fcb1c8cb0741dbc7286132856409e88e150e77 -size 18141029 diff --git a/data/.lfs/assets.tar.gz b/data/.lfs/assets.tar.gz deleted file mode 100644 index b7a2fcbd1c..0000000000 --- a/data/.lfs/assets.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b14b01f5c907f117331213abfce9ef5d0c41d0524e14327b5cc706520fb2035 -size 2306191 diff --git a/data/.lfs/astar_corner_min_cost.png.tar.gz b/data/.lfs/astar_corner_min_cost.png.tar.gz deleted file mode 100644 index 35f3ffe0b6..0000000000 --- a/data/.lfs/astar_corner_min_cost.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:42517c5f67a9f06949cb2015a345f9d6b43d22cafd50e1fefb9b5d24d8b72509 -size 5671 diff --git a/data/.lfs/astar_min_cost.png.tar.gz b/data/.lfs/astar_min_cost.png.tar.gz deleted file mode 100644 index 752a778295..0000000000 --- a/data/.lfs/astar_min_cost.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06b67aa0d18c291c3525e67ca3a2a9ab2530f6fe782a850872ba4c343353a20a -size 12018 diff --git a/data/.lfs/big_office.ply.tar.gz b/data/.lfs/big_office.ply.tar.gz deleted file mode 100644 index c8524a1862..0000000000 --- a/data/.lfs/big_office.ply.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7eabc682f75e1725a07df51bb009d3950190318d119d54d0ad8c6b7104f175e3 -size 2355227 diff --git a/data/.lfs/big_office_height_cost_occupancy.png.tar.gz b/data/.lfs/big_office_height_cost_occupancy.png.tar.gz deleted file mode 100644 index 75addaf103..0000000000 --- a/data/.lfs/big_office_height_cost_occupancy.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6d8e7d096f1108d45ebdad760c4655de1e1d50105ca59c5188e79cb1a7c0d4a9 -size 133051 diff --git a/data/.lfs/big_office_simple_occupancy.png.tar.gz b/data/.lfs/big_office_simple_occupancy.png.tar.gz deleted file mode 100644 index dd667640be..0000000000 --- a/data/.lfs/big_office_simple_occupancy.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dded2e28694de9ec84a91a686b27654b83c604f44f4d3e336d5cd481e88d3249 -size 28146 diff --git a/data/.lfs/cafe-smol.jpg.tar.gz b/data/.lfs/cafe-smol.jpg.tar.gz deleted file mode 100644 index a05beb4900..0000000000 --- a/data/.lfs/cafe-smol.jpg.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dd0c1e5aa5e8ec856cb471c5ed256c2d3a5633ed9a1e052291680eb86bf89a5e -size 8298 diff --git a/data/.lfs/cafe.jpg.tar.gz b/data/.lfs/cafe.jpg.tar.gz deleted file mode 100644 index dbb2d970a1..0000000000 --- a/data/.lfs/cafe.jpg.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8cf30439b41033ccb04b09b9fc8388d18fb544d55b85c155dbf85700b9e7603 -size 136165 diff --git a/data/.lfs/chair-image.png.tar.gz b/data/.lfs/chair-image.png.tar.gz deleted file mode 100644 index 1a2aab4cf5..0000000000 --- a/data/.lfs/chair-image.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f3478f472b5750f118cf7225c2028beeaae41f1b4b726c697ac8c9b004eccbf -size 48504 diff --git a/data/.lfs/command_center.html.tar.gz b/data/.lfs/command_center.html.tar.gz deleted file mode 100644 index f3089d7b87..0000000000 --- a/data/.lfs/command_center.html.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2daabec1baf19c95cb50eadef8d3521289044adde03c86909351894cb90c9843 -size 137595 diff --git a/data/.lfs/drone.tar.gz b/data/.lfs/drone.tar.gz deleted file mode 100644 index 2973c649cd..0000000000 --- a/data/.lfs/drone.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dd73f988eee8fd7b99d6c0bf6a905c2f43a6145a4ef33e9eef64bee5f53e04dd -size 709946060 diff --git a/data/.lfs/expected_occupancy_scene.xml.tar.gz b/data/.lfs/expected_occupancy_scene.xml.tar.gz deleted file mode 100644 index efbe7ce49d..0000000000 --- a/data/.lfs/expected_occupancy_scene.xml.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e3eb91f3c7787882bf26a69df21bb1933d2f6cd71132ca5f0521e2808269bfa2 -size 6777 diff --git a/data/.lfs/g1_zed.tar.gz b/data/.lfs/g1_zed.tar.gz deleted file mode 100644 index 4029f48204..0000000000 --- a/data/.lfs/g1_zed.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:955094035b3ac1edbc257ca1d24fa131f79ac6f502c8b35cc50329025c421dbe -size 1029559759 diff --git a/data/.lfs/gradient_simple.png.tar.gz b/data/.lfs/gradient_simple.png.tar.gz deleted file mode 100644 index 7232282ce4..0000000000 --- a/data/.lfs/gradient_simple.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e418f2a6858c757cb72bd25772749a1664c97a407682d88ad2b51c4bbdcb8006 -size 11568 diff --git a/data/.lfs/gradient_voronoi.png.tar.gz b/data/.lfs/gradient_voronoi.png.tar.gz deleted file mode 100644 index 28e7f263c4..0000000000 --- a/data/.lfs/gradient_voronoi.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3867c0fb5b00f8cb5e0876e5120a70d61f7da121c0a3400010743cc858ee2d54 -size 20680 diff --git a/data/.lfs/inflation_simple.png.tar.gz b/data/.lfs/inflation_simple.png.tar.gz deleted file mode 100644 index ca6586800c..0000000000 --- a/data/.lfs/inflation_simple.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:658ed8cafc24ac7dc610b7e5ae484f23e1963872ffc2add0632ee61a7c20492d -size 3412 diff --git a/data/.lfs/lcm_msgs.tar.gz b/data/.lfs/lcm_msgs.tar.gz deleted file mode 100644 index 2b2f28c252..0000000000 --- a/data/.lfs/lcm_msgs.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:245395d0c3e200fcfcea8de5de217f645362b145b200c81abc3862e0afc1aa7e -size 327201 diff --git a/data/.lfs/make_navigation_map_mixed.png.tar.gz b/data/.lfs/make_navigation_map_mixed.png.tar.gz deleted file mode 100644 index 4fcaa8134a..0000000000 --- a/data/.lfs/make_navigation_map_mixed.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:36ea27a2434836eb309728f35033674736552daeb82f6e41fb7e3eb175d950da -size 13084 diff --git a/data/.lfs/make_navigation_map_simple.png.tar.gz b/data/.lfs/make_navigation_map_simple.png.tar.gz deleted file mode 100644 index f966b459e2..0000000000 --- a/data/.lfs/make_navigation_map_simple.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0d211fa1bc517ef78e8dc548ebff09f58ad34c86d28eb3bd48a09a577ee5d1e -size 11767 diff --git a/data/.lfs/make_path_mask_full.png.tar.gz b/data/.lfs/make_path_mask_full.png.tar.gz deleted file mode 100644 index 0e9336aaea..0000000000 --- a/data/.lfs/make_path_mask_full.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b772d266dffa82ccf14f13c7d8cc2443210202836883c80f016a56d4cfe2b52a -size 11213 diff --git a/data/.lfs/make_path_mask_two_meters.png.tar.gz b/data/.lfs/make_path_mask_two_meters.png.tar.gz deleted file mode 100644 index 7fa9e767b8..0000000000 --- a/data/.lfs/make_path_mask_two_meters.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:da608d410f4a1afee0965abfac814bc05267bdde31b0d3a9622c39515ee4f813 -size 11395 diff --git a/data/.lfs/models_clip.tar.gz b/data/.lfs/models_clip.tar.gz deleted file mode 100644 index a4ab2b5f88..0000000000 --- a/data/.lfs/models_clip.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:102f11bb0aa952b3cebc4491c5ed3f2122e8c38c76002e22400da4f1e5ca90c5 -size 392327708 diff --git a/data/.lfs/models_contact_graspnet.tar.gz b/data/.lfs/models_contact_graspnet.tar.gz deleted file mode 100644 index 73dd44d033..0000000000 --- a/data/.lfs/models_contact_graspnet.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:431c4611a9e096fd8b0a83fecda39c5a575e72fa933f7bd29ff8cfad5bbb5f9d -size 52149165 diff --git a/data/.lfs/models_edgetam.tar.gz b/data/.lfs/models_edgetam.tar.gz deleted file mode 100644 index 64baa5d139..0000000000 --- a/data/.lfs/models_edgetam.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cd452096f91415ce7ca90548a06a87354ccdb19a66925c0242413c80b08f5c57 -size 51988780 diff --git a/data/.lfs/models_fastsam.tar.gz b/data/.lfs/models_fastsam.tar.gz deleted file mode 100644 index 77278f4323..0000000000 --- a/data/.lfs/models_fastsam.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:682cb3816451bd73722cc430fdfce15bbe72a07e50ef2ea81ddaed61d1f22a25 -size 39971209 diff --git a/data/.lfs/models_graspgen.tar.gz b/data/.lfs/models_graspgen.tar.gz deleted file mode 100644 index 8321530922..0000000000 --- a/data/.lfs/models_graspgen.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:058ff764c043dccc516c1519a1e23207500c20a10c432c15eb5e30104477c0a4 -size 2117602984 diff --git a/data/.lfs/models_mobileclip.tar.gz b/data/.lfs/models_mobileclip.tar.gz deleted file mode 100644 index afe82c96e9..0000000000 --- a/data/.lfs/models_mobileclip.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:143747a320e959d9ee9fd239535d0451c378b1a2e165a242e981c4a3e4defb73 -size 1654541503 diff --git a/data/.lfs/models_torchreid.tar.gz b/data/.lfs/models_torchreid.tar.gz deleted file mode 100644 index 6446a049fb..0000000000 --- a/data/.lfs/models_torchreid.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2215070bd8e814ac9867410e3e6c49700f6c3ef7caf29b42d7832be090003743 -size 23873718 diff --git a/data/.lfs/models_yolo.tar.gz b/data/.lfs/models_yolo.tar.gz deleted file mode 100644 index 650d4617ca..0000000000 --- a/data/.lfs/models_yolo.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:01796d5884cf29258820cf0e617bf834e9ffb63d8a4c7a54eea802e96fe6a818 -size 72476992 diff --git a/data/.lfs/models_yoloe.tar.gz b/data/.lfs/models_yoloe.tar.gz deleted file mode 100644 index a0870d71d2..0000000000 --- a/data/.lfs/models_yoloe.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7a78e39477667b25c9454f846cd66dc044dd05981b2f7ebb0d331ef3626de9bc -size 184892540 diff --git a/data/.lfs/mujoco_sim.tar.gz b/data/.lfs/mujoco_sim.tar.gz deleted file mode 100644 index 57833fbbc6..0000000000 --- a/data/.lfs/mujoco_sim.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d178439569ed81dfad05455419dc51da2c52021313b6d7b9259d9e30946db7c6 -size 60186340 diff --git a/data/.lfs/occupancy_general.png.tar.gz b/data/.lfs/occupancy_general.png.tar.gz deleted file mode 100644 index b509151e5a..0000000000 --- a/data/.lfs/occupancy_general.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b770d950cf7206a67ccdfd8660ee0ab818228faa9ebbf1a37cbf6ee9d1ac7539 -size 2970 diff --git a/data/.lfs/occupancy_simple.npy.tar.gz b/data/.lfs/occupancy_simple.npy.tar.gz deleted file mode 100644 index cf42cf3667..0000000000 --- a/data/.lfs/occupancy_simple.npy.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e1cf83464442fb284b6f7ba2752546fc4571a73f3490c24a58fb45987555a66c -size 1954 diff --git a/data/.lfs/occupancy_simple.png.tar.gz b/data/.lfs/occupancy_simple.png.tar.gz deleted file mode 100644 index 4962f13db1..0000000000 --- a/data/.lfs/occupancy_simple.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6c9dac221a594c87d0baa60b8c678c63a0c215325080b34ee60df5cc1e1c331d -size 3311 diff --git a/data/.lfs/office_building_1.tar.gz b/data/.lfs/office_building_1.tar.gz deleted file mode 100644 index 0dc013bd94..0000000000 --- a/data/.lfs/office_building_1.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:70aac31ca76597b3eee1ddfcbe2ba71d432fd427176f66d8281d75da76641f49 -size 1061581652 diff --git a/data/.lfs/office_lidar.tar.gz b/data/.lfs/office_lidar.tar.gz deleted file mode 100644 index 849e9e3d49..0000000000 --- a/data/.lfs/office_lidar.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4958965334660c4765553afa38081f00a769c8adf81e599e63fabc866c490fd -size 28576272 diff --git a/data/.lfs/osm_map_test.tar.gz b/data/.lfs/osm_map_test.tar.gz deleted file mode 100644 index b29104ea17..0000000000 --- a/data/.lfs/osm_map_test.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25097f1bffebd2651f1f4ba93cb749998a064adfdc0cb004981b2317f649c990 -size 1062262 diff --git a/data/.lfs/overlay_occupied.png.tar.gz b/data/.lfs/overlay_occupied.png.tar.gz deleted file mode 100644 index 158a52c6bd..0000000000 --- a/data/.lfs/overlay_occupied.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b55bcf7a2a7a5cbdfdfe8c6a75c53ffe5707197d991d1e39e9aa9dc22503397 -size 3657 diff --git a/data/.lfs/person.tar.gz b/data/.lfs/person.tar.gz deleted file mode 100644 index 1f32d0db58..0000000000 --- a/data/.lfs/person.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:332c3196c6436e7d4c2b7e3314b4a4055865ef358b2e9cf3c8ddd7e173f39b93 -size 2535758 diff --git a/data/.lfs/piper_description.tar.gz b/data/.lfs/piper_description.tar.gz deleted file mode 100644 index 3ab8ab227b..0000000000 --- a/data/.lfs/piper_description.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d4ce51d4ea15f29d80e69b0fff4a4d667f086e010329bb5c66980a881f1ee539 -size 3091511 diff --git a/data/.lfs/raw_odometry_rotate_walk.tar.gz b/data/.lfs/raw_odometry_rotate_walk.tar.gz deleted file mode 100644 index ce8bb1d2b0..0000000000 --- a/data/.lfs/raw_odometry_rotate_walk.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:396345f0cd7a94bb9d85540d4bbce01b027618972f83e713e4550abf1d6ec445 -size 15685 diff --git a/data/.lfs/replay_g1.tar.gz b/data/.lfs/replay_g1.tar.gz deleted file mode 100644 index 67750bd0cf..0000000000 --- a/data/.lfs/replay_g1.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:19ad1c53c4f8f9414c0921b94cd4c87e81bf0ad676881339f15ae2d8a8619311 -size 557410250 diff --git a/data/.lfs/replay_g1_run.tar.gz b/data/.lfs/replay_g1_run.tar.gz deleted file mode 100644 index 86368ec788..0000000000 --- a/data/.lfs/replay_g1_run.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:00cf21f65a15994895150f74044f5d00d7aa873d24f071d249ecbd09cb8f2b26 -size 559554274 diff --git a/data/.lfs/resample_path_simple.png.tar.gz b/data/.lfs/resample_path_simple.png.tar.gz deleted file mode 100644 index 1a8c1118d6..0000000000 --- a/data/.lfs/resample_path_simple.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b5c454ed6cc66cf4446ce4a246464aec27368da4902651b4ad9ed29b3ba56ec -size 118319 diff --git a/data/.lfs/resample_path_smooth.png.tar.gz b/data/.lfs/resample_path_smooth.png.tar.gz deleted file mode 100644 index 80af3d3805..0000000000 --- a/data/.lfs/resample_path_smooth.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6cc0dfd80bada94f2ab1bb577e2ec1734dad6894113f2fe77964bd80d886c3d3 -size 109699 diff --git a/data/.lfs/rgbd_frames.tar.gz b/data/.lfs/rgbd_frames.tar.gz deleted file mode 100644 index 8081c76961..0000000000 --- a/data/.lfs/rgbd_frames.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:381b9fd296a885f5211a668df16c68581d2aee458c8734c3256a7461f0decccd -size 948391033 diff --git a/data/.lfs/smooth_occupied.png.tar.gz b/data/.lfs/smooth_occupied.png.tar.gz deleted file mode 100644 index 0e09e7d15a..0000000000 --- a/data/.lfs/smooth_occupied.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:44c8988b8a7d954ee26a0a5f195b961c62bbdb251b540df6b4d67cd85a72e5ac -size 3511 diff --git a/data/.lfs/three_paths.npy.tar.gz b/data/.lfs/three_paths.npy.tar.gz deleted file mode 100644 index 744eb06305..0000000000 --- a/data/.lfs/three_paths.npy.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ba849a6b648ccc9ed4987bbe985ee164dd9ad0324895076baa9f86196b2a0d5f -size 5180 diff --git a/data/.lfs/three_paths.ply.tar.gz b/data/.lfs/three_paths.ply.tar.gz deleted file mode 100644 index a5bfc6bac4..0000000000 --- a/data/.lfs/three_paths.ply.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:639093004355c1ba796c668cd43476dfcabff137ca0bb430ace07730cc512f0e -size 307187 diff --git a/data/.lfs/three_paths.png.tar.gz b/data/.lfs/three_paths.png.tar.gz deleted file mode 100644 index ade2bd3eb7..0000000000 --- a/data/.lfs/three_paths.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2265ddd76bfb70e7ac44f2158dc0d16e0df264095b0f45a77f95eb85c529d935 -size 2559 diff --git a/data/.lfs/unitree_go2_bigoffice.tar.gz b/data/.lfs/unitree_go2_bigoffice.tar.gz deleted file mode 100644 index 6582702479..0000000000 --- a/data/.lfs/unitree_go2_bigoffice.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a009674153f7ee1f98219af69dc7a92d063f2581bfd9b0aa019762c9235895c -size 2312982327 diff --git a/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz b/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz deleted file mode 100644 index 89ecb54e87..0000000000 --- a/data/.lfs/unitree_go2_bigoffice_map.pickle.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68adb344ae040c3f94d61dd058beb39cc2811c4ae8328f678bc2ba761c504eb5 -size 2331189 diff --git a/data/.lfs/unitree_go2_lidar_corrected.tar.gz b/data/.lfs/unitree_go2_lidar_corrected.tar.gz deleted file mode 100644 index 013f6b3fe1..0000000000 --- a/data/.lfs/unitree_go2_lidar_corrected.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:51a817f2b5664c9e2f2856293db242e030f0edce276e21da0edc2821d947aad2 -size 1212727745 diff --git a/data/.lfs/unitree_go2_office_walk2.tar.gz b/data/.lfs/unitree_go2_office_walk2.tar.gz deleted file mode 100644 index ea392c4b4c..0000000000 --- a/data/.lfs/unitree_go2_office_walk2.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d208cdf537ad01eed2068a4665e454ed30b30894bd9b35c14b4056712faeef5d -size 1693876005 diff --git a/data/.lfs/unitree_office_walk.tar.gz b/data/.lfs/unitree_office_walk.tar.gz deleted file mode 100644 index 419489dbb1..0000000000 --- a/data/.lfs/unitree_office_walk.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bee487130eb662bca73c7d84f14eaea091bd6d7c3f1bfd5173babf660947bdec -size 553620791 diff --git a/data/.lfs/unitree_raw_webrtc_replay.tar.gz b/data/.lfs/unitree_raw_webrtc_replay.tar.gz deleted file mode 100644 index d41ff5c48f..0000000000 --- a/data/.lfs/unitree_raw_webrtc_replay.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a02c622cfee712002afc097825ab5e963071471c3445a20a004ef3532cf59888 -size 756280504 diff --git a/data/.lfs/video.tar.gz b/data/.lfs/video.tar.gz deleted file mode 100644 index 6c0e01a0bb..0000000000 --- a/data/.lfs/video.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:530d2132ef84df228af776bd2a2ef387a31858c63ea21c94fb49c7e579b366c0 -size 4322822 diff --git a/data/.lfs/visualize_occupancy_rainbow.png.tar.gz b/data/.lfs/visualize_occupancy_rainbow.png.tar.gz deleted file mode 100644 index 9bbd2e6ea1..0000000000 --- a/data/.lfs/visualize_occupancy_rainbow.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3dc1e3b6519f7d7ff25b16c3124ee447f02857eeb3eb20930cdab95464b1f0a3 -size 11582 diff --git a/data/.lfs/visualize_occupancy_turbo.png.tar.gz b/data/.lfs/visualize_occupancy_turbo.png.tar.gz deleted file mode 100644 index e2863cdae6..0000000000 --- a/data/.lfs/visualize_occupancy_turbo.png.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c21874bab6ec7cd9692d2b1e67498ddfff3c832ec992e9552fee17093759b270 -size 18593 diff --git a/data/.lfs/xarm7.tar.gz b/data/.lfs/xarm7.tar.gz deleted file mode 100644 index 8e2cfa368a..0000000000 --- a/data/.lfs/xarm7.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47dd79f13845ae6a35368345b7443a9190c7584d548caddd9c3eae224442c6fc -size 3280557 diff --git a/data/.lfs/xarm_description.tar.gz b/data/.lfs/xarm_description.tar.gz deleted file mode 100644 index 4cccd9ab25..0000000000 --- a/data/.lfs/xarm_description.tar.gz +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6e25f1ede8e4022f5053a61717191a2c338ea5af5b81e26bd2c880343aff1316 -size 12709222 diff --git a/default.env b/default.env deleted file mode 100644 index 5098a60892..0000000000 --- a/default.env +++ /dev/null @@ -1,15 +0,0 @@ -OPENAI_API_KEY= -HUGGINGFACE_ACCESS_TOKEN= -ALIBABA_API_KEY= -ANTHROPIC_API_KEY= -HF_TOKEN= -HUGGINGFACE_PRV_ENDPOINT= -ROBOT_IP= -CONN_TYPE=webrtc -WEBRTC_SERVER_HOST=0.0.0.0 -WEBRTC_SERVER_PORT=9991 -DISPLAY=:0 - -# Optional -#DIMOS_MAX_WORKERS= -TEST_RTSP_URL= diff --git a/dimos/__init__.py b/dimos/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/agents/agent.py b/dimos/agents/agent.py deleted file mode 100644 index 76195ccea0..0000000000 --- a/dimos/agents/agent.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -import json -from queue import Empty, Queue -from threading import Event, RLock, Thread -from typing import TYPE_CHECKING, Any, Protocol -import uuid - -from langchain.agents import create_agent -from langchain_core.messages import HumanMessage -from langchain_core.messages.base import BaseMessage -from langchain_core.tools import StructuredTool -from langgraph.graph.state import CompiledStateGraph -from reactivex.disposable import Disposable - -from dimos.agents.system_prompt import SYSTEM_PROMPT -from dimos.agents.utils import pretty_print_langchain_message -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig, SkillInfo -from dimos.core.rpc_client import RpcCall, RPCClient -from dimos.core.stream import In, Out -from dimos.protocol.rpc import RPCSpec -from dimos.spec.utils import Spec - -if TYPE_CHECKING: - from langchain_core.language_models import BaseChatModel - - -@dataclass -class AgentConfig(ModuleConfig): - system_prompt: str | None = SYSTEM_PROMPT - model: str = "gpt-4o" - model_fixture: str | None = None - - -class Agent(Module): - default_config: type[AgentConfig] = AgentConfig - config: AgentConfig - agent: Out[BaseMessage] - human_input: In[str] - agent_idle: Out[bool] - - _lock: RLock - _state_graph: CompiledStateGraph[Any, Any, Any, Any] | None - _message_queue: Queue[BaseMessage] - _history: list[BaseMessage] - _thread: Thread - _stop_event: Event - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._lock = RLock() - self._state_graph = None - self._message_queue = Queue() - self._history = [] - self._thread = Thread( - target=self._thread_loop, - name=f"{self.__class__.__name__}-thread", - daemon=True, - ) - self._stop_event = Event() - - @rpc - def start(self) -> None: - super().start() - - def _on_human_input(string: str) -> None: - self._message_queue.put(HumanMessage(content=string)) - - self._disposables.add(Disposable(self.human_input.subscribe(_on_human_input))) - - @rpc - def stop(self) -> None: - self._stop_event.set() - if self._thread.is_alive(): - self._thread.join(timeout=2.0) - super().stop() - - @rpc - def on_system_modules(self, modules: list[RPCClient]) -> None: - assert self.rpc is not None - - if self.config.model.startswith("ollama:"): - from dimos.agents.ollama_agent import ensure_ollama_model - - ensure_ollama_model(self.config.model.removeprefix("ollama:")) - - model: str | BaseChatModel = self.config.model - if self.config.model_fixture is not None: - from dimos.agents.testing import MockModel - - model = MockModel(json_path=self.config.model_fixture) - - with self._lock: - self._state_graph = create_agent( - model=model, - tools=_get_tools_from_modules(self, modules, self.rpc), - system_prompt=self.config.system_prompt, - ) - self._thread.start() - - @rpc - def add_message(self, message: BaseMessage) -> None: - self._message_queue.put(message) - - def _thread_loop(self) -> None: - while not self._stop_event.is_set(): - try: - message = self._message_queue.get(timeout=0.5) - except Empty: - continue - - with self._lock: - if not self._state_graph: - raise ValueError("No state graph initialized") - self._process_message(self._state_graph, message) - - def _process_message( - self, state_graph: CompiledStateGraph[Any, Any, Any, Any], message: BaseMessage - ) -> None: - self.agent_idle.publish(False) - self._history.append(message) - pretty_print_langchain_message(message) - self.agent.publish(message) - - for update in state_graph.stream({"messages": self._history}, stream_mode="updates"): - for node_output in update.values(): - for msg in node_output.get("messages", []): - self._history.append(msg) - pretty_print_langchain_message(msg) - self.agent.publish(msg) - - if self._message_queue.empty(): - self.agent_idle.publish(True) - - -class AgentSpec(Spec, Protocol): - def add_message(self, message: BaseMessage) -> None: ... - - -def _get_tools_from_modules( - agent: Agent, modules: list[RPCClient], rpc: RPCSpec -) -> list[StructuredTool]: - skills = [skill for module in modules for skill in (module.get_skills() or [])] - return [_skill_to_tool(agent, skill, rpc) for skill in skills] - - -def _skill_to_tool(agent: Agent, skill: SkillInfo, rpc: RPCSpec) -> StructuredTool: - rpc_call = RpcCall(None, rpc, skill.func_name, skill.class_name, []) - - def wrapped_func(*args: Any, **kwargs: Any) -> str | list[dict[str, Any]]: - result = None - - try: - result = rpc_call(*args, **kwargs) - except Exception as e: - return f"Exception: Error: {e}" - - if result is None: - return "It has started. You will be updated later." - - if hasattr(result, "agent_encode"): - uuid_ = str(uuid.uuid4()) - _append_image_to_history(agent, skill, uuid_, result) - return f"Tool call started with UUID: {uuid_}" - - return str(result) - - return StructuredTool( - name=skill.func_name, - func=wrapped_func, - args_schema=json.loads(skill.args_schema), - ) - - -def _append_image_to_history(agent: Agent, skill: SkillInfo, uuid_: str, result: Any) -> None: - agent.add_message( - HumanMessage( - content=[ - { - "type": "text", - "text": f"This is the artefact for the '{skill.func_name}' tool with UUID:={uuid_}.", - }, - *result.agent_encode(), - ] - ) - ) - - -agent = Agent.blueprint - -__all__ = ["Agent", "AgentSpec", "agent"] diff --git a/dimos/agents/agent_test_runner.py b/dimos/agents/agent_test_runner.py deleted file mode 100644 index 7d7fbab03d..0000000000 --- a/dimos/agents/agent_test_runner.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Event, Thread - -from langchain_core.messages import AIMessage -from langchain_core.messages.base import BaseMessage -from reactivex.disposable import Disposable - -from dimos.agents.agent import AgentSpec -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.rpc_client import RPCClient -from dimos.core.stream import In, Out - - -class AgentTestRunner(Module): - agent_spec: AgentSpec - agent: In[BaseMessage] - agent_idle: In[bool] - finished: Out[bool] - added: Out[bool] - - def __init__(self, messages: list[BaseMessage]) -> None: - super().__init__() - self._messages = messages - self._idle_event = Event() - self._subscription_ready = Event() - self._thread = Thread(target=self._thread_loop, daemon=True) - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(Disposable(self.agent.subscribe(self._on_agent_message))) - self._disposables.add(Disposable(self.agent_idle.subscribe(self._on_agent_idle))) - # Signal that subscription is ready - self._subscription_ready.set() - - @rpc - def stop(self) -> None: - super().stop() - - @rpc - def on_system_modules(self, _modules: list[RPCClient]) -> None: - self._thread.start() - - def _on_agent_idle(self, idle: bool) -> None: - if idle: - self._idle_event.set() - - def _on_agent_message(self, message: BaseMessage) -> None: - # Check for final AIMessage (no tool calls) to signal completion - is_ai = isinstance(message, AIMessage) - has_tool_calls = hasattr(message, "tool_calls") and message.tool_calls - if is_ai and not has_tool_calls: - self.added.publish(True) - - def _thread_loop(self) -> None: - # Ensure subscription is ready before sending messages - if not self._subscription_ready.wait(5): - raise TimeoutError("Timed out waiting for subscription to be ready.") - - for message in self._messages: - self._idle_event.clear() - self.agent_spec.add_message(message) - if not self._idle_event.wait(60): - raise TimeoutError("Timed out waiting for message to be processed.") - - self.finished.publish(True) diff --git a/dimos/agents/annotation.py b/dimos/agents/annotation.py deleted file mode 100644 index 083a3cbc53..0000000000 --- a/dimos/agents/annotation.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from typing import Any, TypeVar - -F = TypeVar("F", bound=Callable[..., Any]) - - -def skill(func: F) -> F: - func.__rpc__ = True # type: ignore[attr-defined] - func.__skill__ = True # type: ignore[attr-defined] - return func diff --git a/dimos/agents/conftest.py b/dimos/agents/conftest.py deleted file mode 100644 index 23d888b0fe..0000000000 --- a/dimos/agents/conftest.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from pathlib import Path -from threading import Event - -from dotenv import load_dotenv -from langchain_core.messages.base import BaseMessage -import pytest - -from dimos.agents.agent import Agent -from dimos.agents.agent_test_runner import AgentTestRunner -from dimos.core.blueprints import autoconnect -from dimos.core.global_config import global_config -from dimos.core.transport import pLCMTransport - -load_dotenv() - -FIXTURE_DIR = Path(__file__).parent / "fixtures" - - -@pytest.fixture -def fixture_dir() -> Path: - return FIXTURE_DIR - - -@pytest.fixture -def agent_setup(request): - coordinator = None - transports: list[pLCMTransport] = [] - unsubs: list = [] - recording = bool(os.getenv("RECORD")) - - def fn( - *, - blueprints, - messages: list[BaseMessage], - dask: bool = False, - system_prompt: str | None = None, - fixture: str | None = None, - ) -> list[BaseMessage]: - history: list[BaseMessage] = [] - finished_event = Event() - - agent_transport: pLCMTransport = pLCMTransport("/agent") - finished_transport: pLCMTransport = pLCMTransport("/finished") - transports.extend([agent_transport, finished_transport]) - - def on_message(msg: BaseMessage) -> None: - history.append(msg) - - unsubs.append(agent_transport.subscribe(on_message)) - unsubs.append(finished_transport.subscribe(lambda _: finished_event.set())) - - # Derive fixture path from test name if not explicitly provided. - if fixture is not None: - fixture_path = FIXTURE_DIR / fixture - else: - fixture_path = FIXTURE_DIR / f"{request.node.name}.json" - - agent_kwargs: dict = {"system_prompt": system_prompt} - - if recording or fixture_path.exists(): - # RECORD=1: use real LLM, save responses to fixture file. - # No RECORD but fixture exists: play back recorded responses. - # The MockModel checks RECORD internally to decide record vs playback. - agent_kwargs["model_fixture"] = str(fixture_path) - - blueprint = autoconnect( - *blueprints, - Agent.blueprint(**agent_kwargs), - AgentTestRunner.blueprint(messages=messages), - ) - - global_config.update( - viewer_backend="none", - dask=dask, - ) - - nonlocal coordinator - coordinator = blueprint.build() - - if not finished_event.wait(60): - raise TimeoutError("Timed out waiting for agent to finish processing messages.") - - return history - - yield fn - - if coordinator is not None: - coordinator.stop() - - for transport in transports: - transport.stop() - - for unsub in unsubs: - unsub() diff --git a/dimos/agents/demo_agent.py b/dimos/agents/demo_agent.py deleted file mode 100644 index b3250fba5b..0000000000 --- a/dimos/agents/demo_agent.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import Agent -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.camera import zed -from dimos.hardware.sensors.camera.module import camera_module -from dimos.hardware.sensors.camera.webcam import Webcam - -demo_agent = autoconnect(Agent.blueprint()) - -demo_agent_camera = autoconnect( - Agent.blueprint(), - camera_module( - hardware=lambda: Webcam( - camera_index=0, - fps=15, - camera_info=zed.CameraInfo.SingleWebcam, - ), - ), -) diff --git a/dimos/agents/fixtures/test_can_call_again_on_error[False].json b/dimos/agents/fixtures/test_can_call_again_on_error[False].json deleted file mode 100644 index 762b452cff..0000000000 --- a/dimos/agents/fixtures/test_can_call_again_on_error[False].json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "register_user", - "args": { - "name": "Paul" - }, - "id": "call_gizJWFgoQiYOQMCDqjlshkvk", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "register_user", - "args": { - "name": "paul" - }, - "id": "call_O9p0ktNw0frMfNXjbul6Do1m", - "type": "tool_call" - } - ] - }, - { - "content": "The user named \"paul\" has been registered successfully.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_can_call_again_on_error[True].json b/dimos/agents/fixtures/test_can_call_again_on_error[True].json deleted file mode 100644 index b67efe84e0..0000000000 --- a/dimos/agents/fixtures/test_can_call_again_on_error[True].json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "register_user", - "args": { - "name": "Paul" - }, - "id": "call_4l78eCMbfsbIC2qPL86jA4S0", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "register_user", - "args": { - "name": "paul" - }, - "id": "call_Jzi8RU0jRMeCcUDEcRkHzahs", - "type": "tool_call" - } - ] - }, - { - "content": "The user named \"paul\" has been registered successfully.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_can_call_tool[False].json b/dimos/agents/fixtures/test_can_call_tool[False].json deleted file mode 100644 index 32d7770899..0000000000 --- a/dimos/agents/fixtures/test_can_call_tool[False].json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "add", - "args": { - "x": 33333, - "y": 100 - }, - "id": "call_vIdFXzNnojiCXtnEi9J2gXQN", - "type": "tool_call" - } - ] - }, - { - "content": "33333 + 100 is 33433.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_can_call_tool[True].json b/dimos/agents/fixtures/test_can_call_tool[True].json deleted file mode 100644 index e9431bb8ea..0000000000 --- a/dimos/agents/fixtures/test_can_call_tool[True].json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "add", - "args": { - "x": 33333, - "y": 100 - }, - "id": "call_ZlA2HzNAuHF0H52CKQIPX9Te", - "type": "tool_call" - } - ] - }, - { - "content": "The result of 33333 + 100 is 33433.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_get_gps_position_for_queries.json b/dimos/agents/fixtures/test_get_gps_position_for_queries.json deleted file mode 100644 index c2f163598d..0000000000 --- a/dimos/agents/fixtures/test_get_gps_position_for_queries.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "get_gps_position_for_queries", - "args": { - "queries": [ - "Hyde Park", - "Regent Park", - "Russell Park" - ] - }, - "id": "call_KqAjbd5E9VE69YMWBINdWnkw", - "type": "tool_call" - } - ] - }, - { - "content": "Here are the latitude and longitude coordinates for the parks you asked about:\n\n- **Hyde Park**: Latitude 37.782601, Longitude -122.413201\n- **Regent Park**: Latitude 37.782602, Longitude -122.413202\n- **Russell Park**: Latitude 37.782603, Longitude -122.413203", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_go_to_semantic_location.json b/dimos/agents/fixtures/test_go_to_semantic_location.json deleted file mode 100644 index 8ea3006142..0000000000 --- a/dimos/agents/fixtures/test_go_to_semantic_location.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "navigate_with_text", - "args": { - "query": "bookshelf" - }, - "id": "call_PkS6DWAciWAAAdZfatiXoEdu", - "type": "tool_call" - } - ] - }, - { - "content": "I have successfully navigated to the bookshelf.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_image.json b/dimos/agents/fixtures/test_image.json deleted file mode 100644 index 196c2122ef..0000000000 --- a/dimos/agents/fixtures/test_image.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "take_a_picture", - "args": {}, - "id": "call_11vyrbtZXlsEgoKuQs8jRjP8", - "type": "tool_call" - } - ] - }, - { - "content": "I have taken a picture, let me analyze it to provide a description.\nBased on the image captured, the setting resembles a \"cafe\". It shows an indoor space with tables, chairs, and a warm, inviting atmosphere typical of a cafe environment, where people might gather for drinks and conversation.", - "tool_calls": [] - }, - { - "content": "The image depicts a \"cafe\" setting. It shows people sitting outside at tables, likely enjoying drinks or meals, with a cozy and inviting ambiance typical of a cafe.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_multiple_tool_calls_with_multiple_messages.json b/dimos/agents/fixtures/test_multiple_tool_calls_with_multiple_messages.json deleted file mode 100644 index 7fd8172d35..0000000000 --- a/dimos/agents/fixtures/test_multiple_tool_calls_with_multiple_messages.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "locate_person", - "args": { - "name": "John" - }, - "id": "call_w4cTUUpojE2zRMYda93ma3io", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "register_person", - "args": { - "name": "John" - }, - "id": "call_Rij2ZZHG1u2yEKALNqH4L0WH", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "locate_person", - "args": { - "name": "John" - }, - "id": "call_Pr1HUeq1j2L9bYU3CsTXVmnk", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "go_to_location", - "args": { - "description": "kitchen" - }, - "id": "call_F6Pw6Da2mixJ1mmhb0q4uI3F", - "type": "tool_call" - } - ] - }, - { - "content": "I have moved to the kitchen where John is located.", - "tool_calls": [] - }, - { - "content": "", - "tool_calls": [ - { - "name": "register_person", - "args": { - "name": "Jane" - }, - "id": "call_52hL9l50BzPFPruLoPxigoVa", - "type": "tool_call" - }, - { - "name": "locate_person", - "args": { - "name": "Jane" - }, - "id": "call_0NbGZIn9BOCENTfrKvuacarZ", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "go_to_location", - "args": { - "description": "living room" - }, - "id": "call_XoErhAixvK31yHOeCKiwgrKj", - "type": "tool_call" - } - ] - }, - { - "content": "I have moved to the living room where Jane is located.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_pounce.json b/dimos/agents/fixtures/test_pounce.json deleted file mode 100644 index 4213c12c95..0000000000 --- a/dimos/agents/fixtures/test_pounce.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "execute_sport_command", - "args": { - "command_name": "FrontPounce" - }, - "id": "call_Z7X0sJfUygGiJUUL67I64eRs", - "type": "tool_call" - } - ] - }, - { - "content": "The robot has successfully performed a \"FrontPounce\" action!", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_prompt.json b/dimos/agents/fixtures/test_prompt.json deleted file mode 100644 index b924f66f52..0000000000 --- a/dimos/agents/fixtures/test_prompt.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "responses": [ - { - "content": "My name is Johnny. How can I assist you today?", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_set_gps_travel_points.json b/dimos/agents/fixtures/test_set_gps_travel_points.json deleted file mode 100644 index e1392125a1..0000000000 --- a/dimos/agents/fixtures/test_set_gps_travel_points.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "set_gps_travel_points", - "args": {}, - "id": "call_0Onzw4fDoT68BWNfZUiGlIbg", - "type": "tool_call" - } - ] - }, - { - "content": "It seems there was an issue with the initial attempt. Let me try again by providing the correct parameter.", - "tool_calls": [ - { - "name": "set_gps_travel_points", - "args": { - "points": [ - { - "lat": 37.782654, - "lon": -122.413273 - } - ] - }, - "id": "call_b0QRHVc09ZtY8jbQaUsny7Yx", - "type": "tool_call" - } - ] - }, - { - "content": "The GPS travel point has been successfully set to latitude: 37.782654 and longitude: -122.413273.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_set_gps_travel_points_multiple.json b/dimos/agents/fixtures/test_set_gps_travel_points_multiple.json deleted file mode 100644 index 2391e81fe1..0000000000 --- a/dimos/agents/fixtures/test_set_gps_travel_points_multiple.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "set_gps_travel_points", - "args": {}, - "id": "call_JWgodQUZD16l2tjePmTXGX7V", - "type": "tool_call" - } - ] - }, - { - "content": "", - "tool_calls": [ - { - "name": "set_gps_travel_points", - "args": { - "points": [ - { - "lat": 37.782654, - "lon": -122.413273 - }, - { - "lat": 37.78266, - "lon": -122.41326 - }, - { - "lat": 37.78267, - "lon": -122.41327 - } - ] - }, - "id": "call_e5szuuZrTdq6deN8mUM5kusY", - "type": "tool_call" - } - ] - }, - { - "content": "The GPS travel points have been successfully set in the specified order.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_start_exploration.json b/dimos/agents/fixtures/test_start_exploration.json deleted file mode 100644 index 713e6e2dba..0000000000 --- a/dimos/agents/fixtures/test_start_exploration.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "start_exploration", - "args": { - "timeout": 10 - }, - "id": "call_o5O7xLaI4iayDhszXblOiWVS", - "type": "tool_call" - } - ] - }, - { - "content": "I have completed the exploration for 10 seconds. If there's anything specific you would like to do next, please let me know!", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_stop_movement.json b/dimos/agents/fixtures/test_stop_movement.json deleted file mode 100644 index f9f10af0e0..0000000000 --- a/dimos/agents/fixtures/test_stop_movement.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "stop_movement", - "args": {}, - "id": "call_dHP1UE2Bw180bzxPT2wI2Dam", - "type": "tool_call" - } - ] - }, - { - "content": "The movement has been stopped.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/fixtures/test_where_am_i.json b/dimos/agents/fixtures/test_where_am_i.json deleted file mode 100644 index a4a06a5c72..0000000000 --- a/dimos/agents/fixtures/test_where_am_i.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "responses": [ - { - "content": "", - "tool_calls": [ - { - "name": "where_am_i", - "args": {}, - "id": "call_4eiRtfr8mI7SgLfR0FTjH7Pp", - "type": "tool_call" - } - ] - }, - { - "content": "You are currently on Bourbon Street.", - "tool_calls": [] - } - ] -} diff --git a/dimos/agents/ollama_agent.py b/dimos/agents/ollama_agent.py deleted file mode 100644 index 4b35cc84f8..0000000000 --- a/dimos/agents/ollama_agent.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import ollama - -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def ensure_ollama_model(model_name: str) -> None: - available_models = ollama.list() - model_exists = any(model_name == m.model for m in available_models.models) - if not model_exists: - logger.info(f"Ollama model '{model_name}' not found. Pulling...") - ollama.pull(model_name) - - -def ollama_installed() -> str | None: - try: - ollama.list() - return None - except Exception: - return ( - "Cannot connect to Ollama daemon. Please ensure Ollama is installed and running.\n" - "\n" - " For installation instructions, visit https://ollama.com/download" - ) diff --git a/dimos/agents/skills/demo_calculator_skill.py b/dimos/agents/skills/demo_calculator_skill.py deleted file mode 100644 index 61d66e301a..0000000000 --- a/dimos/agents/skills/demo_calculator_skill.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.annotation import skill -from dimos.core.module import Module - - -class DemoCalculatorSkill(Module): - def start(self) -> None: - super().start() - - def stop(self) -> None: - super().stop() - - @skill - def sum_numbers(self, n1: int, n2: int, *args: int, **kwargs: int) -> str: - """This skill adds two numbers. Always use this tool. Never add up numbers yourself. - - Example: - - sum_numbers(100, 20) - - Args: - sum (str): The sum, as a string. E.g., "120" - """ - - return f"{int(n1) + int(n2)}" - - -demo_calculator_skill = DemoCalculatorSkill.blueprint - -__all__ = ["DemoCalculatorSkill", "demo_calculator_skill"] diff --git a/dimos/agents/skills/demo_google_maps_skill.py b/dimos/agents/skills/demo_google_maps_skill.py deleted file mode 100644 index 13f2ebc19b..0000000000 --- a/dimos/agents/skills/demo_google_maps_skill.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.agents.skills.demo_robot import demo_robot -from dimos.agents.skills.google_maps_skill_container import google_maps_skill -from dimos.core.blueprints import autoconnect - -demo_google_maps_skill = autoconnect( - demo_robot(), - google_maps_skill(), - agent(), -) diff --git a/dimos/agents/skills/demo_gps_nav.py b/dimos/agents/skills/demo_gps_nav.py deleted file mode 100644 index 7a6abd32dd..0000000000 --- a/dimos/agents/skills/demo_gps_nav.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.agents.skills.demo_robot import demo_robot -from dimos.agents.skills.gps_nav_skill import gps_nav_skill -from dimos.core.blueprints import autoconnect - -demo_gps_nav = autoconnect( - demo_robot(), - gps_nav_skill(), - agent(), -) diff --git a/dimos/agents/skills/demo_robot.py b/dimos/agents/skills/demo_robot.py deleted file mode 100644 index aa4e81e2cc..0000000000 --- a/dimos/agents/skills/demo_robot.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from reactivex import interval - -from dimos.core.module import Module -from dimos.core.stream import Out -from dimos.mapping.types import LatLon - - -class DemoRobot(Module): - gps_location: Out[LatLon] - - def start(self) -> None: - super().start() - self._disposables.add(interval(1.0).subscribe(lambda _: self._publish_gps_location())) - - def stop(self) -> None: - super().stop() - - def _publish_gps_location(self) -> None: - self.gps_location.publish(LatLon(lat=37.78092426217621, lon=-122.40682866540769)) - - -demo_robot = DemoRobot.blueprint - - -__all__ = ["DemoRobot", "demo_robot"] diff --git a/dimos/agents/skills/demo_skill.py b/dimos/agents/skills/demo_skill.py deleted file mode 100644 index b067a3fbc2..0000000000 --- a/dimos/agents/skills/demo_skill.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.agents.skills.demo_calculator_skill import demo_calculator_skill -from dimos.core.blueprints import autoconnect - -demo_skill = autoconnect( - demo_calculator_skill(), - agent(), -) diff --git a/dimos/agents/skills/google_maps_skill_container.py b/dimos/agents/skills/google_maps_skill_container.py deleted file mode 100644 index 33b2ee9f10..0000000000 --- a/dimos/agents/skills/google_maps_skill_container.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -from typing import Any - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.stream import In -from dimos.mapping.google_maps.google_maps import GoogleMaps -from dimos.mapping.types import LatLon -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class GoogleMapsSkillContainer(Module): - _latest_location: LatLon | None = None - _client: GoogleMaps - - gps_location: In[LatLon] - - def __init__(self) -> None: - super().__init__() - self._client = GoogleMaps() - self._started = True - self._max_valid_distance = 20000 # meters - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] - - @rpc - def stop(self) -> None: - super().stop() - - def _on_gps_location(self, location: LatLon) -> None: - self._latest_location = location - - def _get_latest_location(self) -> LatLon: - if not self._latest_location: - raise ValueError("The position has not been set yet.") - return self._latest_location - - @skill - def where_am_i(self, context_radius: int = 200) -> str: - """This skill returns information about what street/locality/city/etc - you are in. It also gives you nearby landmarks. - - Example: - - where_am_i(context_radius=200) - - Args: - context_radius (int): default 200, how many meters to look around - """ - - location = self._get_latest_location() - - result = None - try: - result = self._client.get_location_context(location, radius=context_radius) - except Exception: - return "There is an issue with the Google Maps API." - - if not result: - return "Could not find anything about the current location." - - return result.model_dump_json() - - @skill - def get_gps_position_for_queries(self, queries: list[str]) -> str: - """Get the GPS position (latitude/longitude) from Google Maps for know landmarks or searchable locations. - This includes anything that wouldn't be viewable on a physical OSM map including intersections (5th and Natoma) - landmarks (Dolores park), or locations (Tempest bar) - Example: - - get_gps_position_for_queries(['Fort Mason', 'Lafayette Park']) - # returns - [{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}] - - Args: - queries (list[str]): The places you want to look up. - """ - - location = self._get_latest_location() - - results: list[dict[str, Any] | str] = [] - - for query in queries: - try: - latlon = self._client.get_position(query, location) - except Exception: - latlon = None - if latlon: - results.append(latlon.model_dump()) - else: - results.append(f"no result for {query}") - - return json.dumps(results) - - -google_maps_skill = GoogleMapsSkillContainer.blueprint - -__all__ = ["GoogleMapsSkillContainer", "google_maps_skill"] diff --git a/dimos/agents/skills/gps_nav_skill.py b/dimos/agents/skills/gps_nav_skill.py deleted file mode 100644 index 721119f6e6..0000000000 --- a/dimos/agents/skills/gps_nav_skill.py +++ /dev/null @@ -1,109 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.rpc_client import RpcCall -from dimos.core.stream import In, Out -from dimos.mapping.types import LatLon -from dimos.mapping.utils.distance import distance_in_meters -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class GpsNavSkillContainer(Module): - _latest_location: LatLon | None = None - _max_valid_distance: int = 50000 - _set_gps_travel_goal_points: RpcCall | None = None - - gps_location: In[LatLon] - gps_goal: Out[LatLon] - - def __init__(self) -> None: - super().__init__() - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] - - @rpc - def stop(self) -> None: - super().stop() - - @rpc - def set_WebsocketVisModule_set_gps_travel_goal_points(self, callable: RpcCall) -> None: - self._set_gps_travel_goal_points = callable - self._set_gps_travel_goal_points.set_rpc(self.rpc) # type: ignore[arg-type] - - def _on_gps_location(self, location: LatLon) -> None: - self._latest_location = location - - def _get_latest_location(self) -> LatLon: - if not self._latest_location: - raise ValueError("The position has not been set yet.") - return self._latest_location - - @skill - def set_gps_travel_points(self, points: list[dict[str, float]]) -> str: - """Define the movement path determined by GPS coordinates. Requires at least one. You can get the coordinates by using the `get_gps_position_for_queries` skill. - - Example: - - set_gps_travel_goals([{"lat": 37.8059, "lon":-122.4290}, {"lat": 37.7915, "lon": -122.4276}]) - # Travel first to {"lat": 37.8059, "lon":-122.4290} - # then travel to {"lat": 37.7915, "lon": -122.4276} - """ - - new_points = [self._convert_point(x) for x in points] - - if not all(new_points): - parsed = json.dumps([x.__dict__ if x else x for x in new_points]) - return f"Not all points were valid. I parsed this: {parsed}" - - for new_point in new_points: - distance = distance_in_meters(self._get_latest_location(), new_point) # type: ignore[arg-type] - if distance > self._max_valid_distance: - return f"Point {new_point} is too far ({int(distance)} meters away)." - - logger.info(f"Set travel points: {new_points}") - - if self.gps_goal._transport is not None: - self.gps_goal.publish(new_points) # type: ignore[arg-type] - - if self._set_gps_travel_goal_points: - self._set_gps_travel_goal_points(new_points) - - return "I've successfully set the travel points." - - def _convert_point(self, point: dict[str, float]) -> LatLon | None: - if not isinstance(point, dict): - return None - lat = point.get("lat") - lon = point.get("lon") - - if lat is None or lon is None: - return None - - return LatLon(lat=lat, lon=lon) - - -gps_nav_skill = GpsNavSkillContainer.blueprint - - -__all__ = ["GpsNavSkillContainer", "gps_nav_skill"] diff --git a/dimos/agents/skills/navigation.py b/dimos/agents/skills/navigation.py deleted file mode 100644 index 322a09c2bb..0000000000 --- a/dimos/agents/skills/navigation.py +++ /dev/null @@ -1,324 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Any - -from reactivex.disposable import Disposable - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.stream import In -from dimos.models.qwen.video_query import BBox -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.msgs.geometry_msgs.Vector3 import make_vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.navigation.base import NavigationState -from dimos.navigation.visual.query import get_object_bbox_from_image -from dimos.types.robot_location import RobotLocation -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class NavigationSkillContainer(Module): - _latest_image: Image | None = None - _latest_odom: PoseStamped | None = None - _skill_started: bool = False - _similarity_threshold: float = 0.23 - - rpc_calls: list[str] = [ - "SpatialMemory.tag_location", - "SpatialMemory.query_tagged_location", - "SpatialMemory.query_by_text", - "NavigationInterface.set_goal", - "NavigationInterface.get_state", - "NavigationInterface.is_goal_reached", - "NavigationInterface.cancel_goal", - "ObjectTracking.track", - "ObjectTracking.stop_track", - "ObjectTracking.is_tracking", - ] - - color_image: In[Image] - odom: In[PoseStamped] - - def __init__(self) -> None: - super().__init__() - self._skill_started = False - self._vl_model = QwenVlModel() - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(Disposable(self.color_image.subscribe(self._on_color_image))) - self._disposables.add(Disposable(self.odom.subscribe(self._on_odom))) - self._skill_started = True - - @rpc - def stop(self) -> None: - super().stop() - - def _on_color_image(self, image: Image) -> None: - self._latest_image = image - - def _on_odom(self, odom: PoseStamped) -> None: - self._latest_odom = odom - - @skill - def tag_location(self, location_name: str) -> str: - """Tag this location in the spatial memory with a name. - - This associates the current location with the given name in the spatial memory, allowing you to navigate back to it. - - Args: - location_name (str): the name for the location - - Returns: - str: the outcome - """ - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - if not self._latest_odom: - return "No odometry data received yet, cannot tag location." - - position = self._latest_odom.position - rotation = self._latest_odom.orientation - - location = RobotLocation( - name=location_name, - position=(position.x, position.y, position.z), - rotation=(rotation.x, rotation.y, rotation.z), - ) - - tag_location_rpc = self.get_rpc_calls("SpatialMemory.tag_location") - if not tag_location_rpc(location): - return f"Error: Failed to store '{location_name}' in the spatial memory" - - logger.info(f"Tagged {location}") - return f"Tagged '{location_name}': ({position.x},{position.y})." - - @skill - def navigate_with_text(self, query: str) -> str: - """Navigate to a location by querying the existing semantic map using natural language. - - First attempts to locate an object in the robot's camera view using vision. - If the object is found, navigates to it. If not, falls back to querying the - semantic map for a location matching the description. - CALL THIS SKILL FOR ONE SUBJECT AT A TIME. For example: "Go to the person wearing a blue shirt in the living room", - you should call this skill twice, once for the person wearing a blue shirt and once for the living room. - Args: - query: Text query to search for in the semantic map - """ - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - success_msg = self._navigate_by_tagged_location(query) - if success_msg: - return success_msg - - logger.info(f"No tagged location found for {query}") - - success_msg = self._navigate_to_object(query) - if success_msg: - return success_msg - - logger.info(f"No object in view found for {query}") - - success_msg = self._navigate_using_semantic_map(query) - if success_msg: - return success_msg - - return f"No tagged location called '{query}'. No object in view matching '{query}'. No matching location found in semantic map for '{query}'." - - def _navigate_by_tagged_location(self, query: str) -> str | None: - try: - query_tagged_location_rpc = self.get_rpc_calls("SpatialMemory.query_tagged_location") - except Exception: - logger.warning("SpatialMemory module not connected, cannot query tagged locations") - return None - - robot_location = query_tagged_location_rpc(query) - - if not robot_location: - return None - - logger.info("Found tagged location", location=robot_location) - goal_pose = PoseStamped( - position=make_vector3(*robot_location.position), - orientation=Quaternion.from_euler(Vector3(*robot_location.rotation)), - frame_id="map", - ) - - return self._navigate_to(goal_pose, f"Found a tagged location called '{query}'.") - - def _navigate_to(self, pose: PoseStamped, message: str) -> str: - try: - set_goal_rpc = self.get_rpc_calls("NavigationInterface.set_goal") - except Exception: - logger.error("Navigation module not connected properly") - return "Error: Navigation module is not connected, cannot set goal." - - logger.info( - f"Navigating to pose: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" - ) - set_goal_rpc(pose) - - return ( - f"{message}. Started navigating to that position. " - f"To cancel movement call the 'stop_navigation' tool." - ) - - def _navigate_to_object(self, query: str) -> str | None: - try: - bbox = self._get_bbox_for_current_frame(query) - except Exception: - logger.error(f"Failed to get bbox for {query}", exc_info=True) - return None - - if bbox is None: - return None - - try: - track_rpc, stop_track_rpc, is_tracking_rpc = self.get_rpc_calls( - "ObjectTracking.track", "ObjectTracking.stop_track", "ObjectTracking.is_tracking" - ) - except Exception: - logger.error("ObjectTracking module not connected properly") - return None - - try: - get_state_rpc, is_goal_reached_rpc = self.get_rpc_calls( - "NavigationInterface.get_state", "NavigationInterface.is_goal_reached" - ) - except Exception: - logger.error("Navigation module not connected properly") - return None - - logger.info(f"Found {query} at {bbox}") - - # Start tracking - BBoxNavigationModule automatically generates goals - track_rpc(bbox) - - start_time = time.time() - timeout = 30.0 - goal_set = False - - while time.time() - start_time < timeout: - # Check if navigator finished - if get_state_rpc() == NavigationState.IDLE and goal_set: - logger.info("Waiting for goal result") - time.sleep(1.0) - if not is_goal_reached_rpc(): - logger.info(f"Goal cancelled, tracking '{query}' failed") - stop_track_rpc() - return None - else: - logger.info(f"Reached '{query}'") - stop_track_rpc() - return f"Successfully arrived at '{query}'" - - # If goal set and tracking lost, just continue (tracker will resume or timeout) - if goal_set and not is_tracking_rpc(): - continue - - # BBoxNavigationModule automatically sends goals when tracker publishes - # Just check if we have any detections to mark goal_set - if is_tracking_rpc(): - goal_set = True - - time.sleep(0.25) - - logger.warning(f"Navigation to '{query}' timed out after {timeout}s") - stop_track_rpc() - return None - - def _get_bbox_for_current_frame(self, query: str) -> BBox | None: - if self._latest_image is None: - return None - - return get_object_bbox_from_image(self._vl_model, self._latest_image, query) - - def _navigate_using_semantic_map(self, query: str) -> str: - try: - query_by_text_rpc = self.get_rpc_calls("SpatialMemory.query_by_text") - except Exception: - return "Error: The SpatialMemory module is not connected." - - results = query_by_text_rpc(query) - - if not results: - return f"No matching location found in semantic map for '{query}'" - - best_match = results[0] - - goal_pose = self._get_goal_pose_from_result(best_match) - - logger.info("Goal pose for semantic nav", pose=goal_pose) - if not goal_pose: - return f"Found a result for '{query}' but it didn't have a valid position." - - message = f"Found a location in the semantic map matching '{query}'." - return self._navigate_to(goal_pose, message) - - @skill - def stop_navigation(self) -> str: - """Immediatly stop moving.""" - - if not self._skill_started: - raise ValueError(f"{self} has not been started.") - - self._cancel_goal_and_stop() - - return "Stopped" - - def _cancel_goal_and_stop(self) -> None: - try: - cancel_goal_rpc = self.get_rpc_calls("NavigationInterface.cancel_goal") - except Exception: - logger.warning("Navigation module not connected, cannot cancel goal") - return - - cancel_goal_rpc() - - def _get_goal_pose_from_result(self, result: dict[str, Any]) -> PoseStamped | None: - similarity = 1.0 - (result.get("distance") or 1) - if similarity < self._similarity_threshold: - logger.warning( - f"Match found but similarity score ({similarity:.4f}) is below threshold ({self._similarity_threshold})" - ) - return None - - metadata = result.get("metadata") - if not metadata: - return None - first = metadata[0] - pos_x = first.get("pos_x", 0) - pos_y = first.get("pos_y", 0) - theta = first.get("rot_z", 0) - - return PoseStamped( - position=make_vector3(pos_x, pos_y, 0), - orientation=Quaternion.from_euler(make_vector3(0, 0, theta)), - frame_id="map", - ) - - -navigation_skill = NavigationSkillContainer.blueprint - -__all__ = ["NavigationSkillContainer", "navigation_skill"] diff --git a/dimos/agents/skills/osm.py b/dimos/agents/skills/osm.py deleted file mode 100644 index 9bb731cf72..0000000000 --- a/dimos/agents/skills/osm.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.agents.annotation import skill -from dimos.core.module import Module -from dimos.core.stream import In -from dimos.mapping.osm.current_location_map import CurrentLocationMap -from dimos.mapping.types import LatLon -from dimos.mapping.utils.distance import distance_in_meters -from dimos.models.vl.qwen import QwenVlModel -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class OsmSkill(Module): - _latest_location: LatLon | None - _current_location_map: CurrentLocationMap - - gps_location: In[LatLon] - - def __init__(self) -> None: - super().__init__() - self._latest_location = None - self._current_location_map = CurrentLocationMap(QwenVlModel()) - - def start(self) -> None: - super().start() - self._disposables.add(self.gps_location.subscribe(self._on_gps_location)) # type: ignore[arg-type] - - def stop(self) -> None: - super().stop() - - def _on_gps_location(self, location: LatLon) -> None: - self._latest_location = location - - @skill - def map_query(self, query_sentence: str) -> str: - """This skill uses a vision language model to find something on the map - based on the query sentence. You can query it with something like "Where - can I find a coffee shop?" and it returns the latitude and longitude. - - Example: - - map_query("Where can I find a coffee shop?") - - Args: - query_sentence (str): The query sentence. - """ - - self._current_location_map.update_position(self._latest_location) # type: ignore[arg-type] - location = self._current_location_map.query_for_one_position_and_context( - query_sentence, - self._latest_location, # type: ignore[arg-type] - ) - if not location: - return "Could not find anything." - - latlon, context = location - - distance = int(distance_in_meters(latlon, self._latest_location)) # type: ignore[arg-type] - - return f"{context}. It's at position latitude={latlon.lat}, longitude={latlon.lon}. It is {distance} meters away." - - -osm_skill = OsmSkill.blueprint - -__all__ = ["OsmSkill", "osm_skill"] diff --git a/dimos/agents/skills/person_follow.py b/dimos/agents/skills/person_follow.py deleted file mode 100644 index 641055e6f6..0000000000 --- a/dimos/agents/skills/person_follow.py +++ /dev/null @@ -1,270 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Event, RLock, Thread -import time -from typing import TYPE_CHECKING - -from langchain_core.messages import HumanMessage -import numpy as np -from reactivex.disposable import Disposable - -from dimos.agents.agent import AgentSpec -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.global_config import GlobalConfig -from dimos.core.module import Module -from dimos.core.stream import In, Out -from dimos.models.qwen.video_query import BBox -from dimos.models.segmentation.edge_tam import EdgeTAMProcessor -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.geometry_msgs import Twist -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.navigation.visual.query import get_object_bbox_from_image -from dimos.navigation.visual_servoing.detection_navigation import DetectionNavigation -from dimos.navigation.visual_servoing.visual_servoing_2d import VisualServoing2D -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.models.vl.base import VlModel - -logger = setup_logger() - - -class PersonFollowSkillContainer(Module): - """Skill container for following a person. - - This skill uses: - - A VL model (QwenVlModel) to initially detect a person from a text description. - - EdgeTAM for continuous tracking across frames. - - Visual servoing OR 3D navigation to control robot movement towards the person. - - Does not do obstacle avoidance; assumes a clear path. - """ - - color_image: In[Image] - global_map: In[PointCloud2] - cmd_vel: Out[Twist] - - _agent_spec: AgentSpec - _frequency: float = 20.0 # Hz - control loop frequency - _max_lost_frames: int = 15 # number of frames to wait before declaring person lost - - def __init__( - self, - camera_info: CameraInfo, - cfg: GlobalConfig, - use_3d_navigation: bool = False, - ) -> None: - super().__init__() - self._global_config: GlobalConfig = cfg - self._use_3d_navigation: bool = use_3d_navigation - self._latest_image: Image | None = None - self._latest_pointcloud: PointCloud2 | None = None - self._vl_model: VlModel = QwenVlModel() - self._tracker: EdgeTAMProcessor | None = None - self._thread: Thread | None = None - self._should_stop: Event = Event() - self._lock = RLock() - - # Use MuJoCo camera intrinsics in simulation mode - if self._global_config.simulation: - from dimos.robot.unitree.mujoco_connection import MujocoConnection - - camera_info = MujocoConnection.camera_info_static - - self._camera_info = camera_info - self._visual_servo = VisualServoing2D(camera_info, self._global_config.simulation) - self._detection_navigation = DetectionNavigation(self.tf, camera_info) - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(Disposable(self.color_image.subscribe(self._on_color_image))) - if self._use_3d_navigation: - self._disposables.add(Disposable(self.global_map.subscribe(self._on_pointcloud))) - - @rpc - def stop(self) -> None: - self._stop_following() - - with self._lock: - if self._tracker is not None: - self._tracker.stop() - self._tracker = None - - self._vl_model.stop() - super().stop() - - @skill - def follow_person(self, query: str) -> str: - """Follow a person matching the given description using visual servoing. - - The robot will continuously track and follow the person, while keeping - them centered in the camera view. - - Args: - query: Description of the person to follow (e.g., "man with blue shirt") - - Returns: - Status message indicating the result of the following action. - - Example: - follow_person("man with blue shirt") - follow_person("person in the doorway") - """ - - self._stop_following() - - self._should_stop.clear() - - with self._lock: - latest_image = self._latest_image - - if latest_image is None: - return "No image available to detect person." - - initial_bbox = get_object_bbox_from_image( - self._vl_model, - latest_image, - query, - ) - - if initial_bbox is None: - return f"Could not find '{query}' in the current view." - - return self._follow_person(query, initial_bbox) - - @skill - def stop_following(self) -> str: - """Stop following the current person. - - Returns: - Confirmation message. - """ - self._stop_following() - - self.cmd_vel.publish(Twist.zero()) - - if self._thread is not None: - self._thread.join(timeout=2) - self._thread = None - - return "Stopped following." - - def _on_color_image(self, image: Image) -> None: - with self._lock: - self._latest_image = image - - def _on_pointcloud(self, pointcloud: PointCloud2) -> None: - with self._lock: - self._latest_pointcloud = pointcloud - - def _follow_person(self, query: str, initial_bbox: BBox) -> str: - x1, y1, x2, y2 = initial_bbox - box = np.array([x1, y1, x2, y2], dtype=np.float32) - - with self._lock: - if self._tracker is None: - self._tracker = EdgeTAMProcessor() - tracker = self._tracker - latest_image = self._latest_image - if latest_image is None: - return "No image available to start tracking." - - initial_detections = tracker.init_track( - image=latest_image, - box=box, - obj_id=1, - ) - - if len(initial_detections) == 0: - self.cmd_vel.publish(Twist.zero()) - return f"EdgeTAM failed to segment '{query}'." - - logger.info(f"EdgeTAM initialized with {len(initial_detections)} detections") - - self._thread = Thread(target=self._follow_loop, args=(tracker, query)) - self._thread.start() - - return ( - "Found the person. Starting to follow. You can stop following by calling " - "the 'stop_following' tool." - ) - - def _follow_loop(self, tracker: EdgeTAMProcessor, query: str) -> None: - lost_count = 0 - period = 1.0 / self._frequency - next_time = time.monotonic() - - while not self._should_stop.is_set(): - next_time += period - - with self._lock: - latest_image = self._latest_image - assert latest_image is not None - - detections = tracker.process_image(latest_image) - - if len(detections) == 0: - self.cmd_vel.publish(Twist.zero()) - - lost_count += 1 - if lost_count > self._max_lost_frames: - self._send_stop_reason(query, "lost track of the person") - return - else: - lost_count = 0 - best_detection = max(detections.detections, key=lambda d: d.bbox_2d_volume()) - - if self._use_3d_navigation: - with self._lock: - pointcloud = self._latest_pointcloud - if pointcloud is None: - self._send_stop_reason(query, "no pointcloud available for 3D navigation") - return - twist = self._detection_navigation.compute_twist_for_detection_3d( - pointcloud, - best_detection, - latest_image, - ) - if twist is None: - self._send_stop_reason(query, "3D navigation failed") - return - else: - twist = self._visual_servo.compute_twist( - best_detection.bbox, - latest_image.width, - ) - self.cmd_vel.publish(twist) - - now = time.monotonic() - sleep_duration = next_time - now - if sleep_duration > 0: - time.sleep(sleep_duration) - - self._send_stop_reason(query, "it was requested to stop following") - - def _stop_following(self) -> None: - self._should_stop.set() - - def _send_stop_reason(self, query: str, reason: str) -> None: - self.cmd_vel.publish(Twist.zero()) - message = f"Person follow stopped for '{query}'. Reason: {reason}." - self._agent_spec.add_message(HumanMessage(message)) - logger.info("Person follow stopped", query=query, reason=reason) - - -person_follow_skill = PersonFollowSkillContainer.blueprint - -__all__ = ["PersonFollowSkillContainer", "person_follow_skill"] diff --git a/dimos/agents/skills/speak_skill.py b/dimos/agents/skills/speak_skill.py deleted file mode 100644 index aa06d30ba4..0000000000 --- a/dimos/agents/skills/speak_skill.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -from reactivex import Subject - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.stream.audio.node_output import SounddeviceAudioOutput -from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class SpeakSkill(Module): - _tts_node: OpenAITTSNode | None = None - _audio_output: SounddeviceAudioOutput | None = None - _audio_lock: threading.Lock = threading.Lock() - - @rpc - def start(self) -> None: - super().start() - self._tts_node = OpenAITTSNode(speed=1.2, voice=Voice.ONYX) - self._audio_output = SounddeviceAudioOutput(sample_rate=24000) - self._audio_output.consume_audio(self._tts_node.emit_audio()) - - @rpc - def stop(self) -> None: - if self._tts_node: - self._tts_node.dispose() - self._tts_node = None - if self._audio_output: - self._audio_output.stop() - self._audio_output = None - super().stop() - - @skill - def speak(self, text: str) -> str: - """Speak text out loud through the robot's speakers. - - USE THIS TOOL AS OFTEN AS NEEDED. People can't normally see what you say in text, but can hear what you speak. - - Try to be as concise as possible. Remember that speaking takes time, so get to the point quickly. - - Example usage: - - speak("Hello, I am your robot assistant.") - """ - if self._tts_node is None: - return "Error: TTS not initialized" - - # Use lock to prevent simultaneous speech - with self._audio_lock: - text_subject: Subject[str] = Subject() - audio_complete = threading.Event() - self._tts_node.consume_text(text_subject) - - def set_as_complete(_t: str) -> None: - audio_complete.set() - - def set_as_complete_e(_e: Exception) -> None: - audio_complete.set() - - subscription = self._tts_node.emit_text().subscribe( - on_next=set_as_complete, - on_error=set_as_complete_e, - ) - - text_subject.on_next(text) - text_subject.on_completed() - - timeout = max(5, len(text) * 0.1) - - if not audio_complete.wait(timeout=timeout): - logger.warning(f"TTS timeout reached for: {text}") - subscription.dispose() - return f"Warning: TTS timeout while speaking: {text}" - else: - # Small delay to ensure buffers flush - time.sleep(0.3) - - subscription.dispose() - - return f"Spoke: {text}" - - -speak_skill = SpeakSkill.blueprint - -__all__ = ["SpeakSkill", "speak_skill"] diff --git a/dimos/agents/skills/test_google_maps_skill_container.py b/dimos/agents/skills/test_google_maps_skill_container.py deleted file mode 100644 index 84da91e886..0000000000 --- a/dimos/agents/skills/test_google_maps_skill_container.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from langchain_core.messages import HumanMessage -import pytest - -from dimos.agents.skills.google_maps_skill_container import GoogleMapsSkillContainer -from dimos.core.module import Module -from dimos.core.stream import Out -from dimos.mapping.google_maps.types import Coordinates, LocationContext, Position -from dimos.mapping.types import LatLon - - -class FakeGPS(Module): - """Provides a gps_location output so GoogleMapsSkillContainer's input port gets a transport.""" - - gps_location: Out[LatLon] - - -class FakeLocationClient: - def get_location_context(self, location, radius=200): - return LocationContext( - street="Bourbon Street", - coordinates=Coordinates(lat=37.782654, lon=-122.413273), - ) - - -class MockedWhereAmISkill(GoogleMapsSkillContainer): - def __init__(self): - Module.__init__(self) # Skip GoogleMapsSkillContainer's __init__. - self._client = FakeLocationClient() - self._latest_location = LatLon(lat=37.782654, lon=-122.413273) - self._started = True - self._max_valid_distance = 20000 - - -class FakePositionClient: - def __init__(self): - self._positions = iter( - [ - Position(lat=37.782601, lon=-122.413201, description="address 1"), - Position(lat=37.782602, lon=-122.413202, description="address 2"), - Position(lat=37.782603, lon=-122.413203, description="address 3"), - ] - ) - - def get_position(self, query, location): - return next(self._positions) - - -class MockedPositionSkill(GoogleMapsSkillContainer): - def __init__(self): - Module.__init__(self) - self._client = FakePositionClient() - self._latest_location = LatLon(lat=37.782654, lon=-122.413273) - self._started = True - self._max_valid_distance = 20000 - - -@pytest.mark.integration -def test_where_am_i(agent_setup) -> None: - history = agent_setup( - blueprints=[FakeGPS.blueprint(), MockedWhereAmISkill.blueprint()], - messages=[HumanMessage("What street am I on? Use the where_am_i tool.")], - ) - - assert "bourbon" in history[-1].content.lower() - - -@pytest.mark.integration -def test_get_gps_position_for_queries(agent_setup) -> None: - history = agent_setup( - blueprints=[FakeGPS.blueprint(), MockedPositionSkill.blueprint()], - messages=[ - HumanMessage( - "What are the lat/lon for hyde park, regent park, russell park? " - "Use the get_gps_position_for_queries tool." - ) - ], - ) - - regex = r".*37\.782601.*122\.413201.*37\.782602.*122\.413202.*37\.782603.*122\.413203.*" - assert re.match(regex, history[-1].content, re.DOTALL) diff --git a/dimos/agents/skills/test_gps_nav_skills.py b/dimos/agents/skills/test_gps_nav_skills.py deleted file mode 100644 index afcb4d36d0..0000000000 --- a/dimos/agents/skills/test_gps_nav_skills.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from langchain_core.messages import HumanMessage -import pytest - -from dimos.agents.skills.gps_nav_skill import GpsNavSkillContainer -from dimos.core.module import Module -from dimos.core.stream import Out -from dimos.mapping.types import LatLon - - -class FakeGPS(Module): - """Provides a gps_location output so GpsNavSkillContainer's input port gets a transport.""" - - gps_location: Out[LatLon] - - -class MockedGpsNavSkill(GpsNavSkillContainer): - def __init__(self): - Module.__init__(self) - self._latest_location = LatLon(lat=37.782654, lon=-122.413273) - self._started = True - self._max_valid_distance = 50000 - - -@pytest.mark.integration -def test_set_gps_travel_points(agent_setup) -> None: - history = agent_setup( - blueprints=[FakeGPS.blueprint(), MockedGpsNavSkill.blueprint()], - messages=[ - HumanMessage( - 'Set GPS travel points to [{"lat": 37.782654, "lon": -122.413273}]. ' - "Use the set_gps_travel_points tool." - ) - ], - ) - - assert "success" in history[-1].content.lower() - - -@pytest.mark.integration -def test_set_gps_travel_points_multiple(agent_setup) -> None: - history = agent_setup( - blueprints=[FakeGPS.blueprint(), MockedGpsNavSkill.blueprint()], - messages=[ - HumanMessage( - "Set GPS travel points to these locations in order: " - '{"lat": 37.782654, "lon": -122.413273}, ' - '{"lat": 37.782660, "lon": -122.413260}, ' - '{"lat": 37.782670, "lon": -122.413270}. ' - "Use the set_gps_travel_points tool." - ) - ], - ) - - assert "success" in history[-1].content.lower() diff --git a/dimos/agents/skills/test_navigation.py b/dimos/agents/skills/test_navigation.py deleted file mode 100644 index 91737ada77..0000000000 --- a/dimos/agents/skills/test_navigation.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from langchain_core.messages import HumanMessage -import pytest - -from dimos.agents.skills.navigation import NavigationSkillContainer -from dimos.core.module import Module -from dimos.core.stream import Out -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image - - -class FakeCamera(Module): - color_image: Out[Image] - - -class FakeOdom(Module): - odom: Out[PoseStamped] - - -class MockedStopNavSkill(NavigationSkillContainer): - rpc_calls: list[str] = [] - - def __init__(self): - Module.__init__(self) - self._skill_started = True - - def _cancel_goal_and_stop(self): - pass - - -class MockedExploreNavSkill(NavigationSkillContainer): - rpc_calls: list[str] = [] - - def __init__(self): - Module.__init__(self) - self._skill_started = True - - def _start_exploration(self, timeout): - return "Exploration completed successfuly" - - def _cancel_goal_and_stop(self): - pass - - -class MockedSemanticNavSkill(NavigationSkillContainer): - rpc_calls: list[str] = [] - - def __init__(self): - Module.__init__(self) - self._skill_started = True - - def _navigate_by_tagged_location(self, query): - return None - - def _navigate_to_object(self, query): - return None - - def _navigate_using_semantic_map(self, query): - return f"Successfuly arrived at '{query}'" - - -@pytest.mark.integration -def test_stop_movement(agent_setup) -> None: - history = agent_setup( - blueprints=[ - FakeCamera.blueprint(), - FakeOdom.blueprint(), - MockedStopNavSkill.blueprint(), - ], - messages=[HumanMessage("Stop moving. Use the stop_movement tool.")], - ) - - assert "stopped" in history[-1].content.lower() - - -@pytest.mark.integration -def test_start_exploration(agent_setup) -> None: - history = agent_setup( - blueprints=[ - FakeCamera.blueprint(), - FakeOdom.blueprint(), - MockedExploreNavSkill.blueprint(), - ], - messages=[ - HumanMessage("Take a look around for 10 seconds. Use the start_exploration tool.") - ], - ) - - assert "explor" in history[-1].content.lower() - - -@pytest.mark.integration -def test_go_to_semantic_location(agent_setup) -> None: - history = agent_setup( - blueprints=[ - FakeCamera.blueprint(), - FakeOdom.blueprint(), - MockedSemanticNavSkill.blueprint(), - ], - messages=[HumanMessage("Go to the bookshelf. Use the navigate_with_text tool.")], - ) - - assert "success" in history[-1].content.lower() diff --git a/dimos/agents/skills/test_unitree_skill_container.py b/dimos/agents/skills/test_unitree_skill_container.py deleted file mode 100644 index ea1cfba5cf..0000000000 --- a/dimos/agents/skills/test_unitree_skill_container.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import difflib - -from langchain_core.messages import HumanMessage -import pytest - -from dimos.robot.unitree.unitree_skill_container import _UNITREE_COMMANDS, UnitreeSkillContainer - - -class MockedUnitreeSkill(UnitreeSkillContainer): - rpc_calls: list[str] = [] - - def __init__(self): - super().__init__() - # Provide a fake RPC so the real execute_sport_command runs end-to-end. - self._bound_rpc_calls["GO2Connection.publish_request"] = lambda *args, **kwargs: None - - -@pytest.mark.integration -def test_pounce(agent_setup) -> None: - history = agent_setup( - blueprints=[MockedUnitreeSkill.blueprint()], - messages=[HumanMessage("Pounce! Use the execute_sport_command tool.")], - ) - - response = history[-1].content.lower() - assert "pounce" in response - - -def test_did_you_mean() -> None: - suggestions = difflib.get_close_matches("Pounce", _UNITREE_COMMANDS.keys(), n=3, cutoff=0.6) - assert "FrontPounce" in suggestions - assert "Pose" in suggestions diff --git a/dimos/agents/system_prompt.py b/dimos/agents/system_prompt.py deleted file mode 100644 index 54f713f538..0000000000 --- a/dimos/agents/system_prompt.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -SYSTEM_PROMPT = """ -You are Daneel, an AI agent created by Dimensional to control a Unitree Go2 quadruped robot. - -# CRITICAL: SAFETY -Prioritize human safety above all else. Respect personal boundaries. Never take actions that could harm humans, damage property, or damage the robot. - -# IDENTITY -You are Daneel. If someone says "daniel" or similar, ignore it (speech-to-text error). When greeted, briefly introduce yourself as an AI agent operating autonomously in physical space. - -# COMMUNICATION -Users hear you through speakers but cannot see text. Use `speak` to communicate your actions or responses. Be concise—one or two sentences. - -# SKILL COORDINATION - -## Navigation Flow -- Use `navigate_with_text` for most navigation. It searches tagged locations first, then visible objects, then the semantic map. -- Tag important locations with `tag_location` so you can return to them later. -- During `start_exploration`, avoid calling other skills except `stop_movement`. -- Always run `execute_sport_command("RecoveryStand")` after dynamic movements (flips, jumps, sit) before navigating. - -## GPS Navigation Flow -For outdoor/GPS-based navigation: -1. Use `get_gps_position_for_queries` to look up coordinates for landmarks -2. Then use `set_gps_travel_points` with those coordinates - -## Location Awareness -- `where_am_i` gives your current street/area and nearby landmarks -- `map_query` finds places on the OSM map by description and returns coordinates - -# BEHAVIOR - -## Be Proactive -Infer reasonable actions from ambiguous requests. If someone says "greet the new arrivals," head to the front door. Inform the user of your assumption: "Heading to the front door—let me know if I should go elsewhere." - -## Deliveries & Pickups -- Deliveries: announce yourself with `speak`, call `wait` for 5 seconds, then continue. -- Pickups: ask for help with `speak`, wait for a response, then continue. - -""" diff --git a/dimos/agents/test_agent.py b/dimos/agents/test_agent.py deleted file mode 100644 index da69dfb7dc..0000000000 --- a/dimos/agents/test_agent.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from langchain_core.messages import HumanMessage -import pytest - -from dimos.agents.annotation import skill -from dimos.core.module import Module -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -class Adder(Module): - @skill - def add(self, x: int, y: int) -> str: - """adds x and y.""" - return str(x + y) - - -@pytest.mark.integration -@pytest.mark.parametrize("dask", [False, True]) -def test_can_call_tool(dask, agent_setup): - history = agent_setup( - blueprints=[Adder.blueprint()], - messages=[HumanMessage("What is 33333 + 100? Use the tool.")], - dask=dask, - ) - - assert "33433" in history[-1].content - - -class UserRegistration(Module): - def __init__(self): - super().__init__() - self._first_call = True - self._use_upper = False - - @skill - def register_user(self, name: str) -> str: - """registers a user by name.""" - - # If the agent calls with "paul" or "Paul", always say it's the wrong way - # to force it to try again. - - if self._first_call: - self._first_call = False - self._use_upper = not name[0].isupper() - - if self._use_upper and not name[0].isupper(): - return ValueError("Names must start with an uppercase letter.") - if not self._use_upper and name[0].isupper(): - return ValueError("The names must only use lowercase letters.") - - global _correct_name_registered - _correct_name_registered = True - return "User name registered successfully." - - -@pytest.mark.integration -@pytest.mark.parametrize("dask", [False, True]) -def test_can_call_again_on_error(dask, agent_setup): - history = agent_setup( - blueprints=[UserRegistration.blueprint()], - messages=[ - HumanMessage( - "Register a user named 'Paul'. If there are errors, just try again until you succeed." - ) - ], - dask=dask, - ) - - assert any(message.content == "User name registered successfully." for message in history) - - -class MultipleTools(Module): - def __init__(self): - super().__init__() - self._people = {"Ben": "office", "Bob": "garage"} - - @skill - def register_person(self, name: str) -> str: - """Registers a person by name.""" - if name.lower() == "john": - self._people[name] = "kitchen" - elif name.lower() == "jane": - self._people[name] = "living room" - return f"'{name}' has been registered." - - @skill - def locate_person(self, name: str) -> str: - """Locates a person by name.""" - if name not in self._people: - known_people = list(self._people.keys()) - return ( - f"Error: '{name}' is not registered. People cannot be located until they've " - f"been registered in the system. People known so far: {', '.join(known_people)}. " - "Use register_person to register a person." - ) - return f"'{name}' is located at '{self._people[name]}'." - - -class NavigationSkill(Module): - @skill - def go_to_location(self, description: str) -> str: - """Go to a location by a description.""" - if description.strip().lower() not in ["kitchen", "living room"]: - return f"Error: Unknown location description: '{description}'." - return f"Going to the {description}." - - -@pytest.mark.integration -def test_multiple_tool_calls_with_multiple_messages(agent_setup): - history = agent_setup( - blueprints=[MultipleTools.blueprint(), NavigationSkill.blueprint()], - messages=[ - HumanMessage( - "You are a robot assistant. Move to the location where John is. Don't ask me for feedback, just go there." - ), - HumanMessage("Nice job. You did it. Now go to the location where Jane is."), - ], - ) - - # Collect all go_to_location calls from the history - go_to_location_calls = [] - for message in history: - if hasattr(message, "tool_calls"): - for tool_call in message.tool_calls: - if tool_call["name"] == "go_to_location": - go_to_location_calls.append(tool_call) - - # Find the index of the second HumanMessage to split first/second prompt - second_human_idx = None - human_count = 0 - for i, message in enumerate(history): - if isinstance(message, HumanMessage): - human_count += 1 - if human_count == 2: - second_human_idx = i - break - - # Collect go_to_location calls before and after the second prompt - calls_after_first_prompt = [] - calls_after_second_prompt = [] - for i, message in enumerate(history): - if hasattr(message, "tool_calls"): - for tool_call in message.tool_calls: - if tool_call["name"] == "go_to_location": - if i < second_human_idx: - calls_after_first_prompt.append(tool_call) - else: - calls_after_second_prompt.append(tool_call) - - # After the first prompt, go_to_location should be called with "kitchen" - assert len(calls_after_first_prompt) == 1 - assert "kitchen" in calls_after_first_prompt[0]["args"]["description"].lower() - - # After the second prompt, go_to_location should be called with "living room" - assert len(calls_after_second_prompt) == 1 - assert "living room" in calls_after_second_prompt[0]["args"]["description"].lower() - - # There should be exactly two go_to_location calls total - assert len(go_to_location_calls) == 2 - - -@pytest.mark.integration -def test_prompt(agent_setup): - history = agent_setup( - blueprints=[], - messages=[HumanMessage("What is your name?")], - system_prompt="You are a helpful assistant named Johnny.", - ) - - assert "Johnny" in history[-1].content - - -class Visualizer(Module): - @skill - def take_a_picture(self) -> Image: - """Takes a picture.""" - return Image.from_file(get_data("cafe-smol.jpg")).to_rgb() - - -@pytest.mark.integration -def test_image(agent_setup): - history = agent_setup( - blueprints=[Visualizer.blueprint()], - messages=[ - HumanMessage( - "What do you see? Take a picture using your camera and describe it. " - "Please mention one of the words which best match the image: " - "'stadium', 'cafe', 'battleship'." - ) - ], - system_prompt="You are a helpful assistant that can use a camera to take pictures.", - ) - - response = history[-1].content.lower() - assert "cafe" in response - assert "stadium" not in response - assert "battleship" not in response diff --git a/dimos/agents/testing.py b/dimos/agents/testing.py deleted file mode 100644 index d03d3a1263..0000000000 --- a/dimos/agents/testing.py +++ /dev/null @@ -1,194 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Testing utilities for agents.""" - -from collections.abc import Iterator, Sequence -import json -import os -from pathlib import Path -from typing import Any - -from langchain.chat_models import init_chat_model -from langchain_core.callbacks.manager import CallbackManagerForLLMRun -from langchain_core.language_models.chat_models import SimpleChatModel -from langchain_core.messages import ( - AIMessage, - AIMessageChunk, - BaseMessage, -) -from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult -from langchain_core.runnables import Runnable - - -class MockModel(SimpleChatModel): - """Custom fake chat model that supports tool calls for testing. - - Can operate in two modes: - 1. Playback mode (default): Reads responses from a JSON file or list - 2. Record mode: Uses a real LLM and saves responses to a JSON file - - Set the RECORD environment variable to enable record mode. - """ - - responses: list[str | AIMessage] = [] - i: int = 0 - json_path: Path | None = None - record: bool = False - real_model: Any | None = None - recorded_messages: list[dict[str, Any]] = [] - - def __init__(self, **kwargs: Any) -> None: - responses = kwargs.pop("responses", []) - json_path = kwargs.pop("json_path", None) - model_provider = kwargs.pop("model_provider", "openai") - model_name = kwargs.pop("model_name", "gpt-4o") - - super().__init__(**kwargs) - - self.json_path = Path(json_path) if json_path else None - self.record = bool(os.getenv("RECORD")) - self.i = 0 - self._bound_tools: Sequence[Any] | None = None - self.recorded_messages = [] - - if self.record: - self.real_model = init_chat_model(model_provider=model_provider, model=model_name) - self.responses = [] - elif self.json_path: - self.responses = self._load_responses_from_json() # type: ignore[assignment] - elif responses: - self.responses = responses - else: - raise ValueError("no responses") - - @property - def _llm_type(self) -> str: - return "tool-call-fake-chat-model" - - def _load_responses_from_json(self) -> list[AIMessage]: - with open(self.json_path) as f: # type: ignore[arg-type] - data = json.load(f) - - responses = [] - for item in data.get("responses", []): - if isinstance(item, str): - responses.append(AIMessage(content=item)) - else: - msg = AIMessage( - content=item.get("content", ""), tool_calls=item.get("tool_calls", []) - ) - responses.append(msg) - return responses - - def _save_responses_to_json(self) -> None: - if not self.json_path: - return - - self.json_path.parent.mkdir(parents=True, exist_ok=True) - - data = { - "responses": [ - {"content": msg.content, "tool_calls": getattr(msg, "tool_calls", [])} - if isinstance(msg, AIMessage) - else msg - for msg in self.recorded_messages - ] - } - - with open(self.json_path, "w") as f: - f.write(json.dumps(data, indent=2, default=str) + "\n") - - def _call( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> str: - return "" - - def _generate( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> ChatResult: - if self.record: - if not self.real_model: - raise ValueError("Real model not initialized for recording") - - model = self.real_model - if self._bound_tools: - model = model.bind_tools(self._bound_tools) - - result = model.invoke(messages) - self.recorded_messages.append(result) - self._save_responses_to_json() - - generation = ChatGeneration(message=result) - return ChatResult(generations=[generation]) - else: - if not self.responses: - raise ValueError("No responses available for playback.") - - if self.i >= len(self.responses): - response = self.responses[-1] - else: - response = self.responses[self.i] - self.i += 1 - - if isinstance(response, AIMessage): - message = response - else: - message = AIMessage(content=str(response)) - - generation = ChatGeneration(message=message) - return ChatResult(generations=[generation]) - - def _stream( - self, - messages: list[BaseMessage], - stop: list[str] | None = None, - run_manager: CallbackManagerForLLMRun | None = None, - **kwargs: Any, - ) -> Iterator[ChatGenerationChunk]: - result = self._generate(messages, stop, run_manager, **kwargs) - message = result.generations[0].message - chunk = AIMessageChunk( - content=message.content, - tool_call_chunks=[ - { - "name": tc["name"], - "args": json.dumps(tc["args"]), - "id": tc["id"], - "index": i, - } - for i, tc in enumerate(getattr(message, "tool_calls", [])) - ], - ) - yield ChatGenerationChunk(message=chunk) - - def bind_tools( - self, - tools: Sequence[dict[str, Any] | type | Any], - *, - tool_choice: str | None = None, - **kwargs: Any, - ) -> Runnable: # type: ignore[type-arg] - self._bound_tools = tools - if self.record and self.real_model: - self.real_model = self.real_model.bind_tools(tools, tool_choice=tool_choice, **kwargs) - return self diff --git a/dimos/agents/utils.py b/dimos/agents/utils.py deleted file mode 100644 index 061e5ebb13..0000000000 --- a/dimos/agents/utils.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime -from typing import Any - -from langchain_core.messages.base import BaseMessage - -CYAN = "\033[36m" -YELLOW = "\033[33m" -GREEN = "\033[32m" -MAGENTA = "\033[35m" -BLUE = "\033[34m" -GRAY = "\033[90m" -RESET = "\033[0m" -BOLD = "\033[1m" - -TYPE_WIDTH = 12 - - -def pretty_print_langchain_message(msg: BaseMessage) -> None: - d = msg.__dict__ - msg_type = d.get("type", "unknown") - - type_colors = { - "human": CYAN, - "ai": GREEN, - "tool": YELLOW, - "system": MAGENTA, - } - type_color = type_colors.get(msg_type, RESET) - - print(f"{GRAY}{'-' * 60}{RESET}") - - timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] - time_str = f"{GRAY}{timestamp}{RESET} " - type_str = f"{type_color}{msg_type:<{TYPE_WIDTH}}{RESET}" - - content = d.get("content", "") - tool_calls = d.get("tool_calls", []) - - # 12 chars for timestamp + 1 space + TYPE_WIDTH + 1 space - indent = " " * (12 + 1 + TYPE_WIDTH + 1) - first_line = True - - def print_line(text: str) -> None: - nonlocal first_line - if first_line: - print(f"{time_str} {type_str} {text}") - first_line = False - else: - print(f"{indent}{text}") - - if content: - content_str = repr(_try_to_remove_url_data(content)) - if len(content_str) > 2000: - content_str = content_str[:5000] + "... [truncated]" - print_line(f"{BOLD}{type_color}{content_str}{RESET}") - - if tool_calls: - print_line(f"{MAGENTA}tool_calls:{RESET}") - for tc in tool_calls: - name = tc.get("name") - args = tc.get("args") - print_line(f" - {BLUE}{name}{RESET}({CYAN}{args}{RESET})") - - if first_line: - print(f"{time_str} {type_str}") - - -def _try_to_remove_url_data(content: Any) -> Any: - if not isinstance(content, list): - return content - - ret = [] - - for item in content: - if isinstance(item, dict) and item.get("type") == "image_url": - ret.append({**item, "image_url": ""}) - else: - ret.append(item) - - return ret diff --git a/dimos/agents/vlm_agent.py b/dimos/agents/vlm_agent.py deleted file mode 100644 index c99f8afa49..0000000000 --- a/dimos/agents/vlm_agent.py +++ /dev/null @@ -1,131 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -from langchain.chat_models import init_chat_model -from langchain_core.messages import AIMessage, HumanMessage, SystemMessage - -from dimos.agents.system_prompt import SYSTEM_PROMPT -from dimos.core import Module, rpc -from dimos.core.module import ModuleConfig -from dimos.core.stream import In, Out -from dimos.msgs.sensor_msgs import Image -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from langchain_core.language_models.chat_models import BaseChatModel - -logger = setup_logger() - - -@dataclass -class VLMAgentConfig(ModuleConfig): - model: str = "gpt-4o" - system_prompt: str | None = SYSTEM_PROMPT - - -class VLMAgent(Module): - """Stream-first agent for vision queries with optional RPC access.""" - - default_config: type[VLMAgentConfig] = VLMAgentConfig - config: VLMAgentConfig - - color_image: In[Image] - query_stream: In[HumanMessage] - answer_stream: Out[AIMessage] - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - if self.config.model.startswith("ollama:"): - from dimos.agents.ollama_agent import ensure_ollama_model - - ensure_ollama_model(self.config.model.removeprefix("ollama:")) - - self._llm: BaseChatModel = init_chat_model(self.config.model) # type: ignore[assignment] - self._latest_image: Image | None = None - self._history: list[AIMessage | HumanMessage] = [] - self._system_message = SystemMessage(self.config.system_prompt or SYSTEM_PROMPT) - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(self.color_image.subscribe(self._on_image)) # type: ignore[arg-type] - self._disposables.add(self.query_stream.subscribe(self._on_query)) # type: ignore[arg-type] - - @rpc - def stop(self) -> None: - super().stop() - - def _on_image(self, image: Image) -> None: - self._latest_image = image - - def _on_query(self, msg: HumanMessage) -> None: - if not self._latest_image: - self.answer_stream.publish(AIMessage(content="No image available yet.")) - return - - query_text = self._extract_text(msg) - response = self._invoke_image(self._latest_image, query_text) - self.answer_stream.publish(response) - - def _extract_text(self, msg: HumanMessage) -> str: - content = msg.content - if isinstance(content, str): - return content - if isinstance(content, list): - for part in content: - if isinstance(part, dict) and part.get("type") == "text": - return str(part.get("text", "")) - return str(content) - - def _invoke(self, msg: HumanMessage, **kwargs: Any) -> AIMessage: - messages = [self._system_message, msg] - response = self._llm.invoke(messages, **kwargs) - self._history.extend([msg, response]) # type: ignore[arg-type] - return response # type: ignore[return-value] - - def _invoke_image( - self, image: Image, query: str, response_format: dict[str, Any] | None = None - ) -> AIMessage: - content = [{"type": "text", "text": query}, *image.agent_encode()] - kwargs: dict[str, Any] = {} - if response_format: - kwargs["response_format"] = response_format - return self._invoke(HumanMessage(content=content), **kwargs) - - @rpc - def clear_history(self) -> None: - self._history.clear() - - @rpc - def query(self, query: str) -> str: - response = self._invoke(HumanMessage(query)) - content = response.content - return content if isinstance(content, str) else str(content) - - @rpc - def query_image( - self, image: Image, query: str, response_format: dict[str, Any] | None = None - ) -> str: - response = self._invoke_image(image, query, response_format=response_format) - content = response.content - return content if isinstance(content, str) else str(content) - - -vlm_agent = VLMAgent.blueprint - -__all__ = ["VLMAgent", "vlm_agent"] diff --git a/dimos/agents/vlm_stream_tester.py b/dimos/agents/vlm_stream_tester.py deleted file mode 100644 index 79bb802a03..0000000000 --- a/dimos/agents/vlm_stream_tester.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -from langchain_core.messages import AIMessage, HumanMessage - -from dimos.core import Module, rpc -from dimos.core.stream import In, Out -from dimos.msgs.sensor_msgs import Image -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class VlmStreamTester(Module): - """Smoke-test VLMAgent with replayed images and stream queries.""" - - color_image: In[Image] - query_stream: Out[HumanMessage] - answer_stream: In[AIMessage] - - rpc_calls: list[str] = [ - "VLMAgent.query_image", - ] - - def __init__( # type: ignore[no-untyped-def] - self, - prompt: str = "What do you see?", - num_queries: int = 10, - query_interval_s: float = 2.0, - max_image_age_s: float = 1.5, - max_image_gap_s: float = 1.5, - ) -> None: - super().__init__() - self._prompt = prompt - self._num_queries = num_queries - self._query_interval_s = query_interval_s - self._max_image_age_s = max_image_age_s - self._max_image_gap_s = max_image_gap_s - self._latest_image: Image | None = None - self._latest_image_wall_ts: float | None = None - self._last_image_wall_ts: float | None = None - self._max_gap_seen_s = 0.0 - self._answer_count = 0 - self._stop_event = threading.Event() - self._worker: threading.Thread | None = None - - @rpc - def start(self) -> None: - super().start() - self._disposables.add(self.color_image.subscribe(self._on_image)) # type: ignore[arg-type] - self._disposables.add(self.answer_stream.subscribe(self._on_answer)) # type: ignore[arg-type] - self._worker = threading.Thread(target=self._run_queries, daemon=True) - self._worker.start() - - @rpc - def stop(self) -> None: - self._stop_event.set() - if self._worker and self._worker.is_alive(): - self._worker.join(timeout=1.0) - super().stop() - - def _on_image(self, image: Image) -> None: - now = time.time() - if self._last_image_wall_ts is not None: - gap = now - self._last_image_wall_ts - if gap > self._max_gap_seen_s: - self._max_gap_seen_s = gap - self._last_image_wall_ts = now - self._latest_image_wall_ts = now - self._latest_image = image - - def _on_answer(self, msg: AIMessage) -> None: - self._answer_count += 1 - logger.info( - "VLMAgent stream answer", - count=self._answer_count, - content=msg.content, - ) - - def _run_queries(self) -> None: - try: - while not self._stop_event.is_set() and self._latest_image is None: - time.sleep(0.05) - - self._run_stream_queries() - self._run_rpc_queries() - except Exception as exc: - logger.exception("VlmStreamTester query loop failed", error=str(exc)) - finally: - if self._max_gap_seen_s > self._max_image_gap_s: - logger.warning( - "Image stream gap exceeded threshold", - max_gap_s=self._max_gap_seen_s, - threshold_s=self._max_image_gap_s, - ) - - def _run_stream_queries(self) -> None: - for idx in range(self._num_queries): - if self._stop_event.is_set(): - break - if self._latest_image is None: - logger.warning("No image available for stream query.") - break - - image_age = None - if self._latest_image_wall_ts is not None: - image_age = time.time() - self._latest_image_wall_ts - if image_age > self._max_image_age_s: - logger.warning( - "Latest image is stale", - age_s=image_age, - max_age_s=self._max_image_age_s, - ) - - logger.info("Sending stream query", index=idx + 1, total=self._num_queries) - self.query_stream.publish( - HumanMessage(content=f"{self._prompt} (stream query {idx + 1}/{self._num_queries})") - ) - time.sleep(self._query_interval_s) - - def _run_rpc_queries(self) -> None: - rpc_query = None - try: - rpc_query = self.get_rpc_calls("VLMAgent.query_image") - except Exception as exc: - logger.warning("RPC query_image lookup failed", error=str(exc)) - return - - for idx in range(self._num_queries): - if self._stop_event.is_set(): - break - if self._latest_image is None: - logger.warning("No image available for RPC query.") - break - - image_age = None - if self._latest_image_wall_ts is not None: - image_age = time.time() - self._latest_image_wall_ts - if image_age > self._max_image_age_s: - logger.warning( - "Latest image is stale", - age_s=image_age, - max_age_s=self._max_image_age_s, - ) - - logger.info("Sending RPC query", index=idx + 1, total=self._num_queries) - try: - response = rpc_query( - self._latest_image, - f"{self._prompt} (rpc query {idx + 1}/{self._num_queries})", - ) - logger.info( - "VLMAgent RPC answer", - query_index=idx + 1, - image_age_s=image_age, - content=response, - ) - except Exception as exc: - logger.warning("RPC query_image failed", error=str(exc)) - time.sleep(self._query_interval_s) - - -vlm_stream_tester = VlmStreamTester.blueprint - -__all__ = ["VlmStreamTester", "vlm_stream_tester"] diff --git a/dimos/agents/web_human_input.py b/dimos/agents/web_human_input.py deleted file mode 100644 index 09d5400cdc..0000000000 --- a/dimos/agents/web_human_input.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Thread -from typing import TYPE_CHECKING - -import reactivex as rx -import reactivex.operators as ops - -from dimos.core import Module, rpc -from dimos.core.transport import pLCMTransport -from dimos.stream.audio.node_normalizer import AudioNormalizer -from dimos.stream.audio.stt.node_whisper import WhisperNode -from dimos.utils.logging_config import setup_logger -from dimos.web.robot_web_interface import RobotWebInterface - -if TYPE_CHECKING: - from dimos.stream.audio.base import AudioEvent - -logger = setup_logger() - - -class WebInput(Module): - _web_interface: RobotWebInterface | None = None - _thread: Thread | None = None - _human_transport: pLCMTransport[str] | None = None - - @rpc - def start(self) -> None: - super().start() - - self._human_transport = pLCMTransport("/human_input") - - audio_subject: rx.subject.Subject[AudioEvent] = rx.subject.Subject() - - self._web_interface = RobotWebInterface( - port=5555, - text_streams={"agent_responses": rx.subject.Subject()}, - audio_subject=audio_subject, - ) - - normalizer = AudioNormalizer() - stt_node = WhisperNode() - - # Connect audio pipeline: browser audio → normalizer → whisper - normalizer.consume_audio(audio_subject.pipe(ops.share())) - stt_node.consume_audio(normalizer.emit_audio()) - - # Subscribe to both text input sources - # 1. Direct text from web interface - unsub = self._web_interface.query_stream.subscribe(self._human_transport.publish) - self._disposables.add(unsub) - - # 2. Transcribed text from STT - unsub = stt_node.emit_text().subscribe(self._human_transport.publish) - self._disposables.add(unsub) - - self._thread = Thread(target=self._web_interface.run, daemon=True) - self._thread.start() - - logger.info("Web interface started at http://localhost:5555") - - @rpc - def stop(self) -> None: - if self._web_interface: - self._web_interface.shutdown() - if self._thread: - self._thread.join(timeout=1.0) - if self._human_transport: - self._human_transport.lcm.stop() - super().stop() - - -web_input = WebInput.blueprint - -__all__ = ["WebInput", "web_input"] diff --git a/dimos/agents_deprecated/__init__.py b/dimos/agents_deprecated/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/agents_deprecated/agent.py b/dimos/agents_deprecated/agent.py deleted file mode 100644 index 0443b2cc94..0000000000 --- a/dimos/agents_deprecated/agent.py +++ /dev/null @@ -1,917 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agent framework for LLM-based autonomous systems. - -This module provides a flexible foundation for creating agents that can: -- Process image and text inputs through LLM APIs -- Store and retrieve contextual information using semantic memory -- Handle tool/function calling -- Process streaming inputs asynchronously - -The module offers base classes (Agent, LLMAgent) and concrete implementations -like OpenAIAgent that connect to specific LLM providers. -""" - -from __future__ import annotations - -# Standard library imports -import json -import os -import threading -from typing import TYPE_CHECKING, Any - -# Third-party imports -from dotenv import load_dotenv -from openai import NOT_GIVEN, OpenAI -from pydantic import BaseModel -from reactivex import Observable, Observer, create, empty, just, operators as rxops -from reactivex.disposable import CompositeDisposable, Disposable -from reactivex.subject import Subject - -# Local imports -from dimos.agents_deprecated.memory.chroma_impl import OpenAISemanticMemory -from dimos.agents_deprecated.prompt_builder.impl import PromptBuilder -from dimos.agents_deprecated.tokenizer.openai_tokenizer import OpenAITokenizer -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.stream.frame_processor import FrameProcessor -from dimos.stream.stream_merger import create_stream_merger -from dimos.stream.video_operators import Operators as MyOps, VideoOperators as MyVidOps -from dimos.utils.logging_config import setup_logger -from dimos.utils.threadpool import get_scheduler - -if TYPE_CHECKING: - from reactivex.scheduler import ThreadPoolScheduler - - from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory - from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the agent module -logger = setup_logger() - -# Constants -_TOKEN_BUDGET_PARTS = 4 # Number of parts to divide token budget -_MAX_SAVED_FRAMES = 100 # Maximum number of frames to save - - -# ----------------------------------------------------------------------------- -# region Agent Base Class -# ----------------------------------------------------------------------------- -class Agent: - """Base agent that manages memory and subscriptions.""" - - def __init__( - self, - dev_name: str = "NA", - agent_type: str = "Base", - agent_memory: AbstractAgentSemanticMemory | None = None, - pool_scheduler: ThreadPoolScheduler | None = None, - ) -> None: - """ - Initializes a new instance of the Agent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent (e.g., 'Base', 'Vision'). - agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - """ - self.dev_name = dev_name - self.agent_type = agent_type - self.agent_memory = agent_memory or OpenAISemanticMemory() - self.disposables = CompositeDisposable() - self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this agent.""" - if self.disposables: - self.disposables.dispose() - else: - logger.info("No disposables to dispose.") - - -# endregion Agent Base Class - - -# ----------------------------------------------------------------------------- -# region LLMAgent Base Class (Generic LLM Agent) -# ----------------------------------------------------------------------------- -class LLMAgent(Agent): - """Generic LLM agent containing common logic for LLM-based agents. - - This class implements functionality for: - - Updating the query - - Querying the agent's memory (for RAG) - - Building prompts via a prompt builder - - Handling tooling callbacks in responses - - Subscribing to image and query streams - - Emitting responses as an observable stream - - Subclasses must implement the `_send_query` method, which is responsible - for sending the prompt to a specific LLM API. - - Attributes: - query (str): The current query text to process. - prompt_builder (PromptBuilder): Handles construction of prompts. - system_query (str): System prompt for RAG context situations. - image_detail (str): Detail level for image processing ('low','high','auto'). - max_input_tokens_per_request (int): Maximum input token count. - max_output_tokens_per_request (int): Maximum output token count. - max_tokens_per_request (int): Total maximum token count. - rag_query_n (int): Number of results to fetch from memory. - rag_similarity_threshold (float): Minimum similarity for RAG results. - frame_processor (FrameProcessor): Processes video frames. - output_dir (str): Directory for output files. - response_subject (Subject): Subject that emits agent responses. - process_all_inputs (bool): Whether to process every input emission (True) or - skip emissions when the agent is busy processing a previous input (False). - """ - - logging_file_memory_lock = threading.Lock() - - def __init__( - self, - dev_name: str = "NA", - agent_type: str = "LLM", - agent_memory: AbstractAgentSemanticMemory | None = None, - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool = False, - system_query: str | None = None, - max_output_tokens_per_request: int = 16384, - max_input_tokens_per_request: int = 128000, - input_query_stream: Observable | None = None, # type: ignore[type-arg] - input_data_stream: Observable | None = None, # type: ignore[type-arg] - input_video_stream: Observable | None = None, # type: ignore[type-arg] - ) -> None: - """ - Initializes a new instance of the LLMAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - agent_memory (AbstractAgentSemanticMemory): The memory system for the agent. - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - process_all_inputs (bool): Whether to process every input emission (True) or - skip emissions when the agent is busy processing a previous input (False). - """ - super().__init__(dev_name, agent_type, agent_memory, pool_scheduler) - # These attributes can be configured by a subclass if needed. - self.query: str | None = None - self.prompt_builder: PromptBuilder | None = None - self.system_query: str | None = system_query - self.image_detail: str = "low" - self.max_input_tokens_per_request: int = max_input_tokens_per_request - self.max_output_tokens_per_request: int = max_output_tokens_per_request - self.max_tokens_per_request: int = ( - self.max_input_tokens_per_request + self.max_output_tokens_per_request - ) - self.rag_query_n: int = 4 - self.rag_similarity_threshold: float = 0.45 - self.frame_processor: FrameProcessor | None = None - self.output_dir: str = os.path.join(os.getcwd(), "assets", "agent") - self.process_all_inputs: bool = process_all_inputs - os.makedirs(self.output_dir, exist_ok=True) - - # Subject for emitting responses - self.response_subject = Subject() # type: ignore[var-annotated] - - # Conversation history for maintaining context between calls - self.conversation_history = [] # type: ignore[var-annotated] - - # Initialize input streams - self.input_video_stream = input_video_stream - self.input_query_stream = ( - input_query_stream - if (input_data_stream is None) - else ( - input_query_stream.pipe( # type: ignore[misc, union-attr] - rxops.with_latest_from(input_data_stream), - rxops.map( - lambda combined: { - "query": combined[0], # type: ignore[index] - "objects": combined[1] # type: ignore[index] - if len(combined) > 1 # type: ignore[arg-type] - else "No object data available", - } - ), - rxops.map( - lambda data: f"{data['query']}\n\nCurrent objects detected:\n{data['objects']}" # type: ignore[index] - ), - rxops.do_action( - lambda x: print(f"\033[34mEnriched query: {x.split(chr(10))[0]}\033[0m") # type: ignore[arg-type] - or [print(f"\033[34m{line}\033[0m") for line in x.split(chr(10))[1:]] # type: ignore[var-annotated] - ), - ) - ) - ) - - # Setup stream subscriptions based on inputs provided - if (self.input_video_stream is not None) and (self.input_query_stream is not None): - self.merged_stream = create_stream_merger( - data_input_stream=self.input_video_stream, text_query_stream=self.input_query_stream - ) - - logger.info("Subscribing to merged input stream...") - - # Define a query extractor for the merged stream - def query_extractor(emission): # type: ignore[no-untyped-def] - return (emission[0], emission[1][0]) - - self.disposables.add( - self.subscribe_to_image_processing( - self.merged_stream, query_extractor=query_extractor - ) - ) - else: - # If no merged stream, fall back to individual streams - if self.input_video_stream is not None: - logger.info("Subscribing to input video stream...") - self.disposables.add(self.subscribe_to_image_processing(self.input_video_stream)) - if self.input_query_stream is not None: - logger.info("Subscribing to input query stream...") - self.disposables.add(self.subscribe_to_query_processing(self.input_query_stream)) - - def _update_query(self, incoming_query: str | None) -> None: - """Updates the query if an incoming query is provided. - - Args: - incoming_query (str): The new query text. - """ - if incoming_query is not None: - self.query = incoming_query - - def _get_rag_context(self) -> tuple[str, str]: - """Queries the agent memory to retrieve RAG context. - - Returns: - Tuple[str, str]: A tuple containing the formatted results (for logging) - and condensed results (for use in the prompt). - """ - results = self.agent_memory.query( - query_texts=self.query, - n_results=self.rag_query_n, - similarity_threshold=self.rag_similarity_threshold, - ) - formatted_results = "\n".join( - f"Document ID: {doc.id}\nMetadata: {doc.metadata}\nContent: {doc.page_content}\nScore: {score}\n" - for (doc, score) in results - ) - condensed_results = " | ".join(f"{doc.page_content}" for (doc, _) in results) - logger.info(f"Agent Memory Query Results:\n{formatted_results}") - logger.info("=== Results End ===") - return formatted_results, condensed_results - - def _build_prompt( - self, - base64_image: str | None, - dimensions: tuple[int, int] | None, - override_token_limit: bool, - condensed_results: str, - ) -> list: # type: ignore[type-arg] - """Builds a prompt message using the prompt builder. - - Args: - base64_image (str): Optional Base64-encoded image. - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - condensed_results (str): The condensed RAG context. - - Returns: - list: A list of message dictionaries to be sent to the LLM. - """ - # Budget for each component of the prompt - budgets = { - "system_prompt": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "user_query": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "image": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - "rag": self.max_input_tokens_per_request // _TOKEN_BUDGET_PARTS, - } - - # Define truncation policies for each component - policies = { - "system_prompt": "truncate_end", - "user_query": "truncate_middle", - "image": "do_not_truncate", - "rag": "truncate_end", - } - - return self.prompt_builder.build( # type: ignore[no-any-return, union-attr] - user_query=self.query, - override_token_limit=override_token_limit, - base64_image=base64_image, - image_width=dimensions[0] if dimensions is not None else None, - image_height=dimensions[1] if dimensions is not None else None, - image_detail=self.image_detail, - rag_context=condensed_results, - system_prompt=self.system_query, - budgets=budgets, - policies=policies, - ) - - def _handle_tooling(self, response_message, messages): # type: ignore[no-untyped-def] - """Handles tooling callbacks in the response message. - - If tool calls are present, the corresponding functions are executed and - a follow-up query is sent. - - Args: - response_message: The response message containing tool calls. - messages (list): The original list of messages sent. - - Returns: - The final response message after processing tool calls, if any. - """ - - # TODO: Make this more generic or move implementation to OpenAIAgent. - # This is presently OpenAI-specific. - def _tooling_callback(message, messages, response_message, skill_library: SkillLibrary): # type: ignore[no-untyped-def] - has_called_tools = False - new_messages = [] - for tool_call in message.tool_calls: - has_called_tools = True - name = tool_call.function.name - args = json.loads(tool_call.function.arguments) - result = skill_library.call(name, **args) - logger.info(f"Function Call Results: {result}") - new_messages.append( - { - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result), - "name": name, - } - ) - if has_called_tools: - logger.info("Sending Another Query.") - messages.append(response_message) - messages.extend(new_messages) - # Delegate to sending the query again. - return self._send_query(messages) - else: - logger.info("No Need for Another Query.") - return None - - if response_message.tool_calls is not None: - return _tooling_callback( - response_message, - messages, - response_message, - self.skill_library, # type: ignore[attr-defined] - ) - return None - - def _observable_query( # type: ignore[no-untyped-def] - self, - observer: Observer, # type: ignore[type-arg] - base64_image: str | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - incoming_query: str | None = None, - ): - """Prepares and sends a query to the LLM, emitting the response to the observer. - - Args: - observer (Observer): The observer to emit responses to. - base64_image (str): Optional Base64-encoded image. - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - incoming_query (str): Optional query to update the agent's query. - - Raises: - Exception: Propagates any exceptions encountered during processing. - """ - try: - self._update_query(incoming_query) - _, condensed_results = self._get_rag_context() - messages = self._build_prompt( - base64_image, dimensions, override_token_limit, condensed_results - ) - # logger.debug(f"Sending Query: {messages}") - logger.info("Sending Query.") - response_message = self._send_query(messages) - logger.info(f"Received Response: {response_message}") - if response_message is None: - raise Exception("Response message does not exist.") - - # TODO: Make this more generic. The parsed tag and tooling handling may be OpenAI-specific. - # If no skill library is provided or there are no tool calls, emit the response directly. - if ( - self.skill_library is None # type: ignore[attr-defined] - or self.skill_library.get_tools() in (None, NOT_GIVEN) # type: ignore[attr-defined] - or response_message.tool_calls is None - ): - final_msg = ( - response_message.parsed - if hasattr(response_message, "parsed") and response_message.parsed - else ( - response_message.content - if hasattr(response_message, "content") - else response_message - ) - ) - observer.on_next(final_msg) - self.response_subject.on_next(final_msg) - else: - response_message_2 = self._handle_tooling(response_message, messages) # type: ignore[no-untyped-call] - final_msg = ( - response_message_2 if response_message_2 is not None else response_message - ) - if isinstance(final_msg, BaseModel): # TODO: Test - final_msg = str(final_msg.content) # type: ignore[attr-defined] - observer.on_next(final_msg) - self.response_subject.on_next(final_msg) - observer.on_completed() - except Exception as e: - logger.error(f"Query failed in {self.dev_name}: {e}") - observer.on_error(e) - self.response_subject.on_error(e) - - def _send_query(self, messages: list) -> Any: # type: ignore[type-arg] - """Sends the query to the LLM API. - - This method must be implemented by subclasses with specifics of the LLM API. - - Args: - messages (list): The prompt messages to be sent. - - Returns: - Any: The response message from the LLM. - - Raises: - NotImplementedError: Always, unless overridden. - """ - raise NotImplementedError("Subclasses must implement _send_query method.") - - def _log_response_to_file(self, response, output_dir: str | None = None) -> None: # type: ignore[no-untyped-def] - """Logs the LLM response to a file. - - Args: - response: The response message to log. - output_dir (str): The directory where the log file is stored. - """ - if output_dir is None: - output_dir = self.output_dir - if response is not None: - with self.logging_file_memory_lock: - log_path = os.path.join(output_dir, "memory.txt") - with open(log_path, "a") as file: - file.write(f"{self.dev_name}: {response}\n") - logger.info(f"LLM Response [{self.dev_name}]: {response}") - - def subscribe_to_image_processing( # type: ignore[no-untyped-def] - self, - frame_observable: Observable, # type: ignore[type-arg] - query_extractor=None, - ) -> Disposable: - """Subscribes to a stream of video frames for processing. - - This method sets up a subscription to process incoming video frames. - Each frame is encoded and then sent to the LLM by directly calling the - _observable_query method. The response is then logged to a file. - - Args: - frame_observable (Observable): An observable emitting video frames or - (query, frame) tuples if query_extractor is provided. - query_extractor (callable, optional): Function to extract query and frame from - each emission. If None, assumes emissions are - raw frames and uses self.system_query. - - Returns: - Disposable: A disposable representing the subscription. - """ - # Initialize frame processor if not already set - if self.frame_processor is None: - self.frame_processor = FrameProcessor(delete_on_init=True) - - print_emission_args = {"enabled": True, "dev_name": self.dev_name, "counts": {}} - - def _process_frame(emission) -> Observable: # type: ignore[no-untyped-def, type-arg] - """ - Processes a frame or (query, frame) tuple. - """ - # Extract query and frame - if query_extractor: - query, frame = query_extractor(emission) - else: - query = self.system_query - frame = emission - return just(frame).pipe( # type: ignore[call-overload, no-any-return] - MyOps.print_emission(id="B", **print_emission_args), # type: ignore[arg-type] - rxops.observe_on(self.pool_scheduler), - MyOps.print_emission(id="C", **print_emission_args), # type: ignore[arg-type] - rxops.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="D", **print_emission_args), # type: ignore[arg-type] - MyVidOps.with_jpeg_export( - self.frame_processor, # type: ignore[arg-type] - suffix=f"{self.dev_name}_frame_", - save_limit=_MAX_SAVED_FRAMES, - ), - MyOps.print_emission(id="E", **print_emission_args), # type: ignore[arg-type] - MyVidOps.encode_image(), - MyOps.print_emission(id="F", **print_emission_args), # type: ignore[arg-type] - rxops.filter( - lambda base64_and_dims: base64_and_dims is not None - and base64_and_dims[0] is not None # type: ignore[index] - and base64_and_dims[1] is not None # type: ignore[index] - ), - MyOps.print_emission(id="G", **print_emission_args), # type: ignore[arg-type] - rxops.flat_map( - lambda base64_and_dims: create( # type: ignore[arg-type, return-value] - lambda observer, _: self._observable_query( - observer, # type: ignore[arg-type] - base64_image=base64_and_dims[0], - dimensions=base64_and_dims[1], - incoming_query=query, - ) - ) - ), # Use the extracted query - MyOps.print_emission(id="H", **print_emission_args), # type: ignore[arg-type] - ) - - # Use a mutable flag to ensure only one frame is processed at a time. - is_processing = [False] - - def process_if_free(emission): # type: ignore[no-untyped-def] - if not self.process_all_inputs and is_processing[0]: - # Drop frame if a request is in progress and process_all_inputs is False - return empty() - else: - is_processing[0] = True - return _process_frame(emission).pipe( - MyOps.print_emission(id="I", **print_emission_args), # type: ignore[arg-type] - rxops.observe_on(self.pool_scheduler), - MyOps.print_emission(id="J", **print_emission_args), # type: ignore[arg-type] - rxops.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="K", **print_emission_args), # type: ignore[arg-type] - rxops.do_action( - on_completed=lambda: is_processing.__setitem__(0, False), - on_error=lambda e: is_processing.__setitem__(0, False), - ), - MyOps.print_emission(id="L", **print_emission_args), # type: ignore[arg-type] - ) - - observable = frame_observable.pipe( - MyOps.print_emission(id="A", **print_emission_args), # type: ignore[arg-type] - rxops.flat_map(process_if_free), - MyOps.print_emission(id="M", **print_emission_args), # type: ignore[arg-type] - ) - - disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file(response, self.output_dir), - on_error=lambda e: logger.error(f"Error encountered: {e}"), - on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), - ) - self.disposables.add(disposable) - return disposable # type: ignore[no-any-return] - - def subscribe_to_query_processing(self, query_observable: Observable) -> Disposable: # type: ignore[type-arg] - """Subscribes to a stream of queries for processing. - - This method sets up a subscription to process incoming queries by directly - calling the _observable_query method. The responses are logged to a file. - - Args: - query_observable (Observable): An observable emitting queries. - - Returns: - Disposable: A disposable representing the subscription. - """ - print_emission_args = {"enabled": False, "dev_name": self.dev_name, "counts": {}} - - def _process_query(query) -> Observable: # type: ignore[no-untyped-def, type-arg] - """ - Processes a single query by logging it and passing it to _observable_query. - Returns an observable that emits the LLM response. - """ - return just(query).pipe( - MyOps.print_emission(id="Pr A", **print_emission_args), # type: ignore[arg-type] - rxops.flat_map( - lambda query: create( # type: ignore[arg-type, return-value] - lambda observer, _: self._observable_query(observer, incoming_query=query) # type: ignore[arg-type] - ) - ), - MyOps.print_emission(id="Pr B", **print_emission_args), # type: ignore[arg-type] - ) - - # A mutable flag indicating whether a query is currently being processed. - is_processing = [False] - - def process_if_free(query): # type: ignore[no-untyped-def] - logger.info(f"Processing Query: {query}") - if not self.process_all_inputs and is_processing[0]: - # Drop query if a request is already in progress and process_all_inputs is False - return empty() - else: - is_processing[0] = True - logger.info("Processing Query.") - return _process_query(query).pipe( - MyOps.print_emission(id="B", **print_emission_args), # type: ignore[arg-type] - rxops.observe_on(self.pool_scheduler), - MyOps.print_emission(id="C", **print_emission_args), # type: ignore[arg-type] - rxops.subscribe_on(self.pool_scheduler), - MyOps.print_emission(id="D", **print_emission_args), # type: ignore[arg-type] - rxops.do_action( - on_completed=lambda: is_processing.__setitem__(0, False), - on_error=lambda e: is_processing.__setitem__(0, False), - ), - MyOps.print_emission(id="E", **print_emission_args), # type: ignore[arg-type] - ) - - observable = query_observable.pipe( - MyOps.print_emission(id="A", **print_emission_args), # type: ignore[arg-type] - rxops.flat_map(lambda query: process_if_free(query)), # type: ignore[no-untyped-call] - MyOps.print_emission(id="F", **print_emission_args), # type: ignore[arg-type] - ) - - disposable = observable.subscribe( - on_next=lambda response: self._log_response_to_file(response, self.output_dir), - on_error=lambda e: logger.error(f"Error processing query for {self.dev_name}: {e}"), - on_completed=lambda: logger.info(f"Stream processing completed for {self.dev_name}"), - ) - self.disposables.add(disposable) - return disposable # type: ignore[no-any-return] - - def get_response_observable(self) -> Observable: # type: ignore[type-arg] - """Gets an observable that emits responses from this agent. - - Returns: - Observable: An observable that emits string responses from the agent. - """ - return self.response_subject.pipe( - rxops.observe_on(self.pool_scheduler), - rxops.subscribe_on(self.pool_scheduler), - rxops.share(), - ) - - def run_observable_query(self, query_text: str, **kwargs) -> Observable: # type: ignore[no-untyped-def, type-arg] - """Creates an observable that processes a one-off text query to Agent and emits the response. - - This method provides a simple way to send a text query and get an observable - stream of the response. It's designed for one-off queries rather than - continuous processing of input streams. Useful for testing and development. - - Args: - query_text (str): The query text to process. - **kwargs: Additional arguments to pass to _observable_query. Supported args vary by agent type. - For example, ClaudeAgent supports: base64_image, dimensions, override_token_limit, - reset_conversation, thinking_budget_tokens - - Returns: - Observable: An observable that emits the response as a string. - """ - return create( - lambda observer, _: self._observable_query( - observer, # type: ignore[arg-type] - incoming_query=query_text, - **kwargs, - ) - ) - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this agent.""" - super().dispose_all() - self.response_subject.on_completed() - - -# endregion LLMAgent Base Class (Generic LLM Agent) - - -# ----------------------------------------------------------------------------- -# region OpenAIAgent Subclass (OpenAI-Specific Implementation) -# ----------------------------------------------------------------------------- -class OpenAIAgent(LLMAgent): - """OpenAI agent implementation that uses OpenAI's API for processing. - - This class implements the _send_query method to interact with OpenAI's API. - It also sets up OpenAI-specific parameters, such as the client, model name, - tokenizer, and response model. - """ - - def __init__( - self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Observable | None = None, # type: ignore[type-arg] - input_data_stream: Observable | None = None, # type: ignore[type-arg] - input_video_stream: Observable | None = None, # type: ignore[type-arg] - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "gpt-4o", - prompt_builder: PromptBuilder | None = None, - tokenizer: AbstractTokenizer | None = None, - rag_query_n: int = 4, - rag_similarity_threshold: float = 0.45, - skills: AbstractSkill | list[AbstractSkill] | SkillLibrary | None = None, - response_model: BaseModel | None = None, - frame_processor: FrameProcessor | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - openai_client: OpenAI | None = None, - ) -> None: - """ - Initializes a new instance of the OpenAIAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - query (str): The default query text. - input_query_stream (Observable): An observable for query input. - input_data_stream (Observable): An observable for data input. - input_video_stream (Observable): An observable for video frames. - output_dir (str): Directory for output files. - agent_memory (AbstractAgentSemanticMemory): The memory system. - system_query (str): The system prompt to use with RAG context. - max_input_tokens_per_request (int): Maximum tokens for input. - max_output_tokens_per_request (int): Maximum tokens for output. - model_name (str): The OpenAI model name to use. - prompt_builder (PromptBuilder): Custom prompt builder. - tokenizer (AbstractTokenizer): Custom tokenizer for token counting. - rag_query_n (int): Number of results to fetch in RAG queries. - rag_similarity_threshold (float): Minimum similarity for RAG results. - skills (Union[AbstractSkill, List[AbstractSkill], SkillLibrary]): Skills available to the agent. - response_model (BaseModel): Optional Pydantic model for responses. - frame_processor (FrameProcessor): Custom frame processor. - image_detail (str): Detail level for images ("low", "high", "auto"). - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - process_all_inputs (bool): Whether to process all inputs or skip when busy. - If None, defaults to True for text queries and merged streams, False for video streams. - openai_client (OpenAI): The OpenAI client to use. This can be used to specify - a custom OpenAI client if targetting another provider. - """ - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - if input_query_stream is not None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - input_query_stream=input_query_stream, - input_data_stream=input_data_stream, - input_video_stream=input_video_stream, - ) - self.client = openai_client or OpenAI() - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - # Configure skill library. - self.skills = skills - self.skill_library = None - if isinstance(self.skills, SkillLibrary): - self.skill_library = self.skills - elif isinstance(self.skills, list): - self.skill_library = SkillLibrary() - for skill in self.skills: - self.skill_library.add(skill) - elif isinstance(self.skills, AbstractSkill): - self.skill_library = SkillLibrary() - self.skill_library.add(self.skills) - - self.response_model = response_model if response_model is not None else NOT_GIVEN - self.model_name = model_name - self.tokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) - self.prompt_builder = prompt_builder or PromptBuilder( - self.model_name, tokenizer=self.tokenizer - ) - self.rag_query_n = rag_query_n - self.rag_similarity_threshold = rag_similarity_threshold - self.image_detail = image_detail - self.max_output_tokens_per_request = max_output_tokens_per_request - self.max_input_tokens_per_request = max_input_tokens_per_request - self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request - - # Add static context to memory. - self._add_context_to_memory() - - self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) - - logger.info("OpenAI Agent Initialized.") - - def _add_context_to_memory(self) -> None: - """Adds initial context to the agent's memory.""" - context_data = [ - ( - "id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence.", - ), - ( - "id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image.", - ), - ("id2", "Video is a sequence of frames captured at regular intervals."), - ( - "id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", - ), - ( - "id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", - ), - ] - for doc_id, text in context_data: - self.agent_memory.add_vector(doc_id, text) # type: ignore[no-untyped-call] - - def _send_query(self, messages: list) -> Any: # type: ignore[type-arg] - """Sends the query to OpenAI's API. - - Depending on whether a response model is provided, the appropriate API - call is made. - - Args: - messages (list): The prompt messages to send. - - Returns: - The response message from OpenAI. - - Raises: - Exception: If no response message is returned. - ConnectionError: If there's an issue connecting to the API. - ValueError: If the messages or other parameters are invalid. - """ - try: - if self.response_model is not NOT_GIVEN: - response = self.client.beta.chat.completions.parse( - model=self.model_name, - messages=messages, - response_format=self.response_model, # type: ignore[arg-type] - tools=( - self.skill_library.get_tools() # type: ignore[arg-type] - if self.skill_library is not None - else NOT_GIVEN - ), - max_tokens=self.max_output_tokens_per_request, - ) - else: - response = self.client.chat.completions.create( # type: ignore[assignment] - model=self.model_name, - messages=messages, - max_tokens=self.max_output_tokens_per_request, - tools=( - self.skill_library.get_tools() # type: ignore[arg-type] - if self.skill_library is not None - else NOT_GIVEN - ), - ) - response_message = response.choices[0].message - if response_message is None: - logger.error("Response message does not exist.") - raise Exception("Response message does not exist.") - return response_message - except ConnectionError as ce: - logger.error(f"Connection error with API: {ce}") - raise - except ValueError as ve: - logger.error(f"Invalid parameters: {ve}") - raise - except Exception as e: - logger.error(f"Unexpected error in API call: {e}") - raise - - def stream_query(self, query_text: str) -> Observable: # type: ignore[type-arg] - """Creates an observable that processes a text query and emits the response. - - This method provides a simple way to send a text query and get an observable - stream of the response. It's designed for one-off queries rather than - continuous processing of input streams. - - Args: - query_text (str): The query text to process. - - Returns: - Observable: An observable that emits the response as a string. - """ - return create( - lambda observer, _: self._observable_query(observer, incoming_query=query_text) # type: ignore[arg-type] - ) - - -# endregion OpenAIAgent Subclass (OpenAI-Specific Implementation) diff --git a/dimos/agents_deprecated/agent_config.py b/dimos/agents_deprecated/agent_config.py deleted file mode 100644 index 9adae6ad3c..0000000000 --- a/dimos/agents_deprecated/agent_config.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.agents_deprecated.agent import Agent - - -class AgentConfig: - def __init__(self, agents: list[Agent] | None = None) -> None: - """ - Initialize an AgentConfig with a list of agents. - - Args: - agents (List[Agent], optional): List of Agent instances. Defaults to empty list. - """ - self.agents = agents if agents is not None else [] - - def add_agent(self, agent: Agent) -> None: - """ - Add an agent to the configuration. - - Args: - agent (Agent): Agent instance to add - """ - self.agents.append(agent) - - def remove_agent(self, agent: Agent) -> None: - """ - Remove an agent from the configuration. - - Args: - agent (Agent): Agent instance to remove - """ - if agent in self.agents: - self.agents.remove(agent) - - def get_agents(self) -> list[Agent]: - """ - Get the list of configured agents. - - Returns: - List[Agent]: List of configured agents - """ - return self.agents diff --git a/dimos/agents_deprecated/agent_message.py b/dimos/agents_deprecated/agent_message.py deleted file mode 100644 index 87351e0518..0000000000 --- a/dimos/agents_deprecated/agent_message.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""AgentMessage type for multimodal agent communication.""" - -from dataclasses import dataclass, field -import time - -from dimos.agents_deprecated.agent_types import AgentImage -from dimos.msgs.sensor_msgs.Image import Image - - -@dataclass -class AgentMessage: - """Message type for agent communication with text and images. - - This type supports multimodal messages containing both text strings - and AgentImage objects (base64 encoded) for vision-enabled agents. - - The messages field contains multiple text strings that will be combined - into a single message when sent to the LLM. - """ - - messages: list[str] = field(default_factory=list) - images: list[AgentImage] = field(default_factory=list) - sender_id: str | None = None - timestamp: float = field(default_factory=time.time) - - def add_text(self, text: str) -> None: - """Add a text message.""" - if text: # Only add non-empty text - self.messages.append(text) - - def add_image(self, image: Image | AgentImage) -> None: - """Add an image. Converts Image to AgentImage if needed.""" - if isinstance(image, Image): - # Convert to AgentImage - agent_image = AgentImage( - base64_jpeg=image.agent_encode(), # type: ignore[arg-type] - width=image.width, - height=image.height, - metadata={"format": image.format.value, "frame_id": image.frame_id}, - ) - self.images.append(agent_image) - elif isinstance(image, AgentImage): - self.images.append(image) - else: - raise TypeError(f"Expected Image or AgentImage, got {type(image)}") - - def has_text(self) -> bool: - """Check if message contains text.""" - # Check if we have any non-empty messages - return any(msg for msg in self.messages if msg) - - def has_images(self) -> bool: - """Check if message contains images.""" - return len(self.images) > 0 - - def is_multimodal(self) -> bool: - """Check if message contains both text and images.""" - return self.has_text() and self.has_images() - - def get_primary_text(self) -> str | None: - """Get the first text message, if any.""" - return self.messages[0] if self.messages else None - - def get_primary_image(self) -> AgentImage | None: - """Get the first image, if any.""" - return self.images[0] if self.images else None - - def get_combined_text(self) -> str: - """Get all text messages combined into a single string.""" - # Filter out any empty strings and join - return " ".join(msg for msg in self.messages if msg) - - def clear(self) -> None: - """Clear all content.""" - self.messages.clear() - self.images.clear() - - def __repr__(self) -> str: - """String representation.""" - return ( - f"AgentMessage(" - f"texts={len(self.messages)}, " - f"images={len(self.images)}, " - f"sender='{self.sender_id}', " - f"timestamp={self.timestamp})" - ) diff --git a/dimos/agents_deprecated/agent_types.py b/dimos/agents_deprecated/agent_types.py deleted file mode 100644 index f52bafdac6..0000000000 --- a/dimos/agents_deprecated/agent_types.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agent-specific types for message passing.""" - -from dataclasses import dataclass, field -import json -import threading -import time -from typing import Any - - -@dataclass -class AgentImage: - """Image data encoded for agent consumption. - - Images are stored as base64-encoded JPEG strings ready for - direct use by LLM/vision models. - """ - - base64_jpeg: str - width: int | None = None - height: int | None = None - metadata: dict[str, Any] = field(default_factory=dict) - - def __repr__(self) -> str: - return f"AgentImage(size={self.width}x{self.height}, metadata={list(self.metadata.keys())})" - - -@dataclass -class ToolCall: - """Represents a tool/function call request from the LLM.""" - - id: str - name: str - arguments: dict[str, Any] - status: str = "pending" # pending, executing, completed, failed - - def __repr__(self) -> str: - return f"ToolCall(id='{self.id}', name='{self.name}', status='{self.status}')" - - -@dataclass -class AgentResponse: - """Enhanced response from an agent query with tool support. - - Based on common LLM response patterns, includes content and metadata. - """ - - content: str - role: str = "assistant" - tool_calls: list[ToolCall] | None = None - requires_follow_up: bool = False # Indicates if tool execution is needed - metadata: dict[str, Any] = field(default_factory=dict) - timestamp: float = field(default_factory=time.time) - - def __repr__(self) -> str: - content_preview = self.content[:50] + "..." if len(self.content) > 50 else self.content - tool_info = f", tools={len(self.tool_calls)}" if self.tool_calls else "" - return f"AgentResponse(role='{self.role}', content='{content_preview}'{tool_info})" - - -@dataclass -class ConversationMessage: - """Single message in conversation history. - - Represents a message in the conversation that can be converted to - different formats (OpenAI, TensorZero, etc). - """ - - role: str # "system", "user", "assistant", "tool" - content: str | list[dict[str, Any]] # Text or content blocks - tool_calls: list[ToolCall] | None = None - tool_call_id: str | None = None # For tool responses - name: str | None = None # For tool messages (function name) - timestamp: float = field(default_factory=time.time) - - def to_openai_format(self) -> dict[str, Any]: - """Convert to OpenAI API format.""" - msg = {"role": self.role} - - # Handle content - if isinstance(self.content, str): - msg["content"] = self.content - else: - # Content is already a list of content blocks - msg["content"] = self.content # type: ignore[assignment] - - # Add tool calls if present - if self.tool_calls: - # Handle both ToolCall objects and dicts - if isinstance(self.tool_calls[0], dict): - msg["tool_calls"] = self.tool_calls # type: ignore[assignment] - else: - msg["tool_calls"] = [ # type: ignore[assignment] - { - "id": tc.id, - "type": "function", - "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, - } - for tc in self.tool_calls - ] - - # Add tool_call_id for tool responses - if self.tool_call_id: - msg["tool_call_id"] = self.tool_call_id - - # Add name field if present (for tool messages) - if self.name: - msg["name"] = self.name - - return msg - - def __repr__(self) -> str: - content_preview = ( - str(self.content)[:50] + "..." if len(str(self.content)) > 50 else str(self.content) - ) - return f"ConversationMessage(role='{self.role}', content='{content_preview}')" - - -class ConversationHistory: - """Thread-safe conversation history manager. - - Manages conversation history with proper formatting for different - LLM providers and automatic trimming. - """ - - def __init__(self, max_size: int = 20) -> None: - """Initialize conversation history. - - Args: - max_size: Maximum number of messages to keep - """ - self._messages: list[ConversationMessage] = [] - self._lock = threading.Lock() - self.max_size = max_size - - def add_user_message(self, content: str | list[dict[str, Any]]) -> None: - """Add user message to history. - - Args: - content: Text string or list of content blocks (for multimodal) - """ - with self._lock: - self._messages.append(ConversationMessage(role="user", content=content)) - self._trim() - - def add_assistant_message(self, content: str, tool_calls: list[ToolCall] | None = None) -> None: - """Add assistant response to history. - - Args: - content: Response text - tool_calls: Optional list of tool calls made - """ - with self._lock: - self._messages.append( - ConversationMessage(role="assistant", content=content, tool_calls=tool_calls) - ) - self._trim() - - def add_tool_result(self, tool_call_id: str, content: str, name: str | None = None) -> None: - """Add tool execution result to history. - - Args: - tool_call_id: ID of the tool call this is responding to - content: Result of the tool execution - name: Optional name of the tool/function - """ - with self._lock: - self._messages.append( - ConversationMessage( - role="tool", content=content, tool_call_id=tool_call_id, name=name - ) - ) - self._trim() - - def add_raw_message(self, message: dict[str, Any]) -> None: - """Add a raw message dict to history. - - Args: - message: Message dict with role and content - """ - with self._lock: - # Extract fields from raw message - role = message.get("role", "user") - content = message.get("content", "") - - # Handle tool calls if present - tool_calls = None - if "tool_calls" in message: - tool_calls = [ - ToolCall( - id=tc["id"], - name=tc["function"]["name"], - arguments=json.loads(tc["function"]["arguments"]) - if isinstance(tc["function"]["arguments"], str) - else tc["function"]["arguments"], - status="completed", - ) - for tc in message["tool_calls"] - ] - - # Handle tool_call_id for tool responses - tool_call_id = message.get("tool_call_id") - - self._messages.append( - ConversationMessage( - role=role, content=content, tool_calls=tool_calls, tool_call_id=tool_call_id - ) - ) - self._trim() - - def to_openai_format(self) -> list[dict[str, Any]]: - """Export history in OpenAI format. - - Returns: - List of message dicts in OpenAI format - """ - with self._lock: - return [msg.to_openai_format() for msg in self._messages] - - def clear(self) -> None: - """Clear all conversation history.""" - with self._lock: - self._messages.clear() - - def size(self) -> int: - """Get number of messages in history. - - Returns: - Number of messages - """ - with self._lock: - return len(self._messages) - - def _trim(self) -> None: - """Trim history to max_size (must be called within lock).""" - if len(self._messages) > self.max_size: - # Keep the most recent messages - self._messages = self._messages[-self.max_size :] - - def __repr__(self) -> str: - with self._lock: - return f"ConversationHistory(messages={len(self._messages)}, max_size={self.max_size})" diff --git a/dimos/agents_deprecated/claude_agent.py b/dimos/agents_deprecated/claude_agent.py deleted file mode 100644 index 72fde622f1..0000000000 --- a/dimos/agents_deprecated/claude_agent.py +++ /dev/null @@ -1,738 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Claude agent implementation for the DIMOS agent framework. - -This module provides a ClaudeAgent class that implements the LLMAgent interface -for Anthropic's Claude models. It handles conversion between the DIMOS skill format -and Claude's tools format. -""" - -from __future__ import annotations - -import json -import os -from typing import TYPE_CHECKING, Any - -import anthropic -from dotenv import load_dotenv - -# Local imports -from dimos.agents_deprecated.agent import LLMAgent -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.stream.frame_processor import FrameProcessor -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pydantic import BaseModel - from reactivex import Observable - from reactivex.scheduler import ThreadPoolScheduler - - from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory - from dimos.agents_deprecated.prompt_builder.impl import PromptBuilder - -# Initialize environment variables -load_dotenv() - -# Initialize logger for the Claude agent -logger = setup_logger() - - -# Response object compatible with LLMAgent -class ResponseMessage: - def __init__(self, content: str = "", tool_calls=None, thinking_blocks=None) -> None: # type: ignore[no-untyped-def] - self.content = content - self.tool_calls = tool_calls or [] - self.thinking_blocks = thinking_blocks or [] - self.parsed = None - - def __str__(self) -> str: - # Return a string representation for logging - parts = [] - - # Include content if available - if self.content: - parts.append(self.content) - - # Include tool calls if available - if self.tool_calls: - tool_names = [tc.function.name for tc in self.tool_calls] - parts.append(f"[Tools called: {', '.join(tool_names)}]") - - return "\n".join(parts) if parts else "[No content]" - - -class ClaudeAgent(LLMAgent): - """Claude agent implementation that uses Anthropic's API for processing. - - This class implements the _send_query method to interact with Anthropic's API - and overrides _build_prompt to create Claude-formatted messages directly. - """ - - def __init__( - self, - dev_name: str, - agent_type: str = "Vision", - query: str = "What do you see?", - input_query_stream: Observable | None = None, # type: ignore[type-arg] - input_video_stream: Observable | None = None, # type: ignore[type-arg] - input_data_stream: Observable | None = None, # type: ignore[type-arg] - output_dir: str = os.path.join(os.getcwd(), "assets", "agent"), - agent_memory: AbstractAgentSemanticMemory | None = None, - system_query: str | None = None, - max_input_tokens_per_request: int = 128000, - max_output_tokens_per_request: int = 16384, - model_name: str = "claude-3-7-sonnet-20250219", - prompt_builder: PromptBuilder | None = None, - rag_query_n: int = 4, - rag_similarity_threshold: float = 0.45, - skills: AbstractSkill | None = None, - response_model: BaseModel | None = None, - frame_processor: FrameProcessor | None = None, - image_detail: str = "low", - pool_scheduler: ThreadPoolScheduler | None = None, - process_all_inputs: bool | None = None, - thinking_budget_tokens: int | None = 2000, - ) -> None: - """ - Initializes a new instance of the ClaudeAgent. - - Args: - dev_name (str): The device name of the agent. - agent_type (str): The type of the agent. - query (str): The default query text. - input_query_stream (Observable): An observable for query input. - input_video_stream (Observable): An observable for video frames. - output_dir (str): Directory for output files. - agent_memory (AbstractAgentSemanticMemory): The memory system. - system_query (str): The system prompt to use with RAG context. - max_input_tokens_per_request (int): Maximum tokens for input. - max_output_tokens_per_request (int): Maximum tokens for output. - model_name (str): The Claude model name to use. - prompt_builder (PromptBuilder): Custom prompt builder (not used in Claude implementation). - rag_query_n (int): Number of results to fetch in RAG queries. - rag_similarity_threshold (float): Minimum similarity for RAG results. - skills (AbstractSkill): Skills available to the agent. - response_model (BaseModel): Optional Pydantic model for responses. - frame_processor (FrameProcessor): Custom frame processor. - image_detail (str): Detail level for images ("low", "high", "auto"). - pool_scheduler (ThreadPoolScheduler): The scheduler to use for thread pool operations. - process_all_inputs (bool): Whether to process all inputs or skip when busy. - thinking_budget_tokens (int): Number of tokens to allocate for Claude's thinking. 0 disables thinking. - """ - # Determine appropriate default for process_all_inputs if not provided - if process_all_inputs is None: - # Default to True for text queries, False for video streams - if input_query_stream is not None and input_video_stream is None: - process_all_inputs = True - else: - process_all_inputs = False - - super().__init__( - dev_name=dev_name, - agent_type=agent_type, - agent_memory=agent_memory, - pool_scheduler=pool_scheduler, - process_all_inputs=process_all_inputs, - system_query=system_query, - input_query_stream=input_query_stream, - input_video_stream=input_video_stream, - input_data_stream=input_data_stream, - ) - - self.client = anthropic.Anthropic() - self.query = query - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - # Claude-specific parameters - self.thinking_budget_tokens = thinking_budget_tokens - self.claude_api_params = {} # type: ignore[var-annotated] # Will store params for Claude API calls - - # Configure skills - self.skills = skills - self.skill_library = None # Required for error 'ClaudeAgent' object has no attribute 'skill_library' due to skills refactor - if isinstance(self.skills, SkillLibrary): - self.skill_library = self.skills - elif isinstance(self.skills, list): - self.skill_library = SkillLibrary() - for skill in self.skills: - self.skill_library.add(skill) - elif isinstance(self.skills, AbstractSkill): - self.skill_library = SkillLibrary() - self.skill_library.add(self.skills) - - self.response_model = response_model - self.model_name = model_name - self.rag_query_n = rag_query_n - self.rag_similarity_threshold = rag_similarity_threshold - self.image_detail = image_detail - self.max_output_tokens_per_request = max_output_tokens_per_request - self.max_input_tokens_per_request = max_input_tokens_per_request - self.max_tokens_per_request = max_input_tokens_per_request + max_output_tokens_per_request - - # Add static context to memory. - self._add_context_to_memory() - - self.frame_processor = frame_processor or FrameProcessor(delete_on_init=True) - - # Ensure only one input stream is provided. - if self.input_video_stream is not None and self.input_query_stream is not None: - raise ValueError( - "More than one input stream provided. Please provide only one input stream." - ) - - logger.info("Claude Agent Initialized.") - - def _add_context_to_memory(self) -> None: - """Adds initial context to the agent's memory.""" - context_data = [ - ( - "id0", - "Optical Flow is a technique used to track the movement of objects in a video sequence.", - ), - ( - "id1", - "Edge Detection is a technique used to identify the boundaries of objects in an image.", - ), - ("id2", "Video is a sequence of frames captured at regular intervals."), - ( - "id3", - "Colors in Optical Flow are determined by the movement of light, and can be used to track the movement of objects.", - ), - ( - "id4", - "Json is a data interchange format that is easy for humans to read and write, and easy for machines to parse and generate.", - ), - ] - for doc_id, text in context_data: - self.agent_memory.add_vector(doc_id, text) # type: ignore[no-untyped-call] - - def _convert_tools_to_claude_format(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]: - """ - Converts DIMOS tools to Claude format. - - Args: - tools: List of tools in DIMOS format. - - Returns: - List of tools in Claude format. - """ - if not tools: - return [] - - claude_tools = [] - - for tool in tools: - # Skip if not a function - if tool.get("type") != "function": - continue - - function = tool.get("function", {}) - name = function.get("name") - description = function.get("description", "") - parameters = function.get("parameters", {}) - - claude_tool = { - "name": name, - "description": description, - "input_schema": { - "type": "object", - "properties": parameters.get("properties", {}), - "required": parameters.get("required", []), - }, - } - - claude_tools.append(claude_tool) - - return claude_tools - - def _build_prompt( # type: ignore[override] - self, - messages: list, # type: ignore[type-arg] - base64_image: str | list[str] | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - rag_results: str = "", - thinking_budget_tokens: int | None = None, - ) -> list: # type: ignore[type-arg] - """Builds a prompt message specifically for Claude API, using local messages copy.""" - """Builds a prompt message specifically for Claude API. - - This method creates messages in Claude's format directly, without using - any OpenAI-specific formatting or token counting. - - Args: - base64_image (Union[str, List[str]]): Optional Base64-encoded image(s). - dimensions (Tuple[int, int]): Optional image dimensions. - override_token_limit (bool): Whether to override token limits. - rag_results (str): The condensed RAG context. - thinking_budget_tokens (int): Number of tokens to allocate for Claude's thinking. - - Returns: - dict: A dict containing Claude API parameters. - """ - - # Append user query to conversation history while handling RAG - if rag_results: - messages.append({"role": "user", "content": f"{rag_results}\n\n{self.query}"}) - logger.info( - f"Added new user message to conversation history with RAG context (now has {len(messages)} messages)" - ) - else: - messages.append({"role": "user", "content": self.query}) - logger.info( - f"Added new user message to conversation history (now has {len(messages)} messages)" - ) - - if base64_image is not None: - # Handle both single image (str) and multiple images (List[str]) - images = [base64_image] if isinstance(base64_image, str) else base64_image - - # Add each image as a separate entry in conversation history - for img in images: - img_content = [ - { - "type": "image", - "source": {"type": "base64", "media_type": "image/jpeg", "data": img}, - } - ] - messages.append({"role": "user", "content": img_content}) - - if images: - logger.info( - f"Added {len(images)} image(s) as separate entries to conversation history" - ) - - # Create Claude parameters with basic settings - claude_params = { - "model": self.model_name, - "max_tokens": self.max_output_tokens_per_request, - "temperature": 0, # Add temperature to make responses more deterministic - "messages": messages, - } - - # Add system prompt as a top-level parameter (not as a message) - if self.system_query: - claude_params["system"] = self.system_query - - # Store the parameters for use in _send_query - self.claude_api_params = claude_params.copy() - - # Add tools if skills are available - if self.skills and self.skills.get_tools(): - tools = self._convert_tools_to_claude_format(self.skills.get_tools()) - if tools: # Only add if we have valid tools - claude_params["tools"] = tools - # Enable tool calling with proper format - claude_params["tool_choice"] = {"type": "auto"} - - # Add thinking if enabled and hard code required temperature = 1 - if thinking_budget_tokens is not None and thinking_budget_tokens != 0: - claude_params["thinking"] = {"type": "enabled", "budget_tokens": thinking_budget_tokens} - claude_params["temperature"] = ( - 1 # Required to be 1 when thinking is enabled # Default to 0 for deterministic responses - ) - - # Store the parameters for use in _send_query and return them - self.claude_api_params = claude_params.copy() - return messages, claude_params # type: ignore[return-value] - - def _send_query(self, messages: list, claude_params: dict) -> Any: # type: ignore[override, type-arg] - """Sends the query to Anthropic's API using streaming for better thinking visualization. - - Args: - messages: Dict with 'claude_prompt' key containing Claude API parameters. - - Returns: - The response message in a format compatible with LLMAgent's expectations. - """ - try: - # Get Claude parameters - claude_params = claude_params.get("claude_prompt", None) or self.claude_api_params - - # Log request parameters with truncated base64 data - logger.debug(self._debug_api_call(claude_params)) - - # Initialize response containers - text_content = "" - tool_calls = [] - thinking_blocks = [] - - # Log the start of streaming and the query - logger.info("Sending streaming request to Claude API") - - # Log the query to memory.txt - with open(os.path.join(self.output_dir, "memory.txt"), "a") as f: - f.write(f"\n\nQUERY: {self.query}\n\n") - f.flush() - - # Stream the response - with self.client.messages.stream(**claude_params) as stream: - print("\n==== CLAUDE API RESPONSE STREAM STARTED ====") - - # Open the memory file once for the entire stream processing - with open(os.path.join(self.output_dir, "memory.txt"), "a") as memory_file: - # Track the current block being processed - current_block = {"type": None, "id": None, "content": "", "signature": None} - - for event in stream: - # Log each event to console - # print(f"EVENT: {event.type}") - # print(json.dumps(event.model_dump(), indent=2, default=str)) - - if event.type == "content_block_start": - # Initialize a new content block - block_type = event.content_block.type - current_block = { - "type": block_type, - "id": event.index, # type: ignore[dict-item] - "content": "", - "signature": None, - } - logger.debug(f"Starting {block_type} block...") - - elif event.type == "content_block_delta": - if event.delta.type == "thinking_delta": - # Accumulate thinking content - current_block["content"] = event.delta.thinking - memory_file.write(f"{event.delta.thinking}") - memory_file.flush() # Ensure content is written immediately - - elif event.delta.type == "text_delta": - # Accumulate text content - text_content += event.delta.text - current_block["content"] += event.delta.text # type: ignore[operator] - memory_file.write(f"{event.delta.text}") - memory_file.flush() - - elif event.delta.type == "signature_delta": - # Store signature for thinking blocks - current_block["signature"] = event.delta.signature - memory_file.write( - f"\n[Signature received for block {current_block['id']}]\n" - ) - memory_file.flush() - - elif event.type == "content_block_stop": - # Store completed blocks - if current_block["type"] == "thinking": - # IMPORTANT: Store the complete event.content_block to ensure we preserve - # the exact format that Claude expects in subsequent requests - if hasattr(event, "content_block"): - # Use the exact thinking block as provided by Claude - thinking_blocks.append(event.content_block.model_dump()) - memory_file.write( - f"\nTHINKING COMPLETE: block {current_block['id']}\n" - ) - else: - # Fallback to constructed thinking block if content_block missing - thinking_block = { - "type": "thinking", - "thinking": current_block["content"], - "signature": current_block["signature"], - } - thinking_blocks.append(thinking_block) - memory_file.write( - f"\nTHINKING COMPLETE: block {current_block['id']}\n" - ) - - elif current_block["type"] == "redacted_thinking": - # Handle redacted thinking blocks - if hasattr(event, "content_block") and hasattr( - event.content_block, "data" - ): - redacted_block = { - "type": "redacted_thinking", - "data": event.content_block.data, - } - thinking_blocks.append(redacted_block) - - elif current_block["type"] == "tool_use": - # Process tool use blocks when they're complete - if hasattr(event, "content_block"): - tool_block = event.content_block - tool_id = tool_block.id # type: ignore[union-attr] - tool_name = tool_block.name # type: ignore[union-attr] - tool_input = tool_block.input # type: ignore[union-attr] - - # Create a tool call object for LLMAgent compatibility - tool_call_obj = type( - "ToolCall", - (), - { - "id": tool_id, - "function": type( - "Function", - (), - { - "name": tool_name, - "arguments": json.dumps(tool_input), - }, - ), - }, - ) - tool_calls.append(tool_call_obj) - - # Write tool call information to memory.txt - memory_file.write(f"\n\nTOOL CALL: {tool_name}\n") - memory_file.write( - f"ARGUMENTS: {json.dumps(tool_input, indent=2)}\n" - ) - - # Reset current block - current_block = { - "type": None, - "id": None, - "content": "", - "signature": None, - } - memory_file.flush() - - elif ( - event.type == "message_delta" and event.delta.stop_reason == "tool_use" - ): - # When a tool use is detected - logger.info("Tool use stop reason detected in stream") - - # Mark the end of the response in memory.txt - memory_file.write("\n\nRESPONSE COMPLETE\n\n") - memory_file.flush() - - print("\n==== CLAUDE API RESPONSE STREAM COMPLETED ====") - - # Final response - logger.info( - f"Claude streaming complete. Text: {len(text_content)} chars, Tool calls: {len(tool_calls)}, Thinking blocks: {len(thinking_blocks)}" - ) - - # Return the complete response with all components - return ResponseMessage( - content=text_content, - tool_calls=tool_calls if tool_calls else None, - thinking_blocks=thinking_blocks if thinking_blocks else None, - ) - - except ConnectionError as ce: - logger.error(f"Connection error with Anthropic API: {ce}") - raise - except ValueError as ve: - logger.error(f"Invalid parameters for Anthropic API: {ve}") - raise - except Exception as e: - logger.error(f"Unexpected error in Anthropic API call: {e}") - logger.exception(e) # This will print the full traceback - raise - - def _observable_query( - self, - observer: Observer, # type: ignore[name-defined] - base64_image: str | None = None, - dimensions: tuple[int, int] | None = None, - override_token_limit: bool = False, - incoming_query: str | None = None, - reset_conversation: bool = False, - thinking_budget_tokens: int | None = None, - ) -> None: - """Main query handler that manages conversation history and Claude interactions. - - This is the primary method for handling all queries, whether they come through - direct_query or through the observable pattern. It manages the conversation - history, builds prompts, and handles tool calls. - - Args: - observer (Observer): The observer to emit responses to - base64_image (Optional[str]): Optional Base64-encoded image - dimensions (Optional[Tuple[int, int]]): Optional image dimensions - override_token_limit (bool): Whether to override token limits - incoming_query (Optional[str]): Optional query to update the agent's query - reset_conversation (bool): Whether to reset the conversation history - """ - - try: - logger.info("_observable_query called in claude") - import copy - - # Reset conversation history if requested - if reset_conversation: - self.conversation_history = [] - - # Create a local copy of conversation history and record its length - messages = copy.deepcopy(self.conversation_history) - base_len = len(messages) - - # Update query and get context - self._update_query(incoming_query) - _, rag_results = self._get_rag_context() - - # Build prompt and get Claude parameters - budget = ( - thinking_budget_tokens - if thinking_budget_tokens is not None - else self.thinking_budget_tokens - ) - messages, claude_params = self._build_prompt( - messages, base64_image, dimensions, override_token_limit, rag_results, budget - ) - - # Send query and get response - response_message = self._send_query(messages, claude_params) - - if response_message is None: - logger.error("Received None response from Claude API") - observer.on_next("") - observer.on_completed() - return - # Add thinking blocks and text content to conversation history - content_blocks = [] - if response_message.thinking_blocks: - content_blocks.extend(response_message.thinking_blocks) - if response_message.content: - content_blocks.append({"type": "text", "text": response_message.content}) - if content_blocks: - messages.append({"role": "assistant", "content": content_blocks}) - - # Handle tool calls if present - if response_message.tool_calls: - self._handle_tooling(response_message, messages) # type: ignore[no-untyped-call] - - # At the end, append only new messages (including tool-use/results) to the global conversation history under a lock - import threading - - if not hasattr(self, "_history_lock"): - self._history_lock = threading.Lock() - with self._history_lock: - for msg in messages[base_len:]: - self.conversation_history.append(msg) - - # After merging, run tooling callback (outside lock) - if response_message.tool_calls: - self._tooling_callback(response_message) - - # Send response to observers - result = response_message.content or "" - observer.on_next(result) - self.response_subject.on_next(result) - observer.on_completed() - except Exception as e: - logger.error(f"Query failed in {self.dev_name}: {e}") - # Send a user-friendly error message instead of propagating the error - error_message = "I apologize, but I'm having trouble processing your request right now. Please try again." - observer.on_next(error_message) - self.response_subject.on_next(error_message) - observer.on_completed() - - def _handle_tooling(self, response_message, messages): # type: ignore[no-untyped-def] - """Executes tools and appends tool-use/result blocks to messages.""" - if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: - logger.info("No tool calls found in response message") - return None - - if len(response_message.tool_calls) > 1: - logger.warning( - "Multiple tool calls detected in response message. Not a tested feature." - ) - - # Execute all tools first and collect their results - for tool_call in response_message.tool_calls: - logger.info(f"Processing tool call: {tool_call.function.name}") - tool_use_block = { - "type": "tool_use", - "id": tool_call.id, - "name": tool_call.function.name, - "input": json.loads(tool_call.function.arguments), - } - messages.append({"role": "assistant", "content": [tool_use_block]}) - - try: - # Execute the tool - args = json.loads(tool_call.function.arguments) - tool_result = self.skills.call(tool_call.function.name, **args) # type: ignore[union-attr] - - # Check if the result is an error message - if isinstance(tool_result, str) and ( - "Error executing skill" in tool_result or "is not available" in tool_result - ): - # Log the error but provide a user-friendly message - logger.error(f"Tool execution failed: {tool_result}") - tool_result = "I apologize, but I'm having trouble executing that action right now. Please try again or ask for something else." - - # Add tool result to conversation history - if tool_result: - messages.append( - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tool_call.id, - "content": f"{tool_result}", - } - ], - } - ) - except Exception as e: - logger.error(f"Unexpected error executing tool {tool_call.function.name}: {e}") - # Add error result to conversation history - messages.append( - { - "role": "user", - "content": [ - { - "type": "tool_result", - "tool_use_id": tool_call.id, - "content": "I apologize, but I encountered an error while trying to execute that action. Please try again.", - } - ], - } - ) - - def _tooling_callback(self, response_message) -> None: # type: ignore[no-untyped-def] - """Runs the observable query for each tool call in the current response_message""" - if not hasattr(response_message, "tool_calls") or not response_message.tool_calls: - return - - try: - for tool_call in response_message.tool_calls: - tool_name = tool_call.function.name - tool_id = tool_call.id - self.run_observable_query( - query_text=f"Tool {tool_name}, ID: {tool_id} execution complete. Please summarize the results and continue.", - thinking_budget_tokens=0, - ).run() - except Exception as e: - logger.error(f"Error in tooling callback: {e}") - # Continue processing even if the callback fails - pass - - def _debug_api_call(self, claude_params: dict): # type: ignore[no-untyped-def, type-arg] - """Debugging function to log API calls with truncated base64 data.""" - # Remove tools to reduce verbosity - import copy - - log_params = copy.deepcopy(claude_params) - if "tools" in log_params: - del log_params["tools"] - - # Truncate base64 data in images - much cleaner approach - if "messages" in log_params: - for msg in log_params["messages"]: - if "content" in msg: - for content in msg["content"]: - if isinstance(content, dict) and content.get("type") == "image": - source = content.get("source", {}) - if source.get("type") == "base64" and "data" in source: - data = source["data"] - source["data"] = f"{data[:50]}..." - return json.dumps(log_params, indent=2, default=str) diff --git a/dimos/agents_deprecated/memory/__init__.py b/dimos/agents_deprecated/memory/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/agents_deprecated/memory/base.py b/dimos/agents_deprecated/memory/base.py deleted file mode 100644 index 283b7cfdce..0000000000 --- a/dimos/agents_deprecated/memory/base.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import abstractmethod - -from dimos.exceptions.agent_memory_exceptions import ( - AgentMemoryConnectionError, - UnknownConnectionTypeError, -) -from dimos.utils.logging_config import setup_logger - -# TODO -# class AbstractAgentMemory(ABC): - -# TODO -# class AbstractAgentSymbolicMemory(AbstractAgentMemory): - - -class AbstractAgentSemanticMemory: # AbstractAgentMemory): - def __init__(self, connection_type: str = "local", **kwargs) -> None: # type: ignore[no-untyped-def] - """ - Initialize with dynamic connection parameters. - Args: - connection_type (str): 'local' for a local database, 'remote' for a remote connection. - Raises: - UnknownConnectionTypeError: If an unrecognized connection type is specified. - AgentMemoryConnectionError: If initializing the database connection fails. - """ - self.logger = setup_logger() - self.logger.info("Initializing AgentMemory with connection type: %s", connection_type) - self.connection_params = kwargs - self.db_connection = ( - None # Holds the conection, whether local or remote, to the database used. - ) - - if connection_type not in ["local", "remote"]: - error = UnknownConnectionTypeError( - f"Invalid connection_type {connection_type}. Expected 'local' or 'remote'." - ) - self.logger.error(str(error)) - raise error - - try: - if connection_type == "remote": - self.connect() # type: ignore[no-untyped-call] - elif connection_type == "local": - self.create() # type: ignore[no-untyped-call] - except Exception as e: - self.logger.error("Failed to initialize database connection: %s", str(e), exc_info=True) - raise AgentMemoryConnectionError( - "Initialization failed due to an unexpected error.", cause=e - ) from e - - @abstractmethod - def connect(self): # type: ignore[no-untyped-def] - """Establish a connection to the data store using dynamic parameters specified during initialization.""" - - @abstractmethod - def create(self): # type: ignore[no-untyped-def] - """Create a local instance of the data store tailored to specific requirements.""" - - ## Create ## - @abstractmethod - def add_vector(self, vector_id, vector_data): # type: ignore[no-untyped-def] - """Add a vector to the database. - Args: - vector_id (any): Unique identifier for the vector. - vector_data (any): The actual data of the vector to be stored. - """ - - ## Read ## - @abstractmethod - def get_vector(self, vector_id): # type: ignore[no-untyped-def] - """Retrieve a vector from the database by its identifier. - Args: - vector_id (any): The identifier of the vector to retrieve. - """ - - @abstractmethod - def query(self, query_texts, n_results: int = 4, similarity_threshold=None): # type: ignore[no-untyped-def] - """Performs a semantic search in the vector database. - - Args: - query_texts (Union[str, List[str]]): The query text or list of query texts to search for. - n_results (int, optional): Number of results to return. Defaults to 4. - similarity_threshold (float, optional): Minimum similarity score for results to be included [0.0, 1.0]. Defaults to None. - - Returns: - List[Tuple[Document, Optional[float]]]: A list of tuples containing the search results. Each tuple - contains: - Document: The retrieved document object. - Optional[float]: The similarity score of the match, or None if not applicable. - - Raises: - ValueError: If query_texts is empty or invalid. - ConnectionError: If database connection fails during query. - """ - - ## Update ## - @abstractmethod - def update_vector(self, vector_id, new_vector_data): # type: ignore[no-untyped-def] - """Update an existing vector in the database. - Args: - vector_id (any): The identifier of the vector to update. - new_vector_data (any): The new data to replace the existing vector data. - """ - - ## Delete ## - @abstractmethod - def delete_vector(self, vector_id): # type: ignore[no-untyped-def] - """Delete a vector from the database using its identifier. - Args: - vector_id (any): The identifier of the vector to delete. - """ - - -# query(string, metadata/tag, n_rets, kwargs) - -# query by string, timestamp, id, n_rets - -# (some sort of tag/metadata) - -# temporal diff --git a/dimos/agents_deprecated/memory/chroma_impl.py b/dimos/agents_deprecated/memory/chroma_impl.py deleted file mode 100644 index c724b07272..0000000000 --- a/dimos/agents_deprecated/memory/chroma_impl.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Sequence -import os - -from langchain_chroma import Chroma -from langchain_openai import OpenAIEmbeddings -import torch - -from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory - - -class ChromaAgentSemanticMemory(AbstractAgentSemanticMemory): - """Base class for Chroma-based semantic memory implementations.""" - - def __init__(self, collection_name: str = "my_collection") -> None: - """Initialize the connection to the local Chroma DB.""" - self.collection_name = collection_name - self.db_connection = None - self.embeddings = None - super().__init__(connection_type="local") - - def connect(self): # type: ignore[no-untyped-def] - # Stub - return super().connect() # type: ignore[no-untyped-call, safe-super] - - def create(self): # type: ignore[no-untyped-def] - """Create the embedding function and initialize the Chroma database. - This method must be implemented by child classes.""" - raise NotImplementedError("Child classes must implement this method") - - def add_vector(self, vector_id, vector_data): # type: ignore[no-untyped-def] - """Add a vector to the ChromaDB collection.""" - if not self.db_connection: - raise Exception("Collection not initialized. Call connect() first.") - self.db_connection.add_texts( - ids=[vector_id], - texts=[vector_data], - metadatas=[{"name": vector_id}], - ) - - def get_vector(self, vector_id): # type: ignore[no-untyped-def] - """Retrieve a vector from the ChromaDB by its identifier.""" - result = self.db_connection.get(include=["embeddings"], ids=[vector_id]) # type: ignore[attr-defined] - return result - - def query(self, query_texts, n_results: int = 4, similarity_threshold=None): # type: ignore[no-untyped-def] - """Query the collection with a specific text and return up to n results.""" - if not self.db_connection: - raise Exception("Collection not initialized. Call connect() first.") - - if similarity_threshold is not None: - if not (0 <= similarity_threshold <= 1): - raise ValueError("similarity_threshold must be between 0 and 1.") - return self.db_connection.similarity_search_with_relevance_scores( - query=query_texts, k=n_results, score_threshold=similarity_threshold - ) - else: - documents = self.db_connection.similarity_search(query=query_texts, k=n_results) - return [(doc, None) for doc in documents] - - def update_vector(self, vector_id, new_vector_data): # type: ignore[no-untyped-def] - # TODO - return super().connect() # type: ignore[no-untyped-call, safe-super] - - def delete_vector(self, vector_id): # type: ignore[no-untyped-def] - """Delete a vector from the ChromaDB using its identifier.""" - if not self.db_connection: - raise Exception("Collection not initialized. Call connect() first.") - self.db_connection.delete(ids=[vector_id]) - - -class OpenAISemanticMemory(ChromaAgentSemanticMemory): - """Semantic memory implementation using OpenAI's embedding API.""" - - def __init__( - self, - collection_name: str = "my_collection", - model: str = "text-embedding-3-large", - dimensions: int = 1024, - ) -> None: - """Initialize OpenAI-based semantic memory. - - Args: - collection_name (str): Name of the Chroma collection - model (str): OpenAI embedding model to use - dimensions (int): Dimension of the embedding vectors - """ - self.model = model - self.dimensions = dimensions - super().__init__(collection_name=collection_name) - - def create(self): # type: ignore[no-untyped-def] - """Connect to OpenAI API and create the ChromaDB client.""" - # Get OpenAI key - self.OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") - if not self.OPENAI_API_KEY: - raise Exception("OpenAI key was not specified.") - - # Set embeddings - self.embeddings = OpenAIEmbeddings( # type: ignore[assignment] - model=self.model, - dimensions=self.dimensions, - api_key=self.OPENAI_API_KEY, # type: ignore[arg-type] - ) - - # Create the database - self.db_connection = Chroma( # type: ignore[assignment] - collection_name=self.collection_name, - embedding_function=self.embeddings, - collection_metadata={"hnsw:space": "cosine"}, - ) - - -class LocalSemanticMemory(ChromaAgentSemanticMemory): - """Semantic memory implementation using local models.""" - - def __init__( - self, - collection_name: str = "my_collection", - model_name: str = "sentence-transformers/all-MiniLM-L6-v2", - ) -> None: - """Initialize the local semantic memory using SentenceTransformer. - - Args: - collection_name (str): Name of the Chroma collection - model_name (str): Embeddings model - """ - - self.model_name = model_name - super().__init__(collection_name=collection_name) - - def create(self) -> None: - """Create local embedding model and initialize the ChromaDB client.""" - # Load the sentence transformer model - - # Use GPU if available, otherwise fall back to CPU - if torch.cuda.is_available(): - self.device = "cuda" - # MacOS Metal performance shaders - elif torch.backends.mps.is_available() and torch.backends.mps.is_built(): - self.device = "mps" - else: - self.device = "cpu" - - print(f"Using device: {self.device}") - self.model = SentenceTransformer(self.model_name, device=self.device) # type: ignore[name-defined] - - # Create a custom embedding class that implements the embed_query method - class SentenceTransformerEmbeddings: - def __init__(self, model) -> None: # type: ignore[no-untyped-def] - self.model = model - - def embed_query(self, text: str): # type: ignore[no-untyped-def] - """Embed a single query text.""" - return self.model.encode(text, normalize_embeddings=True).tolist() - - def embed_documents(self, texts: Sequence[str]): # type: ignore[no-untyped-def] - """Embed multiple documents/texts.""" - return self.model.encode(texts, normalize_embeddings=True).tolist() - - # Create an instance of our custom embeddings class - self.embeddings = SentenceTransformerEmbeddings(self.model) # type: ignore[assignment] - - # Create the database - self.db_connection = Chroma( # type: ignore[assignment] - collection_name=self.collection_name, - embedding_function=self.embeddings, - collection_metadata={"hnsw:space": "cosine"}, - ) diff --git a/dimos/agents_deprecated/memory/image_embedding.py b/dimos/agents_deprecated/memory/image_embedding.py deleted file mode 100644 index 27e16f1aa8..0000000000 --- a/dimos/agents_deprecated/memory/image_embedding.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Image embedding module for converting images to vector embeddings. - -This module provides a class for generating vector embeddings from images -using pre-trained models like CLIP, ResNet, etc. -""" - -import base64 -import io -import os -import sys - -import cv2 -import numpy as np -from PIL import Image - -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class ImageEmbeddingProvider: - """ - A provider for generating vector embeddings from images. - - This class uses pre-trained models to convert images into vector embeddings - that can be stored in a vector database and used for similarity search. - """ - - def __init__(self, model_name: str = "clip", dimensions: int = 512) -> None: - """ - Initialize the image embedding provider. - - Args: - model_name: Name of the embedding model to use ("clip", "resnet", etc.) - dimensions: Dimensions of the embedding vectors - """ - self.model_name = model_name - self.dimensions = dimensions - self.model = None - self.processor = None - self.model_path = None - - self._initialize_model() # type: ignore[no-untyped-call] - - logger.info(f"ImageEmbeddingProvider initialized with model {model_name}") - - def _initialize_model(self): # type: ignore[no-untyped-def] - """Initialize the specified embedding model.""" - try: - import onnxruntime as ort # type: ignore[import-untyped] - import torch # noqa: F401 - from transformers import ( # type: ignore[import-untyped] - AutoFeatureExtractor, - AutoModel, - CLIPProcessor, - ) - - if self.model_name == "clip": - model_id = get_data("models_clip") / "model.onnx" - self.model_path = str(model_id) # type: ignore[assignment] # Store for pickling - processor_id = "openai/clip-vit-base-patch32" - - providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] - if sys.platform == "darwin": - # 2025-11-17 12:36:47.877215 [W:onnxruntime:, helper.cc:82 IsInputSupported] CoreML does not support input dim > 16384. Input:text_model.embeddings.token_embedding.weight, shape: {49408,512} - # 2025-11-17 12:36:47.878496 [W:onnxruntime:, coreml_execution_provider.cc:107 GetCapability] CoreMLExecutionProvider::GetCapability, number of partitions supported by CoreML: 88 number of nodes in the graph: 1504 number of nodes supported by CoreML: 933 - providers = ["CoreMLExecutionProvider"] + [ - each for each in providers if each != "CUDAExecutionProvider" - ] - - self.model = ort.InferenceSession(str(model_id), providers=providers) - - actual_providers = self.model.get_providers() # type: ignore[attr-defined] - self.processor = CLIPProcessor.from_pretrained(processor_id) - logger.info(f"Loaded CLIP model: {model_id} with providers: {actual_providers}") - elif self.model_name == "resnet": - model_id = "microsoft/resnet-50" # type: ignore[assignment] - self.model = AutoModel.from_pretrained(model_id) - self.processor = AutoFeatureExtractor.from_pretrained(model_id) - logger.info(f"Loaded ResNet model: {model_id}") - else: - raise ValueError(f"Unsupported model: {self.model_name}") - except ImportError as e: - logger.error(f"Failed to import required modules: {e}") - logger.error("Please install with: pip install transformers torch") - # Initialize with dummy model for type checking - self.model = None - self.processor = None - raise - - def get_embedding(self, image: np.ndarray | str | bytes) -> np.ndarray: # type: ignore[type-arg] - """ - Generate an embedding vector for the provided image. - - Args: - image: The image to embed, can be a numpy array (OpenCV format), - a file path, or a base64-encoded string - - Returns: - A numpy array containing the embedding vector - """ - if self.model is None or self.processor is None: - logger.error("Model not initialized. Using fallback random embedding.") - return np.random.randn(self.dimensions).astype(np.float32) - - pil_image = self._prepare_image(image) - - try: - import torch - - if self.model_name == "clip": - inputs = self.processor(images=pil_image, return_tensors="np") - - with torch.no_grad(): - ort_inputs = { - inp.name: inputs[inp.name] - for inp in self.model.get_inputs() - if inp.name in inputs - } - - # If required, add dummy text inputs - input_names = [i.name for i in self.model.get_inputs()] - batch_size = inputs["pixel_values"].shape[0] - if "input_ids" in input_names: - ort_inputs["input_ids"] = np.zeros((batch_size, 1), dtype=np.int64) - if "attention_mask" in input_names: - ort_inputs["attention_mask"] = np.ones((batch_size, 1), dtype=np.int64) - - # Run inference - ort_outputs = self.model.run(None, ort_inputs) - - # Look up correct output name - output_names = [o.name for o in self.model.get_outputs()] - if "image_embeds" in output_names: - image_embedding = ort_outputs[output_names.index("image_embeds")] - else: - raise RuntimeError(f"No 'image_embeds' found in outputs: {output_names}") - - embedding = image_embedding / np.linalg.norm(image_embedding, axis=1, keepdims=True) - embedding = embedding[0] - - elif self.model_name == "resnet": - inputs = self.processor(images=pil_image, return_tensors="pt") - - with torch.no_grad(): - outputs = self.model(**inputs) - - # Get the [CLS] token embedding - embedding = outputs.last_hidden_state[:, 0, :].numpy()[0] - else: - logger.warning(f"Unsupported model: {self.model_name}. Using random embedding.") - embedding = np.random.randn(self.dimensions).astype(np.float32) - - # Normalize and ensure correct dimensions - embedding = embedding / np.linalg.norm(embedding) - - logger.debug(f"Generated embedding with shape {embedding.shape}") - return embedding - - except Exception as e: - logger.error(f"Error generating embedding: {e}") - return np.random.randn(self.dimensions).astype(np.float32) - - def get_text_embedding(self, text: str) -> np.ndarray: # type: ignore[type-arg] - """ - Generate an embedding vector for the provided text. - - Args: - text: The text to embed - - Returns: - A numpy array containing the embedding vector - """ - if self.model is None or self.processor is None: - logger.error("Model not initialized. Using fallback random embedding.") - return np.random.randn(self.dimensions).astype(np.float32) - - if self.model_name != "clip": - logger.warning( - f"Text embeddings are only supported with CLIP model, not {self.model_name}. Using random embedding." - ) - return np.random.randn(self.dimensions).astype(np.float32) - - try: - import torch - - inputs = self.processor(text=[text], return_tensors="np", padding=True) - - with torch.no_grad(): - # Prepare ONNX input dict (handle only what's needed) - ort_inputs = { - inp.name: inputs[inp.name] - for inp in self.model.get_inputs() - if inp.name in inputs - } - # Determine which inputs are expected by the ONNX model - input_names = [i.name for i in self.model.get_inputs()] - batch_size = inputs["input_ids"].shape[0] # pulled from text input - - # If the model expects pixel_values (i.e., fused model), add dummy vision input - if "pixel_values" in input_names: - ort_inputs["pixel_values"] = np.zeros( - (batch_size, 3, 224, 224), dtype=np.float32 - ) - - # Run inference - ort_outputs = self.model.run(None, ort_inputs) - - # Determine correct output (usually 'last_hidden_state' or 'text_embeds') - output_names = [o.name for o in self.model.get_outputs()] - if "text_embeds" in output_names: - text_embedding = ort_outputs[output_names.index("text_embeds")] - else: - text_embedding = ort_outputs[0] # fallback to first output - - # Normalize - text_embedding = text_embedding / np.linalg.norm( - text_embedding, axis=1, keepdims=True - ) - text_embedding = text_embedding[0] # shape: (512,) - - logger.debug( - f"Generated text embedding with shape {text_embedding.shape} for text: '{text}'" - ) - return text_embedding - - except Exception as e: - logger.error(f"Error generating text embedding: {e}") - return np.random.randn(self.dimensions).astype(np.float32) - - def _prepare_image(self, image: np.ndarray | str | bytes) -> Image.Image: # type: ignore[type-arg] - """ - Convert the input image to PIL format required by the models. - - Args: - image: Input image in various formats - - Returns: - PIL Image object - """ - if isinstance(image, np.ndarray): - if len(image.shape) == 3 and image.shape[2] == 3: - image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) - else: - image_rgb = image - - return Image.fromarray(image_rgb) - - elif isinstance(image, str): - if os.path.isfile(image): - return Image.open(image) - else: - try: - image_data = base64.b64decode(image) - return Image.open(io.BytesIO(image_data)) - except Exception as e: - logger.error(f"Failed to decode image string: {e}") - raise ValueError("Invalid image string format") - - elif isinstance(image, bytes): - return Image.open(io.BytesIO(image)) - - else: - raise ValueError(f"Unsupported image format: {type(image)}") diff --git a/dimos/agents_deprecated/memory/spatial_vector_db.py b/dimos/agents_deprecated/memory/spatial_vector_db.py deleted file mode 100644 index c482076325..0000000000 --- a/dimos/agents_deprecated/memory/spatial_vector_db.py +++ /dev/null @@ -1,338 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Spatial vector database for storing and querying images with XY locations. - -This module extends the ChromaDB implementation to support storing images with -their XY locations and querying by location or image similarity. -""" - -from typing import Any - -import chromadb -import numpy as np - -from dimos.agents_deprecated.memory.visual_memory import VisualMemory -from dimos.types.robot_location import RobotLocation -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class SpatialVectorDB: - """ - A vector database for storing and querying images mapped to X,Y,theta absolute locations for SpatialMemory. - - This class extends the ChromaDB implementation to support storing images with - their absolute locations and querying by location, text, or image cosine semantic similarity. - """ - - def __init__( # type: ignore[no-untyped-def] - self, - collection_name: str = "spatial_memory", - chroma_client=None, - visual_memory=None, - embedding_provider=None, - ) -> None: - """ - Initialize the spatial vector database. - - Args: - collection_name: Name of the vector database collection - chroma_client: Optional ChromaDB client for persistence. If None, an in-memory client is used. - visual_memory: Optional VisualMemory instance for storing images. If None, a new one is created. - embedding_provider: Optional ImageEmbeddingProvider instance for computing embeddings. If None, one will be created. - """ - self.collection_name = collection_name - - # Use provided client or create in-memory client - self.client = chroma_client if chroma_client is not None else chromadb.Client() - - # Check if collection already exists - in newer ChromaDB versions list_collections returns names directly - existing_collections = self.client.list_collections() - - # Handle different versions of ChromaDB API - try: - collection_exists = collection_name in existing_collections - except: - try: - collection_exists = collection_name in [c.name for c in existing_collections] - except: - try: - self.client.get_collection(name=collection_name) - collection_exists = True - except Exception: - collection_exists = False - - # Get or create the collection - self.image_collection = self.client.get_or_create_collection( - name=collection_name, metadata={"hnsw:space": "cosine"} - ) - - # Use provided visual memory or create a new one - self.visual_memory = visual_memory if visual_memory is not None else VisualMemory() - - # Store the embedding provider to reuse for all operations - self.embedding_provider = embedding_provider - - # Initialize the location collection for text-based location tagging - location_collection_name = f"{collection_name}_locations" - self.location_collection = self.client.get_or_create_collection( - name=location_collection_name, metadata={"hnsw:space": "cosine"} - ) - - # Log initialization info with details about whether using existing collection - client_type = "persistent" if chroma_client is not None else "in-memory" - try: - count = len(self.image_collection.get(include=[])["ids"]) - if collection_exists: - logger.info( - f"Using EXISTING {client_type} collection '{collection_name}' with {count} entries" - ) - else: - logger.info(f"Created NEW {client_type} collection '{collection_name}'") - except Exception as e: - logger.info( - f"Initialized {client_type} collection '{collection_name}' (count error: {e!s})" - ) - - def add_image_vector( - self, - vector_id: str, - image: np.ndarray, # type: ignore[type-arg] - embedding: np.ndarray, # type: ignore[type-arg] - metadata: dict[str, Any], - ) -> None: - """ - Add an image with its embedding and metadata to the vector database. - - Args: - vector_id: Unique identifier for the vector - image: The image to store - embedding: The pre-computed embedding vector for the image - metadata: Metadata for the image, including x, y coordinates - """ - # Store the image in visual memory - self.visual_memory.add(vector_id, image) - - # Add the vector to ChromaDB - self.image_collection.add( - ids=[vector_id], embeddings=[embedding.tolist()], metadatas=[metadata] - ) - - logger.info(f"Added image vector {vector_id} with metadata: {metadata}") - - def query_by_embedding(self, embedding: np.ndarray, limit: int = 5) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images similar to the provided embedding. - - Args: - embedding: Query embedding vector - limit: Maximum number of results to return - - Returns: - List of results, each containing the image and its metadata - """ - results = self.image_collection.query( - query_embeddings=[embedding.tolist()], n_results=limit - ) - - return self._process_query_results(results) - - # TODO: implement efficient nearest neighbor search - def query_by_location( - self, x: float, y: float, radius: float = 2.0, limit: int = 5 - ) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images near the specified location. - - Args: - x: X coordinate - y: Y coordinate - radius: Search radius in meters - limit: Maximum number of results to return - - Returns: - List of results, each containing the image and its metadata - """ - results = self.image_collection.get() - - if not results or not results["ids"]: - return [] - - filtered_results = {"ids": [], "metadatas": [], "distances": []} # type: ignore[var-annotated] - - for i, metadata in enumerate(results["metadatas"]): # type: ignore[arg-type] - item_x = metadata.get("x") - item_y = metadata.get("y") - - if item_x is not None and item_y is not None: - distance = np.sqrt((x - item_x) ** 2 + (y - item_y) ** 2) - - if distance <= radius: - filtered_results["ids"].append(results["ids"][i]) - filtered_results["metadatas"].append(metadata) - filtered_results["distances"].append(distance) - - sorted_indices = np.argsort(filtered_results["distances"]) - filtered_results["ids"] = [filtered_results["ids"][i] for i in sorted_indices[:limit]] - filtered_results["metadatas"] = [ - filtered_results["metadatas"][i] for i in sorted_indices[:limit] - ] - filtered_results["distances"] = [ - filtered_results["distances"][i] for i in sorted_indices[:limit] - ] - - return self._process_query_results(filtered_results) - - def _process_query_results(self, results) -> list[dict]: # type: ignore[no-untyped-def, type-arg] - """Process query results to include decoded images.""" - if not results or not results["ids"]: - return [] - - processed_results = [] - - for i, vector_id in enumerate(results["ids"]): - if isinstance(vector_id, list) and not vector_id: - continue - - lookup_id = vector_id[0] if isinstance(vector_id, list) else vector_id - - # Create the result dictionary with metadata regardless of image availability - result = { - "metadata": results["metadatas"][i] if "metadatas" in results else {}, - "id": lookup_id, - } - - # Add distance if available - if "distances" in results: - result["distance"] = ( - results["distances"][i][0] - if isinstance(results["distances"][i], list) - else results["distances"][i] - ) - - # Get the image from visual memory - #image = self.visual_memory.get(lookup_id) - #result["image"] = image - - processed_results.append(result) - - return processed_results - - def query_by_text(self, text: str, limit: int = 5) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images matching the provided text description. - - This method uses CLIP's text-to-image matching capability to find images - that semantically match the text query (e.g., "where is the kitchen"). - - Args: - text: Text query to search for - limit: Maximum number of results to return - - Returns: - List of results, each containing the image, its metadata, and similarity score - """ - if self.embedding_provider is None: - from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider - - self.embedding_provider = ImageEmbeddingProvider(model_name="clip") - - text_embedding = self.embedding_provider.get_text_embedding(text) - - results = self.image_collection.query( - query_embeddings=[text_embedding.tolist()], - n_results=limit, - include=["documents", "metadatas", "distances"], - ) - - logger.info( - f"Text query: '{text}' returned {len(results['ids'] if 'ids' in results else [])} results" - ) - return self._process_query_results(results) - - def get_all_locations(self) -> list[tuple[float, float, float]]: - """Get all locations stored in the database.""" - # Get all items from the collection without embeddings - results = self.image_collection.get(include=["metadatas"]) - - if not results or "metadatas" not in results or not results["metadatas"]: - return [] - - # Extract x, y coordinates from metadata - locations = [] - for metadata in results["metadatas"]: - if isinstance(metadata, list) and metadata and isinstance(metadata[0], dict): - metadata = metadata[0] # Handle nested metadata - - if isinstance(metadata, dict) and "x" in metadata and "y" in metadata: - x = metadata.get("x", 0) - y = metadata.get("y", 0) - z = metadata.get("z", 0) if "z" in metadata else 0 - locations.append((x, y, z)) - - return locations - - @property - def image_storage(self): # type: ignore[no-untyped-def] - """Legacy accessor for compatibility with existing code.""" - return self.visual_memory.images - - def tag_location(self, location: RobotLocation) -> None: - """ - Tag a location with a semantic name/description for text-based retrieval. - - Args: - location: RobotLocation object with position/rotation data - """ - - location_id = location.location_id - metadata = location.to_vector_metadata() - - self.location_collection.add( - ids=[location_id], documents=[location.name], metadatas=[metadata] - ) - - def query_tagged_location(self, query: str) -> tuple[RobotLocation | None, float]: - """ - Query for a tagged location using semantic text search. - - Args: - query: Natural language query (e.g., "dining area", "place to eat") - - Returns: - The best matching RobotLocation or None if no matches found - """ - - results = self.location_collection.query( - query_texts=[query], n_results=1, include=["metadatas", "documents", "distances"] - ) - - if not (results and results["ids"] and len(results["ids"][0]) > 0): - return None, 0 - - best_match_metadata = results["metadatas"][0][0] # type: ignore[index] - distance = float(results["distances"][0][0] if "distances" in results else 0.0) # type: ignore[index] - - location = RobotLocation.from_vector_metadata(best_match_metadata) # type: ignore[arg-type] - - logger.info( - f"Found location '{location.name}' for query '{query}' (distance: {distance:.3f})" - if distance - else "" - ) - - return location, distance diff --git a/dimos/agents_deprecated/memory/test_image_embedding.py b/dimos/agents_deprecated/memory/test_image_embedding.py deleted file mode 100644 index 89f0716e7e..0000000000 --- a/dimos/agents_deprecated/memory/test_image_embedding.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test module for the CLIP image embedding functionality in dimos. -""" - -import os -import time - -import numpy as np -import pytest -from reactivex import operators as ops - -from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider -from dimos.stream.video_provider import VideoProvider - - -@pytest.mark.heavy -class TestImageEmbedding: - """Test class for CLIP image embedding functionality.""" - - def test_clip_embedding_initialization(self) -> None: - """Test CLIP embedding provider initializes correctly.""" - try: - # Initialize the embedding provider with CLIP model - embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) - assert embedding_provider.model is not None, "CLIP model failed to initialize" - assert embedding_provider.processor is not None, "CLIP processor failed to initialize" - assert embedding_provider.model_name == "clip", "Model name should be 'clip'" - assert embedding_provider.dimensions == 512, "Embedding dimensions should be 512" - except Exception as e: - pytest.skip(f"Skipping test due to model initialization error: {e}") - - def test_clip_embedding_process_video(self) -> None: - """Test CLIP embedding provider can process video frames and return embeddings.""" - try: - from dimos.utils.data import get_data - - video_path = get_data("assets") / "trimmed_video_office.mov" - - embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) - - assert os.path.exists(video_path), f"Test video not found: {video_path}" - video_provider = VideoProvider(dev_name="test_video", video_source=video_path) - - video_stream = video_provider.capture_video_as_observable(realtime=False, fps=15) - - # Use ReactiveX operators to process the stream - def process_frame(frame): - try: - # Process frame with CLIP - embedding = embedding_provider.get_embedding(frame) - print( - f"Generated CLIP embedding with shape: {embedding.shape}, norm: {np.linalg.norm(embedding):.4f}" - ) - - return {"frame": frame, "embedding": embedding} - except Exception as e: - print(f"Error in process_frame: {e}") - return None - - embedding_stream = video_stream.pipe(ops.map(process_frame)) - - results = [] - frames_processed = 0 - target_frames = 10 - - def on_next(result) -> None: - nonlocal frames_processed, results - if not result: # Skip None results - return - - results.append(result) - frames_processed += 1 - - # Stop processing after target frames - if frames_processed >= target_frames: - subscription.dispose() - - def on_error(error) -> None: - pytest.fail(f"Error in embedding stream: {error}") - - def on_completed() -> None: - pass - - # Subscribe and wait for results - subscription = embedding_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - timeout = 60.0 - start_time = time.time() - while frames_processed < target_frames and time.time() - start_time < timeout: - time.sleep(0.5) - print(f"Processed {frames_processed}/{target_frames} frames") - - # Clean up subscription - subscription.dispose() - video_provider.dispose_all() - - # Check if we have results - if len(results) == 0: - pytest.skip("No embeddings generated, but test connection established correctly") - return - - print(f"Processed {len(results)} frames with CLIP embeddings") - - # Analyze the results - assert len(results) > 0, "No embeddings generated" - - # Check properties of first embedding - first_result = results[0] - assert "embedding" in first_result, "Result doesn't contain embedding" - assert "frame" in first_result, "Result doesn't contain frame" - - # Check embedding shape and normalization - embedding = first_result["embedding"] - assert isinstance(embedding, np.ndarray), "Embedding is not a numpy array" - assert embedding.shape == (512,), ( - f"Embedding has wrong shape: {embedding.shape}, expected (512,)" - ) - assert abs(np.linalg.norm(embedding) - 1.0) < 1e-5, "Embedding is not normalized" - - # Save the first embedding for similarity tests - if len(results) > 1 and "embedding" in results[0]: - # Create a class variable to store embeddings for the similarity test - TestImageEmbedding.test_embeddings = { - "embedding1": results[0]["embedding"], - "embedding2": results[1]["embedding"] if len(results) > 1 else None, - } - print("Saved embeddings for similarity testing") - - print("CLIP embedding test passed successfully!") - - except Exception as e: - pytest.fail(f"Test failed with error: {e}") - - def test_clip_embedding_similarity(self) -> None: - """Test CLIP embedding similarity search and text-to-image queries.""" - try: - # Skip if previous test didn't generate embeddings - if not hasattr(TestImageEmbedding, "test_embeddings"): - pytest.skip("No embeddings available from previous test") - return - - # Get embeddings from previous test - embedding1 = TestImageEmbedding.test_embeddings["embedding1"] - embedding2 = TestImageEmbedding.test_embeddings["embedding2"] - - # Initialize embedding provider for text embeddings - embedding_provider = ImageEmbeddingProvider(model_name="clip", dimensions=512) - - # Test frame-to-frame similarity - if embedding1 is not None and embedding2 is not None: - # Compute cosine similarity - similarity = np.dot(embedding1, embedding2) - print(f"Similarity between first two frames: {similarity:.4f}") - - # Should be in range [-1, 1] - assert -1.0 <= similarity <= 1.0, f"Similarity out of valid range: {similarity}" - - # Test text-to-image similarity - if embedding1 is not None: - # Generate a list of text queries to test - text_queries = ["a video frame", "a person", "an outdoor scene", "a kitchen"] - - # Test each text query - for text_query in text_queries: - # Get text embedding - text_embedding = embedding_provider.get_text_embedding(text_query) - - # Check text embedding properties - assert isinstance(text_embedding, np.ndarray), ( - "Text embedding is not a numpy array" - ) - assert text_embedding.shape == (512,), ( - f"Text embedding has wrong shape: {text_embedding.shape}" - ) - assert abs(np.linalg.norm(text_embedding) - 1.0) < 1e-5, ( - "Text embedding is not normalized" - ) - - # Compute similarity between frame and text - text_similarity = np.dot(embedding1, text_embedding) - print(f"Similarity between frame and '{text_query}': {text_similarity:.4f}") - - # Should be in range [-1, 1] - assert -1.0 <= text_similarity <= 1.0, ( - f"Text-image similarity out of range: {text_similarity}" - ) - - print("CLIP embedding similarity tests passed successfully!") - - except Exception as e: - pytest.fail(f"Similarity test failed with error: {e}") - - -if __name__ == "__main__": - pytest.main(["-v", "--disable-warnings", __file__]) diff --git a/dimos/agents_deprecated/memory/visual_memory.py b/dimos/agents_deprecated/memory/visual_memory.py deleted file mode 100644 index 98ad00e2fd..0000000000 --- a/dimos/agents_deprecated/memory/visual_memory.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Visual memory storage for managing image data persistence and retrieval -""" - -import base64 -import os -import pickle - -import cv2 -import numpy as np - -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class VisualMemory: - """ - A class for storing and retrieving visual memories (images) with persistence. - - This class handles the storage, encoding, and retrieval of images associated - with vector database entries. It provides persistence mechanisms to save and - load the image data from disk. - """ - - def __init__(self, output_dir: str | None = None) -> None: - """ - Initialize the visual memory system. - - Args: - output_dir: Directory to store the serialized image data - """ - self.images = {} # type: ignore[var-annotated] # Maps IDs to encoded images - self.output_dir = output_dir - - if output_dir: - os.makedirs(output_dir, exist_ok=True) - logger.info(f"VisualMemory initialized with output directory: {output_dir}") - else: - logger.info("VisualMemory initialized with no persistence directory") - - def add(self, image_id: str, image: np.ndarray) -> None: # type: ignore[type-arg] - """ - Add an image to visual memory. - - Args: - image_id: Unique identifier for the image - image: The image data as a numpy array - """ - # Encode the image to base64 for storage - success, encoded_image = cv2.imencode(".jpg", image) - if not success: - logger.error(f"Failed to encode image {image_id}") - return - - image_bytes = encoded_image.tobytes() - b64_encoded = base64.b64encode(image_bytes).decode("utf-8") - - # Store the encoded image - self.images[image_id] = b64_encoded - logger.debug(f"Added image {image_id} to visual memory") - - def get(self, image_id: str) -> np.ndarray | None: # type: ignore[type-arg] - """ - Retrieve an image from visual memory. - - Args: - image_id: Unique identifier for the image - - Returns: - The decoded image as a numpy array, or None if not found - """ - if image_id not in self.images: - logger.warning( - f"Image not found in storage for ID {image_id}. Incomplete or corrupted image storage." - ) - return None - - try: - encoded_image = self.images[image_id] - image_bytes = base64.b64decode(encoded_image) - image_array = np.frombuffer(image_bytes, dtype=np.uint8) - image = cv2.imdecode(image_array, cv2.IMREAD_COLOR) - return image - except Exception as e: - logger.warning(f"Failed to decode image for ID {image_id}: {e!s}") - return None - - def contains(self, image_id: str) -> bool: - """ - Check if an image ID exists in visual memory. - - Args: - image_id: Unique identifier for the image - - Returns: - True if the image exists, False otherwise - """ - return image_id in self.images - - def count(self) -> int: - """ - Get the number of images in visual memory. - - Returns: - The number of images stored - """ - return len(self.images) - - def save(self, filename: str | None = None) -> str: - """ - Save the visual memory to disk. - - Args: - filename: Optional filename to save to. If None, uses a default name in the output directory. - - Returns: - The path where the data was saved - """ - if not self.output_dir: - logger.warning("No output directory specified for VisualMemory. Cannot save.") - return "" - - if not filename: - filename = "visual_memory.pkl" - - output_path = os.path.join(self.output_dir, filename) - - try: - with open(output_path, "wb") as f: - pickle.dump(self.images, f) - logger.info(f"Saved {len(self.images)} images to {output_path}") - return output_path - except Exception as e: - logger.error(f"Failed to save visual memory: {e!s}") - return "" - - @classmethod - def load(cls, path: str, output_dir: str | None = None) -> "VisualMemory": - """ - Load visual memory from disk. - - Args: - path: Path to the saved visual memory file - output_dir: Optional output directory for the new instance - - Returns: - A new VisualMemory instance with the loaded data - """ - instance = cls(output_dir=output_dir) - - if not os.path.exists(path): - logger.warning(f"Visual memory file {path} not found") - return instance - - try: - with open(path, "rb") as f: - instance.images = pickle.load(f) - logger.info(f"Loaded {len(instance.images)} images from {path}") - return instance - except Exception as e: - logger.error(f"Failed to load visual memory: {e!s}") - return instance - - def clear(self) -> None: - """Clear all images from memory.""" - self.images = {} - logger.info("Visual memory cleared") diff --git a/dimos/agents_deprecated/modules/__init__.py b/dimos/agents_deprecated/modules/__init__.py deleted file mode 100644 index 99163d55d0..0000000000 --- a/dimos/agents_deprecated/modules/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agent modules for DimOS.""" diff --git a/dimos/agents_deprecated/modules/base.py b/dimos/agents_deprecated/modules/base.py deleted file mode 100644 index 891edbe4bd..0000000000 --- a/dimos/agents_deprecated/modules/base.py +++ /dev/null @@ -1,525 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base agent class with all features (non-module).""" - -import asyncio -from concurrent.futures import ThreadPoolExecutor -import json -from typing import Any - -from reactivex.subject import Subject - -from dimos.agents_deprecated.agent_message import AgentMessage -from dimos.agents_deprecated.agent_types import AgentResponse, ConversationHistory, ToolCall -from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory -from dimos.agents_deprecated.memory.chroma_impl import OpenAISemanticMemory -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -try: - from .gateway import UnifiedGatewayClient -except ImportError: - from dimos.agents_deprecated.modules.gateway import UnifiedGatewayClient - -logger = setup_logger() - -# Vision-capable models -VISION_MODELS = { - "openai::gpt-4o", - "openai::gpt-4o-mini", - "openai::gpt-4-turbo", - "openai::gpt-4-vision-preview", - "anthropic::claude-3-haiku-20240307", - "anthropic::claude-3-sonnet-20241022", - "anthropic::claude-3-opus-20240229", - "anthropic::claude-3-5-sonnet-20241022", - "anthropic::claude-3-5-haiku-latest", - "qwen::qwen-vl-plus", - "qwen::qwen-vl-max", -} - - -class BaseAgent: - """Base agent with all features including memory, skills, and multimodal support. - - This class provides: - - LLM gateway integration - - Conversation history - - Semantic memory (RAG) - - Skills/tools execution - - Multimodal support (text, images, data) - - Model capability detection - """ - - def __init__( # type: ignore[no-untyped-def] - self, - model: str = "openai::gpt-4o-mini", - system_prompt: str | None = None, - skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, - memory: AbstractAgentSemanticMemory | None = None, - temperature: float = 0.0, - max_tokens: int = 4096, - max_input_tokens: int = 128000, - max_history: int = 20, - rag_n: int = 4, - rag_threshold: float = 0.45, - seed: int | None = None, - # Legacy compatibility - dev_name: str = "BaseAgent", - agent_type: str = "LLM", - **kwargs, - ) -> None: - """Initialize the base agent with all features. - - Args: - model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") - system_prompt: System prompt for the agent - skills: Skills/tools available to the agent - memory: Semantic memory system for RAG - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - max_input_tokens: Maximum input tokens - max_history: Maximum conversation history to keep - rag_n: Number of RAG results to fetch - rag_threshold: Minimum similarity for RAG results - seed: Random seed for deterministic outputs (if supported by model) - dev_name: Device/agent name for logging - agent_type: Type of agent for logging - """ - self.model = model - self.system_prompt = system_prompt or "You are a helpful AI assistant." - self.temperature = temperature - self.max_tokens = max_tokens - self.max_input_tokens = max_input_tokens - self._max_history = max_history - self.rag_n = rag_n - self.rag_threshold = rag_threshold - self.seed = seed - self.dev_name = dev_name - self.agent_type = agent_type - - # Initialize skills - if skills is None: - self.skills = SkillLibrary() - elif isinstance(skills, SkillLibrary): - self.skills = skills - elif isinstance(skills, list): - self.skills = SkillLibrary() - for skill in skills: - self.skills.add(skill) - elif isinstance(skills, AbstractSkill): - self.skills = SkillLibrary() - self.skills.add(skills) - else: - self.skills = SkillLibrary() - - # Initialize memory - allow None for testing - if memory is False: # type: ignore[comparison-overlap] # Explicit False means no memory - self.memory = None - else: - self.memory = memory or OpenAISemanticMemory() # type: ignore[has-type] - - # Initialize gateway - self.gateway = UnifiedGatewayClient() - - # Conversation history with proper format management - self.conversation = ConversationHistory(max_size=self._max_history) - - # Thread pool for async operations - self._executor = ThreadPoolExecutor(max_workers=2) - - # Response subject for emitting responses - self.response_subject = Subject() # type: ignore[var-annotated] - - # Check model capabilities - self._supports_vision = self._check_vision_support() - - # Initialize memory with default context - self._initialize_memory() - - @property - def max_history(self) -> int: - """Get max history size.""" - return self._max_history - - @max_history.setter - def max_history(self, value: int) -> None: - """Set max history size and update conversation.""" - self._max_history = value - self.conversation.max_size = value - - def _check_vision_support(self) -> bool: - """Check if the model supports vision.""" - return self.model in VISION_MODELS - - def _initialize_memory(self) -> None: - """Initialize memory with default context.""" - try: - contexts = [ - ("ctx1", "I am an AI assistant that can help with various tasks."), - ("ctx2", f"I am using the {self.model} model."), - ( - "ctx3", - "I have access to tools and skills for specific operations." - if len(self.skills) > 0 - else "I do not have access to external tools.", - ), - ( - "ctx4", - "I can process images and visual content." - if self._supports_vision - else "I cannot process visual content.", - ), - ] - if self.memory: # type: ignore[has-type] - for doc_id, text in contexts: - self.memory.add_vector(doc_id, text) # type: ignore[has-type] - except Exception as e: - logger.warning(f"Failed to initialize memory: {e}") - - async def _process_query_async(self, agent_msg: AgentMessage) -> AgentResponse: - """Process query asynchronously and return AgentResponse.""" - query_text = agent_msg.get_combined_text() - logger.info(f"Processing query: {query_text}") - - # Get RAG context - rag_context = self._get_rag_context(query_text) - - # Check if trying to use images with non-vision model - if agent_msg.has_images() and not self._supports_vision: - logger.warning(f"Model {self.model} does not support vision. Ignoring image input.") - # Clear images from message - agent_msg.images.clear() - - # Build messages - pass AgentMessage directly - messages = self._build_messages(agent_msg, rag_context) - - # Get tools if available - tools = self.skills.get_tools() if len(self.skills) > 0 else None - - # Debug logging before gateway call - logger.debug("=== Gateway Request ===") - logger.debug(f"Model: {self.model}") - logger.debug(f"Number of messages: {len(messages)}") - for i, msg in enumerate(messages): - role = msg.get("role", "unknown") - content = msg.get("content", "") - if isinstance(content, str): - content_preview = content[:100] - elif isinstance(content, list): - content_preview = f"[{len(content)} content blocks]" - else: - content_preview = str(content)[:100] - logger.debug(f" Message {i}: role={role}, content={content_preview}...") - logger.debug(f"Tools available: {len(tools) if tools else 0}") - logger.debug("======================") - - # Prepare inference parameters - inference_params = { - "model": self.model, - "messages": messages, - "tools": tools, - "temperature": self.temperature, - "max_tokens": self.max_tokens, - "stream": False, - } - - # Add seed if provided - if self.seed is not None: - inference_params["seed"] = self.seed - - # Make inference call - response = await self.gateway.ainference(**inference_params) # type: ignore[arg-type] - - # Extract response - message = response["choices"][0]["message"] # type: ignore[index] - content = message.get("content", "") - - # Don't update history yet - wait until we have the complete interaction - # This follows Claude's pattern of locking history until tool execution is complete - - # Check for tool calls - tool_calls = None - if message.get("tool_calls"): - tool_calls = [ - ToolCall( - id=tc["id"], - name=tc["function"]["name"], - arguments=json.loads(tc["function"]["arguments"]), - status="pending", - ) - for tc in message["tool_calls"] - ] - - # Get the user message for history - user_message = messages[-1] - - # Handle tool calls (blocking by default) - final_content = await self._handle_tool_calls(tool_calls, messages, user_message) - - # Return response with tool information - return AgentResponse( - content=final_content, - role="assistant", - tool_calls=tool_calls, - requires_follow_up=False, # Already handled - metadata={"model": self.model}, - ) - else: - # No tools, add both user and assistant messages to history - # Get the user message content from the built message - user_msg = messages[-1] # Last message in messages is the user message - user_content = user_msg["content"] - - # Add to conversation history - logger.info("=== Adding to history (no tools) ===") - logger.info(f" Adding user message: {str(user_content)[:100]}...") - self.conversation.add_user_message(user_content) - logger.info(f" Adding assistant response: {content[:100]}...") - self.conversation.add_assistant_message(content) - logger.info(f" History size now: {self.conversation.size()}") - - return AgentResponse( - content=content, - role="assistant", - tool_calls=None, - requires_follow_up=False, - metadata={"model": self.model}, - ) - - def _get_rag_context(self, query: str) -> str: - """Get relevant context from memory.""" - if not self.memory: # type: ignore[has-type] - return "" - - try: - results = self.memory.query( # type: ignore[has-type] - query_texts=query, n_results=self.rag_n, similarity_threshold=self.rag_threshold - ) - - if results: - contexts = [doc.page_content for doc, _ in results] - return " | ".join(contexts) - except Exception as e: - logger.warning(f"RAG query failed: {e}") - - return "" - - def _build_messages( - self, agent_msg: AgentMessage, rag_context: str = "" - ) -> list[dict[str, Any]]: - """Build messages list from AgentMessage.""" - messages = [] - - # System prompt with RAG context if available - system_content = self.system_prompt - if rag_context: - system_content += f"\n\nRelevant context: {rag_context}" - messages.append({"role": "system", "content": system_content}) - - # Add conversation history in OpenAI format - history_messages = self.conversation.to_openai_format() - messages.extend(history_messages) - - # Debug history state - logger.info(f"=== Building messages with {len(history_messages)} history messages ===") - if history_messages: - for i, msg in enumerate(history_messages): - role = msg.get("role", "unknown") - content = msg.get("content", "") - if isinstance(content, str): - preview = content[:100] - elif isinstance(content, list): - preview = f"[{len(content)} content blocks]" - else: - preview = str(content)[:100] - logger.info(f" History[{i}]: role={role}, content={preview}") - - # Build user message content from AgentMessage - user_content = agent_msg.get_combined_text() if agent_msg.has_text() else "" - - # Handle images for vision models - if agent_msg.has_images() and self._supports_vision: - # Build content array with text and images - content = [] - if user_content: # Only add text if not empty - content.append({"type": "text", "text": user_content}) - - # Add all images from AgentMessage - for img in agent_msg.images: - content.append( - { - "type": "image_url", - "image_url": {"url": f"data:image/jpeg;base64,{img.base64_jpeg}"}, - } - ) - - logger.debug(f"Building message with {len(content)} content items (vision enabled)") - messages.append({"role": "user", "content": content}) # type: ignore[dict-item] - else: - # Text-only message - messages.append({"role": "user", "content": user_content}) - - return messages - - async def _handle_tool_calls( - self, - tool_calls: list[ToolCall], - messages: list[dict[str, Any]], - user_message: dict[str, Any], - ) -> str: - """Handle tool calls from LLM (blocking mode by default).""" - try: - # Build assistant message with tool calls - assistant_msg = { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": tc.id, - "type": "function", - "function": {"name": tc.name, "arguments": json.dumps(tc.arguments)}, - } - for tc in tool_calls - ], - } - messages.append(assistant_msg) - - # Execute tools and collect results - tool_results = [] - for tool_call in tool_calls: - logger.info(f"Executing tool: {tool_call.name}") - - try: - # Execute the tool - result = self.skills.call(tool_call.name, **tool_call.arguments) - tool_call.status = "completed" - - # Format tool result message - tool_result = { - "role": "tool", - "tool_call_id": tool_call.id, - "content": str(result), - "name": tool_call.name, - } - tool_results.append(tool_result) - - except Exception as e: - logger.error(f"Tool execution failed: {e}") - tool_call.status = "failed" - - # Add error result - tool_result = { - "role": "tool", - "tool_call_id": tool_call.id, - "content": f"Error: {e!s}", - "name": tool_call.name, - } - tool_results.append(tool_result) - - # Add tool results to messages - messages.extend(tool_results) - - # Prepare follow-up inference parameters - followup_params = { - "model": self.model, - "messages": messages, - "temperature": self.temperature, - "max_tokens": self.max_tokens, - } - - # Add seed if provided - if self.seed is not None: - followup_params["seed"] = self.seed - - # Get follow-up response - response = await self.gateway.ainference(**followup_params) # type: ignore[arg-type] - - # Extract final response - final_message = response["choices"][0]["message"] # type: ignore[index] - - # Now add all messages to history in order (like Claude does) - # Add user message - user_content = user_message["content"] - self.conversation.add_user_message(user_content) - - # Add assistant message with tool calls - self.conversation.add_assistant_message("", tool_calls) - - # Add tool results - for result in tool_results: - self.conversation.add_tool_result( - tool_call_id=result["tool_call_id"], content=result["content"] - ) - - # Add final assistant response - final_content = final_message.get("content", "") - self.conversation.add_assistant_message(final_content) - - return final_message.get("content", "") # type: ignore[no-any-return] - - except Exception as e: - logger.error(f"Error handling tool calls: {e}") - return f"Error executing tools: {e!s}" - - def query(self, message: str | AgentMessage) -> AgentResponse: - """Synchronous query method for direct usage. - - Args: - message: Either a string query or an AgentMessage with text and/or images - - Returns: - AgentResponse object with content and tool information - """ - # Convert string to AgentMessage if needed - if isinstance(message, str): - agent_msg = AgentMessage() - agent_msg.add_text(message) - else: - agent_msg = message - - # Run async method in a new event loop - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - return loop.run_until_complete(self._process_query_async(agent_msg)) - finally: - loop.close() - - async def aquery(self, message: str | AgentMessage) -> AgentResponse: - """Asynchronous query method. - - Args: - message: Either a string query or an AgentMessage with text and/or images - - Returns: - AgentResponse object with content and tool information - """ - # Convert string to AgentMessage if needed - if isinstance(message, str): - agent_msg = AgentMessage() - agent_msg.add_text(message) - else: - agent_msg = message - - return await self._process_query_async(agent_msg) - - def base_agent_dispose(self) -> None: - """Dispose of all resources and close gateway.""" - self.response_subject.on_completed() - if self._executor: - self._executor.shutdown(wait=False) - if self.gateway: - self.gateway.close() diff --git a/dimos/agents_deprecated/modules/base_agent.py b/dimos/agents_deprecated/modules/base_agent.py deleted file mode 100644 index efe81fd90b..0000000000 --- a/dimos/agents_deprecated/modules/base_agent.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base agent module that wraps BaseAgent for DimOS module usage.""" - -import threading -from typing import Any - -from dimos.agents_deprecated.agent_message import AgentMessage -from dimos.agents_deprecated.agent_types import AgentResponse -from dimos.agents_deprecated.memory.base import AbstractAgentSemanticMemory -from dimos.core import In, Module, Out, rpc -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -try: - from .base import BaseAgent -except ImportError: - from dimos.agents_deprecated.modules.base import BaseAgent - -logger = setup_logger() - - -class BaseAgentModule(BaseAgent, Module): # type: ignore[misc] - """Agent module that inherits from BaseAgent and adds DimOS module interface. - - This provides a thin wrapper around BaseAgent functionality, exposing it - through the DimOS module system with RPC methods and stream I/O. - """ - - # Module I/O - AgentMessage based communication - message_in: In[AgentMessage] # Primary input for AgentMessage - response_out: Out[AgentResponse] # Output AgentResponse objects - - def __init__( # type: ignore[no-untyped-def] - self, - model: str = "openai::gpt-4o-mini", - system_prompt: str | None = None, - skills: SkillLibrary | list[AbstractSkill] | AbstractSkill | None = None, - memory: AbstractAgentSemanticMemory | None = None, - temperature: float = 0.0, - max_tokens: int = 4096, - max_input_tokens: int = 128000, - max_history: int = 20, - rag_n: int = 4, - rag_threshold: float = 0.45, - process_all_inputs: bool = False, - **kwargs, - ) -> None: - """Initialize the agent module. - - Args: - model: Model identifier (e.g., "openai::gpt-4o", "anthropic::claude-3-haiku") - system_prompt: System prompt for the agent - skills: Skills/tools available to the agent - memory: Semantic memory system for RAG - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - max_input_tokens: Maximum input tokens - max_history: Maximum conversation history to keep - rag_n: Number of RAG results to fetch - rag_threshold: Minimum similarity for RAG results - process_all_inputs: Whether to process all inputs or drop when busy - **kwargs: Additional arguments passed to Module - """ - # Initialize Module first (important for DimOS) - Module.__init__(self, **kwargs) - - # Initialize BaseAgent with all functionality - BaseAgent.__init__( - self, - model=model, - system_prompt=system_prompt, - skills=skills, - memory=memory, - temperature=temperature, - max_tokens=max_tokens, - max_input_tokens=max_input_tokens, - max_history=max_history, - rag_n=rag_n, - rag_threshold=rag_threshold, - process_all_inputs=process_all_inputs, - # Don't pass streams - we'll connect them in start() - input_query_stream=None, - input_data_stream=None, - input_video_stream=None, - ) - - # Track module-specific subscriptions - self._module_disposables = [] # type: ignore[var-annotated] - - # For legacy stream support - self._latest_image = None - self._latest_data = None - self._image_lock = threading.Lock() - self._data_lock = threading.Lock() - - @rpc - def start(self) -> None: - """Start the agent module and connect streams.""" - super().start() - logger.info(f"Starting agent module with model: {self.model}") - - # Primary AgentMessage input - if self.message_in and self.message_in.connection is not None: - try: - disposable = self.message_in.observable().subscribe( # type: ignore[no-untyped-call] - lambda msg: self._handle_agent_message(msg) - ) - self._module_disposables.append(disposable) - except Exception as e: - logger.debug(f"Could not connect message_in: {e}") - - # Connect response output - if self.response_out: - disposable = self.response_subject.subscribe( - lambda response: self.response_out.publish(response) - ) - self._module_disposables.append(disposable) - - logger.info("Agent module started") - - @rpc - def stop(self) -> None: - """Stop the agent module.""" - logger.info("Stopping agent module") - - # Dispose module subscriptions - for disposable in self._module_disposables: - disposable.dispose() - self._module_disposables.clear() - - # Dispose BaseAgent resources - self.base_agent_dispose() - - logger.info("Agent module stopped") - super().stop() - - @rpc - def clear_history(self) -> None: - """Clear conversation history.""" - with self._history_lock: # type: ignore[attr-defined] - self.history = [] # type: ignore[var-annotated] - logger.info("Conversation history cleared") - - @rpc - def add_skill(self, skill: AbstractSkill) -> None: - """Add a skill to the agent.""" - self.skills.add(skill) - logger.info(f"Added skill: {skill.__class__.__name__}") - - @rpc - def set_system_prompt(self, prompt: str) -> None: - """Update system prompt.""" - self.system_prompt = prompt - logger.info("System prompt updated") - - @rpc - def get_conversation_history(self) -> list[dict[str, Any]]: - """Get current conversation history.""" - with self._history_lock: # type: ignore[attr-defined] - return self.history.copy() - - def _handle_agent_message(self, message: AgentMessage) -> None: - """Handle AgentMessage from module input.""" - # Process through BaseAgent query method - try: - response = self.query(message) - logger.debug(f"Publishing response: {response}") - self.response_subject.on_next(response) - except Exception as e: - logger.error(f"Agent message processing error: {e}") - self.response_subject.on_error(e) - - def _handle_module_query(self, query: str) -> None: - """Handle legacy query from module input.""" - # For simple text queries, just convert to AgentMessage - agent_msg = AgentMessage() - agent_msg.add_text(query) - - # Process through unified handler - self._handle_agent_message(agent_msg) - - def _update_latest_data(self, data: dict[str, Any]) -> None: - """Update latest data context.""" - with self._data_lock: - self._latest_data = data # type: ignore[assignment] - - def _update_latest_image(self, img: Any) -> None: - """Update latest image.""" - with self._image_lock: - self._latest_image = img - - def _format_data_context(self, data: dict[str, Any]) -> str: - """Format data dictionary as context string.""" - # Simple formatting - can be customized - parts = [] - for key, value in data.items(): - parts.append(f"{key}: {value}") - return "\n".join(parts) diff --git a/dimos/agents_deprecated/modules/gateway/__init__.py b/dimos/agents_deprecated/modules/gateway/__init__.py deleted file mode 100644 index 58ed40cd95..0000000000 --- a/dimos/agents_deprecated/modules/gateway/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Gateway module for unified LLM access.""" - -from .client import UnifiedGatewayClient -from .utils import convert_tools_to_standard_format, parse_streaming_response - -__all__ = ["UnifiedGatewayClient", "convert_tools_to_standard_format", "parse_streaming_response"] diff --git a/dimos/agents_deprecated/modules/gateway/client.py b/dimos/agents_deprecated/modules/gateway/client.py deleted file mode 100644 index 772ca445aa..0000000000 --- a/dimos/agents_deprecated/modules/gateway/client.py +++ /dev/null @@ -1,212 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unified gateway client for LLM access.""" - -import asyncio -from collections.abc import AsyncIterator, Iterator -import logging -import os -from types import TracebackType -from typing import Any - -import httpx -from tenacity import retry, stop_after_attempt, wait_exponential - -from .tensorzero_embedded import TensorZeroEmbeddedGateway - -logger = logging.getLogger(__name__) - - -class UnifiedGatewayClient: - """Clean abstraction over TensorZero or other gateways. - - This client provides a unified interface for accessing multiple LLM providers - through a gateway service, with support for streaming, tools, and async operations. - """ - - def __init__( - self, gateway_url: str | None = None, timeout: float = 60.0, use_simple: bool = False - ) -> None: - """Initialize the gateway client. - - Args: - gateway_url: URL of the gateway service. Defaults to env var or localhost - timeout: Request timeout in seconds - use_simple: Deprecated parameter, always uses TensorZero - """ - self.gateway_url = gateway_url or os.getenv( - "TENSORZERO_GATEWAY_URL", "http://localhost:3000" - ) - self.timeout = timeout - self._client = None - self._async_client = None - self._aclose_task: asyncio.Task[None] | None = None - - # Always use TensorZero embedded gateway - try: - self._tensorzero_client = TensorZeroEmbeddedGateway() - logger.info("Using TensorZero embedded gateway") - except Exception as e: - logger.error(f"Failed to initialize TensorZero: {e}") - raise - - def _get_client(self) -> httpx.Client: - """Get or create sync HTTP client.""" - if self._client is None: - self._client = httpx.Client( # type: ignore[assignment] - base_url=self.gateway_url, # type: ignore[arg-type] - timeout=self.timeout, - headers={"Content-Type": "application/json"}, - ) - return self._client # type: ignore[return-value] - - def _get_async_client(self) -> httpx.AsyncClient: - """Get or create async HTTP client.""" - if self._async_client is None: - self._async_client = httpx.AsyncClient( # type: ignore[assignment] - base_url=self.gateway_url, # type: ignore[arg-type] - timeout=self.timeout, - headers={"Content-Type": "application/json"}, - ) - return self._async_client # type: ignore[return-value] - - @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) - def inference( # type: ignore[no-untyped-def] - self, - model: str, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None = None, - temperature: float = 0.0, - max_tokens: int | None = None, - stream: bool = False, - **kwargs, - ) -> dict[str, Any] | Iterator[dict[str, Any]]: - """Synchronous inference call. - - Args: - model: Model identifier (e.g., "openai::gpt-4o") - messages: List of message dicts with role and content - tools: Optional list of tools in standard format - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - stream: Whether to stream the response - **kwargs: Additional model-specific parameters - - Returns: - Response dict or iterator of response chunks if streaming - """ - return self._tensorzero_client.inference( - model=model, - messages=messages, - tools=tools, - temperature=temperature, - max_tokens=max_tokens, - stream=stream, - **kwargs, - ) - - @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=10)) - async def ainference( # type: ignore[no-untyped-def] - self, - model: str, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None = None, - temperature: float = 0.0, - max_tokens: int | None = None, - stream: bool = False, - **kwargs, - ) -> dict[str, Any] | AsyncIterator[dict[str, Any]]: - """Asynchronous inference call. - - Args: - model: Model identifier (e.g., "anthropic::claude-3-7-sonnet") - messages: List of message dicts with role and content - tools: Optional list of tools in standard format - temperature: Sampling temperature - max_tokens: Maximum tokens to generate - stream: Whether to stream the response - **kwargs: Additional model-specific parameters - - Returns: - Response dict or async iterator of response chunks if streaming - """ - return await self._tensorzero_client.ainference( - model=model, - messages=messages, - tools=tools, - temperature=temperature, - max_tokens=max_tokens, - stream=stream, - **kwargs, - ) - - def close(self) -> None: - """Close the HTTP clients.""" - if self._client: - self._client.close() - self._client = None - if self._async_client: - # This needs to be awaited in an async context - # We'll handle this in __del__ with asyncio - pass - self._tensorzero_client.close() - - async def aclose(self) -> None: - """Async close method.""" - if self._async_client: - await self._async_client.aclose() - self._async_client = None - await self._tensorzero_client.aclose() - - def __del__(self) -> None: - """Cleanup on deletion.""" - self.close() - if self._async_client: - # Try to close async client if event loop is available - try: - loop = asyncio.get_event_loop() - if loop.is_running(): - self._aclose_task = loop.create_task(self.aclose()) - else: - loop.run_until_complete(self.aclose()) - except RuntimeError: - # No event loop, just let it be garbage collected - pass - - def __enter__(self): # type: ignore[no-untyped-def] - """Context manager entry.""" - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Context manager exit.""" - self.close() - - async def __aenter__(self): # type: ignore[no-untyped-def] - """Async context manager entry.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Async context manager exit.""" - await self.aclose() diff --git a/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py b/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py deleted file mode 100644 index 4708788241..0000000000 --- a/dimos/agents_deprecated/modules/gateway/tensorzero_embedded.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""TensorZero embedded gateway client with correct config format.""" - -from collections.abc import AsyncIterator, Iterator -import logging -from pathlib import Path -from typing import Any - -logger = logging.getLogger(__name__) - - -class TensorZeroEmbeddedGateway: - """TensorZero embedded gateway using patch_openai_client.""" - - def __init__(self) -> None: - """Initialize TensorZero embedded gateway.""" - self._client = None - self._config_path = None - self._setup_config() - self._initialize_client() # type: ignore[no-untyped-call] - - def _setup_config(self) -> None: - """Create TensorZero configuration with correct format.""" - config_dir = Path("/tmp/tensorzero_embedded") - config_dir.mkdir(exist_ok=True) - self._config_path = config_dir / "tensorzero.toml" # type: ignore[assignment] - - # Create config using the correct format from working example - config_content = """ -# OpenAI Models -[models.gpt_4o_mini] -routing = ["openai"] - -[models.gpt_4o_mini.providers.openai] -type = "openai" -model_name = "gpt-4o-mini" - -[models.gpt_4o] -routing = ["openai"] - -[models.gpt_4o.providers.openai] -type = "openai" -model_name = "gpt-4o" - -# Claude Models -[models.claude_3_haiku] -routing = ["anthropic"] - -[models.claude_3_haiku.providers.anthropic] -type = "anthropic" -model_name = "claude-3-haiku-20240307" - -[models.claude_3_sonnet] -routing = ["anthropic"] - -[models.claude_3_sonnet.providers.anthropic] -type = "anthropic" -model_name = "claude-3-5-sonnet-20241022" - -[models.claude_3_opus] -routing = ["anthropic"] - -[models.claude_3_opus.providers.anthropic] -type = "anthropic" -model_name = "claude-3-opus-20240229" - -# Cerebras Models - disabled for CI (no API key) -# [models.llama_3_3_70b] -# routing = ["cerebras"] -# -# [models.llama_3_3_70b.providers.cerebras] -# type = "openai" -# model_name = "llama-3.3-70b" -# api_base = "https://api.cerebras.ai/v1" -# api_key_location = "env::CEREBRAS_API_KEY" - -# Qwen Models -[models.qwen_plus] -routing = ["qwen"] - -[models.qwen_plus.providers.qwen] -type = "openai" -model_name = "qwen-plus" -api_base = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" -api_key_location = "env::ALIBABA_API_KEY" - -[models.qwen_vl_plus] -routing = ["qwen"] - -[models.qwen_vl_plus.providers.qwen] -type = "openai" -model_name = "qwen-vl-plus" -api_base = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" -api_key_location = "env::ALIBABA_API_KEY" - -# Object storage - disable for embedded mode -[object_storage] -type = "disabled" - -# Single chat function with all models -# TensorZero will automatically skip models that don't support the input type -[functions.chat] -type = "chat" - -[functions.chat.variants.openai] -type = "chat_completion" -model = "gpt_4o_mini" -weight = 1.0 - -[functions.chat.variants.claude] -type = "chat_completion" -model = "claude_3_haiku" -weight = 0.5 - -# Cerebras disabled for CI (no API key) -# [functions.chat.variants.cerebras] -# type = "chat_completion" -# model = "llama_3_3_70b" -# weight = 0.0 - -[functions.chat.variants.qwen] -type = "chat_completion" -model = "qwen_plus" -weight = 0.3 - -# For vision queries, Qwen VL can be used -[functions.chat.variants.qwen_vision] -type = "chat_completion" -model = "qwen_vl_plus" -weight = 0.4 -""" - - with open(self._config_path, "w") as f: # type: ignore[call-overload] - f.write(config_content) - - logger.info(f"Created TensorZero config at {self._config_path}") - - def _initialize_client(self): # type: ignore[no-untyped-def] - """Initialize OpenAI client with TensorZero patch.""" - try: - from openai import OpenAI - from tensorzero import patch_openai_client - - self._client = OpenAI() # type: ignore[assignment] - - # Patch with TensorZero embedded gateway - patch_openai_client( - self._client, - clickhouse_url=None, # In-memory storage - config_file=str(self._config_path), - async_setup=False, - ) - - logger.info("TensorZero embedded gateway initialized successfully") - - except Exception as e: - logger.error(f"Failed to initialize TensorZero: {e}") - raise - - def _map_model_to_tensorzero(self, model: str) -> str: - """Map provider::model format to TensorZero function format.""" - # Always use the chat function - TensorZero will handle model selection - # based on input type and model capabilities automatically - return "tensorzero::function_name::chat" - - def inference( # type: ignore[no-untyped-def] - self, - model: str, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None = None, - temperature: float = 0.0, - max_tokens: int | None = None, - stream: bool = False, - **kwargs, - ) -> dict[str, Any] | Iterator[dict[str, Any]]: - """Synchronous inference call through TensorZero.""" - - # Map model to TensorZero function - tz_model = self._map_model_to_tensorzero(model) - - # Prepare parameters - params = { - "model": tz_model, - "messages": messages, - "temperature": temperature, - } - - if max_tokens: - params["max_tokens"] = max_tokens - - if tools: - params["tools"] = tools - - if stream: - params["stream"] = True - - # Add any extra kwargs - params.update(kwargs) - - try: - # Make the call through patched client - if stream: - # Return streaming iterator - stream_response = self._client.chat.completions.create(**params) # type: ignore[attr-defined] - - def stream_generator(): # type: ignore[no-untyped-def] - for chunk in stream_response: - yield chunk.model_dump() - - return stream_generator() # type: ignore[no-any-return, no-untyped-call] - else: - response = self._client.chat.completions.create(**params) # type: ignore[attr-defined] - return response.model_dump() # type: ignore[no-any-return] - - except Exception as e: - logger.error(f"TensorZero inference failed: {e}") - raise - - async def ainference( # type: ignore[no-untyped-def] - self, - model: str, - messages: list[dict[str, Any]], - tools: list[dict[str, Any]] | None = None, - temperature: float = 0.0, - max_tokens: int | None = None, - stream: bool = False, - **kwargs, - ) -> dict[str, Any] | AsyncIterator[dict[str, Any]]: - """Async inference with streaming support.""" - import asyncio - - loop = asyncio.get_event_loop() - - if stream: - # Create async generator from sync streaming - async def stream_generator(): # type: ignore[no-untyped-def] - # Run sync streaming in executor - sync_stream = await loop.run_in_executor( - None, - lambda: self.inference( - model, messages, tools, temperature, max_tokens, stream=True, **kwargs - ), - ) - - # Convert sync iterator to async - for chunk in sync_stream: - yield chunk - - return stream_generator() # type: ignore[no-any-return, no-untyped-call] - else: - result = await loop.run_in_executor( - None, - lambda: self.inference( - model, messages, tools, temperature, max_tokens, stream, **kwargs - ), - ) - return result # type: ignore[return-value] - - def close(self) -> None: - """Close the client.""" - # TensorZero embedded doesn't need explicit cleanup - pass - - async def aclose(self) -> None: - """Async close.""" - # TensorZero embedded doesn't need explicit cleanup - pass diff --git a/dimos/agents_deprecated/modules/gateway/tensorzero_simple.py b/dimos/agents_deprecated/modules/gateway/tensorzero_simple.py deleted file mode 100644 index 4c9dbe4e26..0000000000 --- a/dimos/agents_deprecated/modules/gateway/tensorzero_simple.py +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Minimal TensorZero test to get it working.""" - -from pathlib import Path - -from dotenv import load_dotenv -from openai import OpenAI -from tensorzero import patch_openai_client - -load_dotenv() - -# Create minimal config -config_dir = Path("/tmp/tz_test") -config_dir.mkdir(exist_ok=True) -config_path = config_dir / "tensorzero.toml" - -# Minimal config based on TensorZero docs -config = """ -[models.gpt_4o_mini] -routing = ["openai"] - -[models.gpt_4o_mini.providers.openai] -type = "openai" -model_name = "gpt-4o-mini" - -[functions.my_function] -type = "chat" - -[functions.my_function.variants.my_variant] -type = "chat_completion" -model = "gpt_4o_mini" -""" - -with open(config_path, "w") as f: - f.write(config) - -print(f"Created config at {config_path}") - -# Create OpenAI client -client = OpenAI() - -# Patch with TensorZero -try: - patch_openai_client( - client, - clickhouse_url=None, # In-memory - config_file=str(config_path), - async_setup=False, - ) - print("✅ TensorZero initialized successfully!") -except Exception as e: - print(f"❌ Failed to initialize TensorZero: {e}") - exit(1) - -# Test basic inference -print("\nTesting basic inference...") -try: - response = client.chat.completions.create( - model="tensorzero::function_name::my_function", - messages=[{"role": "user", "content": "What is 2+2?"}], - temperature=0.0, - max_tokens=10, - ) - - content = response.choices[0].message.content - print(f"Response: {content}") - print("✅ Basic inference worked!") - -except Exception as e: - print(f"❌ Basic inference failed: {e}") - import traceback - - traceback.print_exc() - -print("\nTesting streaming...") -try: - stream = client.chat.completions.create( - model="tensorzero::function_name::my_function", - messages=[{"role": "user", "content": "Count from 1 to 3"}], - temperature=0.0, - max_tokens=20, - stream=True, - ) - - print("Stream response: ", end="", flush=True) - for chunk in stream: - if chunk.choices[0].delta.content: - print(chunk.choices[0].delta.content, end="", flush=True) - print("\n✅ Streaming worked!") - -except Exception as e: - print(f"\n❌ Streaming failed: {e}") diff --git a/dimos/agents_deprecated/modules/gateway/utils.py b/dimos/agents_deprecated/modules/gateway/utils.py deleted file mode 100644 index 526d3b9724..0000000000 --- a/dimos/agents_deprecated/modules/gateway/utils.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utility functions for gateway operations.""" - -import logging -from typing import Any - -logger = logging.getLogger(__name__) - - -def convert_tools_to_standard_format(tools: list[dict[str, Any]]) -> list[dict[str, Any]]: - """Convert DimOS tool format to standard format accepted by gateways. - - DimOS tools come from pydantic_function_tool and have this format: - { - "type": "function", - "function": { - "name": "tool_name", - "description": "tool description", - "parameters": { - "type": "object", - "properties": {...}, - "required": [...] - } - } - } - - We keep this format as it's already standard JSON Schema format. - """ - if not tools: - return [] - - # Tools are already in the correct format from pydantic_function_tool - return tools - - -def parse_streaming_response(chunk: dict[str, Any]) -> dict[str, Any]: - """Parse a streaming response chunk into a standard format. - - Args: - chunk: Raw chunk from the gateway - - Returns: - Parsed chunk with standard fields: - - type: "content" | "tool_call" | "error" | "done" - - content: The actual content (text for content type, tool info for tool_call) - - metadata: Additional information - """ - # Handle TensorZero streaming format - if "choices" in chunk: - # OpenAI-style format from TensorZero - choice = chunk["choices"][0] if chunk["choices"] else {} - delta = choice.get("delta", {}) - - if "content" in delta: - return { - "type": "content", - "content": delta["content"], - "metadata": {"index": choice.get("index", 0)}, - } - elif "tool_calls" in delta: - tool_calls = delta["tool_calls"] - if tool_calls: - tool_call = tool_calls[0] - return { - "type": "tool_call", - "content": { - "id": tool_call.get("id"), - "name": tool_call.get("function", {}).get("name"), - "arguments": tool_call.get("function", {}).get("arguments", ""), - }, - "metadata": {"index": tool_call.get("index", 0)}, - } - elif choice.get("finish_reason"): - return { - "type": "done", - "content": None, - "metadata": {"finish_reason": choice["finish_reason"]}, - } - - # Handle direct content chunks - if isinstance(chunk, str): - return {"type": "content", "content": chunk, "metadata": {}} - - # Handle error responses - if "error" in chunk: - return {"type": "error", "content": chunk["error"], "metadata": chunk} - - # Default fallback - return {"type": "unknown", "content": chunk, "metadata": {}} - - -def create_tool_response(tool_id: str, result: Any, is_error: bool = False) -> dict[str, Any]: - """Create a properly formatted tool response. - - Args: - tool_id: The ID of the tool call - result: The result from executing the tool - is_error: Whether this is an error response - - Returns: - Formatted tool response message - """ - content = str(result) if not isinstance(result, str) else result - - return { - "role": "tool", - "tool_call_id": tool_id, - "content": content, - "name": None, # Will be filled by the calling code - } - - -def extract_image_from_message(message: dict[str, Any]) -> dict[str, Any] | None: - """Extract image data from a message if present. - - Args: - message: Message dict that may contain image data - - Returns: - Dict with image data and metadata, or None if no image - """ - content = message.get("content", []) - - # Handle list content (multimodal) - if isinstance(content, list): - for item in content: - if isinstance(item, dict): - # OpenAI format - if item.get("type") == "image_url": - return { - "format": "openai", - "data": item["image_url"]["url"], - "detail": item["image_url"].get("detail", "auto"), - } - # Anthropic format - elif item.get("type") == "image": - return { - "format": "anthropic", - "data": item["source"]["data"], - "media_type": item["source"].get("media_type", "image/jpeg"), - } - - return None diff --git a/dimos/agents_deprecated/prompt_builder/__init__.py b/dimos/agents_deprecated/prompt_builder/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/agents_deprecated/prompt_builder/impl.py b/dimos/agents_deprecated/prompt_builder/impl.py deleted file mode 100644 index 35c864062a..0000000000 --- a/dimos/agents_deprecated/prompt_builder/impl.py +++ /dev/null @@ -1,224 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from textwrap import dedent - -from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer -from dimos.agents_deprecated.tokenizer.openai_tokenizer import OpenAITokenizer - -# TODO: Make class more generic when implementing other tokenizers. Presently its OpenAI specific. -# TODO: Build out testing and logging - - -class PromptBuilder: - DEFAULT_SYSTEM_PROMPT = dedent(""" - You are an AI assistant capable of understanding and analyzing both visual and textual information. - Your task is to provide accurate and insightful responses based on the data provided to you. - Use the following information to assist the user with their query. Do not rely on any internal - knowledge or make assumptions beyond the provided data. - - Visual Context: You may have been given an image to analyze. Use the visual details to enhance your response. - Textual Context: There may be some text retrieved from a relevant database to assist you - - Instructions: - - Combine insights from both the image and the text to answer the user's question. - - If the information is insufficient to provide a complete answer, acknowledge the limitation. - - Maintain a professional and informative tone in your response. - """) - - def __init__( - self, - model_name: str = "gpt-4o", - max_tokens: int = 128000, - tokenizer: AbstractTokenizer | None = None, - ) -> None: - """ - Initialize the prompt builder. - Args: - model_name (str): Model used (e.g., 'gpt-4o', 'gpt-4', 'gpt-3.5-turbo'). - max_tokens (int): Maximum tokens allowed in the input prompt. - tokenizer (AbstractTokenizer): The tokenizer to use for token counting and truncation. - """ - self.model_name = model_name - self.max_tokens = max_tokens - self.tokenizer: AbstractTokenizer = tokenizer or OpenAITokenizer(model_name=self.model_name) - - def truncate_tokens(self, text: str, max_tokens, strategy): # type: ignore[no-untyped-def] - """ - Truncate text to fit within max_tokens using a specified strategy. - Args: - text (str): Input text to truncate. - max_tokens (int): Maximum tokens allowed. - strategy (str): Truncation strategy ('truncate_head', 'truncate_middle', 'truncate_end', 'do_not_truncate'). - Returns: - str: Truncated text. - """ - if strategy == "do_not_truncate" or not text: - return text - - tokens = self.tokenizer.tokenize_text(text) - if len(tokens) <= max_tokens: - return text - - if strategy == "truncate_head": - truncated = tokens[-max_tokens:] - elif strategy == "truncate_end": - truncated = tokens[:max_tokens] - elif strategy == "truncate_middle": - half = max_tokens // 2 - truncated = tokens[:half] + tokens[-half:] - else: - raise ValueError(f"Unknown truncation strategy: {strategy}") - - return self.tokenizer.detokenize_text(truncated) # type: ignore[no-untyped-call] - - def build( # type: ignore[no-untyped-def] - self, - system_prompt=None, - user_query=None, - base64_image=None, - image_width=None, - image_height=None, - image_detail: str = "low", - rag_context=None, - budgets=None, - policies=None, - override_token_limit: bool = False, - ): - """ - Builds a dynamic prompt tailored to token limits, respecting budgets and policies. - - Args: - system_prompt (str): System-level instructions. - user_query (str, optional): User's query. - base64_image (str, optional): Base64-encoded image string. - image_width (int, optional): Width of the image. - image_height (int, optional): Height of the image. - image_detail (str, optional): Detail level for the image ("low" or "high"). - rag_context (str, optional): Retrieved context. - budgets (dict, optional): Token budgets for each input type. Defaults to equal allocation. - policies (dict, optional): Truncation policies for each input type. - override_token_limit (bool, optional): Whether to override the token limit. Defaults to False. - - Returns: - dict: Messages array ready to send to the OpenAI API. - """ - if user_query is None: - raise ValueError("User query is required.") - - # Debug: - # base64_image = None - - budgets = budgets or { - "system_prompt": self.max_tokens // 4, - "user_query": self.max_tokens // 4, - "image": self.max_tokens // 4, - "rag": self.max_tokens // 4, - } - policies = policies or { - "system_prompt": "truncate_end", - "user_query": "truncate_middle", - "image": "do_not_truncate", - "rag": "truncate_end", - } - - # Validate and sanitize image_detail - if image_detail not in {"low", "high"}: - image_detail = "low" # Default to "low" if invalid or None - - # Determine which system prompt to use - if system_prompt is None: - system_prompt = self.DEFAULT_SYSTEM_PROMPT - - rag_context = rag_context or "" - - # Debug: - # print("system_prompt: ", system_prompt) - # print("rag_context: ", rag_context) - - # region Token Counts - if not override_token_limit: - rag_token_cnt = self.tokenizer.token_count(rag_context) - system_prompt_token_cnt = self.tokenizer.token_count(system_prompt) - user_query_token_cnt = self.tokenizer.token_count(user_query) - image_token_cnt = ( - self.tokenizer.image_token_count(image_width, image_height, image_detail) - if base64_image - else 0 - ) - else: - rag_token_cnt = 0 - system_prompt_token_cnt = 0 - user_query_token_cnt = 0 - image_token_cnt = 0 - # endregion Token Counts - - # Create a component dictionary for dynamic allocation - components = { - "system_prompt": {"text": system_prompt, "tokens": system_prompt_token_cnt}, - "user_query": {"text": user_query, "tokens": user_query_token_cnt}, - "image": {"text": None, "tokens": image_token_cnt}, - "rag": {"text": rag_context, "tokens": rag_token_cnt}, - } - - if not override_token_limit: - # Adjust budgets and apply truncation - total_tokens = sum(comp["tokens"] for comp in components.values()) - excess_tokens = total_tokens - self.max_tokens - if excess_tokens > 0: - for key, component in components.items(): - if excess_tokens <= 0: - break - if policies[key] != "do_not_truncate": - max_allowed = max(0, budgets[key] - excess_tokens) - components[key]["text"] = self.truncate_tokens( - component["text"], max_allowed, policies[key] - ) - tokens_after = self.tokenizer.token_count(components[key]["text"]) - excess_tokens -= component["tokens"] - tokens_after - component["tokens"] = tokens_after - - # Build the `messages` structure (OpenAI specific) - messages = [{"role": "system", "content": components["system_prompt"]["text"]}] - - if components["rag"]["text"]: - user_content = [ - { - "type": "text", - "text": f"{components['rag']['text']}\n\n{components['user_query']['text']}", - } - ] - else: - user_content = [{"type": "text", "text": components["user_query"]["text"]}] - - if base64_image: - user_content.append( - { - "type": "image_url", - "image_url": { # type: ignore[dict-item] - "url": f"data:image/jpeg;base64,{base64_image}", - "detail": image_detail, - }, - } - ) - messages.append({"role": "user", "content": user_content}) - - # Debug: - # print("system_prompt: ", system_prompt) - # print("user_query: ", user_query) - # print("user_content: ", user_content) - # print(f"Messages: {messages}") - - return messages diff --git a/dimos/agents_deprecated/tokenizer/__init__.py b/dimos/agents_deprecated/tokenizer/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/agents_deprecated/tokenizer/base.py b/dimos/agents_deprecated/tokenizer/base.py deleted file mode 100644 index 97535bcfaa..0000000000 --- a/dimos/agents_deprecated/tokenizer/base.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -# TODO: Add a class for specific tokenizer exceptions -# TODO: Build out testing and logging -# TODO: Create proper doc strings after multiple tokenizers are implemented - - -class AbstractTokenizer(ABC): - @abstractmethod - def tokenize_text(self, text: str): # type: ignore[no-untyped-def] - pass - - @abstractmethod - def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] - pass - - @abstractmethod - def token_count(self, text: str): # type: ignore[no-untyped-def] - pass - - @abstractmethod - def image_token_count(self, image_width, image_height, image_detail: str = "low"): # type: ignore[no-untyped-def] - pass diff --git a/dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py b/dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py deleted file mode 100644 index ad7d27dc82..0000000000 --- a/dimos/agents_deprecated/tokenizer/huggingface_tokenizer.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from transformers import AutoTokenizer # type: ignore[import-untyped] - -from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer -from dimos.utils.logging_config import setup_logger - - -class HuggingFaceTokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "Qwen/Qwen2.5-0.5B", **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - # Initilize the tokenizer for the huggingface models - self.model_name = model_name - try: - self.tokenizer = AutoTokenizer.from_pretrained(self.model_name) - except Exception as e: - raise ValueError( - f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" - ) - - def tokenize_text(self, text: str): # type: ignore[no-untyped-def] - """ - Tokenize a text string using the openai tokenizer. - """ - return self.tokenizer.encode(text) - - def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] - """ - Detokenize a text string using the openai tokenizer. - """ - try: - return self.tokenizer.decode(tokenized_text, errors="ignore") - except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {e!s}") - - def token_count(self, text: str): # type: ignore[no-untyped-def] - """ - Gets the token count of a text string using the openai tokenizer. - """ - return len(self.tokenize_text(text)) if text else 0 - - @staticmethod - def image_token_count(image_width, image_height, image_detail: str = "high"): # type: ignore[no-untyped-def] - """ - Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. - """ - logger = setup_logger() - - if image_detail == "low": - return 85 - elif image_detail == "high": - # Image dimensions - logger.debug(f"Image Width: {image_width}, Image Height: {image_height}") - if image_width is None or image_height is None: - raise ValueError( - "Image width and height must be provided for high detail image token count calculation." - ) - - # Scale image to fit within 2048 x 2048 - max_dimension = max(image_width, image_height) - if max_dimension > 2048: - scale_factor = 2048 / max_dimension - image_width = int(image_width * scale_factor) - image_height = int(image_height * scale_factor) - - # Scale shortest side to 768px - min_dimension = min(image_width, image_height) - scale_factor = 768 / min_dimension - image_width = int(image_width * scale_factor) - image_height = int(image_height * scale_factor) - - # Calculate number of 512px squares - num_squares = (image_width // 512) * (image_height // 512) - return 170 * num_squares + 85 - else: - raise ValueError("Detail specification of image is not 'low' or 'high'") diff --git a/dimos/agents_deprecated/tokenizer/openai_tokenizer.py b/dimos/agents_deprecated/tokenizer/openai_tokenizer.py deleted file mode 100644 index 876e5ca881..0000000000 --- a/dimos/agents_deprecated/tokenizer/openai_tokenizer.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import tiktoken - -from dimos.agents_deprecated.tokenizer.base import AbstractTokenizer -from dimos.utils.logging_config import setup_logger - - -class OpenAITokenizer(AbstractTokenizer): - def __init__(self, model_name: str = "gpt-4o", **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - # Initilize the tokenizer for the openai set of models - self.model_name = model_name - try: - self.tokenizer = tiktoken.encoding_for_model(self.model_name) - except Exception as e: - raise ValueError( - f"Failed to initialize tokenizer for model {self.model_name}. Error: {e!s}" - ) - - def tokenize_text(self, text: str): # type: ignore[no-untyped-def] - """ - Tokenize a text string using the openai tokenizer. - """ - return self.tokenizer.encode(text) - - def detokenize_text(self, tokenized_text): # type: ignore[no-untyped-def] - """ - Detokenize a text string using the openai tokenizer. - """ - try: - return self.tokenizer.decode(tokenized_text, errors="ignore") - except Exception as e: - raise ValueError(f"Failed to detokenize text. Error: {e!s}") - - def token_count(self, text: str): # type: ignore[no-untyped-def] - """ - Gets the token count of a text string using the openai tokenizer. - """ - return len(self.tokenize_text(text)) if text else 0 - - @staticmethod - def image_token_count(image_width, image_height, image_detail: str = "high"): # type: ignore[no-untyped-def] - """ - Calculate the number of tokens in an image. Low detail is 85 tokens, high detail is 170 tokens per 512x512 square. - """ - logger = setup_logger() - - if image_detail == "low": - return 85 - elif image_detail == "high": - # Image dimensions - logger.debug(f"Image Width: {image_width}, Image Height: {image_height}") - if image_width is None or image_height is None: - raise ValueError( - "Image width and height must be provided for high detail image token count calculation." - ) - - # Scale image to fit within 2048 x 2048 - max_dimension = max(image_width, image_height) - if max_dimension > 2048: - scale_factor = 2048 / max_dimension - image_width = int(image_width * scale_factor) - image_height = int(image_height * scale_factor) - - # Scale shortest side to 768px - min_dimension = min(image_width, image_height) - scale_factor = 768 / min_dimension - image_width = int(image_width * scale_factor) - image_height = int(image_height * scale_factor) - - # Calculate number of 512px squares - num_squares = (image_width // 512) * (image_height // 512) - return 170 * num_squares + 85 - else: - raise ValueError("Detail specification of image is not 'low' or 'high'") diff --git a/dimos/assets/foxglove_dashboards/Overwatch.json b/dimos/assets/foxglove_dashboards/Overwatch.json deleted file mode 100644 index 5a26abbc90..0000000000 --- a/dimos/assets/foxglove_dashboards/Overwatch.json +++ /dev/null @@ -1,522 +0,0 @@ -{ - "_watermark": { - "creator": "DIMOS Navigation", - "author": "bona", - "organization": "Dimensional", - "version": "1.0", - "timestamp": "2026-01-17", - "signature": "dimos-nav-overwatch-layout" - }, - "configById": { - "3D!main": { - "cameraState": { - "perspective": true, - "distance": 11.376001845526945, - "phi": 60.000000000004256, - "thetaOffset": -154.65065502183666, - "targetOffset": [ - -1.3664627921863377, - -0.4507491962036497, - 2.2548288625522925e-16 - ], - "target": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "fovy": 45, - "near": 0.01, - "far": 5000 - }, - "followMode": "follow-none", - "followTf": "map", - "scene": { - "backgroundColor": "#000000", - "enableStats": false, - "syncCamera": false - }, - "transforms": { - "frame:vehicle": { - "visible": true - }, - "frame:map": { - "visible": true - } - }, - "topics": { - "/registered_scan": { - "visible": true, - "colorField": "z", - "colorMode": "colormap", - "flatColor": "#ffffff", - "pointSize": 2, - "decayTime": 1 - }, - "/overall_map": { - "visible": true, - "colorField": "intensity", - "colorMode": "flat", - "flatColor": "#ffffff", - "pointSize": 2, - "decayTime": 0 - }, - "/free_paths": { - "visible": true, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "turbo", - "pointSize": 2 - }, - "/path": { - "visible": true, - "type": "line", - "lineWidth": 0.05, - "color": "#19ff00" - }, - "/way_point": { - "visible": true, - "color": "#cc29cc" - }, - "/navigation_boundary": { - "visible": true, - "color": "#00ff00", - "lineWidth": 0.2 - }, - "/goal_pose": { - "visible": true, - "color": "#ff1900", - "type": "arrow" - }, - "/terrain_map": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "rainbow", - "pointSize": 4 - }, - "/terrain_map_ext": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "rainbow", - "pointSize": 4 - }, - "/sensor_scan": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "flatColor": "#ffffff", - "pointSize": 2 - }, - "/added_obstacles": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "flatColor": "#ff1900", - "pointSize": 3 - }, - "/explored_areas": { - "visible": false, - "colorField": "intensity", - "colorMode": "flat", - "flatColor": "#00aaff", - "pointSize": 2 - }, - "/trajectory": { - "visible": false, - "colorField": "intensity", - "colorMode": "colormap", - "colorMap": "rainbow", - "pointSize": 7 - }, - "/viz_graph_topic": { - "visible": true, - "namespaces": { - "angle_direct": { - "visible": false - }, - "boundary_edge": { - "visible": false - }, - "boundary_vertex": { - "visible": false - }, - "freespace_vertex": { - "visible": false - }, - "freespace_vgraph": { - "visible": true - }, - "frontier_vertex": { - "visible": false - }, - "global_vertex": { - "visible": false - }, - "global_vgraph": { - "visible": false - }, - "localrange_vertex": { - "visible": false - }, - "odom_edge": { - "visible": false - }, - "polygon_edge": { - "visible": true - }, - "to_goal_edge": { - "visible": false - }, - "trajectory_edge": { - "visible": false - }, - "trajectory_vertex": { - "visible": false - }, - "updating_vertex": { - "visible": false - }, - "vertex_angle": { - "visible": false - }, - "vertices_matches": { - "visible": false - }, - "visibility_edge": { - "visible": false - } - } - } - }, - "layers": { - "grid": { - "layerId": "foxglove.Grid", - "visible": true, - "frameLocked": true, - "label": "Grid", - "position": [ - 0, - 0, - 0 - ], - "rotation": [ - 0, - 0, - 0 - ], - "color": "#248f24", - "size": 100, - "divisions": 100, - "lineWidth": 1, - "frameId": "map" - } - }, - "publish": { - "type": "pose", - "poseTopic": "/goal_pose", - "pointTopic": "/way_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939, - "frameId": "map" - }, - "imageMode": {}, - "fixedFrame": "map" - }, - "Image!camera": { - "cameraTopic": "/camera/image", - "enabledMarkerTopics": [], - "synchronize": false, - "transformMarkers": true, - "smooth": false, - "flipHorizontal": false, - "flipVertical": false, - "minValue": 0, - "maxValue": 1, - "rotation": 0, - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {} - }, - "Image!semantic": { - "cameraTopic": "/camera/semantic_image", - "enabledMarkerTopics": [], - "synchronize": false, - "transformMarkers": true, - "smooth": false, - "flipHorizontal": false, - "flipVertical": false, - "minValue": 0, - "maxValue": 1, - "rotation": 0, - "cameraState": { - "distance": 20, - "perspective": true, - "phi": 60, - "target": [ - 0, - 0, - 0 - ], - "targetOffset": [ - 0, - 0, - 0 - ], - "targetOrientation": [ - 0, - 0, - 0, - 1 - ], - "thetaOffset": 45, - "fovy": 45, - "near": 0.5, - "far": 5000 - }, - "followMode": "follow-pose", - "scene": {}, - "transforms": {}, - "topics": {}, - "layers": {}, - "publish": { - "type": "point", - "poseTopic": "/move_base_simple/goal", - "pointTopic": "/clicked_point", - "poseEstimateTopic": "/initialpose", - "poseEstimateXDeviation": 0.5, - "poseEstimateYDeviation": 0.5, - "poseEstimateThetaDeviation": 0.26179939 - }, - "imageMode": {} - }, - "Teleop!teleop": { - "topic": "/foxglove_teleop", - "publishRate": 10, - "upButton": { - "field": "linear.x", - "value": 0.5 - }, - "downButton": { - "field": "linear.x", - "value": -0.5 - }, - "leftButton": { - "field": "angular.z", - "value": 0.5 - }, - "rightButton": { - "field": "angular.z", - "value": -0.5 - } - }, - "RawMessages!odom": { - "diffEnabled": false, - "diffMethod": "custom", - "diffTopicPath": "", - "showFullMessageForDiff": false, - "topicPath": "/state_estimation.pose.pose" - }, - "RawMessages!cmdvel": { - "diffEnabled": false, - "diffMethod": "custom", - "diffTopicPath": "", - "showFullMessageForDiff": false, - "topicPath": "/cmd_vel.twist", - "fontSize": 12 - }, - "Plot!speed": { - "paths": [ - { - "value": "/cmd_vel.twist.linear.x", - "enabled": true, - "timestampMethod": "receiveTime", - "label": "Linear X" - }, - { - "value": "/cmd_vel.twist.linear.y", - "enabled": true, - "timestampMethod": "receiveTime", - "label": "Linear Y" - }, - { - "value": "/cmd_vel.twist.angular.z", - "enabled": true, - "timestampMethod": "receiveTime", - "label": "Angular Z" - } - ], - "showXAxisLabels": true, - "showYAxisLabels": true, - "showLegend": true, - "legendDisplay": "floating", - "showPlotValuesInLegend": true, - "isSynced": true, - "xAxisVal": "timestamp", - "sidebarDimension": 240, - "minYValue": -2, - "maxYValue": 2, - "followingViewWidth": 30 - }, - "Indicator!goalreached": { - "path": "/goal_reached.data", - "style": "background", - "fallbackColor": "#a0a0a0", - "fallbackLabel": "No Data", - "rules": [ - { - "operator": "=", - "rawValue": "true", - "color": "#68e24a", - "label": "Goal Reached" - }, - { - "operator": "=", - "rawValue": "false", - "color": "#f5f5f5", - "label": "Navigating" - } - ], - "fontSize": 36 - }, - "Indicator!stop": { - "path": "/stop.data", - "style": "background", - "fallbackColor": "#a0a0a0", - "fallbackLabel": "No Data", - "rules": [ - { - "operator": "=", - "rawValue": "0", - "color": "#68e24a", - "label": "OK" - }, - { - "operator": "=", - "rawValue": "1", - "color": "#f5ba42", - "label": "Speed Stop" - }, - { - "operator": "=", - "rawValue": "2", - "color": "#eb4034", - "label": "Full Stop" - } - ], - "fontSize": 36 - }, - "Indicator!autonomy": { - "path": "/joy.axes[2]", - "style": "background", - "fallbackColor": "#a0a0a0", - "fallbackLabel": "No Joystick", - "rules": [ - { - "operator": "<", - "rawValue": "-0.1", - "color": "#68e24a", - "label": "Autonomy ON" - }, - { - "operator": ">=", - "rawValue": "-0.1", - "color": "#eb4034", - "label": "Autonomy OFF" - } - ], - "fontSize": 36 - } - }, - "globalVariables": {}, - "userNodes": {}, - "playbackConfig": { - "speed": 1 - }, - "layout": { - "first": { - "first": "3D!main", - "second": { - "first": "Image!camera", - "second": "Image!semantic", - "direction": "column", - "splitPercentage": 50 - }, - "direction": "row", - "splitPercentage": 70 - }, - "second": { - "first": { - "first": "Teleop!teleop", - "second": { - "first": "Indicator!autonomy", - "second": { - "first": "Indicator!goalreached", - "second": "Indicator!stop", - "direction": "row", - "splitPercentage": 50 - }, - "direction": "row", - "splitPercentage": 33 - }, - "direction": "row", - "splitPercentage": 40 - }, - "second": { - "first": "RawMessages!cmdvel", - "second": "Plot!speed", - "direction": "row", - "splitPercentage": 30 - }, - "direction": "column", - "splitPercentage": 40 - }, - "direction": "column", - "splitPercentage": 75 - } -} diff --git a/dimos/conftest.py b/dimos/conftest.py deleted file mode 100644 index 5d1ca2b860..0000000000 --- a/dimos/conftest.py +++ /dev/null @@ -1,169 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import threading - -from dotenv import load_dotenv -import pytest - -from dimos.protocol.service.lcmservice import autoconf - -load_dotenv() - - -def _has_cuda(): - try: - import torch - except Exception: - return False - - try: - return bool(torch.cuda.is_available()) - except Exception: - return False - - -@pytest.hookimpl() -def pytest_collection_modifyitems(config, items): - if not _has_cuda(): - skip_marker = pytest.mark.skip( - reason="CUDA is not available (torch.cuda.is_available() returned False)" - ) - for item in items: - if item.get_closest_marker("cuda"): - item.add_marker(skip_marker) - - -@pytest.fixture -def event_loop(): - loop = asyncio.new_event_loop() - yield loop - loop.close() - - -@pytest.fixture(scope="session", autouse=True) -def _autoconf(request): - """Run autoconf() before all tests with capture suspended so people see `sudo` commands.""" - - capman = request.config.pluginmanager.getplugin("capturemanager") - capman.suspend_global_capture(in_=True) - try: - autoconf() - finally: - capman.resume_global_capture() - - -_session_threads = set() -_seen_threads = set() -_seen_threads_lock = threading.RLock() -_before_test_threads = {} # Map test name to set of thread IDs before test - -_skip_for = ["lcm", "heavy", "ros"] - - -@pytest.fixture(scope="module") -def dimos_cluster(): - from dimos.core import start - - dimos = start(4) - try: - yield dimos - finally: - dimos.stop() - - -@pytest.hookimpl() -def pytest_sessionfinish(session): - """Track threads that exist at session start - these are not leaks.""" - - yield - - # Check for session-level thread leaks at teardown - final_threads = [ - t - for t in threading.enumerate() - if t.name != "MainThread" and t.ident not in _session_threads - ] - - if final_threads: - thread_info = [f"{t.name} (daemon={t.daemon})" for t in final_threads] - pytest.fail( - f"\n{len(final_threads)} thread(s) leaked during test session: {thread_info}\n" - "Session-scoped fixtures must clean up all threads in their teardown." - ) - - -@pytest.fixture(autouse=True) -def monitor_threads(request): - # Skip monitoring for tests marked with specified markers - if any(request.node.get_closest_marker(marker) for marker in _skip_for): - yield - return - - # Capture threads before test runs - test_name = request.node.nodeid - with _seen_threads_lock: - _before_test_threads[test_name] = { - t.ident for t in threading.enumerate() if t.ident is not None - } - - yield - - with _seen_threads_lock: - before = _before_test_threads.get(test_name, set()) - current = {t.ident for t in threading.enumerate() if t.ident is not None} - - # New threads are ones that exist now but didn't exist before this test - new_thread_ids = current - before - - if not new_thread_ids: - return - - # Get the actual thread objects for new threads - new_threads = [ - t for t in threading.enumerate() if t.ident in new_thread_ids and t.name != "MainThread" - ] - - # Filter out expected persistent threads that are shared globally - # These threads are intentionally left running and cleaned up on process exit - expected_persistent_thread_prefixes = [ - "Dask-Offload", - # HuggingFace safetensors conversion thread - no user cleanup API - # https://github.com/huggingface/transformers/issues/29513 - "Thread-auto_conversion", - ] - new_threads = [ - t - for t in new_threads - if not any(t.name.startswith(prefix) for prefix in expected_persistent_thread_prefixes) - ] - - # Filter out threads we've already seen (from previous tests) - truly_new = [t for t in new_threads if t.ident not in _seen_threads] - - # Mark all new threads as seen - for t in new_threads: - if t.ident is not None: - _seen_threads.add(t.ident) - - if not truly_new: - return - - thread_names = [t.name for t in truly_new] - - pytest.fail( - f"Non-closed threads created during this test. Thread names: {thread_names}. " - "Please look at the first test that fails and fix that." - ) diff --git a/dimos/constants.py b/dimos/constants.py deleted file mode 100644 index 4e74ccbe1b..0000000000 --- a/dimos/constants.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - -DIMOS_PROJECT_ROOT = Path(__file__).parent.parent - -DIMOS_LOG_DIR = DIMOS_PROJECT_ROOT / "logs" - -""" -Constants for shared memory -Usually, auto-detection for size would be preferred. Sadly, though, channels are made -and frozen *before* the first frame is received. -Therefore, a maximum capacity for color image and depth image transfer should be defined -ahead of time. -""" -# Default color image size: 1920x1080 frame x 3 (RGB) x uint8 -DEFAULT_CAPACITY_COLOR_IMAGE = 1920 * 1080 * 3 -# Default depth image size: 1280x720 frame * 4 (float32 size) -DEFAULT_CAPACITY_DEPTH_IMAGE = 1280 * 720 * 4 - -# From https://github.com/lcm-proj/lcm.git -LCM_MAX_CHANNEL_NAME_LENGTH = 63 diff --git a/dimos/control/README.md b/dimos/control/README.md deleted file mode 100644 index 755bfbd939..0000000000 --- a/dimos/control/README.md +++ /dev/null @@ -1,208 +0,0 @@ -# Control Coordinator - -Centralized control system for multi-arm robots with per-joint arbitration. - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ControlCoordinator │ -│ │ -│ ┌──────────────────────────────────────────────────────┐ │ -│ │ TickLoop (100Hz) │ │ -│ │ │ │ -│ │ READ ──► COMPUTE ──► ARBITRATE ──► ROUTE ──► WRITE │ │ -│ └──────────────────────────────────────────────────────┘ │ -│ │ │ │ │ │ -│ ▼ ▼ ▼ ▼ │ -│ ┌─────────┐ ┌───────┐ ┌─────────┐ ┌──────────┐ │ -│ │Connected│ │ Tasks │ │Priority │ │ Adapters │ │ -│ │Hardware │ │ │ │ Winners │ │ │ │ -│ └─────────┘ └───────┘ └─────────┘ └──────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Quick Start - -```bash -# Terminal 1: Run coordinator -dimos run coordinator-mock # Single 7-DOF mock arm -dimos run coordinator-dual-mock # Dual arms (7+6 DOF) -dimos run coordinator-piper-xarm # Real hardware - -# Terminal 2: Control via CLI -python -m dimos.manipulation.control.coordinator_client -``` - -## Core Concepts - -### Tick Loop -Single deterministic loop at 100Hz: -1. **Read** - Get joint positions from all hardware -2. **Compute** - Each task calculates desired output -3. **Arbitrate** - Per-joint, highest priority wins -4. **Route** - Group commands by hardware -5. **Write** - Send commands to adapters - -### Tasks (Controllers) -Tasks are passive controllers called by the coordinator: - -```python -class MyController: - def claim(self) -> ResourceClaim: - return ResourceClaim(joints={"joint1", "joint2"}, priority=10) - - def compute(self, state: CoordinatorState) -> JointCommandOutput: - # Your control law here (PID, impedance, etc.) - return JointCommandOutput( - joint_names=["joint1", "joint2"], - positions=[0.5, 0.3], - mode=ControlMode.POSITION, - ) -``` - -### Priority & Arbitration -Higher priority always wins. Arbitration happens every tick: - -``` -traj_arm (priority=10) wants joint1 = 0.5 -safety (priority=100) wants joint1 = 0.0 - ↓ - safety wins, traj_arm preempted -``` - -### Preemption -When a task loses a joint to higher priority, it gets notified: - -```python -def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - self._state = TrajectoryState.PREEMPTED -``` - -## Files - -``` -dimos/control/ -├── coordinator.py # Module + RPC interface -├── tick_loop.py # 100Hz control loop -├── task.py # ControlTask protocol + types -├── hardware_interface.py # ConnectedHardware wrapper -├── components.py # HardwareComponent config + type aliases -├── blueprints.py # Pre-configured setups -└── tasks/ - └── trajectory_task.py # Joint trajectory controller -``` - -## Configuration - -```python -from dimos.control import control_coordinator, HardwareComponent, TaskConfig - -my_robot = control_coordinator( - tick_rate=100.0, - hardware=[ - HardwareComponent( - hardware_id="left_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("left_arm", 7), - adapter_type="xarm", - address="192.168.1.100", - ), - HardwareComponent( - hardware_id="right_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("right_arm", 6), - adapter_type="piper", - address="can0", - ), - ], - tasks=[ - TaskConfig(name="traj_left", type="trajectory", joint_names=[...], priority=10), - TaskConfig(name="traj_right", type="trajectory", joint_names=[...], priority=10), - TaskConfig(name="safety", type="trajectory", joint_names=[...], priority=100), - ], -) -``` - -## RPC Methods - -| Method | Description | -|--------|-------------| -| `list_hardware()` | List hardware IDs | -| `list_joints()` | List all joint names | -| `list_tasks()` | List task names | -| `get_joint_positions()` | Get current positions | -| `execute_trajectory(task, traj)` | Execute trajectory | -| `get_trajectory_status(task)` | Get task status | -| `cancel_trajectory(task)` | Cancel active trajectory | - -## Control Modes - -Tasks output commands in one of three modes: - -| Mode | Output | Use Case | -|------|--------|----------| -| POSITION | `q` | Trajectory following | -| VELOCITY | `q_dot` | Joystick teleoperation | -| TORQUE | `tau` | Force control, impedance | - -## Writing a Custom Task - -```python -from dimos.control.task import ControlTask, ResourceClaim, JointCommandOutput, ControlMode - -class PIDController: - def __init__(self, joints: list[str], priority: int = 10): - self._name = "pid_controller" - self._claim = ResourceClaim(joints=frozenset(joints), priority=priority) - self._joints = joints - self.Kp, self.Ki, self.Kd = 10.0, 0.1, 1.0 - self._integral = [0.0] * len(joints) - self._last_error = [0.0] * len(joints) - self.target = [0.0] * len(joints) - - @property - def name(self) -> str: - return self._name - - def claim(self) -> ResourceClaim: - return self._claim - - def is_active(self) -> bool: - return True - - def compute(self, state) -> JointCommandOutput: - positions = [state.joints.joint_positions[j] for j in self._joints] - error = [t - p for t, p in zip(self.target, positions)] - - # PID - self._integral = [i + e * state.dt for i, e in zip(self._integral, error)] - derivative = [(e - le) / state.dt for e, le in zip(error, self._last_error)] - output = [self.Kp*e + self.Ki*i + self.Kd*d - for e, i, d in zip(error, self._integral, derivative)] - self._last_error = error - - return JointCommandOutput( - joint_names=self._joints, - positions=output, - mode=ControlMode.POSITION, - ) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - pass # Handle preemption -``` - -## Joint State Output - -The coordinator publishes one aggregated `JointState` message containing all joints: - -```python -JointState( - name=["left_arm_joint1", ..., "right_arm_joint1", ...], # All joints - position=[...], - velocity=[...], - effort=[...], -) -``` - -Subscribe via: `/coordinator/joint_state` diff --git a/dimos/control/__init__.py b/dimos/control/__init__.py deleted file mode 100644 index 23ac02836b..0000000000 --- a/dimos/control/__init__.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ControlCoordinator - Centralized control for multi-arm coordination. - -This module provides a centralized control coordinator that replaces -per-driver/per-controller loops with a single deterministic tick-based system. - -Features: -- Single tick loop (read -> compute -> arbitrate -> route -> write) -- Per-joint arbitration (highest priority wins) -- Mode conflict detection -- Partial command support (hold last value) -- Aggregated preemption notifications - -Example: - >>> from dimos.control import ControlCoordinator - >>> from dimos.control.tasks import JointTrajectoryTask, JointTrajectoryTaskConfig - >>> from dimos.hardware.manipulators.xarm import XArmAdapter - >>> - >>> # Create coordinator - >>> coord = ControlCoordinator(tick_rate=100.0) - >>> - >>> # Add hardware - >>> adapter = XArmAdapter(ip="192.168.1.185", dof=7) - >>> adapter.connect() - >>> coord.add_hardware("left_arm", adapter) - >>> - >>> # Add task - >>> joints = [f"left_arm_joint{i+1}" for i in range(7)] - >>> task = JointTrajectoryTask( - ... "traj_left", - ... JointTrajectoryTaskConfig(joint_names=joints, priority=10), - ... ) - >>> coord.add_task(task) - >>> - >>> # Start - >>> coord.start() -""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "components": [ - "HardwareComponent", - "HardwareId", - "HardwareType", - "JointName", - "JointState", - "make_joints", - ], - "coordinator": [ - "ControlCoordinator", - "ControlCoordinatorConfig", - "TaskConfig", - "control_coordinator", - ], - "hardware_interface": ["ConnectedHardware"], - "task": [ - "ControlMode", - "ControlTask", - "CoordinatorState", - "JointCommandOutput", - "JointStateSnapshot", - "ResourceClaim", - ], - "tick_loop": ["TickLoop"], - }, -) diff --git a/dimos/control/blueprints.py b/dimos/control/blueprints.py deleted file mode 100644 index 8762ebd95b..0000000000 --- a/dimos/control/blueprints.py +++ /dev/null @@ -1,638 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pre-configured blueprints for the ControlCoordinator. - -This module provides ready-to-use coordinator blueprints for common setups. - -Usage: - # Run via CLI: - dimos run coordinator-mock # Mock 7-DOF arm - dimos run coordinator-xarm7 # XArm7 real hardware - dimos run coordinator-dual-mock # Dual mock arms - - # Or programmatically: - from dimos.control.blueprints import coordinator_mock - coordinator = coordinator_mock.build() - coordinator.loop() -""" - -from __future__ import annotations - -from dimos.control.components import HardwareComponent, HardwareType, make_joints -from dimos.control.coordinator import TaskConfig, control_coordinator -from dimos.core.transport import LCMTransport -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import JointState -from dimos.teleop.quest.quest_types import Buttons -from dimos.utils.data import LfsPath - -_PIPER_MODEL_PATH = LfsPath("piper_description/mujoco_model/piper_no_gripper_description.xml") -_XARM6_MODEL_PATH = LfsPath("xarm_description/urdf/xarm6/xarm6.urdf") - - -# ============================================================================= -# Single Arm Blueprints -# ============================================================================= - -# Mock 7-DOF arm (for testing) -coordinator_mock = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 7), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="traj_arm", - type="trajectory", - joint_names=[f"arm_joint{i + 1}" for i in range(7)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# XArm7 real hardware -coordinator_xarm7 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 7), - adapter_type="xarm", - address="192.168.2.235", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="traj_arm", - type="trajectory", - joint_names=[f"arm_joint{i + 1}" for i in range(7)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# XArm6 real hardware -coordinator_xarm6 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="traj_xarm", - type="trajectory", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# Piper arm (6-DOF, CAN bus) -coordinator_piper = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="piper", - address="can0", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="traj_piper", - type="trajectory", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -# ============================================================================= -# Dual Arm Blueprints -# ============================================================================= - -# Dual mock arms (7-DOF left, 6-DOF right) -coordinator_dual_mock = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="left_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("left_arm", 7), - adapter_type="mock", - ), - HardwareComponent( - hardware_id="right_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("right_arm", 6), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="traj_left", - type="trajectory", - joint_names=[f"left_arm_joint{i + 1}" for i in range(7)], - priority=10, - ), - TaskConfig( - name="traj_right", - type="trajectory", - joint_names=[f"right_arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# Dual XArm (XArm7 left, XArm6 right) -coordinator_dual_xarm = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="left_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("left_arm", 7), - adapter_type="xarm", - address="192.168.2.235", - auto_enable=True, - ), - HardwareComponent( - hardware_id="right_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("right_arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="traj_left", - type="trajectory", - joint_names=[f"left_arm_joint{i + 1}" for i in range(7)], - priority=10, - ), - TaskConfig( - name="traj_right", - type="trajectory", - joint_names=[f"right_arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# Dual arm (XArm6 + Piper) -coordinator_piper_xarm = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="xarm_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("xarm_arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - HardwareComponent( - hardware_id="piper_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("piper_arm", 6), - adapter_type="piper", - address="can0", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="traj_xarm", - type="trajectory", - joint_names=[f"xarm_arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - TaskConfig( - name="traj_piper", - type="trajectory", - joint_names=[f"piper_arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -# ============================================================================= -# Streaming Control Blueprints -# ============================================================================= - -# XArm6 teleop - streaming position control -coordinator_teleop_xarm6 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="servo_arm", - type="servo", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("joint_command", JointState): LCMTransport("/teleop/joint_command", JointState), - } -) - -# XArm6 velocity control - streaming velocity for joystick -coordinator_velocity_xarm6 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="velocity_arm", - type="velocity", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("joint_command", JointState): LCMTransport("/joystick/joint_command", JointState), - } -) - -# XArm6 combined (servo + velocity tasks) -coordinator_combined_xarm6 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="servo_arm", - type="servo", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - TaskConfig( - name="velocity_arm", - type="velocity", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("joint_command", JointState): LCMTransport("/control/joint_command", JointState), - } -) - - -# ============================================================================= -# Cartesian IK Blueprints (internal Pinocchio IK solver) -# ============================================================================= - - -# Mock 6-DOF arm with CartesianIK -coordinator_cartesian_ik_mock = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="cartesian_ik_arm", - type="cartesian_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_PIPER_MODEL_PATH, - ee_joint_id=6, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - } -) - -# Piper arm with CartesianIK -coordinator_cartesian_ik_piper = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="piper", - address="can0", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="cartesian_ik_arm", - type="cartesian_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_PIPER_MODEL_PATH, - ee_joint_id=6, - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - } -) - - -# ============================================================================= -# Teleop IK Blueprints (VR teleoperation with internal Pinocchio IK) -# ============================================================================= - -# Single XArm6 with TeleopIK -coordinator_teleop_xarm6 = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="teleop_xarm", - type="teleop_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_XARM6_MODEL_PATH, - ee_joint_id=6, - hand="right", - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - -# Single Piper with TeleopIK -coordinator_teleop_piper = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="piper", - address="can0", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="teleop_piper", - type="teleop_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_PIPER_MODEL_PATH, - ee_joint_id=6, - hand="left", - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - -# Dual arm teleop: XArm6 + Piper with TeleopIK -coordinator_teleop_dual = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="xarm_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("xarm_arm", 6), - adapter_type="xarm", - address="192.168.1.210", - auto_enable=True, - ), - HardwareComponent( - hardware_id="piper_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("piper_arm", 6), - adapter_type="piper", - address="can0", - auto_enable=True, - ), - ], - tasks=[ - TaskConfig( - name="teleop_xarm", - type="teleop_ik", - joint_names=[f"xarm_arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_XARM6_MODEL_PATH, - ee_joint_id=6, - hand="left", - ), - TaskConfig( - name="teleop_piper", - type="teleop_ik", - joint_names=[f"piper_arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_PIPER_MODEL_PATH, - ee_joint_id=6, - hand="right", - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - - -# ============================================================================= -# Raw Blueprints (for programmatic setup) -# ============================================================================= - -coordinator_basic = control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -# ============================================================================= -# Exports -# ============================================================================= - -__all__ = [ - # Raw - "coordinator_basic", - # Cartesian IK - "coordinator_cartesian_ik_mock", - "coordinator_cartesian_ik_piper", - # Streaming control - "coordinator_combined_xarm6", - # Dual arm - "coordinator_dual_mock", - "coordinator_dual_xarm", - # Single arm - "coordinator_mock", - "coordinator_piper", - "coordinator_piper_xarm", - # Teleop IK - "coordinator_teleop_dual", - "coordinator_teleop_piper", - "coordinator_teleop_xarm6", - "coordinator_velocity_xarm6", - "coordinator_xarm6", - "coordinator_xarm7", -] diff --git a/dimos/control/components.py b/dimos/control/components.py deleted file mode 100644 index e3022468ed..0000000000 --- a/dimos/control/components.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Hardware component schema for the ControlCoordinator.""" - -from dataclasses import dataclass, field -from enum import Enum - -HardwareId = str -JointName = str -TaskName = str - - -class HardwareType(Enum): - MANIPULATOR = "manipulator" - BASE = "base" - GRIPPER = "gripper" - - -@dataclass(frozen=True) -class JointState: - """State of a single joint.""" - - position: float - velocity: float - effort: float - - -@dataclass -class HardwareComponent: - """Configuration for a hardware component. - - Attributes: - hardware_id: Unique identifier, also used as joint name prefix - hardware_type: Type of hardware (MANIPULATOR, BASE, GRIPPER) - joints: List of joint names (e.g., ["arm_joint1", "arm_joint2", ...]) - adapter_type: Adapter type ("mock", "xarm", "piper") - address: Connection address - IP for TCP, port for CAN - auto_enable: Whether to auto-enable servos - """ - - hardware_id: HardwareId - hardware_type: HardwareType - joints: list[JointName] = field(default_factory=list) - adapter_type: str = "mock" - address: str | None = None - auto_enable: bool = True - - -def make_joints(hardware_id: HardwareId, dof: int) -> list[JointName]: - """Create joint names for hardware. - - Args: - hardware_id: The hardware identifier (e.g., "left_arm") - dof: Degrees of freedom - - Returns: - List of joint names like ["left_arm_joint1", "left_arm_joint2", ...] - """ - return [f"{hardware_id}_joint{i + 1}" for i in range(dof)] - - -__all__ = [ - "HardwareComponent", - "HardwareId", - "HardwareType", - "JointName", - "JointState", - "TaskName", - "make_joints", -] diff --git a/dimos/control/coordinator.py b/dimos/control/coordinator.py deleted file mode 100644 index 5685a9f9c7..0000000000 --- a/dimos/control/coordinator.py +++ /dev/null @@ -1,668 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ControlCoordinator module. - -Centralized control coordinator that replaces per-driver/per-controller -loops with a single deterministic tick-based system. - -Features: -- Single tick loop (read -> compute -> arbitrate -> route -> write) -- Per-joint arbitration (highest priority wins) -- Mode conflict detection -- Partial command support (hold last value) -- Aggregated preemption notifications -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -import threading -import time -from typing import TYPE_CHECKING, Any - -from dimos.control.components import HardwareComponent, HardwareId, JointName, TaskName -from dimos.control.hardware_interface import ConnectedHardware -from dimos.control.task import ControlTask -from dimos.control.tick_loop import TickLoop -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import ( - PoseStamped, # noqa: TC001 - needed at runtime for In[PoseStamped] -) -from dimos.msgs.sensor_msgs import ( - JointState, # noqa: TC001 - needed at runtime for Out[JointState] -) -from dimos.teleop.quest.quest_types import Buttons # noqa: TC001 - needed for teleop buttons -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - - from dimos.hardware.manipulators.spec import ManipulatorAdapter - -logger = setup_logger() - - -# ============================================================================= -# Configuration -# ============================================================================= - - -@dataclass -class TaskConfig: - """Configuration for a control task. - - Attributes: - name: Task name (e.g., "traj_arm") - type: Task type ("trajectory", "servo", "velocity", "cartesian_ik", "teleop_ik") - joint_names: List of joint names this task controls - priority: Task priority (higher wins arbitration) - model_path: Path to URDF/MJCF for IK solver (cartesian_ik/teleop_ik only) - ee_joint_id: End-effector joint ID in model (cartesian_ik/teleop_ik only) - """ - - name: str - type: str = "trajectory" - joint_names: list[str] = field(default_factory=lambda: []) - priority: int = 10 - # Cartesian IK / Teleop IK specific - model_path: str | Path | None = None - ee_joint_id: int = 6 - hand: str = "" # teleop_ik only: "left" or "right" controller - - -@dataclass -class ControlCoordinatorConfig(ModuleConfig): - """Configuration for the ControlCoordinator. - - Attributes: - tick_rate: Control loop frequency in Hz (default: 100) - publish_joint_state: Whether to publish aggregated JointState - joint_state_frame_id: Frame ID for published JointState - log_ticks: Whether to log tick information (verbose) - hardware: List of hardware configurations to create on start - tasks: List of task configurations to create on start - """ - - tick_rate: float = 100.0 - publish_joint_state: bool = True - joint_state_frame_id: str = "coordinator" - log_ticks: bool = False - hardware: list[HardwareComponent] = field(default_factory=lambda: []) - tasks: list[TaskConfig] = field(default_factory=lambda: []) - - -# ============================================================================= -# ControlCoordinator Module -# ============================================================================= - - -class ControlCoordinator(Module[ControlCoordinatorConfig]): - """Centralized control coordinator with per-joint arbitration. - - Single tick loop that: - 1. Reads state from all hardware - 2. Runs all active tasks - 3. Arbitrates conflicts per-joint (highest priority wins) - 4. Routes commands to hardware - 5. Publishes aggregated joint state - - Key design decisions: - - Joint-centric commands (not hardware-centric) - - Per-joint arbitration (not per-hardware) - - Centralized time (tasks use state.t_now, never time.time()) - - Partial commands OK (hardware holds last value) - - Aggregated preemption (one notification per task per tick) - - Example: - >>> from dimos.control import ControlCoordinator - >>> from dimos.hardware.manipulators.xarm import XArmAdapter - >>> - >>> orch = ControlCoordinator(tick_rate=100.0) - >>> adapter = XArmAdapter(ip="192.168.1.185", dof=7) - >>> adapter.connect() - >>> orch.add_hardware("left_arm", adapter, joint_prefix="left") - >>> orch.start() - """ - - # Output: Aggregated joint state for external consumers - joint_state: Out[JointState] - - # Input: Streaming joint commands for real-time control - joint_command: In[JointState] - - # Input: Streaming cartesian commands for CartesianIKTask - # Uses frame_id as task name for routing - cartesian_command: In[PoseStamped] - - # Input: Teleop buttons for engage/disengage signaling - buttons: In[Buttons] - - config: ControlCoordinatorConfig - default_config = ControlCoordinatorConfig - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - # Connected hardware (keyed by hardware_id) - self._hardware: dict[HardwareId, ConnectedHardware] = {} - self._hardware_lock = threading.Lock() - - # Joint -> hardware mapping (built when hardware added) - self._joint_to_hardware: dict[JointName, HardwareId] = {} - - # Registered tasks - self._tasks: dict[TaskName, ControlTask] = {} - self._task_lock = threading.Lock() - - # Tick loop (created on start) - self._tick_loop: TickLoop | None = None - - # Subscription handles for streaming commands - self._joint_command_unsub: Callable[[], None] | None = None - self._cartesian_command_unsub: Callable[[], None] | None = None - self._buttons_unsub: Callable[[], None] | None = None - - logger.info(f"ControlCoordinator initialized at {self.config.tick_rate}Hz") - - # ========================================================================= - # Config-based Setup - # ========================================================================= - - def _setup_from_config(self) -> None: - """Create hardware and tasks from config (called on start).""" - hardware_added: list[str] = [] - - try: - for component in self.config.hardware: - self._setup_hardware(component) - hardware_added.append(component.hardware_id) - - for task_cfg in self.config.tasks: - task = self._create_task_from_config(task_cfg) - self.add_task(task) - - except Exception: - # Rollback: clean up all successfully added hardware - for hw_id in hardware_added: - try: - self.remove_hardware(hw_id) - except Exception: - pass - raise - - def _setup_hardware(self, component: HardwareComponent) -> None: - """Connect and add a single hardware adapter.""" - adapter = self._create_adapter(component) - - if not adapter.connect(): - raise RuntimeError(f"Failed to connect to {component.adapter_type} adapter") - - try: - if component.auto_enable and hasattr(adapter, "write_enable"): - adapter.write_enable(True) - - self.add_hardware(adapter, component) - except Exception: - adapter.disconnect() - raise - - def _create_adapter(self, component: HardwareComponent) -> ManipulatorAdapter: - """Create a manipulator adapter from component config.""" - from dimos.hardware.manipulators.registry import adapter_registry - - return adapter_registry.create( - component.adapter_type, - dof=len(component.joints), - address=component.address, - ) - - def _create_task_from_config(self, cfg: TaskConfig) -> ControlTask: - """Create a control task from config.""" - task_type = cfg.type.lower() - - if task_type == "trajectory": - from dimos.control.tasks import JointTrajectoryTask, JointTrajectoryTaskConfig - - return JointTrajectoryTask( - cfg.name, - JointTrajectoryTaskConfig( - joint_names=cfg.joint_names, - priority=cfg.priority, - ), - ) - - elif task_type == "servo": - from dimos.control.tasks import JointServoTask, JointServoTaskConfig - - return JointServoTask( - cfg.name, - JointServoTaskConfig( - joint_names=cfg.joint_names, - priority=cfg.priority, - ), - ) - - elif task_type == "velocity": - from dimos.control.tasks import JointVelocityTask, JointVelocityTaskConfig - - return JointVelocityTask( - cfg.name, - JointVelocityTaskConfig( - joint_names=cfg.joint_names, - priority=cfg.priority, - ), - ) - - elif task_type == "cartesian_ik": - from dimos.control.tasks import CartesianIKTask, CartesianIKTaskConfig - - if cfg.model_path is None: - raise ValueError(f"CartesianIKTask '{cfg.name}' requires model_path in TaskConfig") - - return CartesianIKTask( - cfg.name, - CartesianIKTaskConfig( - joint_names=cfg.joint_names, - model_path=cfg.model_path, - ee_joint_id=cfg.ee_joint_id, - priority=cfg.priority, - ), - ) - - elif task_type == "teleop_ik": - from dimos.control.tasks.teleop_task import TeleopIKTask, TeleopIKTaskConfig - - if cfg.model_path is None: - raise ValueError(f"TeleopIKTask '{cfg.name}' requires model_path in TaskConfig") - - return TeleopIKTask( - cfg.name, - TeleopIKTaskConfig( - joint_names=cfg.joint_names, - model_path=cfg.model_path, - ee_joint_id=cfg.ee_joint_id, - priority=cfg.priority, - hand=cfg.hand, - ), - ) - - else: - raise ValueError(f"Unknown task type: {task_type}") - - # ========================================================================= - # Hardware Management (RPC) - # ========================================================================= - - @rpc - def add_hardware( - self, - adapter: ManipulatorAdapter, - component: HardwareComponent, - ) -> bool: - """Register a hardware adapter with the coordinator.""" - with self._hardware_lock: - if component.hardware_id in self._hardware: - logger.warning(f"Hardware {component.hardware_id} already registered") - return False - - connected = ConnectedHardware( - adapter=adapter, - component=component, - ) - self._hardware[component.hardware_id] = connected - - for joint_name in connected.joint_names: - self._joint_to_hardware[joint_name] = component.hardware_id - - logger.info( - f"Added hardware {component.hardware_id} with joints: {connected.joint_names}" - ) - return True - - @rpc - def remove_hardware(self, hardware_id: str) -> bool: - """Remove a hardware interface. - - Note: For safety, call this only when no tasks are actively using this - hardware. Consider stopping the coordinator before removing hardware. - """ - with self._hardware_lock: - if hardware_id not in self._hardware: - return False - - interface = self._hardware[hardware_id] - hw_joints = set(interface.joint_names) - - with self._task_lock: - for task in self._tasks.values(): - if task.is_active(): - claimed_joints = task.claim().joints - overlap = hw_joints & claimed_joints - if overlap: - logger.error( - f"Cannot remove hardware {hardware_id}: " - f"task '{task.name}' is actively using joints {overlap}" - ) - return False - - for joint_name in interface.joint_names: - del self._joint_to_hardware[joint_name] - - interface.disconnect() - del self._hardware[hardware_id] - logger.info(f"Removed hardware {hardware_id}") - return True - - @rpc - def list_hardware(self) -> list[str]: - """List registered hardware IDs.""" - with self._hardware_lock: - return list(self._hardware.keys()) - - @rpc - def list_joints(self) -> list[str]: - """List all joint names across all hardware.""" - with self._hardware_lock: - return list(self._joint_to_hardware.keys()) - - @rpc - def get_joint_positions(self) -> dict[str, float]: - """Get current joint positions for all joints.""" - with self._hardware_lock: - positions: dict[str, float] = {} - for hw in self._hardware.values(): - state = hw.read_state() # {joint_name: JointState} - for joint_name, joint_state in state.items(): - positions[joint_name] = joint_state.position - return positions - - # ========================================================================= - # Task Management (RPC) - # ========================================================================= - - @rpc - def add_task(self, task: ControlTask) -> bool: - """Register a task with the coordinator.""" - if not isinstance(task, ControlTask): - raise TypeError("task must implement ControlTask") - - with self._task_lock: - if task.name in self._tasks: - logger.warning(f"Task {task.name} already registered") - return False - self._tasks[task.name] = task - logger.info(f"Added task {task.name}") - return True - - @rpc - def remove_task(self, task_name: TaskName) -> bool: - """Remove a task by name.""" - with self._task_lock: - if task_name in self._tasks: - del self._tasks[task_name] - logger.info(f"Removed task {task_name}") - return True - return False - - @rpc - def get_task(self, task_name: TaskName) -> ControlTask | None: - """Get a task by name.""" - with self._task_lock: - return self._tasks.get(task_name) - - @rpc - def list_tasks(self) -> list[str]: - """List registered task names.""" - with self._task_lock: - return list(self._tasks.keys()) - - @rpc - def get_active_tasks(self) -> list[str]: - """List currently active task names.""" - with self._task_lock: - return [name for name, task in self._tasks.items() if task.is_active()] - - # ========================================================================= - # Streaming Control - # ========================================================================= - - def _on_joint_command(self, msg: JointState) -> None: - """Route incoming JointState to streaming tasks by joint name. - - Routes position data to servo tasks and velocity data to velocity tasks. - Each task only receives data for joints it claims. - """ - if not msg.name: - return - - t_now = time.perf_counter() - incoming_joints = set(msg.name) - - with self._task_lock: - for task in self._tasks.values(): - claimed_joints = task.claim().joints - - # Skip if no overlap between incoming and claimed joints - if not (claimed_joints & incoming_joints): - continue - - # Route to servo tasks (position control) - if msg.position: - positions_by_name = dict(zip(msg.name, msg.position, strict=False)) - task.set_target_by_name(positions_by_name, t_now) - - # Route to velocity tasks (velocity control) - elif msg.velocity: - velocities_by_name = dict(zip(msg.name, msg.velocity, strict=False)) - task.set_velocities_by_name(velocities_by_name, t_now) - - def _on_cartesian_command(self, msg: PoseStamped) -> None: - """Route incoming PoseStamped to CartesianIKTask by task name. - - Uses frame_id as the target task name for routing. - """ - task_name = msg.frame_id - if not task_name: - logger.warning("Received cartesian_command with empty frame_id (task name)") - return - - t_now = time.perf_counter() - - with self._task_lock: - task = self._tasks.get(task_name) - if task is None: - logger.warning(f"Cartesian command for unknown task: {task_name}") - return - - task.on_cartesian_command(msg, t_now) - - def _on_buttons(self, msg: Buttons) -> None: - """Forward button state to all tasks.""" - with self._task_lock: - for task in self._tasks.values(): - task.on_buttons(msg) - - @rpc - def task_invoke( - self, task_name: TaskName, method: str, kwargs: dict[str, Any] | None = None - ) -> Any: - """Invoke a method on a task. Pass t_now=None to auto-inject current time.""" - with self._task_lock: - task = self._tasks.get(task_name) - if task is None: - logger.warning(f"Task {task_name} not found") - return None - - if not hasattr(task, method): - logger.warning(f"Task {task_name} has no method {method}") - return None - - kwargs = kwargs or {} - - # Auto-inject t_now if requested (None means "use current time") - if "t_now" in kwargs and kwargs["t_now"] is None: - kwargs["t_now"] = time.perf_counter() - - return getattr(task, method)(**kwargs) - - # ========================================================================= - # Gripper - # ========================================================================= - - @rpc - def set_gripper_position(self, hardware_id: str, position: float) -> bool: - """Set gripper position on a specific hardware device. - - Args: - hardware_id: ID of the hardware with the gripper - position: Gripper position in meters - """ - with self._hardware_lock: - hw = self._hardware.get(hardware_id) - if hw is None: - logger.warning(f"Hardware '{hardware_id}' not found for gripper command") - return False - return hw.adapter.write_gripper_position(position) - - @rpc - def get_gripper_position(self, hardware_id: str) -> float | None: - """Get gripper position from a specific hardware device. - - Args: - hardware_id: ID of the hardware with the gripper - """ - with self._hardware_lock: - hw = self._hardware.get(hardware_id) - if hw is None: - return None - return hw.adapter.read_gripper_position() - - # ========================================================================= - # Lifecycle - # ========================================================================= - - @rpc - def start(self) -> None: - """Start the coordinator control loop.""" - if self._tick_loop and self._tick_loop.is_running: - logger.warning("Coordinator already running") - return - - super().start() - - # Setup hardware and tasks from config (if any) - if self.config.hardware or self.config.tasks: - self._setup_from_config() - - # Create and start tick loop - publish_cb = self.joint_state.publish if self.config.publish_joint_state else None - self._tick_loop = TickLoop( - tick_rate=self.config.tick_rate, - hardware=self._hardware, - hardware_lock=self._hardware_lock, - tasks=self._tasks, - task_lock=self._task_lock, - joint_to_hardware=self._joint_to_hardware, - publish_callback=publish_cb, - frame_id=self.config.joint_state_frame_id, - log_ticks=self.config.log_ticks, - ) - self._tick_loop.start() - - # Subscribe to joint commands if any streaming tasks configured - streaming_types = ("servo", "velocity") - has_streaming = any(t.type in streaming_types for t in self.config.tasks) - if has_streaming: - try: - self._joint_command_unsub = self.joint_command.subscribe(self._on_joint_command) - logger.info("Subscribed to joint_command for streaming tasks") - except Exception: - logger.warning( - "Streaming tasks configured but could not subscribe to joint_command. " - "Use task_invoke RPC or set transport via blueprint." - ) - - # Subscribe to cartesian commands if any cartesian_ik tasks configured - has_cartesian_ik = any(t.type in ("cartesian_ik", "teleop_ik") for t in self.config.tasks) - if has_cartesian_ik: - try: - self._cartesian_command_unsub = self.cartesian_command.subscribe( - self._on_cartesian_command - ) - logger.info("Subscribed to cartesian_command for CartesianIK/TeleopIK tasks") - except Exception: - logger.warning( - "CartesianIK/TeleopIK tasks configured but could not subscribe to cartesian_command. " - "Use task_invoke RPC or set transport via blueprint." - ) - - # Subscribe to buttons if any teleop_ik tasks configured (engage/disengage) - has_teleop_ik = any(t.type == "teleop_ik" for t in self.config.tasks) - if has_teleop_ik: - self._buttons_unsub = self.buttons.subscribe(self._on_buttons) - logger.info("Subscribed to buttons for engage/disengage") - - logger.info(f"ControlCoordinator started at {self.config.tick_rate}Hz") - - @rpc - def stop(self) -> None: - """Stop the coordinator.""" - logger.info("Stopping ControlCoordinator...") - - # Unsubscribe from streaming commands - if self._joint_command_unsub: - self._joint_command_unsub() - self._joint_command_unsub = None - if self._cartesian_command_unsub: - self._cartesian_command_unsub() - self._cartesian_command_unsub = None - if self._buttons_unsub: - self._buttons_unsub() - self._buttons_unsub = None - - if self._tick_loop: - self._tick_loop.stop() - - # Disconnect all hardware adapters - with self._hardware_lock: - for hw_id, interface in self._hardware.items(): - try: - interface.disconnect() - logger.info(f"Disconnected hardware {hw_id}") - except Exception as e: - logger.error(f"Error disconnecting hardware {hw_id}: {e}") - - super().stop() - logger.info("ControlCoordinator stopped") - - @rpc - def get_tick_count(self) -> int: - """Get the number of ticks since start.""" - return self._tick_loop.tick_count if self._tick_loop else 0 - - -# Blueprint export -control_coordinator = ControlCoordinator.blueprint - - -__all__ = [ - "ControlCoordinator", - "ControlCoordinatorConfig", - "HardwareComponent", - "TaskConfig", - "control_coordinator", -] diff --git a/dimos/control/examples/cartesian_ik_jogger.py b/dimos/control/examples/cartesian_ik_jogger.py deleted file mode 100644 index d2a2f4d119..0000000000 --- a/dimos/control/examples/cartesian_ik_jogger.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pygame-based cartesian jogger for CartesianIKTask. - -Publishes PoseStamped commands to the coordinator via LCM. -The frame_id is used as the task name for routing. - -Keyboard controls for jogging robot end-effector in world frame: - W/S: +X/-X (forward/backward) - A/D: -Y/+Y (left/right) - Q/E: +Z/-Z (up/down) - R/F: +Roll/-Roll - T/G: +Pitch/-Pitch - Y/H: +Yaw/-Yaw - SPACE: Reset to home pose - ESC: Quit - -Usage: - python -m dimos.control.examples.cartesian_ik_jogger -""" - -from __future__ import annotations - -from dataclasses import dataclass -import time -from typing import Any - -import numpy as np - -try: - import pygame -except ImportError: - print("pygame not installed. Install with: pip install pygame") - raise - - -@dataclass -class JogState: - """Current jogging state.""" - - x: float = 0.0 - y: float = 0.0 - z: float = 0.0 - roll: float = 0.0 - pitch: float = 0.0 - yaw: float = 0.0 - - def copy(self) -> JogState: - return JogState( - x=self.x, - y=self.y, - z=self.z, - roll=self.roll, - pitch=self.pitch, - yaw=self.yaw, - ) - - @classmethod - def from_fk(cls, model_path: str, ee_joint_id: int) -> JogState: - """Create JogState from forward kinematics at zero configuration. - - This ensures the initial pose is reachable by the robot. - """ - import pinocchio # type: ignore[import-untyped] - - # Load model - if model_path.endswith(".xml"): - model = pinocchio.buildModelFromMJCF(model_path) - else: - model = pinocchio.buildModelFromUrdf(model_path) - - data = model.createData() - - # Compute FK at zero configuration - q_zero = np.zeros(model.nq) - pinocchio.forwardKinematics(model, data, q_zero) - - # Get EE pose - ee_pose = data.oMi[ee_joint_id] - position = ee_pose.translation - rotation = ee_pose.rotation - - # Convert rotation matrix to RPY - rpy = pinocchio.rpy.matrixToRpy(rotation) - - print("Initial EE pose from FK at q=0:") - print(f" Position: x={position[0]:.3f}, y={position[1]:.3f}, z={position[2]:.3f}") - print( - f" Orientation: roll={np.degrees(rpy[0]):.1f}°, pitch={np.degrees(rpy[1]):.1f}°, yaw={np.degrees(rpy[2]):.1f}°" - ) - - return cls( - x=float(position[0]), - y=float(position[1]), - z=float(position[2]), - roll=float(rpy[0]), - pitch=float(rpy[1]), - yaw=float(rpy[2]), - ) - - def to_pose_stamped(self, task_name: str) -> Any: - """Convert to PoseStamped for LCM publishing. - - Args: - task_name: Task name to use as frame_id for routing - """ - from dimos.msgs.geometry_msgs import PoseStamped - from dimos.msgs.geometry_msgs.Quaternion import Quaternion - from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - position = Vector3(self.x, self.y, self.z) - orientation = Quaternion.from_euler(Vector3(self.roll, self.pitch, self.yaw)) - - return PoseStamped( - ts=time.time(), - frame_id=task_name, # Used for task routing - position=position, - orientation=orientation, - ) - - -# Jog speeds -LINEAR_SPEED = 0.05 # m/s -ANGULAR_SPEED = 0.5 # rad/s - -# Position limits (workspace bounds) - will be updated based on initial pose -X_LIMITS = (-0.5, 0.5) -Y_LIMITS = (-0.5, 0.5) -Z_LIMITS = (-0.2, 0.6) - -# Task name for routing (must match blueprint config) -TASK_NAME = "cartesian_ik_arm" - - -def clamp(value: float, min_val: float, max_val: float) -> float: - return max(min_val, min(max_val, value)) - - -def _get_piper_model_path() -> str: - """Get path to Piper MJCF model.""" - from dimos.utils.data import get_data - - piper_path = get_data("piper_description") - return str(piper_path / "mujoco_model" / "piper_no_gripper_description.xml") - - -def run_jogger_ui(model_path: str | None = None, ee_joint_id: int = 6) -> None: - """Run the pygame-based cartesian jogger UI. - - This is ONLY the UI - it publishes PoseStamped to LCM. - The coordinator must be running separately to receive commands. - - Args: - model_path: Path to robot model (MJCF/URDF) for computing initial FK pose. - If None, uses Piper model. - ee_joint_id: End-effector joint ID in the model - """ - from dimos.core.transport import LCMTransport - from dimos.msgs.geometry_msgs import PoseStamped - - # Use Piper model if not specified - if model_path is None: - model_path = _get_piper_model_path() - - print("Starting Cartesian IK Jogger UI...") - print("Publishing to /coordinator/cartesian_command") - print("(Coordinator must be running separately to receive commands)") - - # Create LCM publisher for sending cartesian commands - transport: LCMTransport[PoseStamped] = LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ) - - # Initialize pygame - pygame.init() - screen = pygame.display.set_mode((600, 400)) - pygame.display.set_caption("Cartesian IK Jogger") - font = pygame.font.Font(None, 28) - clock = pygame.time.Clock() - - # Initial pose from forward kinematics at zero configuration - # This ensures we start at a pose that's reachable from q=[0,0,0,0,0,0] - home_pose = JogState.from_fk(model_path, ee_joint_id) - current_pose = home_pose.copy() - - # Send initial pose via LCM - transport.publish(current_pose.to_pose_stamped(TASK_NAME)) - - running = True - last_time = time.perf_counter() - - print("\nControls:") - print(" W/S: +X/-X (forward/backward)") - print(" A/D: -Y/+Y (left/right)") - print(" Q/E: +Z/-Z (up/down)") - print(" R/F: +Roll/-Roll") - print(" T/G: +Pitch/-Pitch") - print(" Y/H: +Yaw/-Yaw") - print(" SPACE: Reset to home") - print(" ESC: Quit") - print() - - while running: - dt = time.perf_counter() - last_time - last_time = time.perf_counter() - - # Handle events - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - running = False - elif event.key == pygame.K_SPACE: - current_pose = home_pose.copy() - print("Reset to home pose") - - # Get pressed keys for continuous jogging - keys = pygame.key.get_pressed() - - # Linear motion - if keys[pygame.K_w]: - current_pose.x += LINEAR_SPEED * dt - if keys[pygame.K_s]: - current_pose.x -= LINEAR_SPEED * dt - if keys[pygame.K_a]: - current_pose.y -= LINEAR_SPEED * dt - if keys[pygame.K_d]: - current_pose.y += LINEAR_SPEED * dt - if keys[pygame.K_q]: - current_pose.z += LINEAR_SPEED * dt - if keys[pygame.K_e]: - current_pose.z -= LINEAR_SPEED * dt - - # Angular motion - if keys[pygame.K_r]: - current_pose.roll += ANGULAR_SPEED * dt - if keys[pygame.K_f]: - current_pose.roll -= ANGULAR_SPEED * dt - if keys[pygame.K_t]: - current_pose.pitch += ANGULAR_SPEED * dt - if keys[pygame.K_g]: - current_pose.pitch -= ANGULAR_SPEED * dt - if keys[pygame.K_y]: - current_pose.yaw += ANGULAR_SPEED * dt - if keys[pygame.K_h]: - current_pose.yaw -= ANGULAR_SPEED * dt - - # Clamp to workspace limits - current_pose.x = clamp(current_pose.x, *X_LIMITS) - current_pose.y = clamp(current_pose.y, *Y_LIMITS) - current_pose.z = clamp(current_pose.z, *Z_LIMITS) - - # Publish pose via LCM (frame_id = task name for routing) - transport.publish(current_pose.to_pose_stamped(TASK_NAME)) - - # Draw UI - screen.fill((30, 30, 30)) - - # Title - title = font.render("Cartesian IK Jogger", True, (255, 255, 255)) - screen.blit(title, (200, 20)) - - # Position display - y_offset = 70 - pos_text = ( - f"Position: X={current_pose.x:.3f} Y={current_pose.y:.3f} Z={current_pose.z:.3f}" - ) - pos_surf = font.render(pos_text, True, (100, 255, 100)) - screen.blit(pos_surf, (50, y_offset)) - - # Orientation display - y_offset += 30 - ori_text = f"Orientation: R={np.degrees(current_pose.roll):.1f}° P={np.degrees(current_pose.pitch):.1f}° Y={np.degrees(current_pose.yaw):.1f}°" - ori_surf = font.render(ori_text, True, (100, 200, 255)) - screen.blit(ori_surf, (50, y_offset)) - - # Controls - y_offset += 50 - controls = [ - ("W/S", "+X/-X (forward/back)"), - ("A/D", "-Y/+Y (left/right)"), - ("Q/E", "+Z/-Z (up/down)"), - ("R/F", "+Roll/-Roll"), - ("T/G", "+Pitch/-Pitch"), - ("Y/H", "+Yaw/-Yaw"), - ("SPACE", "Reset to home"), - ("ESC", "Quit"), - ] - - for key, desc in controls: - text = f"{key}: {desc}" - surf = font.render(text, True, (180, 180, 180)) - screen.blit(surf, (50, y_offset)) - y_offset += 25 - - # Active keys indicator - y_offset += 20 - active_keys = [] - if keys[pygame.K_w]: - active_keys.append("W") - if keys[pygame.K_s]: - active_keys.append("S") - if keys[pygame.K_a]: - active_keys.append("A") - if keys[pygame.K_d]: - active_keys.append("D") - if keys[pygame.K_q]: - active_keys.append("Q") - if keys[pygame.K_e]: - active_keys.append("E") - - if active_keys: - active_text = f"Active: {' '.join(active_keys)}" - active_surf = font.render(active_text, True, (255, 255, 0)) - screen.blit(active_surf, (50, y_offset)) - - pygame.display.flip() - clock.tick(50) # 50 Hz update rate - - # Cleanup - print("Jogger UI stopped.") - pygame.quit() - - -def main() -> None: - """Run the jogger UI standalone. - - Note: This only runs the UI. The coordinator must be started separately: - Terminal 1: dimos run coordinator-cartesian-ik-mock - Terminal 2: python -m dimos.control.examples.cartesian_ik_jogger - """ - run_jogger_ui() - - -if __name__ == "__main__": - main() diff --git a/dimos/control/hardware_interface.py b/dimos/control/hardware_interface.py deleted file mode 100644 index 9f6eb99851..0000000000 --- a/dimos/control/hardware_interface.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Connected hardware for the ControlCoordinator. - -Wraps ManipulatorAdapter with coordinator-specific features: -- Namespaced joint names (e.g., "left_joint1") -- Unified read/write interface -- Hold-last-value for partial commands -""" - -from __future__ import annotations - -import logging -import time -from typing import TYPE_CHECKING - -from dimos.hardware.manipulators.spec import ControlMode, ManipulatorAdapter - -if TYPE_CHECKING: - from dimos.control.components import HardwareComponent, HardwareId, JointName, JointState - -logger = logging.getLogger(__name__) - - -class ConnectedHardware: - """Runtime wrapper for hardware connected to the coordinator. - - Wraps a ManipulatorAdapter with coordinator-specific features: - - Joint names from HardwareComponent config - - Hold-last-value for partial commands - - Converts between joint names and array indices - - Created when hardware is added to the coordinator. One instance - per physical hardware device. - """ - - def __init__( - self, - adapter: ManipulatorAdapter, - component: HardwareComponent, - ) -> None: - """Initialize hardware interface. - - Args: - adapter: ManipulatorAdapter instance (XArmAdapter, PiperAdapter, etc.) - component: Hardware component with joints config - """ - if not isinstance(adapter, ManipulatorAdapter): - raise TypeError("adapter must implement ManipulatorAdapter") - - self._adapter = adapter - self._component = component - self._joint_names = component.joints - - # Track last commanded values for hold-last behavior - self._last_commanded: dict[str, float] = {} - self._initialized = False - self._warned_unknown_joints: set[str] = set() - self._current_mode: ControlMode | None = None - - @property - def adapter(self) -> ManipulatorAdapter: - """The underlying hardware adapter.""" - return self._adapter - - @property - def hardware_id(self) -> HardwareId: - """Unique ID for this hardware.""" - return self._component.hardware_id - - @property - def joint_names(self) -> list[JointName]: - """Ordered list of joint names.""" - return self._joint_names - - @property - def component(self) -> HardwareComponent: - """The hardware component config.""" - return self._component - - @property - def dof(self) -> int: - """Degrees of freedom.""" - return len(self._joint_names) - - def disconnect(self) -> None: - """Disconnect the underlying adapter.""" - self._adapter.disconnect() - - def read_state(self) -> dict[JointName, JointState]: - """Read state as {joint_name: JointState}. - - Returns: - Dict mapping joint name to JointState with position, velocity, effort - """ - from dimos.control.components import JointState - - positions = self._adapter.read_joint_positions() - velocities = self._adapter.read_joint_velocities() - efforts = self._adapter.read_joint_efforts() - - return { - name: JointState( - position=positions[i], - velocity=velocities[i], - effort=efforts[i], - ) - for i, name in enumerate(self._joint_names) - } - - def write_command(self, commands: dict[str, float], mode: ControlMode) -> bool: - """Write commands - allows partial joint sets, holds last for missing. - - This is critical for: - - Partial WBC overrides - - Safety controllers - - Mixed task ownership - - Args: - commands: {joint_name: value} - can be partial - mode: Control mode - - Returns: - True if command was sent successfully - """ - # Initialize on first write if needed - if not self._initialized: - self._initialize_last_commanded() - - # Update last commanded for joints we received - for joint_name, value in commands.items(): - if joint_name in self._joint_names: - self._last_commanded[joint_name] = value - elif joint_name not in self._warned_unknown_joints: - logger.warning( - f"Hardware {self.hardware_id} received command for unknown joint " - f"{joint_name}. Valid joints: {self._joint_names}" - ) - self._warned_unknown_joints.add(joint_name) - - # Build ordered list for adapter - ordered = self._build_ordered_command() - - # Switch control mode if needed - if mode != self._current_mode: - if not self._adapter.set_control_mode(mode): - logger.warning(f"Hardware {self.hardware_id} failed to switch to {mode.name}") - return False - self._current_mode = mode - - # Send to adapter - match mode: - case ControlMode.POSITION | ControlMode.SERVO_POSITION: - return self._adapter.write_joint_positions(ordered) - case ControlMode.VELOCITY: - return self._adapter.write_joint_velocities(ordered) - case ControlMode.TORQUE: - logger.warning(f"Hardware {self.hardware_id} does not support torque mode") - return False - case _: - return False - - def _initialize_last_commanded(self) -> None: - """Initialize last_commanded with current hardware positions.""" - for _ in range(10): - try: - current = self._adapter.read_joint_positions() - for i, name in enumerate(self._joint_names): - self._last_commanded[name] = current[i] - self._initialized = True - return - except Exception: - time.sleep(0.01) - - raise RuntimeError( - f"Hardware {self.hardware_id} failed to read initial positions after retries" - ) - - def _build_ordered_command(self) -> list[float]: - """Build ordered command list from last_commanded dict.""" - return [self._last_commanded[name] for name in self._joint_names] - - -__all__ = [ - "ConnectedHardware", -] diff --git a/dimos/control/task.py b/dimos/control/task.py deleted file mode 100644 index ecdf9ab7f4..0000000000 --- a/dimos/control/task.py +++ /dev/null @@ -1,346 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ControlTask protocol and types for the ControlCoordinator. - -This module defines: -- Data types used by tasks and the coordinator (ResourceClaim, JointStateSnapshot, etc.) -- ControlTask protocol that all tasks must implement - -Tasks are "passive" - they don't own threads. The coordinator calls -compute() at each tick, passing current state and time. - -CRITICAL: Tasks must NEVER call time.time() directly. -Use the t_now passed in CoordinatorState. -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Protocol, runtime_checkable - -from dimos.control.components import JointName -from dimos.hardware.manipulators.spec import ControlMode - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs import Pose, PoseStamped - from dimos.teleop.quest.quest_types import Buttons - -# ============================================================================= -# Data Types -# ============================================================================= - - -@dataclass(frozen=True) -class ResourceClaim: - """Declares which joints a task wants to control. - - Used by the coordinator to determine resource ownership and - resolve conflicts between competing tasks. - - Attributes: - joints: Set of joint names this task wants to control. - Example: frozenset({"left_joint1", "left_joint2"}) - priority: Priority level for conflict resolution. Higher wins. - Typical values: 10 (trajectory), 50 (WBC), 100 (safety) - mode: Control mode (POSITION, VELOCITY, TORQUE) - """ - - joints: frozenset[JointName] - priority: int = 0 - mode: ControlMode = ControlMode.POSITION - - def conflicts_with(self, other: ResourceClaim) -> bool: - """Check if two claims compete for the same joints.""" - return bool(self.joints & other.joints) - - -@dataclass -class JointStateSnapshot: - """Aggregated joint states from all hardware. - - Provides a unified view of all joint states across all hardware - interfaces, indexed by fully-qualified joint name. - - Attributes: - joint_positions: Joint name -> position (radians) - joint_velocities: Joint name -> velocity (rad/s) - joint_efforts: Joint name -> effort (Nm) - timestamp: Unix timestamp when state was read - """ - - joint_positions: dict[JointName, float] = field(default_factory=dict) - joint_velocities: dict[JointName, float] = field(default_factory=dict) - joint_efforts: dict[JointName, float] = field(default_factory=dict) - timestamp: float = 0.0 - - def get_position(self, joint_name: JointName) -> float | None: - """Get position for a specific joint.""" - return self.joint_positions.get(joint_name) - - def get_velocity(self, joint_name: JointName) -> float | None: - """Get velocity for a specific joint.""" - return self.joint_velocities.get(joint_name) - - def get_effort(self, joint_name: JointName) -> float | None: - """Get effort for a specific joint.""" - return self.joint_efforts.get(joint_name) - - -@dataclass -class CoordinatorState: - """Complete state snapshot for tasks to read. - - Passed to each task's compute() method every tick. Contains - all information a task needs to compute its output. - - CRITICAL: Tasks should use t_now for timing, never time.time()! - - Attributes: - joints: Aggregated joint states from all hardware - t_now: Current tick time (time.perf_counter()) - dt: Time since last tick (seconds) - """ - - joints: JointStateSnapshot - t_now: float # Coordinator time (perf_counter) - USE THIS, NOT time.time()! - dt: float # Time since last tick - - -@dataclass -class JointCommandOutput: - """Joint-centric command output from a task. - - Commands are addressed by joint name, NOT by hardware ID. - The coordinator routes commands to the appropriate hardware. - - This design enables: - - WBC spanning multiple hardware interfaces - - Partial joint ownership - - Per-joint arbitration - - Attributes: - joint_names: Which joints this command is for - positions: Position commands (radians), or None - velocities: Velocity commands (rad/s), or None - efforts: Effort commands (Nm), or None - mode: Control mode - must match which field is populated - """ - - joint_names: list[JointName] - positions: list[float] | None = None - velocities: list[float] | None = None - efforts: list[float] | None = None - mode: ControlMode = ControlMode.POSITION - - def __post_init__(self) -> None: - """Validate that lengths match and at least one value field is set.""" - n = len(self.joint_names) - - if self.positions is not None and len(self.positions) != n: - raise ValueError(f"positions length {len(self.positions)} != joint_names length {n}") - if self.velocities is not None and len(self.velocities) != n: - raise ValueError(f"velocities length {len(self.velocities)} != joint_names length {n}") - if self.efforts is not None and len(self.efforts) != n: - raise ValueError(f"efforts length {len(self.efforts)} != joint_names length {n}") - - def get_values(self) -> list[float] | None: - """Get the active values based on mode.""" - match self.mode: - case ControlMode.POSITION | ControlMode.SERVO_POSITION: - return self.positions - case ControlMode.VELOCITY: - return self.velocities - case ControlMode.TORQUE: - return self.efforts - case _: - return None - - -# ============================================================================= -# ControlTask Protocol -# ============================================================================= - - -@runtime_checkable -class ControlTask(Protocol): - """Protocol for passive tasks that run within the coordinator. - - Tasks are "passive" - they don't own threads. The coordinator - calls compute() at each tick, passing current state and time. - - Lifecycle: - 1. Task is added to coordinator via add_task() - 2. Coordinator calls claim() to understand resource needs - 3. Each tick: is_active() → compute() → output merged via arbitration - 4. Task removed via remove_task() or transitions to inactive - - CRITICAL: Tasks must NEVER call time.time() directly. - Use state.t_now passed to compute() for all timing. - - Example: - >>> class MyTask: - ... @property - ... def name(self) -> str: - ... return "my_task" - ... - ... def claim(self) -> ResourceClaim: - ... return ResourceClaim( - ... joints=frozenset(["left_joint1", "left_joint2"]), - ... priority=10, - ... ) - ... - ... def is_active(self) -> bool: - ... return self._executing - ... - ... def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - ... # Use state.t_now, NOT time.time()! - ... t_elapsed = state.t_now - self._start_time - ... positions = self._trajectory.sample(t_elapsed) - ... return JointCommandOutput( - ... joint_names=["left_joint1", "left_joint2"], - ... positions=positions, - ... ) - ... - ... def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - ... print(f"Preempted by {by_task} on {joints}") - """ - - @property - def name(self) -> str: - """Unique identifier for this task instance. - - Used for logging, debugging, and task management. - Must be unique across all tasks in the coordinator. - """ - ... - - def claim(self) -> ResourceClaim: - """Declare resource requirements. - - Called by coordinator to determine: - - Which joints this task wants to control - - Priority for conflict resolution - - Control mode (position/velocity/effort) - - Returns: - ResourceClaim with joints (frozenset) and priority (int) - - Note: - The claim can change dynamically - coordinator calls this - every tick for active tasks. - """ - ... - - def is_active(self) -> bool: - """Check if task should run this tick. - - Inactive tasks: - - Skip compute() call - - Don't participate in arbitration - - Don't consume resources - - Returns: - True if task should execute this tick - """ - ... - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Compute output command given current state. - - Called by coordinator for active tasks each tick. - - CRITICAL: Use state.t_now for timing, NEVER time.time()! - This ensures deterministic behavior and enables simulation. - - Args: - state: CoordinatorState containing: - - joints: JointStateSnapshot with all joint states - - t_now: Current tick time (use this for all timing!) - - dt: Time since last tick - - Returns: - JointCommandOutput with joint_names and values, or None if - no command should be sent this tick. - """ - ... - - def on_preempted(self, by_task: str, joints: frozenset[JointName]) -> None: - """Called ONCE per tick with ALL preempted joints aggregated. - - Called when a higher-priority task takes control of some of this - task's joints. Allows task to gracefully handle being overridden. - - This is called ONCE per tick with ALL preempted joints, not once - per joint. This reduces noise and improves performance. - - Args: - by_task: Name of the preempting task (or "arbitration" if multiple) - joints: All joints that were preempted this tick - """ - ... - - def on_buttons(self, msg: Buttons) -> bool: - """Handle button state from teleop controllers.""" - ... - - def on_cartesian_command(self, pose: Pose | PoseStamped, t_now: float) -> bool: - """Handle incoming cartesian command (target or delta pose).""" - ... - - def set_target_by_name(self, positions: dict[str, float], t_now: float) -> bool: - """Handle servo position commands by joint name.""" - ... - - def set_velocities_by_name(self, velocities: dict[str, float], t_now: float) -> bool: - """Handle velocity commands by joint name.""" - ... - - -class BaseControlTask(ControlTask): - """Base class with no-op defaults for optional listener methods. - - Inherit from this to avoid implementing empty methods - in tasks that don't need them. Only override what your task uses. - """ - - def on_buttons(self, msg: Buttons) -> bool: - """No-op default.""" - return False - - def on_cartesian_command(self, pose: Pose | PoseStamped, t_now: float) -> bool: - """No-op default.""" - return False - - def set_target_by_name(self, positions: dict[str, float], t_now: float) -> bool: - """No-op default.""" - return False - - def set_velocities_by_name(self, velocities: dict[str, float], t_now: float) -> bool: - """No-op default.""" - return False - - -__all__ = [ - # Protocol + Base - "BaseControlTask", - # Types - "ControlMode", - "ControlTask", - "CoordinatorState", - "JointCommandOutput", - "JointName", - "JointStateSnapshot", - "ResourceClaim", -] diff --git a/dimos/control/tasks/__init__.py b/dimos/control/tasks/__init__.py deleted file mode 100644 index 5b869b01f9..0000000000 --- a/dimos/control/tasks/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Task implementations for the ControlCoordinator.""" - -from dimos.control.tasks.cartesian_ik_task import ( - CartesianIKTask, - CartesianIKTaskConfig, -) -from dimos.control.tasks.servo_task import ( - JointServoTask, - JointServoTaskConfig, -) -from dimos.control.tasks.teleop_task import ( - TeleopIKTask, - TeleopIKTaskConfig, -) -from dimos.control.tasks.trajectory_task import ( - JointTrajectoryTask, - JointTrajectoryTaskConfig, -) -from dimos.control.tasks.velocity_task import ( - JointVelocityTask, - JointVelocityTaskConfig, -) - -__all__ = [ - "CartesianIKTask", - "CartesianIKTaskConfig", - "JointServoTask", - "JointServoTaskConfig", - "JointTrajectoryTask", - "JointTrajectoryTaskConfig", - "JointVelocityTask", - "JointVelocityTaskConfig", - "TeleopIKTask", - "TeleopIKTaskConfig", -] diff --git a/dimos/control/tasks/cartesian_ik_task.py b/dimos/control/tasks/cartesian_ik_task.py deleted file mode 100644 index 6ea5ddc55b..0000000000 --- a/dimos/control/tasks/cartesian_ik_task.py +++ /dev/null @@ -1,335 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Cartesian control task with internal Pinocchio IK solver. - -Accepts streaming cartesian poses (e.g., from teleoperation, visual servoing) -and computes inverse kinematics internally to output joint commands. -Participates in joint-level arbitration. - -CRITICAL: Uses t_now from CoordinatorState, never calls time.time() -""" - -from __future__ import annotations - -from dataclasses import dataclass -import threading -from typing import TYPE_CHECKING, Any - -import numpy as np - -from dimos.control.task import ( - BaseControlTask, - ControlMode, - CoordinatorState, - JointCommandOutput, - ResourceClaim, -) -from dimos.manipulation.planning.kinematics.pinocchio_ik import ( - PinocchioIK, - check_joint_delta, - get_worst_joint_delta, - pose_to_se3, -) -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pathlib import Path - - from numpy.typing import NDArray - import pinocchio # type: ignore[import-untyped] - - from dimos.msgs.geometry_msgs import Pose, PoseStamped - -logger = setup_logger() - - -@dataclass -class CartesianIKTaskConfig: - """Configuration for cartesian IK task. - - Attributes: - joint_names: List of joint names this task controls (must match model DOF) - model_path: Path to URDF or MJCF file for IK solver - ee_joint_id: End-effector joint ID in the kinematic chain - priority: Priority for arbitration (higher wins) - timeout: If no command received for this many seconds, go inactive (0 = never) - max_joint_delta_deg: Maximum allowed joint change per tick (safety limit) - """ - - joint_names: list[str] - model_path: str | Path - ee_joint_id: int - priority: int = 10 - timeout: float = 0.5 - max_joint_delta_deg: float = 15.0 # ~1500°/s at 100Hz - - -class CartesianIKTask(BaseControlTask): - """Cartesian control task with internal Pinocchio IK solver. - - Accepts streaming cartesian poses via on_cartesian_command() and computes IK - internally to output joint commands. Uses current joint state from - CoordinatorState as IK warm-start for fast convergence. - - Unlike CartesianServoTask (which bypasses joint arbitration), this task - outputs JointCommandOutput and participates in joint-level arbitration. - - Example: - >>> from dimos.utils.data import get_data - >>> piper_path = get_data("piper_description") - >>> task = CartesianIKTask( - ... name="cartesian_arm", - ... config=CartesianIKTaskConfig( - ... joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"], - ... model_path=piper_path / "mujoco_model" / "piper_no_gripper_description.xml", - ... ee_joint_id=6, - ... priority=10, - ... timeout=0.5, - ... ), - ... ) - >>> coordinator.add_task(task) - >>> task.start() - >>> - >>> # From teleop callback or other source: - >>> task.on_cartesian_command(pose_stamped, t_now=time.perf_counter()) - """ - - def __init__(self, name: str, config: CartesianIKTaskConfig) -> None: - """Initialize cartesian IK task. - - Args: - name: Unique task name - config: Task configuration - """ - if not config.joint_names: - raise ValueError(f"CartesianIKTask '{name}' requires at least one joint") - if not config.model_path: - raise ValueError(f"CartesianIKTask '{name}' requires model_path for IK solver") - - self._name = name - self._config = config - self._joint_names = frozenset(config.joint_names) - self._joint_names_list = list(config.joint_names) - self._num_joints = len(config.joint_names) - - # Create IK solver from model - self._ik = PinocchioIK.from_model_path(config.model_path, config.ee_joint_id) - - # Validate DOF matches joint names - if self._ik.nq != self._num_joints: - logger.warning( - f"CartesianIKTask {name}: model DOF ({self._ik.nq}) != " - f"joint_names count ({self._num_joints})" - ) - - # Thread-safe target state - self._lock = threading.Lock() - self._target_pose: Pose | PoseStamped | None = None - self._last_update_time: float = 0.0 - self._active = False - - # Cache last successful IK solution for warm-starting - self._last_q_solution: NDArray[np.floating[Any]] | None = None - - logger.info( - f"CartesianIKTask {name} initialized with model: {config.model_path}, " - f"ee_joint_id={config.ee_joint_id}, joints={config.joint_names}" - ) - - @property - def name(self) -> str: - """Unique task identifier.""" - return self._name - - def claim(self) -> ResourceClaim: - """Declare resource requirements.""" - return ResourceClaim( - joints=self._joint_names, - priority=self._config.priority, - mode=ControlMode.SERVO_POSITION, - ) - - def is_active(self) -> bool: - """Check if task should run this tick.""" - with self._lock: - return self._active and self._target_pose is not None - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Compute IK and output joint positions. - - Args: - state: Current coordinator state (contains joint positions for IK warm-start) - - Returns: - JointCommandOutput with positions, or None if inactive/timed out/IK failed - """ - with self._lock: - if not self._active or self._target_pose is None: - return None - # Check timeout - if self._config.timeout > 0: - time_since_update = state.t_now - self._last_update_time - if time_since_update > self._config.timeout: - logger.warning( - f"CartesianIKTask {self._name} timed out " - f"(no update for {time_since_update:.3f}s)" - ) - self._active = False - return None - raw_pose = self._target_pose - - # Convert to SE3 right before use - target_pose = pose_to_se3(raw_pose) - # Get current joint positions for IK warm-start - q_current = self._get_current_joints(state) - if q_current is None: - logger.debug(f"CartesianIKTask {self._name}: missing joint state for IK warm-start") - return None - - # Compute IK - q_solution, converged, final_error = self._ik.solve(target_pose, q_current) - # Use the solution even if it didn't fully converge - if not converged: - logger.debug( - f"CartesianIKTask {self._name}: IK did not converge " - f"(error={final_error:.4f}), using partial solution" - ) - - # Safety check: reject if any joint delta exceeds limit - if not check_joint_delta(q_solution, q_current, self._config.max_joint_delta_deg): - worst_idx, worst_deg = get_worst_joint_delta(q_solution, q_current) - logger.warning( - f"CartesianIKTask {self._name}: rejecting motion - " - f"joint {self._joint_names_list[worst_idx]} delta " - f"{worst_deg:.1f}° exceeds limit {self._config.max_joint_delta_deg}°" - ) - return None - - # Cache solution for next warm-start - with self._lock: - self._last_q_solution = q_solution.copy() - return JointCommandOutput( - joint_names=self._joint_names_list, - positions=q_solution.flatten().tolist(), - mode=ControlMode.SERVO_POSITION, - ) - - def _get_current_joints(self, state: CoordinatorState) -> NDArray[np.floating[Any]] | None: - """Get current joint positions from coordinator state. - - Falls back to last IK solution if joint state unavailable. - """ - positions = [] - for joint_name in self._joint_names_list: - pos = state.joints.get_position(joint_name) - if pos is None: - # Fallback to last solution - if self._last_q_solution is not None: - result: NDArray[np.floating[Any]] = self._last_q_solution.copy() - return result - return None - positions.append(pos) - return np.array(positions, dtype=np.float64) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - """Handle preemption by higher-priority task. - - Args: - by_task: Name of preempting task - joints: Joints that were preempted - """ - if joints & self._joint_names: - logger.warning( - f"CartesianIKTask {self._name} preempted by {by_task} on joints {joints}" - ) - - # ========================================================================= - # Task-specific methods - # ========================================================================= - - def on_cartesian_command(self, pose: Pose | PoseStamped, t_now: float) -> bool: - """Handle incoming cartesian command (target EE pose). - - Args: - pose: Target end-effector pose (Pose or PoseStamped) - t_now: Current time (from coordinator or time.perf_counter()) - - Returns: - True if accepted - """ - with self._lock: - self._target_pose = pose # Store raw, convert to SE3 in compute() - self._last_update_time = t_now - self._active = True - - return True - - def start(self) -> None: - """Activate the task (start accepting and outputting commands).""" - with self._lock: - self._active = True - logger.info(f"CartesianIKTask {self._name} started") - - def stop(self) -> None: - """Deactivate the task (stop outputting commands).""" - with self._lock: - self._active = False - logger.info(f"CartesianIKTask {self._name} stopped") - - def clear(self) -> None: - """Clear current target and deactivate.""" - with self._lock: - self._target_pose = None - self._active = False - logger.info(f"CartesianIKTask {self._name} cleared") - - def is_tracking(self) -> bool: - """Check if actively receiving and outputting commands.""" - with self._lock: - return self._active and self._target_pose is not None - - def get_current_ee_pose(self, state: CoordinatorState) -> pinocchio.SE3 | None: - """Get current end-effector pose via forward kinematics. - - Useful for getting initial pose before starting tracking. - - Args: - state: Current coordinator state - - Returns: - Current EE pose as SE3, or None if joint state unavailable - """ - q_current = self._get_current_joints(state) - if q_current is None: - return None - - return self._ik.forward_kinematics(q_current) - - def forward_kinematics(self, joint_positions: NDArray[np.floating[Any]]) -> pinocchio.SE3: - """Compute end-effector pose from joint positions. - - Args: - joint_positions: Joint angles in radians - - Returns: - End-effector pose as SE3 - """ - return self._ik.forward_kinematics(joint_positions) - - -__all__ = [ - "CartesianIKTask", - "CartesianIKTaskConfig", -] diff --git a/dimos/control/tasks/servo_task.py b/dimos/control/tasks/servo_task.py deleted file mode 100644 index b69b4dd099..0000000000 --- a/dimos/control/tasks/servo_task.py +++ /dev/null @@ -1,242 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Streaming joint servo task for real-time position control. - -Accepts streaming joint positions (e.g., from teleoperation) and outputs them -directly to hardware each tick. Useful for teleoperation, visual servoing, -or any real-time control where you don't want trajectory planning overhead. - -CRITICAL: Uses t_now from CoordinatorState, never calls time.time() -""" - -from __future__ import annotations - -from dataclasses import dataclass -import threading - -from dimos.control.task import ( - BaseControlTask, - ControlMode, - CoordinatorState, - JointCommandOutput, - ResourceClaim, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class JointServoTaskConfig: - """Configuration for servo task. - - Attributes: - joint_names: List of joint names this task controls - priority: Priority for arbitration (higher wins) - timeout: If no command received for this many seconds, go inactive (0 = never timeout) - """ - - joint_names: list[str] - priority: int = 10 - timeout: float = 0.5 # 500ms default timeout - - -class JointServoTask(BaseControlTask): - """Streaming joint position control for teleoperation/visual servoing. - - Accepts target positions via set_target() or set_target_by_name() and - outputs them each tick. Uses SERVO_POSITION mode for high-frequency control. - - No trajectory planning - just pass-through with optional timeout. - - Example: - >>> task = JointServoTask( - ... name="servo_arm", - ... config=JointServoTaskConfig( - ... joint_names=["arm_joint1", "arm_joint2", "arm_joint3"], - ... priority=10, - ... timeout=0.5, - ... ), - ... ) - >>> coordinator.add_task(task) - >>> task.start() - >>> - >>> # From teleop callback or other source: - >>> task.set_target([0.1, 0.2, 0.3], t_now=time.perf_counter()) - """ - - def __init__(self, name: str, config: JointServoTaskConfig) -> None: - """Initialize servo task. - - Args: - name: Unique task name - config: Task configuration - """ - if not config.joint_names: - raise ValueError(f"JointServoTask '{name}' requires at least one joint") - - self._name = name - self._config = config - self._joint_names = frozenset(config.joint_names) - self._joint_names_list = list(config.joint_names) - self._num_joints = len(config.joint_names) - - # Current target (thread-safe) - self._lock = threading.Lock() - self._target: list[float] | None = None - self._last_update_time: float = 0.0 - self._active = False - - logger.info(f"JointServoTask {name} initialized for joints: {config.joint_names}") - - @property - def name(self) -> str: - """Unique task identifier.""" - return self._name - - def claim(self) -> ResourceClaim: - """Declare resource requirements.""" - return ResourceClaim( - joints=self._joint_names, - priority=self._config.priority, - mode=ControlMode.SERVO_POSITION, - ) - - def is_active(self) -> bool: - """Check if task should run this tick.""" - with self._lock: - return self._active and self._target is not None - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Output current target positions. - - Args: - state: Current coordinator state - - Returns: - JointCommandOutput with positions, or None if inactive/timed out - """ - with self._lock: - if not self._active or self._target is None: - return None - - # Check timeout - if self._config.timeout > 0: - time_since_update = state.t_now - self._last_update_time - if time_since_update > self._config.timeout: - logger.warning( - f"JointServoTask {self._name} timed out " - f"(no update for {time_since_update:.3f}s)" - ) - self._active = False - return None - - return JointCommandOutput( - joint_names=self._joint_names_list, - positions=list(self._target), - mode=ControlMode.SERVO_POSITION, - ) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - """Handle preemption by higher-priority task. - - Args: - by_task: Name of preempting task - joints: Joints that were preempted - """ - if joints & self._joint_names: - logger.warning(f"JointServoTask {self._name} preempted by {by_task} on joints {joints}") - - # ========================================================================= - # Task-specific methods - # ========================================================================= - - def set_target(self, positions: list[float], t_now: float) -> bool: - """Set target joint positions. - - Call this from your teleop callback or other data source. - - Args: - positions: Joint positions in radians (must match joint_names length) - t_now: Current time (from coordinator or time.perf_counter()) - - Returns: - True if accepted, False if wrong number of joints - """ - if len(positions) != self._num_joints: - logger.warning( - f"JointServoTask {self._name}: expected {self._num_joints} " - f"positions, got {len(positions)}" - ) - return False - - with self._lock: - self._target = list(positions) - self._last_update_time = t_now - self._active = True - - return True - - def set_target_by_name(self, positions: dict[str, float], t_now: float) -> bool: - """Set target positions by joint name. - - Extracts only the joints this task controls from the dict. - Useful for routing when multiple tasks share an input stream. - - Args: - positions: {joint_name: position} dict (can contain extra joints) - t_now: Current time - - Returns: - True if all required joints found, False if any missing - """ - ordered = [] - for name in self._joint_names_list: - if name not in positions: - # Missing joint - don't update - return False - ordered.append(positions[name]) - - return self.set_target(ordered, t_now) - - def start(self) -> None: - """Activate the task (start accepting and outputting commands).""" - with self._lock: - self._active = True - logger.info(f"JointServoTask {self._name} started") - - def stop(self) -> None: - """Deactivate the task (stop outputting commands).""" - with self._lock: - self._active = False - logger.info(f"JointServoTask {self._name} stopped") - - def clear(self) -> None: - """Clear current target and deactivate.""" - with self._lock: - self._target = None - self._active = False - logger.info(f"JointServoTask {self._name} cleared") - - def is_streaming(self) -> bool: - """Check if actively receiving and outputting commands.""" - with self._lock: - return self._active and self._target is not None - - -__all__ = [ - "JointServoTask", - "JointServoTaskConfig", -] diff --git a/dimos/control/tasks/teleop_task.py b/dimos/control/tasks/teleop_task.py deleted file mode 100644 index d1a90eb6d0..0000000000 --- a/dimos/control/tasks/teleop_task.py +++ /dev/null @@ -1,332 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleop cartesian control task with internal Pinocchio IK solver. - -Accepts streaming cartesian delta poses from teleoperation and computes -inverse kinematics internally to output joint commands. Deltas are applied -relative to the EE pose captured at engage time. - -Participates in joint-level arbitration. - -CRITICAL: Uses t_now from CoordinatorState, never calls time.time() -""" - -from __future__ import annotations - -from dataclasses import dataclass -import threading -from typing import TYPE_CHECKING, Any - -import numpy as np -import pinocchio # type: ignore[import-untyped] - -from dimos.control.task import ( - BaseControlTask, - ControlMode, - CoordinatorState, - JointCommandOutput, - ResourceClaim, -) -from dimos.manipulation.planning.kinematics.pinocchio_ik import ( - PinocchioIK, - check_joint_delta, - pose_to_se3, -) -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pathlib import Path - - from numpy.typing import NDArray - - from dimos.msgs.geometry_msgs import Pose, PoseStamped - from dimos.teleop.quest.quest_types import Buttons - -logger = setup_logger() - - -@dataclass -class TeleopIKTaskConfig: - """Configuration for teleop IK task. - - Attributes: - joint_names: List of joint names this task controls (must match model DOF) - model_path: Path to URDF or MJCF file for IK solver - ee_joint_id: End-effector joint ID in the kinematic chain - priority: Priority for arbitration (higher wins) - timeout: If no command received for this many seconds, go inactive (0 = never) - max_joint_delta_deg: Maximum allowed joint change per tick (safety limit) - hand: "left" or "right" — which controller's primary button to listen to - """ - - joint_names: list[str] - model_path: str | Path - ee_joint_id: int - priority: int = 10 - timeout: float = 0.5 - max_joint_delta_deg: float = 5.0 # ~500°/s at 100Hz - hand: str = "" - - -class TeleopIKTask(BaseControlTask): - """Teleop cartesian control task with internal Pinocchio IK solver. - - Accepts streaming cartesian delta poses via on_cartesian_command() and computes IK - internally to output joint commands. Deltas are applied relative to the EE pose - captured at engage time (first compute). - - Uses current joint state from CoordinatorState as IK warm-start for fast convergence. - Outputs JointCommandOutput and participates in joint-level arbitration. - - Example: - >>> from dimos.utils.data import get_data - >>> piper_path = get_data("piper_description") - >>> task = TeleopIKTask( - ... name="teleop_arm", - ... config=TeleopIKTaskConfig( - ... joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"], - ... model_path=piper_path / "mujoco_model" / "piper_no_gripper_description.xml", - ... ee_joint_id=6, - ... priority=10, - ... timeout=0.5, - ... ), - ... ) - >>> coordinator.add_task(task) - >>> task.start() - >>> - >>> # From teleop callback: - >>> task.on_cartesian_command(delta_pose, t_now=time.perf_counter()) - """ - - def __init__(self, name: str, config: TeleopIKTaskConfig) -> None: - """Initialize teleop IK task. - - Args: - name: Unique task name - config: Task configuration - """ - if not config.joint_names: - raise ValueError(f"TeleopIKTask '{name}' requires at least one joint") - if not config.model_path: - raise ValueError(f"TeleopIKTask '{name}' requires model_path for IK solver") - - self._name = name - self._config = config - self._joint_names = frozenset(config.joint_names) - self._joint_names_list = list(config.joint_names) - self._num_joints = len(config.joint_names) - - # Create IK solver from model - self._ik = PinocchioIK.from_model_path(config.model_path, config.ee_joint_id) - - # Validate DOF matches joint names - if self._ik.nq != self._num_joints: - logger.warning( - f"TeleopIKTask {name}: model DOF ({self._ik.nq}) != " - f"joint_names count ({self._num_joints})" - ) - - # Thread-safe target state - self._lock = threading.Lock() - self._target_pose: Pose | PoseStamped | None = None - self._last_update_time: float = 0.0 - self._active = False - - # Initial EE pose for delta application - self._initial_ee_pose: pinocchio.SE3 | None = None - self._prev_primary: bool = False - - logger.info( - f"TeleopIKTask {name} initialized with model: {config.model_path}, " - f"ee_joint_id={config.ee_joint_id}, joints={config.joint_names}" - ) - - @property - def name(self) -> str: - """Unique task identifier.""" - return self._name - - def claim(self) -> ResourceClaim: - """Declare resource requirements.""" - return ResourceClaim( - joints=self._joint_names, - priority=self._config.priority, - mode=ControlMode.SERVO_POSITION, - ) - - def is_active(self) -> bool: - """Check if task should run this tick.""" - with self._lock: - return self._active and self._target_pose is not None - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Compute IK and output joint positions. - - Args: - state: Current coordinator state (contains joint positions for IK warm-start) - - Returns: - JointCommandOutput with positions, or None if inactive/timed out/IK failed - """ - with self._lock: - if not self._active or self._target_pose is None: - return None - - # Timeout safety: stop if teleop stream drops - if self._config.timeout > 0: - time_since_update = state.t_now - self._last_update_time - if time_since_update > self._config.timeout: - logger.warning( - f"TeleopIKTask {self._name} timed out " - f"(no update for {time_since_update:.3f}s)" - ) - self._target_pose = None - self._active = False - return None - raw_pose = self._target_pose - - # Convert to SE3 right before use - delta_se3 = pose_to_se3(raw_pose) - # Capture initial EE pose if not set (first command after engage) - with self._lock: - need_capture = self._initial_ee_pose is None - - if need_capture: - q_current = self._get_current_joints(state) - if q_current is None: - logger.debug( - f"TeleopIKTask {self._name}: cannot capture initial pose, joint state unavailable" - ) - return None - initial_pose = self._ik.forward_kinematics(q_current) - with self._lock: - self._initial_ee_pose = initial_pose - - # Apply delta to initial pose: target = initial + delta - with self._lock: - if self._initial_ee_pose is None: - return None - target_pose = pinocchio.SE3( - delta_se3.rotation @ self._initial_ee_pose.rotation, - self._initial_ee_pose.translation + delta_se3.translation, - ) - - # Get current joint positions for IK warm-start - q_current = self._get_current_joints(state) - if q_current is None: - logger.debug(f"TeleopIKTask {self._name}: missing joint state for IK warm-start") - return None - - # Compute IK - q_solution, converged, final_error = self._ik.solve(target_pose, q_current) - # Use the solution even if it didn't fully converge - if not converged: - logger.debug( - f"TeleopIKTask {self._name}: IK did not converge " - f"(error={final_error:.4f}), using partial solution" - ) - # Safety: reject if any joint would jump too far in one tick - if not check_joint_delta(q_solution, q_current, self._config.max_joint_delta_deg): - logger.warning( - f"TeleopIKTask {self._name}: joint delta exceeds " - f"{self._config.max_joint_delta_deg}°, rejecting solution" - ) - return None - - positions = q_solution.flatten().tolist() - return JointCommandOutput( - joint_names=self._joint_names_list, - positions=positions, - mode=ControlMode.SERVO_POSITION, - ) - - def _get_current_joints(self, state: CoordinatorState) -> NDArray[np.floating[Any]] | None: - """Get current joint positions from coordinator state.""" - positions = [] - for joint_name in self._joint_names_list: - pos = state.joints.get_position(joint_name) - if pos is None: - return None - positions.append(pos) - return np.array(positions) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - """Handle preemption by higher-priority task. - - Args: - by_task: Name of preempting task - joints: Joints that were preempted - """ - if joints & self._joint_names: - logger.warning(f"TeleopIKTask {self._name} preempted by {by_task} on joints {joints}") - - # ========================================================================= - # Task-specific methods - # ========================================================================= - - def on_buttons(self, msg: Buttons) -> bool: - """Press-and-hold engage: hold primary button to track, release to stop. - - Checks only the button matching self._config.hand (left_primary or right_primary). - If hand is not set, listens to both. - """ - hand = self._config.hand - if hand == "left": - primary = msg.left_primary - elif hand == "right": - primary = msg.right_primary - else: - primary = msg.left_primary or msg.right_primary - - if primary and not self._prev_primary: - # Rising edge: reset initial pose so compute() recaptures - logger.info(f"TeleopIKTask {self._name}: engage") - with self._lock: - self._initial_ee_pose = None - elif not primary and self._prev_primary: - # Falling edge: stop tracking - logger.info(f"TeleopIKTask {self._name}: disengage") - with self._lock: - self._target_pose = None - self._initial_ee_pose = None - self._prev_primary = primary - return True - - def on_cartesian_command(self, pose: Pose | PoseStamped, t_now: float) -> bool: - """Handle incoming cartesian command (delta pose from teleop)""" - with self._lock: - self._target_pose = pose # Store raw, convert to SE3 in compute() - self._last_update_time = t_now - self._active = True - - return True - - def start(self) -> None: - """Activate the task (start accepting and outputting commands).""" - with self._lock: - self._active = True - logger.info(f"TeleopIKTask {self._name} started") - - def stop(self) -> None: - """Deactivate the task (stop outputting commands).""" - with self._lock: - self._active = False - logger.info(f"TeleopIKTask {self._name} stopped") - - -__all__ = [ - "TeleopIKTask", - "TeleopIKTaskConfig", -] diff --git a/dimos/control/tasks/trajectory_task.py b/dimos/control/tasks/trajectory_task.py deleted file mode 100644 index 4d2eaa188b..0000000000 --- a/dimos/control/tasks/trajectory_task.py +++ /dev/null @@ -1,261 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Joint trajectory task for the ControlCoordinator. - -Passive trajectory execution - called by coordinator each tick. -Unlike JointTrajectoryController which owns a thread, this task -is compute-only and relies on the coordinator for timing. - -CRITICAL: Uses t_now from CoordinatorState, never calls time.time() -""" - -from __future__ import annotations - -from dataclasses import dataclass - -from dimos.control.task import ( - BaseControlTask, - ControlMode, - CoordinatorState, - JointCommandOutput, - ResourceClaim, -) -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryState -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class JointTrajectoryTaskConfig: - """Configuration for trajectory task. - - Attributes: - joint_names: List of joint names this task controls - priority: Priority for arbitration (higher wins) - """ - - joint_names: list[str] - priority: int = 10 - - -class JointTrajectoryTask(BaseControlTask): - """Passive trajectory execution task. - - Unlike JointTrajectoryController which owns a thread, this task - is called by the coordinator at each tick. - - CRITICAL: Uses t_now from CoordinatorState, never calls time.time() - - State Machine: - IDLE ──execute()──► EXECUTING ──done──► COMPLETED - ▲ │ │ - │ cancel() reset() - │ ▼ │ - └─────reset()───── ABORTED ◄──────────────┘ - - Example: - >>> task = JointTrajectoryTask( - ... name="traj_left", - ... config=JointTrajectoryTaskConfig( - ... joint_names=["left_joint1", "left_joint2"], - ... priority=10, - ... ), - ... ) - >>> coordinator.add_task(task) - >>> task.execute(my_trajectory, t_now=coordinator_t_now) - """ - - def __init__(self, name: str, config: JointTrajectoryTaskConfig) -> None: - """Initialize trajectory task. - - Args: - name: Unique task name - config: Task configuration - """ - if not config.joint_names: - raise ValueError(f"JointTrajectoryTask '{name}' requires at least one joint") - self._name = name - self._config = config - self._joint_names = frozenset(config.joint_names) - self._joint_names_list = list(config.joint_names) - - # State machine - self._state = TrajectoryState.IDLE - self._trajectory: JointTrajectory | None = None - self._start_time: float = 0.0 - self._pending_start: bool = False # Defer start time to first compute() - - logger.info(f"JointTrajectoryTask {name} initialized for joints: {config.joint_names}") - - @property - def name(self) -> str: - """Unique task identifier.""" - return self._name - - def claim(self) -> ResourceClaim: - """Declare resource requirements.""" - return ResourceClaim( - joints=self._joint_names, - priority=self._config.priority, - mode=ControlMode.SERVO_POSITION, - ) - - def is_active(self) -> bool: - """Check if task should run this tick.""" - return self._state == TrajectoryState.EXECUTING - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Compute trajectory output for this tick. - - CRITICAL: Uses state.t_now for timing, NOT time.time()! - - Args: - state: Current coordinator state - - Returns: - JointCommandOutput with positions, or None if not executing - """ - if self._trajectory is None: - return None - - # Set start time on first compute() for consistent timing - if self._pending_start: - self._start_time = state.t_now - self._pending_start = False - - t_elapsed = state.t_now - self._start_time - - # Check completion - clamp to final position to ensure we reach goal - if t_elapsed >= self._trajectory.duration: - self._state = TrajectoryState.COMPLETED - logger.info(f"Trajectory {self._name} completed after {t_elapsed:.3f}s") - # Return final position to hold at goal - q_ref, _ = self._trajectory.sample(self._trajectory.duration) - return JointCommandOutput( - joint_names=self._joint_names_list, - positions=list(q_ref), - mode=ControlMode.SERVO_POSITION, - ) - - # Sample trajectory - q_ref, _ = self._trajectory.sample(t_elapsed) - - return JointCommandOutput( - joint_names=self._joint_names_list, - positions=list(q_ref), - mode=ControlMode.SERVO_POSITION, - ) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - """Handle preemption by higher-priority task. - - Args: - by_task: Name of preempting task - joints: Joints that were preempted - """ - logger.warning(f"Trajectory {self._name} preempted by {by_task} on joints {joints}") - # Abort if any of our joints were preempted - if joints & self._joint_names: - self._state = TrajectoryState.ABORTED - - # ========================================================================= - # Task-specific methods - # ========================================================================= - - def execute(self, trajectory: JointTrajectory) -> bool: - """Start executing a trajectory. - - Args: - trajectory: Trajectory to execute - - Returns: - True if accepted, False if invalid or in FAULT state - """ - if self._state == TrajectoryState.FAULT: - logger.warning(f"Cannot execute: {self._name} in FAULT state") - return False - - if trajectory is None or trajectory.duration <= 0: - logger.warning(f"Invalid trajectory for {self._name}") - return False - - if not trajectory.points: - logger.warning(f"Empty trajectory for {self._name}") - return False - - # Preempt any active trajectory - if self._state == TrajectoryState.EXECUTING: - logger.info(f"Preempting active trajectory on {self._name}") - - self._trajectory = trajectory - self._pending_start = True # Start time set on first compute() - self._state = TrajectoryState.EXECUTING - - logger.info( - f"Executing trajectory on {self._name}: " - f"{len(trajectory.points)} points, duration={trajectory.duration:.3f}s" - ) - return True - - def cancel(self) -> bool: - """Cancel current trajectory. - - Returns: - True if cancelled, False if not executing - """ - if self._state != TrajectoryState.EXECUTING: - return False - self._state = TrajectoryState.ABORTED - logger.info(f"Trajectory {self._name} cancelled") - return True - - def reset(self) -> bool: - """Reset to idle state. - - Returns: - True if reset, False if currently executing - """ - if self._state == TrajectoryState.EXECUTING: - logger.warning(f"Cannot reset {self._name} while executing") - return False - self._state = TrajectoryState.IDLE - self._trajectory = None - logger.info(f"Trajectory {self._name} reset to IDLE") - return True - - def get_state(self) -> TrajectoryState: - """Get current state.""" - return self._state - - def get_progress(self, t_now: float) -> float: - """Get execution progress (0.0 to 1.0). - - Args: - t_now: Current coordinator time - - Returns: - Progress as fraction, or 0.0 if not executing - """ - if self._state != TrajectoryState.EXECUTING or self._trajectory is None: - return 0.0 - t_elapsed = t_now - self._start_time - return min(1.0, t_elapsed / self._trajectory.duration) - - -__all__ = [ - "JointTrajectoryTask", - "JointTrajectoryTaskConfig", -] diff --git a/dimos/control/tasks/velocity_task.py b/dimos/control/tasks/velocity_task.py deleted file mode 100644 index 163bc09827..0000000000 --- a/dimos/control/tasks/velocity_task.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Streaming joint velocity task for real-time velocity control. - -Accepts streaming joint velocities (e.g., from joystick) and outputs them -directly to hardware each tick. Useful for joystick control, force feedback, -or any velocity-mode real-time control. - -SAFETY: On timeout, sends zero velocities to stop motion (configurable). - -CRITICAL: Uses t_now from CoordinatorState, never calls time.time() -""" - -from __future__ import annotations - -from dataclasses import dataclass -import threading - -from dimos.control.task import ( - BaseControlTask, - ControlMode, - CoordinatorState, - JointCommandOutput, - ResourceClaim, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class JointVelocityTaskConfig: - """Configuration for velocity task. - - Attributes: - joint_names: List of joint names this task controls - priority: Priority for arbitration (higher wins) - timeout: If no command received for this many seconds, trigger timeout behavior - zero_on_timeout: If True, send zero velocities on timeout (safety). If False, go inactive. - """ - - joint_names: list[str] - priority: int = 10 - timeout: float = 0.2 # 200ms default - shorter for safety - zero_on_timeout: bool = True # Send zeros to stop motion - - -class JointVelocityTask(BaseControlTask): - """Streaming joint velocity control for joystick/force feedback. - - Accepts target velocities via set_velocities() or set_velocities_by_name() - and outputs them each tick. Uses VELOCITY mode for direct velocity control. - - SAFETY: On timeout (no update for timeout seconds): - - If zero_on_timeout=True: sends zero velocities to stop motion - - If zero_on_timeout=False: goes inactive (hardware may coast) - - Example: - >>> task = JointVelocityTask( - ... name="velocity_arm", - ... config=JointVelocityTaskConfig( - ... joint_names=["arm_joint1", "arm_joint2", "arm_joint3"], - ... priority=10, - ... timeout=0.2, - ... zero_on_timeout=True, - ... ), - ... ) - >>> coordinator.add_task(task) - >>> task.start() - >>> - >>> # From joystick callback: - >>> task.set_velocities([0.1, -0.05, 0.0], t_now=time.perf_counter()) - """ - - def __init__(self, name: str, config: JointVelocityTaskConfig) -> None: - """Initialize velocity task. - - Args: - name: Unique task name - config: Task configuration - """ - if not config.joint_names: - raise ValueError(f"JointVelocityTask '{name}' requires at least one joint") - - self._name = name - self._config = config - self._joint_names = frozenset(config.joint_names) - self._joint_names_list = list(config.joint_names) - self._num_joints = len(config.joint_names) - - # Current velocities (thread-safe) - self._lock = threading.Lock() - self._velocities: list[float] | None = None - self._last_update_time: float = 0.0 - self._active = False - self._timed_out = False # Track timeout state for logging - - logger.info(f"JointVelocityTask {name} initialized for joints: {config.joint_names}") - - @property - def name(self) -> str: - """Unique task identifier.""" - return self._name - - def claim(self) -> ResourceClaim: - """Declare resource requirements.""" - return ResourceClaim( - joints=self._joint_names, - priority=self._config.priority, - mode=ControlMode.VELOCITY, - ) - - def is_active(self) -> bool: - """Check if task should run this tick.""" - with self._lock: - # Active if started, even if timed out (we still send zeros) - if self._config.zero_on_timeout: - return self._active - else: - return self._active and self._velocities is not None - - def compute(self, state: CoordinatorState) -> JointCommandOutput | None: - """Output current target velocities. - - Args: - state: Current coordinator state - - Returns: - JointCommandOutput with velocities, or None if inactive - """ - with self._lock: - if not self._active: - return None - - # Check timeout - if self._config.timeout > 0 and self._velocities is not None: - time_since_update = state.t_now - self._last_update_time - if time_since_update > self._config.timeout: - if not self._timed_out: - logger.warning( - f"JointVelocityTask {self._name} timed out " - f"(no update for {time_since_update:.3f}s)" - ) - self._timed_out = True - - if self._config.zero_on_timeout: - # SAFETY: Send zeros to stop motion - return JointCommandOutput( - joint_names=self._joint_names_list, - velocities=[0.0] * self._num_joints, - mode=ControlMode.VELOCITY, - ) - else: - # Go inactive - self._active = False - return None - - if self._velocities is None: - return None - - # Reset timeout flag on successful output - self._timed_out = False - - return JointCommandOutput( - joint_names=self._joint_names_list, - velocities=list(self._velocities), - mode=ControlMode.VELOCITY, - ) - - def on_preempted(self, by_task: str, joints: frozenset[str]) -> None: - """Handle preemption by higher-priority task. - - Args: - by_task: Name of preempting task - joints: Joints that were preempted - """ - if joints & self._joint_names: - logger.warning( - f"JointVelocityTask {self._name} preempted by {by_task} on joints {joints}" - ) - - # ========================================================================= - # Task-specific methods - # ========================================================================= - - def set_velocities(self, velocities: list[float], t_now: float) -> bool: - """Set target joint velocities. - - Call this from your joystick callback or other data source. - - Args: - velocities: Joint velocities in rad/s (must match joint_names length) - t_now: Current time (from coordinator or time.perf_counter()) - - Returns: - True if accepted, False if wrong number of joints - """ - if len(velocities) != self._num_joints: - logger.warning( - f"JointVelocityTask {self._name}: expected {self._num_joints} " - f"velocities, got {len(velocities)}" - ) - return False - - with self._lock: - self._velocities = list(velocities) - self._last_update_time = t_now - self._active = True - self._timed_out = False - - return True - - def set_velocities_by_name(self, velocities: dict[str, float], t_now: float) -> bool: - """Set target velocities by joint name. - - Extracts only the joints this task controls from the dict. - Useful for routing when multiple tasks share an input stream. - - Args: - velocities: {joint_name: velocity} dict (can contain extra joints) - t_now: Current time - - Returns: - True if all required joints found, False if any missing - """ - ordered = [] - for name in self._joint_names_list: - if name not in velocities: - # Missing joint - don't update - return False - ordered.append(velocities[name]) - - return self.set_velocities(ordered, t_now) - - def start(self) -> None: - """Activate the task (start accepting and outputting commands).""" - with self._lock: - self._active = True - self._timed_out = False - logger.info(f"JointVelocityTask {self._name} started") - - def stop(self) -> None: - """Deactivate the task (stop outputting commands).""" - with self._lock: - self._active = False - logger.info(f"JointVelocityTask {self._name} stopped") - - def clear(self) -> None: - """Clear current velocities and deactivate.""" - with self._lock: - self._velocities = None - self._active = False - self._timed_out = False - logger.info(f"JointVelocityTask {self._name} cleared") - - def is_streaming(self) -> bool: - """Check if actively receiving and outputting commands.""" - with self._lock: - return self._active and self._velocities is not None and not self._timed_out - - -__all__ = [ - "JointVelocityTask", - "JointVelocityTaskConfig", -] diff --git a/dimos/control/test_control.py b/dimos/control/test_control.py deleted file mode 100644 index 656678d167..0000000000 --- a/dimos/control/test_control.py +++ /dev/null @@ -1,557 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for the Control Coordinator module.""" - -from __future__ import annotations - -import threading -import time -from unittest.mock import MagicMock - -import pytest - -from dimos.control.components import HardwareComponent, HardwareType, make_joints -from dimos.control.hardware_interface import ConnectedHardware -from dimos.control.task import ( - ControlMode, - CoordinatorState, - JointCommandOutput, - JointStateSnapshot, - ResourceClaim, -) -from dimos.control.tasks.trajectory_task import ( - JointTrajectoryTask, - JointTrajectoryTaskConfig, - TrajectoryState, -) -from dimos.control.tick_loop import TickLoop -from dimos.hardware.manipulators.spec import ManipulatorAdapter -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryPoint - -# ============================================================================= -# Fixtures -# ============================================================================= - - -@pytest.fixture -def mock_adapter(): - """Create a mock manipulator adapter.""" - adapter = MagicMock(spec=ManipulatorAdapter) - adapter.get_dof.return_value = 6 - adapter.read_joint_positions.return_value = [0.0] * 6 - adapter.read_joint_velocities.return_value = [0.0] * 6 - adapter.read_joint_efforts.return_value = [0.0] * 6 - adapter.write_joint_positions.return_value = True - adapter.write_joint_velocities.return_value = True - adapter.set_control_mode.return_value = True - return adapter - - -@pytest.fixture -def connected_hardware(mock_adapter): - """Create a ConnectedHardware instance with mock adapter.""" - component = HardwareComponent( - hardware_id="test_arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - ) - return ConnectedHardware(adapter=mock_adapter, component=component) - - -@pytest.fixture -def trajectory_task(): - """Create a JointTrajectoryTask for testing.""" - config = JointTrajectoryTaskConfig( - joint_names=["arm_joint1", "arm_joint2", "arm_joint3"], - priority=10, - ) - return JointTrajectoryTask(name="test_traj", config=config) - - -@pytest.fixture -def simple_trajectory(): - """Create a simple 2-point trajectory.""" - return JointTrajectory( - joint_names=["arm_joint1", "arm_joint2", "arm_joint3"], - points=[ - TrajectoryPoint( - positions=[0.0, 0.0, 0.0], - velocities=[0.0, 0.0, 0.0], - time_from_start=0.0, - ), - TrajectoryPoint( - positions=[1.0, 0.5, 0.25], - velocities=[0.0, 0.0, 0.0], - time_from_start=1.0, - ), - ], - ) - - -@pytest.fixture -def coordinator_state(): - """Create a sample CoordinatorState.""" - joints = JointStateSnapshot( - joint_positions={"arm_joint1": 0.0, "arm_joint2": 0.0, "arm_joint3": 0.0}, - joint_velocities={"arm_joint1": 0.0, "arm_joint2": 0.0, "arm_joint3": 0.0}, - joint_efforts={"arm_joint1": 0.0, "arm_joint2": 0.0, "arm_joint3": 0.0}, - timestamp=time.perf_counter(), - ) - return CoordinatorState(joints=joints, t_now=time.perf_counter(), dt=0.01) - - -# ============================================================================= -# Test JointCommandOutput -# ============================================================================= - - -class TestJointCommandOutput: - def test_position_output(self): - output = JointCommandOutput( - joint_names=["j1", "j2"], - positions=[0.5, 1.0], - mode=ControlMode.POSITION, - ) - assert output.get_values() == [0.5, 1.0] - assert output.mode == ControlMode.POSITION - - def test_velocity_output(self): - output = JointCommandOutput( - joint_names=["j1", "j2"], - velocities=[0.1, 0.2], - mode=ControlMode.VELOCITY, - ) - assert output.get_values() == [0.1, 0.2] - assert output.mode == ControlMode.VELOCITY - - def test_torque_output(self): - output = JointCommandOutput( - joint_names=["j1", "j2"], - efforts=[5.0, 10.0], - mode=ControlMode.TORQUE, - ) - assert output.get_values() == [5.0, 10.0] - assert output.mode == ControlMode.TORQUE - - def test_no_values_returns_none(self): - output = JointCommandOutput( - joint_names=["j1"], - mode=ControlMode.POSITION, - ) - assert output.get_values() is None - - -# ============================================================================= -# Test JointStateSnapshot -# ============================================================================= - - -class TestJointStateSnapshot: - def test_get_position(self): - snapshot = JointStateSnapshot( - joint_positions={"j1": 0.5, "j2": 1.0}, - joint_velocities={"j1": 0.0, "j2": 0.1}, - joint_efforts={"j1": 1.0, "j2": 2.0}, - timestamp=100.0, - ) - assert snapshot.get_position("j1") == 0.5 - assert snapshot.get_position("j2") == 1.0 - assert snapshot.get_position("nonexistent") is None - - -# ============================================================================= -# Test ConnectedHardware -# ============================================================================= - - -class TestConnectedHardware: - def test_joint_names_prefixed(self, connected_hardware): - names = connected_hardware.joint_names - assert names == [ - "arm_joint1", - "arm_joint2", - "arm_joint3", - "arm_joint4", - "arm_joint5", - "arm_joint6", - ] - - def test_read_state(self, connected_hardware): - state = connected_hardware.read_state() - assert "arm_joint1" in state - assert len(state) == 6 - joint_state = state["arm_joint1"] - assert joint_state.position == 0.0 - assert joint_state.velocity == 0.0 - assert joint_state.effort == 0.0 - - def test_write_command(self, connected_hardware, mock_adapter): - commands = { - "arm_joint1": 0.5, - "arm_joint2": 1.0, - } - connected_hardware.write_command(commands, ControlMode.POSITION) - mock_adapter.write_joint_positions.assert_called() - - -# ============================================================================= -# Test JointTrajectoryTask -# ============================================================================= - - -class TestJointTrajectoryTask: - def test_initial_state(self, trajectory_task): - assert trajectory_task.name == "test_traj" - assert not trajectory_task.is_active() - assert trajectory_task.get_state() == TrajectoryState.IDLE - - def test_claim(self, trajectory_task): - claim = trajectory_task.claim() - assert claim.priority == 10 - assert "arm_joint1" in claim.joints - assert "arm_joint2" in claim.joints - assert "arm_joint3" in claim.joints - - def test_execute_trajectory(self, trajectory_task, simple_trajectory): - time.perf_counter() - result = trajectory_task.execute(simple_trajectory) - assert result is True - assert trajectory_task.is_active() - assert trajectory_task.get_state() == TrajectoryState.EXECUTING - - def test_compute_during_trajectory(self, trajectory_task, simple_trajectory, coordinator_state): - t_start = time.perf_counter() - trajectory_task.execute(simple_trajectory) - - # First compute sets start time (deferred start) - state0 = CoordinatorState( - joints=coordinator_state.joints, - t_now=t_start, - dt=0.01, - ) - trajectory_task.compute(state0) - - # Compute at 0.5s into trajectory - state = CoordinatorState( - joints=coordinator_state.joints, - t_now=t_start + 0.5, - dt=0.01, - ) - output = trajectory_task.compute(state) - - assert output is not None - assert output.mode == ControlMode.SERVO_POSITION - assert len(output.positions) == 3 - assert 0.4 < output.positions[0] < 0.6 - - def test_trajectory_completes(self, trajectory_task, simple_trajectory, coordinator_state): - t_start = time.perf_counter() - trajectory_task.execute(simple_trajectory) - - # First compute sets start time (deferred start) - state0 = CoordinatorState( - joints=coordinator_state.joints, - t_now=t_start, - dt=0.01, - ) - trajectory_task.compute(state0) - - # Compute past trajectory duration - state = CoordinatorState( - joints=coordinator_state.joints, - t_now=t_start + 1.5, - dt=0.01, - ) - output = trajectory_task.compute(state) - - # On completion, returns final position (not None) to hold at goal - assert output is not None - assert output.positions == [1.0, 0.5, 0.25] # Final trajectory point - assert not trajectory_task.is_active() - assert trajectory_task.get_state() == TrajectoryState.COMPLETED - - def test_cancel_trajectory(self, trajectory_task, simple_trajectory): - trajectory_task.execute(simple_trajectory) - assert trajectory_task.is_active() - - trajectory_task.cancel() - assert not trajectory_task.is_active() - assert trajectory_task.get_state() == TrajectoryState.ABORTED - - def test_preemption(self, trajectory_task, simple_trajectory): - trajectory_task.execute(simple_trajectory) - - trajectory_task.on_preempted("safety_task", frozenset({"arm_joint1"})) - assert trajectory_task.get_state() == TrajectoryState.ABORTED - assert not trajectory_task.is_active() - - def test_progress(self, trajectory_task, simple_trajectory, coordinator_state): - t_start = time.perf_counter() - trajectory_task.execute(simple_trajectory) - - # First compute sets start time (deferred start) - state0 = CoordinatorState( - joints=coordinator_state.joints, - t_now=t_start, - dt=0.01, - ) - trajectory_task.compute(state0) - - assert trajectory_task.get_progress(t_start) == pytest.approx(0.0, abs=0.01) - assert trajectory_task.get_progress(t_start + 0.5) == pytest.approx(0.5, abs=0.01) - assert trajectory_task.get_progress(t_start + 1.0) == pytest.approx(1.0, abs=0.01) - - -# ============================================================================= -# Test Arbitration Logic -# ============================================================================= - - -class TestArbitration: - def test_single_task_wins(self): - outputs = [ - ( - MagicMock(name="task1"), - ResourceClaim(joints=frozenset({"j1"}), priority=10), - JointCommandOutput(joint_names=["j1"], positions=[0.5], mode=ControlMode.POSITION), - ), - ] - - winners = {} - for task, claim, output in outputs: - if output is None: - continue - values = output.get_values() - if values is None: - continue - for i, joint in enumerate(output.joint_names): - if joint not in winners: - winners[joint] = (claim.priority, values[i], output.mode, task.name) - - assert "j1" in winners - assert winners["j1"][1] == 0.5 - - def test_higher_priority_wins(self): - task_low = MagicMock() - task_low.name = "low_priority" - task_high = MagicMock() - task_high.name = "high_priority" - - outputs = [ - ( - task_low, - ResourceClaim(joints=frozenset({"j1"}), priority=10), - JointCommandOutput(joint_names=["j1"], positions=[0.5], mode=ControlMode.POSITION), - ), - ( - task_high, - ResourceClaim(joints=frozenset({"j1"}), priority=100), - JointCommandOutput(joint_names=["j1"], positions=[0.0], mode=ControlMode.POSITION), - ), - ] - - winners = {} - for task, claim, output in outputs: - if output is None: - continue - values = output.get_values() - if values is None: - continue - for i, joint in enumerate(output.joint_names): - if joint not in winners: - winners[joint] = (claim.priority, values[i], output.mode, task.name) - elif claim.priority > winners[joint][0]: - winners[joint] = (claim.priority, values[i], output.mode, task.name) - - assert winners["j1"][3] == "high_priority" - assert winners["j1"][1] == 0.0 - - def test_non_overlapping_joints(self): - task1 = MagicMock() - task1.name = "task1" - task2 = MagicMock() - task2.name = "task2" - - outputs = [ - ( - task1, - ResourceClaim(joints=frozenset({"j1", "j2"}), priority=10), - JointCommandOutput( - joint_names=["j1", "j2"], - positions=[0.5, 0.6], - mode=ControlMode.POSITION, - ), - ), - ( - task2, - ResourceClaim(joints=frozenset({"j3", "j4"}), priority=10), - JointCommandOutput( - joint_names=["j3", "j4"], - positions=[0.7, 0.8], - mode=ControlMode.POSITION, - ), - ), - ] - - winners = {} - for task, claim, output in outputs: - if output is None: - continue - values = output.get_values() - if values is None: - continue - for i, joint in enumerate(output.joint_names): - if joint not in winners: - winners[joint] = (claim.priority, values[i], output.mode, task.name) - - assert winners["j1"][3] == "task1" - assert winners["j2"][3] == "task1" - assert winners["j3"][3] == "task2" - assert winners["j4"][3] == "task2" - - -# ============================================================================= -# Test TickLoop -# ============================================================================= - - -class TestTickLoop: - def test_tick_loop_starts_and_stops(self, mock_adapter): - component = HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - ) - hw = ConnectedHardware(mock_adapter, component) - hardware = {"arm": hw} - tasks: dict = {} - joint_to_hardware = {f"arm_joint{i + 1}": "arm" for i in range(6)} - - tick_loop = TickLoop( - tick_rate=100.0, - hardware=hardware, - hardware_lock=threading.Lock(), - tasks=tasks, - task_lock=threading.Lock(), - joint_to_hardware=joint_to_hardware, - ) - - tick_loop.start() - time.sleep(0.05) - assert tick_loop.tick_count > 0 - - tick_loop.stop() - final_count = tick_loop.tick_count - time.sleep(0.02) - assert tick_loop.tick_count == final_count - - def test_tick_loop_calls_compute(self, mock_adapter): - component = HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - ) - hw = ConnectedHardware(mock_adapter, component) - hardware = {"arm": hw} - - mock_task = MagicMock() - mock_task.name = "test_task" - mock_task.is_active.return_value = True - mock_task.claim.return_value = ResourceClaim( - joints=frozenset({"arm_joint1"}), - priority=10, - ) - mock_task.compute.return_value = JointCommandOutput( - joint_names=["arm_joint1"], - positions=[0.5], - mode=ControlMode.POSITION, - ) - - tasks = {"test_task": mock_task} - joint_to_hardware = {f"arm_joint{i + 1}": "arm" for i in range(6)} - - tick_loop = TickLoop( - tick_rate=100.0, - hardware=hardware, - hardware_lock=threading.Lock(), - tasks=tasks, - task_lock=threading.Lock(), - joint_to_hardware=joint_to_hardware, - ) - - tick_loop.start() - time.sleep(0.05) - tick_loop.stop() - - assert mock_task.compute.call_count > 0 - - -# ============================================================================= -# Integration Test -# ============================================================================= - - -class TestIntegration: - def test_full_trajectory_execution(self, mock_adapter): - component = HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - ) - hw = ConnectedHardware(mock_adapter, component) - hardware = {"arm": hw} - - config = JointTrajectoryTaskConfig( - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - ) - traj_task = JointTrajectoryTask(name="traj_arm", config=config) - tasks = {"traj_arm": traj_task} - - joint_to_hardware = {f"arm_joint{i + 1}": "arm" for i in range(6)} - - tick_loop = TickLoop( - tick_rate=100.0, - hardware=hardware, - hardware_lock=threading.Lock(), - tasks=tasks, - task_lock=threading.Lock(), - joint_to_hardware=joint_to_hardware, - ) - - trajectory = JointTrajectory( - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - points=[ - TrajectoryPoint( - positions=[0.0] * 6, - velocities=[0.0] * 6, - time_from_start=0.0, - ), - TrajectoryPoint( - positions=[0.5] * 6, - velocities=[0.0] * 6, - time_from_start=0.5, - ), - ], - ) - - tick_loop.start() - traj_task.execute(trajectory) - - time.sleep(0.6) - tick_loop.stop() - - assert traj_task.get_state() == TrajectoryState.COMPLETED - assert mock_adapter.write_joint_positions.call_count > 0 diff --git a/dimos/control/tick_loop.py b/dimos/control/tick_loop.py deleted file mode 100644 index e0020a34da..0000000000 --- a/dimos/control/tick_loop.py +++ /dev/null @@ -1,400 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tick loop for the ControlCoordinator. - -This module contains the core control loop logic: -- Read state from all hardware -- Compute outputs from all active tasks -- Arbitrate conflicts per-joint (highest priority wins) -- Route commands to hardware -- Publish aggregated joint state - -Separated from coordinator.py following the DimOS pattern of -splitting coordination logic from module wrapper. -""" - -from __future__ import annotations - -import threading -import time -from typing import TYPE_CHECKING, NamedTuple - -from dimos.control.task import ( - ControlTask, - CoordinatorState, - JointCommandOutput, - JointStateSnapshot, - ResourceClaim, -) -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - - from dimos.control.components import HardwareId, JointName, TaskName - from dimos.control.hardware_interface import ConnectedHardware - from dimos.hardware.manipulators.spec import ControlMode - -logger = setup_logger() - - -class JointWinner(NamedTuple): - """Tracks the winning task for a joint during arbitration.""" - - priority: int - value: float - mode: ControlMode - task_name: str - - -class TickLoop: - """Core tick loop for the control coordinator. - - Runs the deterministic control cycle: - 1. READ: Collect joint state from all hardware - 2. COMPUTE: Run all active tasks - 3. ARBITRATE: Per-joint conflict resolution (highest priority wins) - 4. NOTIFY: Send preemption notifications to affected tasks - 5. ROUTE: Convert joint-centric commands to hardware-centric - 6. WRITE: Send commands to hardware - 7. PUBLISH: Output aggregated JointState - - Args: - tick_rate: Control loop frequency in Hz - hardware: Dict of hardware_id -> ConnectedHardware - hardware_lock: Lock protecting hardware dict - tasks: Dict of task_name -> ControlTask - task_lock: Lock protecting tasks dict - joint_to_hardware: Dict mapping joint_name -> hardware_id - publish_callback: Optional callback to publish JointState - frame_id: Frame ID for published JointState - log_ticks: Whether to log tick information - """ - - def __init__( - self, - tick_rate: float, - hardware: dict[HardwareId, ConnectedHardware], - hardware_lock: threading.Lock, - tasks: dict[TaskName, ControlTask], - task_lock: threading.Lock, - joint_to_hardware: dict[JointName, HardwareId], - publish_callback: Callable[[JointState], None] | None = None, - frame_id: str = "coordinator", - log_ticks: bool = False, - ) -> None: - self._tick_rate = tick_rate - self._hardware = hardware - self._hardware_lock = hardware_lock - self._tasks = tasks - self._task_lock = task_lock - self._joint_to_hardware = joint_to_hardware - self._publish_callback = publish_callback - self._frame_id = frame_id - self._log_ticks = log_ticks - - self._stop_event = threading.Event() - self._stop_event.set() # Initially stopped - self._tick_thread: threading.Thread | None = None - self._last_tick_time: float = 0.0 - self._tick_count: int = 0 - - @property - def tick_count(self) -> int: - """Number of ticks since start.""" - return self._tick_count - - @property - def is_running(self) -> bool: - """Whether the tick loop is currently running.""" - return not self._stop_event.is_set() - - def start(self) -> None: - """Start the tick loop in a daemon thread.""" - if not self._stop_event.is_set(): - logger.warning("TickLoop already running") - return - - self._stop_event.clear() - self._last_tick_time = time.perf_counter() - self._tick_count = 0 - - self._tick_thread = threading.Thread( - target=self._loop, - name="ControlCoordinator-Tick", - daemon=True, - ) - self._tick_thread.start() - logger.info(f"TickLoop started at {self._tick_rate}Hz") - - def stop(self) -> None: - """Stop the tick loop.""" - self._stop_event.set() - if self._tick_thread and self._tick_thread.is_alive(): - self._tick_thread.join(timeout=2.0) - logger.info("TickLoop stopped") - - def _loop(self) -> None: - """Main control loop - deterministic read → compute → arbitrate → write.""" - period = 1.0 / self._tick_rate - - while not self._stop_event.is_set(): - tick_start = time.perf_counter() - - try: - self._tick() - except Exception as e: - logger.error(f"TickLoop tick error: {e}") - - # Rate control - recalculate sleep time to account for overhead - next_tick_time = tick_start + period - sleep_time = next_tick_time - time.perf_counter() - if sleep_time > 0: - time.sleep(sleep_time) - - def _tick(self) -> None: - """Single tick: read → compute → arbitrate → route → write.""" - t_now = time.perf_counter() - dt = t_now - self._last_tick_time - self._last_tick_time = t_now - self._tick_count += 1 - - # === PHASE 1: READ ALL HARDWARE === - joint_states = self._read_all_hardware() - state = CoordinatorState(joints=joint_states, t_now=t_now, dt=dt) - - # === PHASE 2: COMPUTE ALL ACTIVE TASKS === - commands = self._compute_all_tasks(state) - - # === PHASE 3: ARBITRATE (with mode validation) === - joint_commands, preemptions = self._arbitrate(commands) - - # === PHASE 4: NOTIFY PREEMPTIONS (once per task) === - self._notify_preemptions(preemptions) - - # === PHASE 5: ROUTE TO HARDWARE === - hw_commands = self._route_to_hardware(joint_commands) - - # === PHASE 6: WRITE TO HARDWARE === - self._write_all_hardware(hw_commands) - - # === PHASE 7: PUBLISH AGGREGATED STATE === - if self._publish_callback: - self._publish_joint_state(joint_states) - - # Optional logging - if self._log_ticks: - active = len([c for c in commands if c[2] is not None]) - logger.debug( - f"Tick {self._tick_count}: dt={dt:.4f}s, " - f"{len(joint_states.joint_positions)} joints, " - f"{active} active tasks" - ) - - def _read_all_hardware(self) -> JointStateSnapshot: - """Read state from all hardware interfaces.""" - joint_positions: dict[str, float] = {} - joint_velocities: dict[str, float] = {} - joint_efforts: dict[str, float] = {} - - with self._hardware_lock: - for hw in self._hardware.values(): - try: - state = hw.read_state() - for joint_name, joint_state in state.items(): - joint_positions[joint_name] = joint_state.position - joint_velocities[joint_name] = joint_state.velocity - joint_efforts[joint_name] = joint_state.effort - except Exception as e: - logger.error(f"Failed to read {hw.hardware_id}: {e}") - - return JointStateSnapshot( - joint_positions=joint_positions, - joint_velocities=joint_velocities, - joint_efforts=joint_efforts, - timestamp=time.time(), - ) - - def _compute_all_tasks( - self, state: CoordinatorState - ) -> list[tuple[ControlTask, ResourceClaim, JointCommandOutput | None]]: - """Compute outputs from all active tasks.""" - results: list[tuple[ControlTask, ResourceClaim, JointCommandOutput | None]] = [] - - with self._task_lock: - for task in self._tasks.values(): - if not task.is_active(): - continue - - try: - claim = task.claim() - output = task.compute(state) - results.append((task, claim, output)) - except Exception as e: - logger.error(f"Task {task.name} compute error: {e}") - - return results - - def _arbitrate( - self, - commands: list[tuple[ControlTask, ResourceClaim, JointCommandOutput | None]], - ) -> tuple[ - dict[str, tuple[float, ControlMode, str]], - dict[str, dict[str, str]], - ]: - """Per-joint arbitration with mode conflict detection. - - Returns: - Tuple of: - - joint_commands: {joint_name: (value, mode, task_name)} - - preemptions: {preempted_task: {joint: winning_task}} - """ - winners: dict[str, JointWinner] = {} # joint_name -> current winner - preemptions: dict[str, dict[str, str]] = {} # loser_task -> {joint: winner_task} - - for task, claim, output in commands: - if output is None: - continue - - values = output.get_values() - if values is None: - continue - - for i, joint_name in enumerate(output.joint_names): - candidate = JointWinner(claim.priority, values[i], output.mode, task.name) - - # First claim on this joint - if joint_name not in winners: - winners[joint_name] = candidate - continue - - current = winners[joint_name] - - # Lower priority loses - notify preemption - if candidate.priority < current.priority: - preemptions.setdefault(task.name, {})[joint_name] = current.task_name - continue - - # Higher priority - take over - if candidate.priority > current.priority: - preemptions.setdefault(current.task_name, {})[joint_name] = task.name - winners[joint_name] = candidate - continue - - # Same priority - check for mode conflict - if candidate.mode != current.mode: - logger.warning( - f"Mode conflict on {joint_name}: {task.name} wants " - f"{candidate.mode.name}, but {current.task_name} wants " - f"{current.mode.name}. Dropping {task.name}." - ) - preemptions.setdefault(task.name, {})[joint_name] = current.task_name - # Same priority + same mode: first wins (keep current) - - # Convert to output format: joint -> (value, mode, task_name) - joint_commands = {joint: (w.value, w.mode, w.task_name) for joint, w in winners.items()} - - return joint_commands, preemptions - - def _notify_preemptions(self, preemptions: dict[str, dict[str, str]]) -> None: - """Notify each preempted task with affected joints, grouped by winning task.""" - with self._task_lock: - for task_name, joint_winners in preemptions.items(): - task = self._tasks.get(task_name) - if not task: - continue - - # Group joints by winning task - by_winner: dict[str, set[str]] = {} - for joint, winner in joint_winners.items(): - if winner not in by_winner: - by_winner[winner] = set() - by_winner[winner].add(joint) - - # Notify once per distinct winning task - for winner, joints in by_winner.items(): - try: - task.on_preempted( - by_task=winner, - joints=frozenset(joints), - ) - except Exception as e: - logger.error(f"Error notifying {task_name} of preemption: {e}") - - def _route_to_hardware( - self, - joint_commands: dict[str, tuple[float, ControlMode, str]], - ) -> dict[str, tuple[dict[str, float], ControlMode]]: - """Route joint-centric commands to hardware. - - Returns: - {hardware_id: ({joint: value}, mode)} - """ - hw_commands: dict[str, tuple[dict[str, float], ControlMode]] = {} - - with self._hardware_lock: - for joint_name, (value, mode, _) in joint_commands.items(): - hw_id = self._joint_to_hardware.get(joint_name) - if hw_id is None: - logger.warning(f"Unknown joint {joint_name}, cannot route") - continue - - if hw_id not in hw_commands: - hw_commands[hw_id] = ({}, mode) - else: - # Check for mode conflict across joints on same hardware - existing_mode = hw_commands[hw_id][1] - if mode != existing_mode: - logger.error( - f"Mode conflict for hardware {hw_id}: joint {joint_name} wants " - f"{mode.name} but hardware already has {existing_mode.name}. " - f"Dropping command for {joint_name}." - ) - continue - - hw_commands[hw_id][0][joint_name] = value - - return hw_commands - - def _write_all_hardware( - self, - hw_commands: dict[str, tuple[dict[str, float], ControlMode]], - ) -> None: - """Write commands to all hardware interfaces.""" - with self._hardware_lock: - for hw_id, (positions, mode) in hw_commands.items(): - if hw_id in self._hardware: - try: - self._hardware[hw_id].write_command(positions, mode) - except Exception as e: - logger.error(f"Failed to write to {hw_id}: {e}") - - def _publish_joint_state(self, snapshot: JointStateSnapshot) -> None: - """Publish aggregated JointState for external consumers.""" - names = list(snapshot.joint_positions.keys()) - msg = JointState( - ts=snapshot.timestamp, - frame_id=self._frame_id, - name=names, - position=[snapshot.joint_positions[n] for n in names], - velocity=[snapshot.joint_velocities.get(n, 0.0) for n in names], - effort=[snapshot.joint_efforts.get(n, 0.0) for n in names], - ) - if self._publish_callback: - self._publish_callback(msg) - - -__all__ = ["TickLoop"] diff --git a/dimos/core/__init__.py b/dimos/core/__init__.py deleted file mode 100644 index 2b6296b623..0000000000 --- a/dimos/core/__init__.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import annotations - -import multiprocessing as mp -import time -from typing import TYPE_CHECKING, cast - -import lazy_loader as lazy -from rich.console import Console - -from dimos.core.core import rpc -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - # Avoid runtime import to prevent circular import; ruff's TC001 would otherwise move it. - from dask.distributed import LocalCluster - - from dimos.core._dask_exports import DimosCluster - from dimos.core.module import Module - from dimos.core.rpc_client import ModuleProxy - -logger = setup_logger() - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submodules=["colors"], - submod_attrs={ - "blueprints": ["autoconnect", "Blueprint"], - "_dask_exports": ["DimosCluster"], - "_protocol_exports": ["LCMRPC", "RPCSpec", "LCMTF", "TF", "PubSubTF", "TFConfig", "TFSpec"], - "module": ["Module", "ModuleBase", "ModuleConfig", "ModuleConfigT"], - "stream": ["In", "Out", "RemoteIn", "RemoteOut", "Transport"], - "transport": [ - "LCMTransport", - "SHMTransport", - "ZenohTransport", - "pLCMTransport", - "pSHMTransport", - ], - }, -) -__all__ += ["DimosCluster", "Module", "rpc", "start", "wait_exit"] - - -class CudaCleanupPlugin: - """Dask worker plugin to cleanup CUDA resources on shutdown.""" - - def setup(self, worker) -> None: # type: ignore[no-untyped-def] - """Called when worker starts.""" - pass - - def teardown(self, worker) -> None: # type: ignore[no-untyped-def] - """Clean up CUDA resources when worker shuts down.""" - try: - import sys - - if "cupy" in sys.modules: - import cupy as cp # type: ignore[import-not-found, import-untyped] - - # Clear memory pools - mempool = cp.get_default_memory_pool() - pinned_mempool = cp.get_default_pinned_memory_pool() - mempool.free_all_blocks() - pinned_mempool.free_all_blocks() - cp.cuda.Stream.null.synchronize() - mempool.free_all_blocks() - pinned_mempool.free_all_blocks() - except Exception: - pass - - -def patch_actor(actor, cls) -> None: ... # type: ignore[no-untyped-def] - - -def patchdask(dask_client: DimosCluster, local_cluster: LocalCluster) -> DimosCluster: - from dimos.core.rpc_client import RPCClient - from dimos.utils.actor_registry import ActorRegistry - - def deploy( # type: ignore[no-untyped-def] - actor_class: type[Module], - *args, - **kwargs, - ) -> ModuleProxy: - from dimos.core.docker_runner import DockerModule, is_docker_module - - # Check if this module should run in Docker (based on its default_config) - if is_docker_module(actor_class): - logger.info("Deploying module in Docker.", module=actor_class.__name__) - dm = DockerModule(actor_class, *args, **kwargs) - dm.start() # Explicit start - follows create -> configure -> start lifecycle - dask_client._docker_modules.append(dm) # type: ignore[attr-defined] - return dm # type: ignore[return-value] - - logger.info("Deploying module.", module=actor_class.__name__) - actor = dask_client.submit( # type: ignore[no-untyped-call] - actor_class, - *args, - **kwargs, - actor=True, - ).result() - - worker = actor.set_ref(actor).result() - logger.info("Deployed module.", module=actor._cls.__name__, worker_id=worker) - - # Register actor deployment in shared memory - ActorRegistry.update(str(actor), str(worker)) - - return cast("ModuleProxy", RPCClient(actor, actor_class)) - - def check_worker_memory() -> None: - """Check memory usage of all workers.""" - info = dask_client.scheduler_info() - - console = Console() - total_workers = len(info.get("workers", {})) - total_memory_used = 0 - total_memory_limit = 0 - - for worker_addr, worker_info in info.get("workers", {}).items(): - metrics = worker_info.get("metrics", {}) - memory_used = metrics.get("memory", 0) - memory_limit = worker_info.get("memory_limit", 0) - - cpu_percent = metrics.get("cpu", 0) - managed_bytes = metrics.get("managed_bytes", 0) - spilled = metrics.get("spilled_bytes", {}).get("memory", 0) - worker_status = worker_info.get("status", "unknown") - worker_id = worker_info.get("id", "?") - - memory_used_gb = memory_used / 1e9 - memory_limit_gb = memory_limit / 1e9 - managed_gb = managed_bytes / 1e9 - spilled / 1e9 - - total_memory_used += memory_used - total_memory_limit += memory_limit - - percentage = (memory_used_gb / memory_limit_gb * 100) if memory_limit_gb > 0 else 0 - - if worker_status == "paused": - status = "[red]PAUSED" - elif percentage >= 95: - status = "[red]CRITICAL" - elif percentage >= 80: - status = "[yellow]WARNING" - else: - status = "[green]OK" - - console.print( - f"Worker-{worker_id} {worker_addr}: " - f"{memory_used_gb:.2f}/{memory_limit_gb:.2f}GB ({percentage:.1f}%) " - f"CPU:{cpu_percent:.0f}% Managed:{managed_gb:.2f}GB " - f"{status}" - ) - - if total_workers > 0: - total_used_gb = total_memory_used / 1e9 - total_limit_gb = total_memory_limit / 1e9 - total_percentage = (total_used_gb / total_limit_gb * 100) if total_limit_gb > 0 else 0 - console.print( - f"[bold]Total: {total_used_gb:.2f}/{total_limit_gb:.2f}GB ({total_percentage:.1f}%) across {total_workers} workers[/bold]" - ) - - def close_all() -> None: - # Prevents multiple calls to close_all - if hasattr(dask_client, "_closed") and dask_client._closed: - return - dask_client._closed = True # type: ignore[attr-defined] - - # Stop all Docker modules (in reverse order of deployment) - for dm in reversed(dask_client._docker_modules): # type: ignore[attr-defined] - try: - dm.stop() - except Exception: - pass - dask_client._docker_modules.clear() # type: ignore[attr-defined] - - # Stop all SharedMemory transports before closing Dask - # This prevents the "leaked shared_memory objects" warning and hangs - try: - import gc - - from dimos.protocol.pubsub.impl import shmpubsub - - for obj in gc.get_objects(): - if isinstance(obj, shmpubsub.SharedMemoryPubSubBase): - try: - obj.stop() - except Exception: - pass - except Exception: - pass - - # Get the event loop before shutting down - loop = dask_client.loop - - # Clear the actor registry - ActorRegistry.clear() - - # Close cluster and client with reasonable timeout - # The CudaCleanupPlugin will handle CUDA cleanup on each worker - try: - local_cluster.close(timeout=5) - except Exception: - pass - - try: - dask_client.close(timeout=5) # type: ignore[no-untyped-call] - except Exception: - pass - - if loop and hasattr(loop, "add_callback") and hasattr(loop, "stop"): - try: - loop.add_callback(loop.stop) - except Exception: - pass - - # Note: We do NOT shutdown the _offload_executor here because it's a global - # module-level ThreadPoolExecutor shared across all Dask clients in the process. - # Shutting it down here would break subsequent Dask client usage (e.g., in tests). - # The executor will be cleaned up when the Python process exits. - - # Give threads time to clean up - # Dask's IO loop and Profile threads are daemon threads - # that will be cleaned up when the process exits - # This is needed, solves race condition in CI thread check - time.sleep(0.1) - - dask_client._docker_modules = [] # type: ignore[attr-defined] - dask_client.deploy = deploy # type: ignore[attr-defined] - dask_client.check_worker_memory = check_worker_memory # type: ignore[attr-defined] - dask_client.stop = lambda: dask_client.close() # type: ignore[attr-defined, no-untyped-call] - dask_client.close_all = close_all # type: ignore[attr-defined] - return dask_client # type: ignore[return-value] - - -def start(n: int | None = None, memory_limit: str = "auto") -> DimosCluster: - """Start a Dask LocalCluster with specified workers and memory limits. - - Args: - n: Number of workers (defaults to CPU count) - memory_limit: Memory limit per worker (e.g., '4GB', '2GiB', or 'auto' for Dask's default) - - Returns: - DimosCluster: A patched Dask client with deploy(), check_worker_memory(), stop(), and close_all() methods - """ - - from dask.distributed import Client, LocalCluster - - console = Console() - if not n: - n = mp.cpu_count() - with console.status( - f"[green]Initializing dimos local cluster with [bright_blue]{n} workers", spinner="arc" - ): - cluster = LocalCluster( # type: ignore[no-untyped-call] - n_workers=n, - threads_per_worker=4, - memory_limit=memory_limit, - plugins=[CudaCleanupPlugin()], # Register CUDA cleanup plugin - ) - client = Client(cluster) # type: ignore[no-untyped-call] - - console.print( - f"[green]Initialized dimos local cluster with [bright_blue]{n} workers, memory limit: {memory_limit}" - ) - - patched_client = patchdask(client, cluster) - - return patched_client - - -def wait_exit() -> None: - while True: - try: - time.sleep(1) - except KeyboardInterrupt: - print("exiting...") - return diff --git a/dimos/core/_dask_exports.py b/dimos/core/_dask_exports.py deleted file mode 100644 index cb257e7804..0000000000 --- a/dimos/core/_dask_exports.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dask.distributed import Client as DimosCluster - -__all__ = ["DimosCluster"] diff --git a/dimos/core/_protocol_exports.py b/dimos/core/_protocol_exports.py deleted file mode 100644 index be77fd8323..0000000000 --- a/dimos/core/_protocol_exports.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.protocol.rpc import LCMRPC -from dimos.protocol.rpc.spec import RPCSpec -from dimos.protocol.tf import LCMTF, TF, PubSubTF, TFConfig, TFSpec - -__all__ = ["LCMRPC", "LCMTF", "TF", "PubSubTF", "RPCSpec", "TFConfig", "TFSpec"] diff --git a/dimos/core/_test_future_annotations_helper.py b/dimos/core/_test_future_annotations_helper.py deleted file mode 100644 index 08c5ec0063..0000000000 --- a/dimos/core/_test_future_annotations_helper.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Helper module for testing blueprint handling with PEP 563 (future annotations). - -This file exists because `from __future__ import annotations` affects the entire file. -""" - -from __future__ import annotations - -from dimos.core.module import Module -from dimos.core.stream import In, Out # noqa - - -class FutureData: - pass - - -class FutureModuleOut(Module): - data: Out[FutureData] = None # type: ignore[assignment] - - -class FutureModuleIn(Module): - data: In[FutureData] = None # type: ignore[assignment] diff --git a/dimos/core/blueprints.py b/dimos/core/blueprints.py deleted file mode 100644 index 605517e6cf..0000000000 --- a/dimos/core/blueprints.py +++ /dev/null @@ -1,511 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC -from collections import defaultdict -from collections.abc import Callable, Mapping -from dataclasses import dataclass, field -from functools import cached_property, reduce -import inspect -import operator -import sys -from types import MappingProxyType -from typing import Any, Literal, get_args, get_origin, get_type_hints - -from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module import Module, is_module_type -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.stream import In, Out -from dimos.core.transport import LCMTransport, PubSubTransport, pLCMTransport -from dimos.spec.utils import Spec, is_spec, spec_annotation_compliance, spec_structural_compliance -from dimos.utils.generic import short_id -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass(frozen=True) -class StreamRef: - name: str - type: type - direction: Literal["in", "out"] - - -@dataclass(frozen=True) -class ModuleRef: - name: str - spec: type[Spec] | type[Module] - - -@dataclass(frozen=True) -class _BlueprintAtom: - module: type[Module] - streams: tuple[StreamRef, ...] - module_refs: tuple[ModuleRef, ...] - args: tuple[Any, ...] - kwargs: dict[str, Any] - - @classmethod - def create( - cls, module: type[Module], args: tuple[Any, ...], kwargs: dict[str, Any] - ) -> "_BlueprintAtom": - streams: list[StreamRef] = [] - module_refs: list[ModuleRef] = [] - - # Use get_type_hints() to properly resolve string annotations. - try: - all_annotations = get_type_hints(module) - except Exception: - # Fallback to raw annotations if get_type_hints fails. - all_annotations = {} - for base_class in reversed(module.__mro__): - if hasattr(base_class, "__annotations__"): - all_annotations.update(base_class.__annotations__) - - for name, annotation in all_annotations.items(): - origin = get_origin(annotation) - # Streams - if origin in (In, Out): - direction = "in" if origin == In else "out" - type_ = get_args(annotation)[0] - streams.append( - StreamRef(name=name, type=type_, direction=direction) # type: ignore[arg-type] - ) - # linking to unknown module via Spec - elif is_spec(annotation): - module_refs.append(ModuleRef(name=name, spec=annotation)) - # linking to specific/known module directly - elif is_module_type(annotation): - module_refs.append(ModuleRef(name=name, spec=annotation)) - - return cls( - module=module, - streams=tuple(streams), - module_refs=tuple(module_refs), - args=args, - kwargs=kwargs, - ) - - -@dataclass(frozen=True) -class Blueprint: - blueprints: tuple[_BlueprintAtom, ...] - transport_map: Mapping[tuple[str, type], PubSubTransport[Any]] = field( - default_factory=lambda: MappingProxyType({}) - ) - global_config_overrides: Mapping[str, Any] = field(default_factory=lambda: MappingProxyType({})) - remapping_map: Mapping[tuple[type[Module], str], str | type[Module] | type[Spec]] = field( - default_factory=lambda: MappingProxyType({}) - ) - requirement_checks: tuple[Callable[[], str | None], ...] = field(default_factory=tuple) - - @classmethod - def create(cls, module: type[Module], *args: Any, **kwargs: Any) -> "Blueprint": - blueprint = _BlueprintAtom.create(module, args, kwargs) - return cls(blueprints=(blueprint,)) - - def transports(self, transports: dict[tuple[str, type], Any]) -> "Blueprint": - return Blueprint( - blueprints=self.blueprints, - transport_map=MappingProxyType({**self.transport_map, **transports}), - global_config_overrides=self.global_config_overrides, - remapping_map=self.remapping_map, - requirement_checks=self.requirement_checks, - ) - - def global_config(self, **kwargs: Any) -> "Blueprint": - return Blueprint( - blueprints=self.blueprints, - transport_map=self.transport_map, - global_config_overrides=MappingProxyType({**self.global_config_overrides, **kwargs}), - remapping_map=self.remapping_map, - requirement_checks=self.requirement_checks, - ) - - def remappings( - self, remappings: list[tuple[type[Module], str, str | type[Module] | type[Spec]]] - ) -> "Blueprint": - remappings_dict = dict(self.remapping_map) - for module, old, new in remappings: - remappings_dict[(module, old)] = new - - return Blueprint( - blueprints=self.blueprints, - transport_map=self.transport_map, - global_config_overrides=self.global_config_overrides, - remapping_map=MappingProxyType(remappings_dict), - requirement_checks=self.requirement_checks, - ) - - def requirements(self, *checks: Callable[[], str | None]) -> "Blueprint": - return Blueprint( - blueprints=self.blueprints, - transport_map=self.transport_map, - global_config_overrides=self.global_config_overrides, - remapping_map=self.remapping_map, - requirement_checks=self.requirement_checks + tuple(checks), - ) - - def _check_ambiguity( - self, - requested_method_name: str, - interface_methods: Mapping[str, list[tuple[type[Module], Callable[..., Any]]]], - requesting_module: type[Module], - ) -> None: - if ( - requested_method_name in interface_methods - and len(interface_methods[requested_method_name]) > 1 - ): - modules_str = ", ".join( - impl[0].__name__ for impl in interface_methods[requested_method_name] - ) - raise ValueError( - f"Ambiguous RPC method '{requested_method_name}' requested by " - f"{requesting_module.__name__}. Multiple implementations found: " - f"{modules_str}. Please use a concrete class name instead." - ) - - def _get_transport_for(self, name: str, stream_type: type) -> PubSubTransport[Any]: - transport = self.transport_map.get((name, stream_type), None) - if transport: - return transport - - use_pickled = getattr(stream_type, "lcm_encode", None) is None - topic = f"/{name}" if self._is_name_unique(name) else f"/{short_id()}" - transport = pLCMTransport(topic) if use_pickled else LCMTransport(topic, stream_type) - - return transport - - @cached_property - def _all_name_types(self) -> set[tuple[str, type]]: - # Apply remappings to get the actual names that will be used - result = set() - for blueprint in self.blueprints: - for conn in blueprint.streams: - # Check if this stream should be remapped - remapped_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) - if isinstance(remapped_name, str): - result.add((remapped_name, conn.type)) - return result - - def _is_name_unique(self, name: str) -> bool: - return sum(1 for n, _ in self._all_name_types if n == name) == 1 - - def _check_requirements(self) -> None: - errors = [] - red = "\033[31m" - reset = "\033[0m" - - for check in self.requirement_checks: - error = check() - if error: - errors.append(error) - - if errors: - for error in errors: - print(f"{red}Error: {error}{reset}", file=sys.stderr) - sys.exit(1) - - def _verify_no_name_conflicts(self) -> None: - name_to_types = defaultdict(set) - name_to_modules = defaultdict(list) - - for blueprint in self.blueprints: - for conn in blueprint.streams: - stream_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) - name_to_types[stream_name].add(conn.type) - name_to_modules[stream_name].append((blueprint.module, conn.type)) - - conflicts = {} - for conn_name, types in name_to_types.items(): - if len(types) > 1: - modules_by_type = defaultdict(list) - for module, conn_type in name_to_modules[conn_name]: - modules_by_type[conn_type].append(module) - conflicts[conn_name] = modules_by_type - - if not conflicts: - return - - error_lines = ["Blueprint cannot start because there are conflicting streams."] - for name, modules_by_type in conflicts.items(): - type_entries = [] - for conn_type, modules in modules_by_type.items(): - for module in modules: - type_str = f"{conn_type.__module__}.{conn_type.__name__}" - module_str = module.__name__ - type_entries.append((type_str, module_str)) - if len(type_entries) >= 2: - locations = ", ".join(f"{type_} in {module}" for type_, module in type_entries) - error_lines.append(f" - '{name}' has conflicting types. {locations}") - - raise ValueError("\n".join(error_lines)) - - def _deploy_all_modules( - self, module_coordinator: ModuleCoordinator, global_config: GlobalConfig - ) -> None: - module_specs: list[tuple[type[Module], tuple[Any, ...], dict[str, Any]]] = [] - for blueprint in self.blueprints: - kwargs = {**blueprint.kwargs} - sig = inspect.signature(blueprint.module.__init__) - if "cfg" in sig.parameters: - kwargs["cfg"] = global_config - module_specs.append((blueprint.module, blueprint.args, kwargs)) - - module_coordinator.deploy_parallel(module_specs) - - def _connect_streams(self, module_coordinator: ModuleCoordinator) -> None: - # dict when given (final/remapped) stream name+type, provides a list of modules + original (non-remapped) stream names - streams = defaultdict(list) - - for blueprint in self.blueprints: - for conn in blueprint.streams: - # Check if this stream should be remapped - remapped_name = self.remapping_map.get((blueprint.module, conn.name), conn.name) - if isinstance(remapped_name, str): - # Group by remapped name and type - streams[remapped_name, conn.type].append((blueprint.module, conn.name)) - - # Connect all In/Out streams by remapped name and type. - for remapped_name, stream_type in streams.keys(): - transport = self._get_transport_for(remapped_name, stream_type) - for module, original_name in streams[(remapped_name, stream_type)]: - instance = module_coordinator.get_instance(module) # type: ignore[assignment] - instance.set_transport(original_name, transport) # type: ignore[union-attr] - logger.info( - "Transport", - name=remapped_name, - original_name=original_name, - topic=str(getattr(transport, "topic", None)), - type=f"{stream_type.__module__}.{stream_type.__qualname__}", - module=module.__name__, - transport=transport.__class__.__name__, - ) - - def _connect_module_refs(self, module_coordinator: ModuleCoordinator) -> None: - # partly fill out the mod_and_mod_ref_to_proxy - mod_and_mod_ref_to_proxy = { - (module, name): replacement - for (module, name), replacement in self.remapping_map.items() - if is_spec(replacement) or is_module_type(replacement) - } - - # after this loop we should have an exact module for every module_ref on every blueprint - for blueprint in self.blueprints: - for each_module_ref in blueprint.module_refs: - # we've got to find a another module that implements this spec - spec = mod_and_mod_ref_to_proxy.get( - (blueprint.module, each_module_ref.name), each_module_ref.spec - ) - - # if the spec is actually module, use that (basically a user override) - if is_module_type(spec): - mod_and_mod_ref_to_proxy[blueprint.module, each_module_ref.name] = spec - continue - - # find all available candidates - possible_module_candidates = [ - each_other_blueprint.module - for each_other_blueprint in self.blueprints - if ( - each_other_blueprint != blueprint - and spec_structural_compliance(each_other_blueprint.module, spec) - ) - ] - # we keep valid separate from invalid to provide a better error message for "almost" valid cases - valid_module_candidates = [ - each_candidate - for each_candidate in possible_module_candidates - if spec_annotation_compliance(each_candidate, spec) - ] - # none - if len(possible_module_candidates) == 0: - raise Exception( - f"""The {blueprint.module.__name__} has a module reference ({each_module_ref}) which requested a module that fills out the {each_module_ref.spec.__name__} spec. But I couldn't find a module that met that spec.\n""" - ) - # exactly one structurally valid candidate - elif len(possible_module_candidates) == 1: - if len(valid_module_candidates) == 0: - logger.warning( - f"""The {blueprint.module.__name__} has a module reference ({each_module_ref}) which requested a module that fills out the {each_module_ref.spec.__name__} spec. I found a module ({possible_module_candidates[0].__name__}) that met that spec structurally, but it had a mismatch in type annotations.\nPlease either change the {each_module_ref.spec.__name__} spec or the {possible_module_candidates[0].__name__} module.\n""" - ) - mod_and_mod_ref_to_proxy[blueprint.module, each_module_ref.name] = ( - possible_module_candidates[0] - ) - continue - # more than one - elif len(valid_module_candidates) > 1: - raise Exception( - f"""The {blueprint.module.__name__} has a module reference ({each_module_ref}) which requested a module that fills out the {each_module_ref.spec.__name__} spec. But I found multiple modules that met that spec: {possible_module_candidates}.\nTo fix this use .remappings, for example:\n autoconnect(...).remappings([ ({blueprint.module.__name__}, {each_module_ref.name!r}, ) ])\n""" - ) - # structural candidates, but no valid candidates - elif len(valid_module_candidates) == 0: - possible_module_candidates_str = ", ".join( - [each_candidate.__name__ for each_candidate in possible_module_candidates] - ) - raise Exception( - f"""The {blueprint.module.__name__} has a module reference ({each_module_ref}) which requested a module that fills out the {each_module_ref.spec.__name__} spec. Some modules ({possible_module_candidates_str}) met the spec structurally but had a mismatch in type annotations\n""" - ) - # one valid candidate (and more than one structurally valid candidate) - else: - mod_and_mod_ref_to_proxy[blueprint.module, each_module_ref.name] = ( - valid_module_candidates[0] - ) - - # now that we know the streams, we mutate the RPCClient objects - for (base_module, module_ref_name), target_module in mod_and_mod_ref_to_proxy.items(): - base_module_proxy = module_coordinator.get_instance(base_module) - target_module_proxy = module_coordinator.get_instance(target_module) # type: ignore[type-var,arg-type] - setattr( - base_module_proxy, - module_ref_name, - target_module_proxy, - ) - # Ensure the remote module instance can use the module ref inside its own RPC handlers. - base_module_proxy.set_module_ref(module_ref_name, target_module_proxy) - - def _connect_rpc_methods(self, module_coordinator: ModuleCoordinator) -> None: - # Gather all RPC methods. - rpc_methods = {} - rpc_methods_dot = {} - - # Track interface methods to detect ambiguity. - interface_methods: defaultdict[str, list[tuple[type[Module], Callable[..., Any]]]] = ( - defaultdict(list) - ) # interface_name_method -> [(module_class, method)] - interface_methods_dot: defaultdict[str, list[tuple[type[Module], Callable[..., Any]]]] = ( - defaultdict(list) - ) # interface_name.method -> [(module_class, method)] - - for blueprint in self.blueprints: - for method_name in blueprint.module.rpcs.keys(): # type: ignore[attr-defined] - module_proxy = module_coordinator.get_instance(blueprint.module) # type: ignore[assignment] - method_for_rpc_client = getattr(module_proxy, method_name) - # Register under concrete class name (backward compatibility) - rpc_methods[f"{blueprint.module.__name__}_{method_name}"] = method_for_rpc_client - rpc_methods_dot[f"{blueprint.module.__name__}.{method_name}"] = ( - method_for_rpc_client - ) - - # Also register under any interface names - for base in blueprint.module.mro(): - # Check if this base is an abstract interface with the method - if ( - base is not Module - and issubclass(base, ABC) - and hasattr(base, method_name) - and getattr(base, method_name, None) is not None - ): - interface_key = f"{base.__name__}.{method_name}" - interface_methods_dot[interface_key].append( - (blueprint.module, method_for_rpc_client) - ) - interface_key_underscore = f"{base.__name__}_{method_name}" - interface_methods[interface_key_underscore].append( - (blueprint.module, method_for_rpc_client) - ) - - # Check for ambiguity in interface methods and add non-ambiguous ones - for interface_key, implementations in interface_methods_dot.items(): - if len(implementations) == 1: - rpc_methods_dot[interface_key] = implementations[0][1] - for interface_key, implementations in interface_methods.items(): - if len(implementations) == 1: - rpc_methods[interface_key] = implementations[0][1] - - # Fulfil method requests (so modules can call each other). - for blueprint in self.blueprints: - instance = module_coordinator.get_instance(blueprint.module) # type: ignore[assignment] - - for method_name in blueprint.module.rpcs.keys(): # type: ignore[attr-defined] - if not method_name.startswith("set_"): - continue - - linked_name = method_name.removeprefix("set_") - - self._check_ambiguity(linked_name, interface_methods, blueprint.module) - - if linked_name not in rpc_methods: - continue - - getattr(instance, method_name)(rpc_methods[linked_name]) - - for requested_method_name in instance.get_rpc_method_names(): # type: ignore[union-attr] - self._check_ambiguity( - requested_method_name, interface_methods_dot, blueprint.module - ) - - if requested_method_name not in rpc_methods_dot: - continue - - instance.set_rpc_method( # type: ignore[union-attr] - requested_method_name, rpc_methods_dot[requested_method_name] - ) - - def build( - self, - cli_config_overrides: Mapping[str, Any] | None = None, - ) -> ModuleCoordinator: - global_config.update(**dict(self.global_config_overrides)) - if cli_config_overrides: - global_config.update(**dict(cli_config_overrides)) - - self._check_requirements() - self._verify_no_name_conflicts() - - module_coordinator = ModuleCoordinator(cfg=global_config) - module_coordinator.start() - - # all module constructors are called here (each of them setup their own) - self._deploy_all_modules(module_coordinator, global_config) - self._connect_streams(module_coordinator) - self._connect_rpc_methods(module_coordinator) - self._connect_module_refs(module_coordinator) - - module_coordinator.start_all_modules() - - return module_coordinator - - -def autoconnect(*blueprints: Blueprint) -> Blueprint: - all_blueprints = tuple(_eliminate_duplicates([bp for bs in blueprints for bp in bs.blueprints])) - all_transports = dict( # type: ignore[var-annotated] - reduce(operator.iadd, [list(x.transport_map.items()) for x in blueprints], []) - ) - all_config_overrides = dict( # type: ignore[var-annotated] - reduce(operator.iadd, [list(x.global_config_overrides.items()) for x in blueprints], []) - ) - all_remappings = dict( # type: ignore[var-annotated] - reduce(operator.iadd, [list(x.remapping_map.items()) for x in blueprints], []) - ) - all_requirement_checks = tuple(check for bs in blueprints for check in bs.requirement_checks) - - return Blueprint( - blueprints=all_blueprints, - transport_map=MappingProxyType(all_transports), - global_config_overrides=MappingProxyType(all_config_overrides), - remapping_map=MappingProxyType(all_remappings), - requirement_checks=all_requirement_checks, - ) - - -def _eliminate_duplicates(blueprints: list[_BlueprintAtom]) -> list[_BlueprintAtom]: - # The duplicates are eliminated in reverse so that newer blueprints override older ones. - seen = set() - unique_blueprints = [] - for bp in reversed(blueprints): - if bp.module not in seen: - seen.add(bp.module) - unique_blueprints.append(bp) - return list(reversed(unique_blueprints)) diff --git a/dimos/core/colors.py b/dimos/core/colors.py deleted file mode 100644 index 294cf5d43b..0000000000 --- a/dimos/core/colors.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def green(text: str) -> str: - """Return the given text in green color.""" - return f"\033[92m{text}\033[0m" - - -def blue(text: str) -> str: - """Return the given text in blue color.""" - return f"\033[94m{text}\033[0m" - - -def red(text: str) -> str: - """Return the given text in red color.""" - return f"\033[91m{text}\033[0m" - - -def yellow(text: str) -> str: - """Return the given text in yellow color.""" - return f"\033[93m{text}\033[0m" - - -def cyan(text: str) -> str: - """Return the given text in cyan color.""" - return f"\033[96m{text}\033[0m" - - -def orange(text: str) -> str: - """Return the given text in orange color.""" - return f"\033[38;5;208m{text}\033[0m" diff --git a/dimos/core/core.py b/dimos/core/core.py deleted file mode 100644 index 6c95700926..0000000000 --- a/dimos/core/core.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import ( - TYPE_CHECKING, - TypeVar, -) - -from dimos.core.o3dpickle import register_picklers - -if TYPE_CHECKING: - from collections.abc import Callable - -# injects pickling system into o3d -register_picklers() -T = TypeVar("T") - -from typing import ParamSpec, TypeVar - -P = ParamSpec("P") -R = TypeVar("R") - - -def rpc(fn: Callable[P, R]) -> Callable[P, R]: - fn.__rpc__ = True # type: ignore[attr-defined] - return fn diff --git a/dimos/core/docker_build.py b/dimos/core/docker_build.py deleted file mode 100644 index 7ee90fc5c3..0000000000 --- a/dimos/core/docker_build.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Docker image building and Dockerfile conversion utilities. -Converts any Dockerfile into a DimOS module container by appending a footer -that installs DimOS and creates the module entrypoint. -""" - -from __future__ import annotations - -import subprocess -from typing import TYPE_CHECKING - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pathlib import Path - - from dimos.core.docker_runner import DockerModuleConfig - -logger = setup_logger() - -# Timeout for quick Docker commands -DOCKER_CMD_TIMEOUT = 20 - -# Sentinel value to detect already-converted Dockerfiles (UUID ensures uniqueness) -DIMOS_SENTINEL = "DIMOS-MODULE-CONVERSION-427593ae-c6e8-4cf1-9b2d-ee81a420a5dc" - -# Footer appended to Dockerfiles for DimOS module conversion -DIMOS_FOOTER = f""" -# ==== {DIMOS_SENTINEL} ==== -# Copy DimOS source from build context -COPY dimos /dimos/source/dimos/ -COPY pyproject.toml /dimos/source/ -COPY docker/python/module-install.sh /tmp/module-install.sh - -# Install DimOS and create entrypoint -RUN bash /tmp/module-install.sh /dimos/source && rm /tmp/module-install.sh - -ENTRYPOINT ["/dimos/entrypoint.sh"] -""" - - -def _run(cmd: list[str], *, timeout: float | None = None) -> subprocess.CompletedProcess[str]: - """Run a command and return the result.""" - return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False) - - -def _run_streaming(cmd: list[str]) -> int: - """Run command and stream output to terminal. Returns exit code.""" - result = subprocess.run(cmd, text=True) - return result.returncode - - -def _docker_bin(cfg: DockerModuleConfig) -> str: - """Get docker binary path.""" - return cfg.docker_bin or "docker" - - -def _image_exists(docker_bin: str, image_name: str) -> bool: - """Check if a Docker image exists locally.""" - r = _run([docker_bin, "image", "inspect", image_name], timeout=DOCKER_CMD_TIMEOUT) - return r.returncode == 0 - - -def _convert_dockerfile(dockerfile: Path) -> Path: - """Append DimOS footer to Dockerfile. Returns path to converted file.""" - content = dockerfile.read_text() - - # Already converted? - if DIMOS_SENTINEL in content: - return dockerfile - - logger.info(f"Converting {dockerfile.name} to DimOS format") - - converted = dockerfile.parent / f".{dockerfile.name}.dimos" - converted.write_text(content.rstrip() + "\n" + DIMOS_FOOTER.lstrip("\n")) - return converted - - -def build_image(cfg: DockerModuleConfig) -> None: - """Build Docker image using footer mode conversion.""" - if cfg.docker_file is None: - raise ValueError("docker_file is required for building Docker images") - dockerfile = _convert_dockerfile(cfg.docker_file) - - context = cfg.docker_build_context or cfg.docker_file.parent - cmd = [_docker_bin(cfg), "build", "-t", cfg.docker_image, "-f", str(dockerfile)] - for k, v in cfg.docker_build_args.items(): - cmd.extend(["--build-arg", f"{k}={v}"]) - cmd.append(str(context)) - - logger.info(f"Building Docker image: {cfg.docker_image}") - exit_code = _run_streaming(cmd) - if exit_code != 0: - raise RuntimeError(f"Docker build failed with exit code {exit_code}") - - -def image_exists(cfg: DockerModuleConfig) -> bool: - """Check if the configured Docker image exists locally.""" - return _image_exists(_docker_bin(cfg), cfg.docker_image) - - -__all__ = [ - "DIMOS_FOOTER", - "build_image", - "image_exists", -] diff --git a/dimos/core/docker_runner.py b/dimos/core/docker_runner.py deleted file mode 100644 index 9be2ff6012..0000000000 --- a/dimos/core/docker_runner.py +++ /dev/null @@ -1,521 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -import argparse -from contextlib import suppress -from dataclasses import dataclass, field -import importlib -import json -import os -import signal -import subprocess -import threading -import time -from typing import TYPE_CHECKING, Any - -from dimos.core.docker_build import build_image, image_exists -from dimos.core.module import Module, ModuleConfig -from dimos.core.rpc_client import RpcCall -from dimos.protocol.rpc import LCMRPC -from dimos.utils.logging_config import setup_logger -from dimos.visualization.rerun.bridge import RERUN_GRPC_PORT, RERUN_WEB_PORT - -if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - -logger = setup_logger() - -DOCKER_RUN_TIMEOUT = 120 # Timeout for `docker run` command execution -DOCKER_CMD_TIMEOUT = 20 # Timeout for quick Docker commands (inspect, rm, logs) -DOCKER_STATUS_TIMEOUT = 10 # Timeout for container status checks -DOCKER_STOP_TIMEOUT = 30 # Timeout for `docker stop` command (graceful shutdown) -RPC_READY_TIMEOUT = 3.0 # Timeout for RPC readiness probe during container startup -LOG_TAIL_LINES = 200 # Number of log lines to include in error messages - - -@dataclass(kw_only=True) -class DockerModuleConfig(ModuleConfig): - """ - Configuration for running a DimOS module inside Docker. - - For advanced Docker options not listed here, use docker_extra_args. - Example: docker_extra_args=["--cap-add=SYS_ADMIN", "--read-only"] - """ - - # Build / image - docker_image: str - docker_file: Path | None = None # Required on host for building, not needed in container - docker_build_context: Path | None = None - docker_build_args: dict[str, str] = field(default_factory=dict) - - # Identity - docker_container_name: str | None = None - docker_labels: dict[str, str] = field(default_factory=dict) - - # Networking (host mode recommended for LCM multicast) - docker_network_mode: str = "host" - docker_network: str | None = None - docker_ports: list[tuple[int, int, str]] = field( - default_factory=list - ) # (host, container, proto) - - # Runtime resources - docker_gpus: str | None = "all" - docker_shm_size: str = "2g" - docker_restart_policy: str = "on-failure:3" - - # Env + volumes + devices - docker_env_files: list[str] = field(default_factory=list) - docker_env: dict[str, str] = field(default_factory=dict) - docker_volumes: list[tuple[str, str, str]] = field( - default_factory=list - ) # (host, container, mode) - docker_devices: list[str] = field(default_factory=list) # --device args as strings - - # Security - docker_privileged: bool = False - - # Lifecycle / overrides - docker_rm: bool = False - docker_entrypoint: str | None = None - docker_command: list[str] | None = None - docker_extra_args: list[str] = field(default_factory=list) - - # Startup readiness - docker_startup_timeout: float = 120.0 - docker_poll_interval: float = 1.0 - - # Advanced - docker_bin: str = "docker" - - -def is_docker_module(module_class: type) -> bool: - """Check if a module class should run in Docker based on its default_config.""" - default_config = getattr(module_class, "default_config", None) - return default_config is not None and issubclass(default_config, DockerModuleConfig) - - -# Docker helpers - - -def _run(cmd: list[str], *, timeout: float | None = None) -> subprocess.CompletedProcess[str]: - logger.debug(f"exec: {' '.join(cmd)}") - return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False) - - -def _docker_bin(cfg: DockerModuleConfig) -> str: - """Get docker binary path, defaulting to 'docker' if empty/None.""" - return cfg.docker_bin or "docker" - - -def _remove_container(cfg: DockerModuleConfig, name: str) -> None: - _run([_docker_bin(cfg), "rm", "-f", name], timeout=DOCKER_CMD_TIMEOUT) - - -def _is_container_running(cfg: DockerModuleConfig, name: str) -> bool: - r = _run( - [_docker_bin(cfg), "inspect", "-f", "{{.State.Running}}", name], - timeout=DOCKER_STATUS_TIMEOUT, - ) - return r.returncode == 0 and r.stdout.strip() == "true" - - -def _tail_logs(cfg: DockerModuleConfig, name: str, n: int = LOG_TAIL_LINES) -> str: - r = _run([_docker_bin(cfg), "logs", "--tail", str(n), name], timeout=DOCKER_CMD_TIMEOUT) - out = (r.stdout or "").rstrip() - err = (r.stderr or "").rstrip() - return out + ("\n" + err if err else "") - - -def _extract_module_config(cfg: DockerModuleConfig) -> dict[str, Any]: - """Extract JSON-serializable config fields for the container (excludes docker_* fields).""" - out: dict[str, Any] = {} - for k, v in cfg.__dict__.items(): - if k.startswith("docker_") or isinstance(v, type) or callable(v): - continue - try: - json.dumps(v) - out[k] = v - except (TypeError, ValueError): - logger.debug(f"Config field '{k}' not JSON-serializable, skipping") - return out - - -# Host-side Docker-backed Module handle - - -class DockerModule: - """ - Host-side handle for a module running inside Docker. - - Lifecycle: - - start(): launches container, waits for module ready via RPC - - stop(): stops container - - __getattr__: exposes RpcCall for @rpc methods on remote module - - Communication: All RPC happens via LCM multicast (requires --network=host). - """ - - def __init__(self, module_class: type[Module], *args: Any, **kwargs: Any) -> None: - # Config - config_class = getattr(module_class, "default_config", DockerModuleConfig) - config = config_class(**kwargs) - - # Module info - self._module_class = module_class - self._config = config - self._args = args - self._kwargs = kwargs - self._running = False - self.remote_name = module_class.__name__ - self._container_name = ( - config.docker_container_name - or f"dimos_{module_class.__name__.lower()}_{os.getpid()}_{int(time.time())}" - ) - - # RPC setup - self.rpc = LCMRPC() - self.rpcs = set(module_class.rpcs.keys()) # type: ignore[attr-defined] - self.rpc_calls: list[str] = getattr(module_class, "rpc_calls", []) - self._unsub_fns: list[Callable[[], None]] = [] - self._bound_rpc_calls: dict[str, RpcCall] = {} - - # Build image if needed (but don't start - caller must call start() explicitly) - if not image_exists(config): - logger.info(f"Building {config.docker_image}") - build_image(config) - - def set_rpc_method(self, method: str, callable: RpcCall) -> None: - callable.set_rpc(self.rpc) - self._bound_rpc_calls[method] = callable - - def get_rpc_calls(self, *methods: str) -> RpcCall | tuple[RpcCall, ...]: - # Check all requested methods exist - missing = set(methods) - self._bound_rpc_calls.keys() - if missing: - raise ValueError(f"RPC methods not found: {missing}") - # Return single RpcCall or tuple - calls = tuple(self._bound_rpc_calls[m] for m in methods) - return calls[0] if len(calls) == 1 else calls - - def start(self) -> None: - if self._running: - return - - cfg = self._config - - # Prevent accidental kill of running container with same name - if _is_container_running(cfg, self._container_name): - raise RuntimeError( - f"Container '{self._container_name}' already running. " - "Choose a different container_name or stop the existing container." - ) - _remove_container(cfg, self._container_name) - - cmd = self._build_docker_run_command() - logger.info(f"Starting docker container: {self._container_name}") - r = _run(cmd, timeout=DOCKER_RUN_TIMEOUT) - if r.returncode != 0: - raise RuntimeError( - f"Failed to start container.\nSTDOUT:\n{r.stdout}\nSTDERR:\n{r.stderr}" - ) - - self.rpc.start() - self._running = True - self._wait_for_ready() - - def stop(self) -> None: - """Gracefully stop the Docker container and clean up resources.""" - # Signal remote module, stop RPC, unsubscribe handlers (ignore failures) - with suppress(Exception): - if self._running: - self.rpc.call_nowait(f"{self.remote_name}/stop", ([], {})) - with suppress(Exception): - self.rpc.stop() - for unsub in self._unsub_fns: - with suppress(Exception): - unsub() - self._unsub_fns.clear() - - # Stop and remove container - _run([_docker_bin(self._config), "stop", self._container_name], timeout=DOCKER_STOP_TIMEOUT) - _remove_container(self._config, self._container_name) - self._running = False - logger.info(f"Stopped container: {self._container_name}") - - def status(self) -> dict[str, Any]: - cfg = self._config - return { - "module": self.remote_name, - "container_name": self._container_name, - "image": cfg.docker_image, - "running": bool(self._running and _is_container_running(cfg, self._container_name)), - } - - def tail_logs(self, n: int = 200) -> str: - return _tail_logs(self._config, self._container_name, n=n) - - def set_transport(self, stream_name: str, transport: Any) -> bool: - """Configure stream transport in container. Mirrors DaskModule.set_transport() for autoconnect().""" - topic = getattr(transport, "topic", None) - if topic is None: - return False - if hasattr(topic, "topic"): - topic = topic.topic - result, _ = self.rpc.call_sync( - f"{self.remote_name}/configure_stream", ([stream_name, str(topic)], {}) - ) - return bool(result) - - def __getattr__(self, name: str) -> Any: - if name in self.rpcs: - original_method = getattr(self._module_class, name, None) - return RpcCall(original_method, self.rpc, name, self.remote_name, self._unsub_fns, None) - raise AttributeError(f"{name} not found on {self._module_class.__name__}") - - # Docker command building (split into focused helpers for readability) - - def _build_docker_run_command(self) -> list[str]: - """Build the complete `docker run` command.""" - cfg = self._config - self._validate_config(cfg) - - cmd = [_docker_bin(cfg), "run", "-d"] - self._add_lifecycle_args(cmd, cfg) - self._add_network_args(cmd, cfg) - self._add_port_args(cmd, cfg) - self._add_resource_args(cmd, cfg) - self._add_security_args(cmd, cfg) - self._add_device_args(cmd, cfg) - self._add_label_args(cmd, cfg) - self._add_env_args(cmd, cfg) - self._add_volume_args(cmd, cfg) - self._add_entrypoint_args(cmd, cfg) - cmd.extend(cfg.docker_extra_args) - - cmd.append(cfg.docker_image) - cmd.extend(self._build_container_command(cfg)) - return cmd - - def _validate_config(self, cfg: DockerModuleConfig) -> None: - """Validate config before building command.""" - # Warn about network mode - LCM multicast requires host network - using_host_network = cfg.docker_network is None and cfg.docker_network_mode == "host" - if not using_host_network: - logger.warning( - "DockerModule not using host network. LCM multicast requires --network=host. " - "RPC communication may not work with bridge/custom networks." - ) - - def _add_lifecycle_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --rm and --name args.""" - if cfg.docker_rm: - cmd.append("--rm") - if cfg.docker_restart_policy and cfg.docker_restart_policy != "no": - logger.warning( - "--rm with docker_restart_policy is unusual; consider docker_restart_policy='no'." - ) - cmd.extend(["--name", self._container_name]) - - def _add_network_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --network args.""" - if cfg.docker_network and cfg.docker_network_mode != "host": - logger.warning( - "Both 'docker_network' and 'docker_network_mode' set; using 'docker_network' and ignoring 'docker_network_mode'." - ) - if cfg.docker_network: - cmd.extend(["--network", cfg.docker_network]) - else: - cmd.append(f"--network={cfg.docker_network_mode}") - - def _add_port_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add -p port args. No-op for host network (ports auto-exposed).""" - if cfg.docker_network is None and cfg.docker_network_mode == "host": - return - # Non-host network: map Rerun ports + any custom ports - for port in (RERUN_GRPC_PORT, RERUN_WEB_PORT): - cmd.extend(["-p", f"{port}:{port}/tcp"]) - for host_port, container_port, proto in cfg.docker_ports: - cmd.extend(["-p", f"{host_port}:{container_port}/{proto or 'tcp'}"]) - - def _add_resource_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --shm-size, --restart, --gpus args.""" - cmd.append(f"--shm-size={cfg.docker_shm_size}") - if cfg.docker_restart_policy: - cmd.append(f"--restart={cfg.docker_restart_policy}") - if cfg.docker_gpus: - cmd.extend(["--gpus", cfg.docker_gpus]) - - def _add_security_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --privileged if enabled.""" - if cfg.docker_privileged: - cmd.append("--privileged") - - def _add_device_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --device args.""" - for dev in cfg.docker_devices: - cmd.extend(["--device", dev]) - - def _add_label_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --label args with DimOS defaults.""" - labels = dict(cfg.docker_labels) - labels.setdefault("dimos.kind", "module") - labels.setdefault("dimos.module", self._module_class.__name__) - for k, v in labels.items(): - cmd.extend(["--label", f"{k}={v}"]) - - def _add_env_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add -e and --env-file args.""" - cmd.extend(["-e", "PYTHONUNBUFFERED=1"]) - for env_file in cfg.docker_env_files: - cmd.extend(["--env-file", env_file]) - for k, v in cfg.docker_env.items(): - cmd.extend(["-e", f"{k}={v}"]) - - def _add_volume_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add -v volume args.""" - for host_path, container_path, mode in cfg.docker_volumes: - cmd.extend(["-v", f"{host_path}:{container_path}:{mode}"]) - - def _add_entrypoint_args(self, cmd: list[str], cfg: DockerModuleConfig) -> None: - """Add --entrypoint override.""" - if cfg.docker_entrypoint: - cmd.extend(["--entrypoint", cfg.docker_entrypoint]) - - def _build_container_command(self, cfg: DockerModuleConfig) -> list[str]: - """Build the container command (module runner or custom).""" - if cfg.docker_command: - return list(cfg.docker_command) - - module_path = f"{self._module_class.__module__}.{self._module_class.__name__}" - # Filter out docker-specific kwargs (paths, etc.) - only pass module config - kwargs = {"config": _extract_module_config(cfg)} - payload = {"module_path": module_path, "args": list(self._args), "kwargs": kwargs} - # DimOS base image entrypoint already runs "dimos.core.docker_runner run" - return ["--payload", json.dumps(payload, separators=(",", ":"))] - - def _wait_for_ready(self) -> None: - """Poll the module's RPC endpoint until ready, crashed, or timeout.""" - cfg = self._config - start_time = time.time() - - logger.info(f"Waiting for {self.remote_name} to be ready...") - - while (time.time() - start_time) < cfg.docker_startup_timeout: - if not _is_container_running(cfg, self._container_name): - logs = _tail_logs(cfg, self._container_name) - raise RuntimeError(f"Container died during startup:\n{logs}") - - try: - self.rpc.call_sync( - f"{self.remote_name}/start", ([], {}), rpc_timeout=RPC_READY_TIMEOUT - ) - elapsed = time.time() - start_time - logger.info(f"{self.remote_name} ready ({elapsed:.1f}s)") - return - except (TimeoutError, ConnectionError, OSError): - # Module not ready yet - retry after poll interval - time.sleep(cfg.docker_poll_interval) - - logs = _tail_logs(cfg, self._container_name) - raise RuntimeError( - f"Timeout waiting for {self.remote_name} after {cfg.docker_startup_timeout:.1f}s:\n{logs}" - ) - - -# Container-side runner - - -class StandaloneModuleRunner: - """Runs a module inside Docker container. Blocks until SIGTERM/SIGINT.""" - - def __init__(self, module_path: str, args: list[Any], kwargs: dict[str, Any]) -> None: - self._module_path = module_path - self._args = args - self._module: Module | None = None - self._shutdown = threading.Event() - - # Merge config fields into kwargs (Configurable creates config from these) - if "config" in kwargs: - config_dict = kwargs.pop("config") - kwargs = {**config_dict, **kwargs} - self._kwargs = kwargs - - def start(self) -> None: - mod_path, class_name = self._module_path.rsplit(".", 1) - mod = importlib.import_module(mod_path) - module_class = getattr(mod, class_name) - - self._module = module_class(*self._args, **self._kwargs) - logger.info(f"[docker runner] module constructed: {class_name}") - - def stop(self) -> None: - self._shutdown.set() - if self._module is not None: - try: - self._module.stop() - except Exception as e: - logger.error(f"[docker runner] error stopping module: {e}") - - def wait(self) -> None: - self._shutdown.wait() - - -def _install_signal_handlers(runner: StandaloneModuleRunner) -> None: - def shutdown(_sig: int, _frame: Any) -> None: - runner.stop() - - signal.signal(signal.SIGTERM, shutdown) - signal.signal(signal.SIGINT, shutdown) - - -def _cli_run(payload_json: str) -> None: - payload = json.loads(payload_json) - runner = StandaloneModuleRunner( - payload["module_path"], - payload.get("args", []), - payload.get("kwargs", {}), - ) - _install_signal_handlers(runner) - runner.start() - runner.wait() - - -def main(argv: list[str] | None = None) -> None: - parser = argparse.ArgumentParser(prog="dimos.core.docker_runner") - sub = parser.add_subparsers(dest="cmd", required=True) - - runp = sub.add_parser("run", help="Run a module inside a container") - runp.add_argument("--payload", required=True, help="JSON payload with module_path and config") - - args = parser.parse_args(argv) - - if args.cmd == "run": - _cli_run(args.payload) - return - - raise ValueError(f"Unknown cmd: {args.cmd}") - - -if __name__ == "__main__": - main() - - -__all__ = [ - "DockerModule", - "DockerModuleConfig", - "is_docker_module", -] diff --git a/dimos/core/global_config.py b/dimos/core/global_config.py deleted file mode 100644 index 080c2c8bbc..0000000000 --- a/dimos/core/global_config.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re -from typing import Literal, TypeAlias - -from pydantic_settings import BaseSettings, SettingsConfigDict - -from dimos.mapping.occupancy.path_map import NavigationStrategy - -ViewerBackend: TypeAlias = Literal["rerun", "rerun-web", "foxglove", "none"] - - -def _get_all_numbers(s: str) -> list[float]: - return [float(x) for x in re.findall(r"-?\d+\.?\d*", s)] - - -class GlobalConfig(BaseSettings): - robot_ip: str | None = None - simulation: bool = False - replay: bool = False - viewer_backend: ViewerBackend = "rerun-web" - n_dask_workers: int = 2 - memory_limit: str = "auto" - mujoco_camera_position: str | None = None - mujoco_room: str | None = None - mujoco_room_from_occupancy: str | None = None - mujoco_global_costmap_from_occupancy: str | None = None - mujoco_global_map_from_pointcloud: str | None = None - mujoco_start_pos: str = "-1.0, 1.0" - mujoco_steps_per_frame: int = 7 - robot_model: str | None = None - robot_width: float = 0.3 - robot_rotation_diameter: float = 0.6 - planner_strategy: NavigationStrategy = "simple" - planner_robot_speed: float | None = None - dask: bool = True - - model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - extra="ignore", - ) - - def update(self, **kwargs: object) -> None: - """Update config fields in place.""" - for key, value in kwargs.items(): - if not hasattr(self, key): - raise AttributeError(f"GlobalConfig has no field '{key}'") - setattr(self, key, value) - - @property - def unitree_connection_type(self) -> str: - if self.replay: - return "replay" - if self.simulation: - return "mujoco" - return "webrtc" - - @property - def mujoco_start_pos_float(self) -> tuple[float, float]: - x, y = _get_all_numbers(self.mujoco_start_pos) - return (x, y) - - @property - def mujoco_camera_position_float(self) -> tuple[float, ...]: - if self.mujoco_camera_position is None: - return (-0.906, 0.008, 1.101, 4.931, 89.749, -46.378) - return tuple(_get_all_numbers(self.mujoco_camera_position)) - - -global_config = GlobalConfig() diff --git a/dimos/core/introspection/__init__.py b/dimos/core/introspection/__init__.py deleted file mode 100644 index c40c3d49e6..0000000000 --- a/dimos/core/introspection/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Module and blueprint introspection utilities.""" - -from dimos.core.introspection.module import INTERNAL_RPCS, render_module_io -from dimos.core.introspection.svg import to_svg - -__all__ = ["INTERNAL_RPCS", "render_module_io", "to_svg"] diff --git a/dimos/core/introspection/blueprint/__init__.py b/dimos/core/introspection/blueprint/__init__.py deleted file mode 100644 index 6545b39dfa..0000000000 --- a/dimos/core/introspection/blueprint/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Blueprint introspection and rendering. - -Renderers: - - dot: Graphviz DOT format (hub-style with type nodes as intermediate hubs) -""" - -from dimos.core.introspection.blueprint import dot -from dimos.core.introspection.blueprint.dot import LayoutAlgo, render_svg - -__all__ = ["LayoutAlgo", "dot", "render_svg"] diff --git a/dimos/core/introspection/blueprint/dot.py b/dimos/core/introspection/blueprint/dot.py deleted file mode 100644 index c60ad06fc8..0000000000 --- a/dimos/core/introspection/blueprint/dot.py +++ /dev/null @@ -1,253 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Hub-style Graphviz DOT renderer for blueprint visualization. - -This renderer creates intermediate "type nodes" for data flow, making it clearer -when one output fans out to multiple consumers: - - ModuleA --> [name:Type] --> ModuleB - --> ModuleC -""" - -from collections import defaultdict -from enum import Enum, auto - -from dimos.core.blueprints import Blueprint -from dimos.core.introspection.utils import ( - GROUP_COLORS, - TYPE_COLORS, - color_for_string, - sanitize_id, -) -from dimos.core.module import Module -from dimos.utils.cli import theme - - -class LayoutAlgo(Enum): - """Layout algorithms for controlling graph structure.""" - - STACK_CLUSTERS = auto() # Stack clusters vertically (invisible edges between clusters) - STACK_NODES = auto() # Stack nodes within clusters vertically - FDP = auto() # Use fdp (force-directed) layout engine instead of dot - - -# Connections to ignore (too noisy/common) -DEFAULT_IGNORED_CONNECTIONS = {("odom", "PoseStamped")} - -DEFAULT_IGNORED_MODULES = { - "WebsocketVisModule", - "UtilizationModule", - # "FoxgloveBridge", -} - - -def render( - blueprint_set: Blueprint, - *, - layout: set[LayoutAlgo] | None = None, - ignored_streams: set[tuple[str, str]] | None = None, - ignored_modules: set[str] | None = None, -) -> str: - """Generate a hub-style DOT graph from a Blueprint. - - This creates intermediate "type nodes" that represent data channels, - connecting producers to consumers through a central hub node. - - Args: - blueprint_set: The blueprint set to visualize. - layout: Set of layout algorithms to apply. Default is none (let graphviz decide). - ignored_streams: Set of (name, type_name) tuples to ignore. - ignored_modules: Set of module names to ignore. - - Returns: - A string in DOT format showing modules as nodes, type nodes as - small colored hubs, and edges connecting them. - """ - if layout is None: - layout = set() - if ignored_streams is None: - ignored_streams = DEFAULT_IGNORED_CONNECTIONS - if ignored_modules is None: - ignored_modules = DEFAULT_IGNORED_MODULES - - # Collect all outputs: (name, type) -> list of producer modules - producers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list) - # Collect all inputs: (name, type) -> list of consumer modules - consumers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list) - # Module name -> module class (for getting package info) - module_classes: dict[str, type[Module]] = {} - - for bp in blueprint_set.blueprints: - module_classes[bp.module.__name__] = bp.module - for conn in bp.streams: - # Apply remapping - remapped_name = blueprint_set.remapping_map.get((bp.module, conn.name), conn.name) - key = (remapped_name, conn.type) - if conn.direction == "out": - producers[key].append(bp.module) # type: ignore[index] - else: - consumers[key].append(bp.module) # type: ignore[index] - - # Find all active channels (have both producers AND consumers) - active_channels: dict[tuple[str, type], str] = {} # key -> color - for key in producers: - name, type_ = key - type_name = type_.__name__ - if key not in consumers: - continue - if (name, type_name) in ignored_streams: - continue - # Check if all modules are ignored - valid_producers = [m for m in producers[key] if m.__name__ not in ignored_modules] - valid_consumers = [m for m in consumers[key] if m.__name__ not in ignored_modules] - if not valid_producers or not valid_consumers: - continue - label = f"{name}:{type_name}" - active_channels[key] = color_for_string(TYPE_COLORS, label) - - # Group modules by package - def get_group(mod_class: type[Module]) -> str: - module_path = mod_class.__module__ - parts = module_path.split(".") - if len(parts) >= 2 and parts[0] == "dimos": - return parts[1] - return "other" - - by_group: dict[str, list[str]] = defaultdict(list) - for mod_name, mod_class in module_classes.items(): - if mod_name in ignored_modules: - continue - group = get_group(mod_class) - by_group[group].append(mod_name) - - # Build DOT output - lines = [ - "digraph modules {", - " bgcolor=transparent;", - " rankdir=LR;", - # " nodesep=1;", # horizontal spacing between nodes - # " ranksep=1.5;", # vertical spacing between ranks - " splines=true;", - f' node [shape=box, style=filled, fillcolor="{theme.BACKGROUND}", fontcolor="{theme.FOREGROUND}", color="{theme.BLUE}", fontname=fixed, fontsize=12, margin="0.1,0.1"];', - " edge [fontname=fixed, fontsize=10];", - "", - ] - - # Add subgraphs for each module group - sorted_groups = sorted(by_group.keys()) - for group in sorted_groups: - mods = sorted(by_group[group]) - color = color_for_string(GROUP_COLORS, group) - lines.append(f" subgraph cluster_{group} {{") - lines.append(f' label="{group}";') - lines.append(" labeljust=r;") - lines.append(" fontname=fixed;") - lines.append(" fontsize=14;") - lines.append(f' fontcolor="{theme.FOREGROUND}";') - lines.append(' style="filled,dashed";') - lines.append(f' color="{color}";') - lines.append(" penwidth=1;") - lines.append(f' fillcolor="{color}10";') - for mod in mods: - lines.append(f" {mod};") - # Stack nodes vertically within cluster - if LayoutAlgo.STACK_NODES in layout and len(mods) > 1: - for i in range(len(mods) - 1): - lines.append(f" {mods[i]} -> {mods[i + 1]} [style=invis];") - lines.append(" }") - lines.append("") - - # Add invisible edges between clusters to force vertical stacking - if LayoutAlgo.STACK_CLUSTERS in layout and len(sorted_groups) > 1: - lines.append(" // Force vertical cluster layout") - for i in range(len(sorted_groups) - 1): - group_a = sorted_groups[i] - group_b = sorted_groups[i + 1] - # Pick first node from each cluster - node_a = sorted(by_group[group_a])[0] - node_b = sorted(by_group[group_b])[0] - lines.append(f" {node_a} -> {node_b} [style=invis, weight=10];") - lines.append("") - - # Add type nodes (outside all clusters) - lines.append(" // Type nodes (data channels)") - for key, color in sorted( - active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}" - ): - name, type_ = key - type_name = type_.__name__ - node_id = sanitize_id(f"chan_{name}_{type_name}") - label = f"{name}:{type_name}" - lines.append( - f' {node_id} [label="{label}", shape=note, style=filled, ' - f'fillcolor="{color}35", color="{color}", fontcolor="{theme.FOREGROUND}", ' - f'width=0, height=0, margin="0.1,0.05", fontsize=10];' - ) - - lines.append("") - - # Add edges: producer -> type_node -> consumer - lines.append(" // Edges") - for key, color in sorted( - active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}" - ): - name, type_ = key - type_name = type_.__name__ - node_id = sanitize_id(f"chan_{name}_{type_name}") - - # Edges from producers to type node (no arrow, kept close) - for producer in producers[key]: - if producer.__name__ in ignored_modules: - continue - lines.append(f' {producer.__name__} -> {node_id} [color="{color}", arrowhead=none];') - - # Edges from type node to consumers (with arrow) - for consumer in consumers[key]: - if consumer.__name__ in ignored_modules: - continue - lines.append(f' {node_id} -> {consumer.__name__} [color="{color}"];') - - lines.append("}") - return "\n".join(lines) - - -def render_svg( - blueprint_set: Blueprint, - output_path: str, - *, - layout: set[LayoutAlgo] | None = None, -) -> None: - """Generate an SVG file from a Blueprint using graphviz. - - Args: - blueprint_set: The blueprint set to visualize. - output_path: Path to write the SVG file. - layout: Set of layout algorithms to apply. - """ - import subprocess - - if layout is None: - layout = set() - - dot_code = render(blueprint_set, layout=layout) - engine = "fdp" if LayoutAlgo.FDP in layout else "dot" - result = subprocess.run( - [engine, "-Tsvg", "-o", output_path], - input=dot_code, - text=True, - capture_output=True, - ) - if result.returncode != 0: - raise RuntimeError(f"graphviz failed: {result.stderr}") diff --git a/dimos/core/introspection/module/__init__.py b/dimos/core/introspection/module/__init__.py deleted file mode 100644 index 444d0e24f3..0000000000 --- a/dimos/core/introspection/module/__init__.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Module introspection and rendering. - -Renderers: - - ansi: ANSI terminal output (default) - - dot: Graphviz DOT format -""" - -from dimos.core.introspection.module import ansi, dot -from dimos.core.introspection.module.info import ( - INTERNAL_RPCS, - ModuleInfo, - ParamInfo, - RpcInfo, - SkillInfo, - StreamInfo, - extract_module_info, -) -from dimos.core.introspection.module.render import render_module_io - -__all__ = [ - "INTERNAL_RPCS", - "ModuleInfo", - "ParamInfo", - "RpcInfo", - "SkillInfo", - "StreamInfo", - "ansi", - "dot", - "extract_module_info", - "render_module_io", -] diff --git a/dimos/core/introspection/module/ansi.py b/dimos/core/introspection/module/ansi.py deleted file mode 100644 index 6e835d63d3..0000000000 --- a/dimos/core/introspection/module/ansi.py +++ /dev/null @@ -1,96 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ANSI terminal renderer for module IO diagrams.""" - -from dimos.core import colors -from dimos.core.introspection.module.info import ( - ModuleInfo, - ParamInfo, - RpcInfo, - SkillInfo, - StreamInfo, -) - - -def render(info: ModuleInfo, color: bool = True) -> str: - """Render module info as an ANSI terminal diagram. - - Args: - info: ModuleInfo structure to render. - color: Whether to include ANSI color codes. - - Returns: - ASCII/Unicode diagram with optional ANSI colors. - """ - # Color functions that become identity when color=False - _green = colors.green if color else (lambda x: x) - _blue = colors.blue if color else (lambda x: x) - _yellow = colors.yellow if color else (lambda x: x) - _cyan = colors.cyan if color else (lambda x: x) - - def _box(name: str) -> list[str]: - return [ - "┌┴" + "─" * (len(name) + 1) + "┐", - f"│ {name} │", - "└┬" + "─" * (len(name) + 1) + "┘", - ] - - def format_stream(stream: StreamInfo) -> str: - return f"{_yellow(stream.name)}: {_green(stream.type_name)}" - - def format_param(param: ParamInfo) -> str: - result = param.name - if param.type_name: - result += ": " + _green(param.type_name) - if param.default: - result += f" = {param.default}" - return result - - def format_rpc(rpc: RpcInfo) -> str: - params = ", ".join(format_param(p) for p in rpc.params) - result = _blue(rpc.name) + f"({params})" - if rpc.return_type: - result += " -> " + _green(rpc.return_type) - return result - - def format_skill(skill: SkillInfo) -> str: - info_parts = [] - if skill.stream: - info_parts.append(f"stream={skill.stream}") - if skill.reducer: - info_parts.append(f"reducer={skill.reducer}") - if skill.output: - info_parts.append(f"output={skill.output}") - info = f" ({', '.join(info_parts)})" if info_parts else "" - return _cyan(skill.name) + info - - # Build output - lines = [ - *(f" ├─ {format_stream(s)}" for s in info.inputs), - *_box(info.name), - *(f" ├─ {format_stream(s)}" for s in info.outputs), - ] - - if info.rpcs: - lines.append(" │") - for rpc in info.rpcs: - lines.append(f" ├─ RPC {format_rpc(rpc)}") - - if info.skills: - lines.append(" │") - for skill in info.skills: - lines.append(f" ├─ Skill {format_skill(skill)}") - - return "\n".join(lines) diff --git a/dimos/core/introspection/module/dot.py b/dimos/core/introspection/module/dot.py deleted file mode 100644 index 829957a8e3..0000000000 --- a/dimos/core/introspection/module/dot.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Graphviz DOT renderer for module IO diagrams.""" - -from dimos.core.introspection.module.info import ModuleInfo -from dimos.core.introspection.utils import ( - RPC_COLOR, - SKILL_COLOR, - TYPE_COLORS, - color_for_string, - sanitize_id, -) -from dimos.utils.cli import theme - - -def render(info: ModuleInfo) -> str: - """Render module info as a DOT graph. - - Shows the module as a central node with input streams as nodes - pointing in and output streams as nodes pointing out. - - Args: - info: ModuleInfo structure to render. - - Returns: - DOT format string. - """ - lines = [ - "digraph module {", - " bgcolor=transparent;", - " rankdir=LR;", - " compound=true;", - " splines=true;", - f' node [shape=box, style=filled, fillcolor="{theme.BACKGROUND}", fontcolor="{theme.FOREGROUND}", color="{theme.BLUE}", fontname=fixed, fontsize=12, margin="0.1,0.1"];', - " edge [fontname=fixed, fontsize=10, penwidth=1];", - "", - ] - - # Module node (central, larger) - module_id = sanitize_id(info.name) - lines.append(f' {module_id} [label="{info.name}", width=2, height=0.8];') - lines.append("") - - # Input stream nodes (on the left) - if info.inputs: - lines.append(" // Input streams") - lines.append(" subgraph cluster_inputs {") - lines.append(' label="";') - lines.append(" style=invis;") - lines.append(' rank="same";') - for stream in info.inputs: - label = f"{stream.name}:{stream.type_name}" - color = color_for_string(TYPE_COLORS, label) - node_id = sanitize_id(f"in_{stream.name}") - lines.append( - f' {node_id} [label="{label}", shape=note, style=filled, ' - f'fillcolor="{color}35", color="{color}", ' - f'width=0, height=0, margin="0.1,0.05", fontsize=10];' - ) - lines.append(" }") - lines.append("") - - # Output stream nodes (on the right) - if info.outputs: - lines.append(" // Output streams") - lines.append(" subgraph cluster_outputs {") - lines.append(' label="";') - lines.append(" style=invis;") - lines.append(' rank="same";') - for stream in info.outputs: - label = f"{stream.name}:{stream.type_name}" - color = color_for_string(TYPE_COLORS, label) - node_id = sanitize_id(f"out_{stream.name}") - lines.append( - f' {node_id} [label="{label}", shape=note, style=filled, ' - f'fillcolor="{color}35", color="{color}", ' - f'width=0, height=0, margin="0.1,0.05", fontsize=10];' - ) - lines.append(" }") - lines.append("") - - # RPC nodes (in subgraph) - if info.rpcs: - lines.append(" // RPCs") - lines.append(" subgraph cluster_rpcs {") - lines.append(' label="RPCs";') - lines.append(" labeljust=l;") - lines.append(" fontname=fixed;") - lines.append(" fontsize=14;") - lines.append(f' fontcolor="{theme.FOREGROUND}";') - lines.append(' style="filled,dashed";') - lines.append(f' color="{RPC_COLOR}";') - lines.append(" penwidth=1;") - lines.append(f' fillcolor="{RPC_COLOR}10";') - for rpc in info.rpcs: - params = ", ".join( - f"{p.name}: {p.type_name}" if p.type_name else p.name for p in rpc.params - ) - ret = f" -> {rpc.return_type}" if rpc.return_type else "" - label = f"{rpc.name}({params}){ret}" - node_id = sanitize_id(f"rpc_{rpc.name}") - lines.append( - f' {node_id} [label="{label}", shape=cds, style=filled, ' - f'fillcolor="{RPC_COLOR}35", color="{RPC_COLOR}", ' - f'width=0, height=0, margin="0.1,0.05", fontsize=9];' - ) - lines.append(" }") - lines.append("") - - # Skill nodes (in subgraph) - if info.skills: - lines.append(" // Skills") - lines.append(" subgraph cluster_skills {") - lines.append(' label="Skills";') - lines.append(" labeljust=l;") - lines.append(" fontname=fixed;") - lines.append(" fontsize=14;") - lines.append(f' fontcolor="{theme.FOREGROUND}";') - lines.append(' style="filled,dashed";') - lines.append(f' color="{SKILL_COLOR}";') - lines.append(" penwidth=1;") - lines.append(f' fillcolor="{SKILL_COLOR}20";') - for skill in info.skills: - parts = [skill.name] - if skill.stream: - parts.append(f"stream={skill.stream}") - if skill.reducer: - parts.append(f"reducer={skill.reducer}") - label = " ".join(parts) - node_id = sanitize_id(f"skill_{skill.name}") - lines.append( - f' {node_id} [label="{label}", shape=cds, style=filled, ' - f'fillcolor="{SKILL_COLOR}35", color="{SKILL_COLOR}", ' - f'width=0, height=0, margin="0.1,0.05", fontsize=9];' - ) - lines.append(" }") - lines.append("") - - # Edges: inputs -> module - lines.append(" // Edges") - for stream in info.inputs: - label = f"{stream.name}:{stream.type_name}" - color = color_for_string(TYPE_COLORS, label) - node_id = sanitize_id(f"in_{stream.name}") - lines.append(f' {node_id} -> {module_id} [color="{color}"];') - - # Edges: module -> outputs - for stream in info.outputs: - label = f"{stream.name}:{stream.type_name}" - color = color_for_string(TYPE_COLORS, label) - node_id = sanitize_id(f"out_{stream.name}") - lines.append(f' {module_id} -> {node_id} [color="{color}"];') - - # Edge: module -> RPCs cluster (dashed, no arrow) - if info.rpcs: - first_rpc_id = sanitize_id(f"rpc_{info.rpcs[0].name}") - lines.append( - f" {module_id} -> {first_rpc_id} [lhead=cluster_rpcs, style=filled, weight=3" - f'color="{RPC_COLOR}", arrowhead=none];' - ) - - # Edge: module -> Skills cluster (dashed, no arrow) - if info.skills: - first_skill_id = sanitize_id(f"skill_{info.skills[0].name}") - lines.append( - f" {module_id} -> {first_skill_id} [lhead=cluster_skills, style=filled, weight=3" - f'color="{SKILL_COLOR}", arrowhead=none];' - ) - - lines.append("}") - return "\n".join(lines) - - -def render_svg(info: ModuleInfo, output_path: str) -> None: - """Generate an SVG file from ModuleInfo using graphviz. - - Args: - info: ModuleInfo structure to render. - output_path: Path to write the SVG file. - """ - import subprocess - - dot_code = render(info) - result = subprocess.run( - ["dot", "-Tsvg", "-o", output_path], - input=dot_code, - text=True, - capture_output=True, - ) - if result.returncode != 0: - raise RuntimeError(f"graphviz failed: {result.stderr}") diff --git a/dimos/core/introspection/module/info.py b/dimos/core/introspection/module/info.py deleted file mode 100644 index 8fcad76006..0000000000 --- a/dimos/core/introspection/module/info.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Module introspection data structures.""" - -from collections.abc import Callable -from dataclasses import dataclass, field -import inspect -from typing import Any - -# Internal RPCs to hide from io() output -INTERNAL_RPCS = { - "dynamic_skills", - "get_rpc_method_names", - "set_rpc_method", - "skills", - "_io_instance", -} - - -@dataclass -class StreamInfo: - """Information about a module stream (input or output).""" - - name: str - type_name: str - - -@dataclass -class ParamInfo: - """Information about an RPC parameter.""" - - name: str - type_name: str | None = None - default: str | None = None - - -@dataclass -class RpcInfo: - """Information about an RPC method.""" - - name: str - params: list[ParamInfo] = field(default_factory=list) - return_type: str | None = None - - -@dataclass -class SkillInfo: - """Information about a skill.""" - - name: str - stream: str | None = None # None means "none" - reducer: str | None = None # None means "latest" - output: str | None = None # None means "standard" - - -@dataclass -class ModuleInfo: - """Extracted information about a module's IO interface.""" - - name: str - inputs: list[StreamInfo] = field(default_factory=list) - outputs: list[StreamInfo] = field(default_factory=list) - rpcs: list[RpcInfo] = field(default_factory=list) - skills: list[SkillInfo] = field(default_factory=list) - - -def extract_rpc_info(fn: Callable) -> RpcInfo: # type: ignore[type-arg] - """Extract RPC information from a callable.""" - sig = inspect.signature(fn) - params = [] - - for pname, p in sig.parameters.items(): - if pname == "self": - continue - type_name = None - if p.annotation != inspect.Parameter.empty: - type_name = getattr(p.annotation, "__name__", str(p.annotation)) - default = None - if p.default != inspect.Parameter.empty: - default = str(p.default) - params.append(ParamInfo(name=pname, type_name=type_name, default=default)) - - return_type = None - if sig.return_annotation != inspect.Signature.empty: - return_type = getattr(sig.return_annotation, "__name__", str(sig.return_annotation)) - - return RpcInfo(name=fn.__name__, params=params, return_type=return_type) - - -def extract_skill_info(fn: Callable) -> SkillInfo: # type: ignore[type-arg] - """Extract skill information from a skill-decorated callable.""" - cfg = fn._skill_config # type: ignore[attr-defined] - - stream = cfg.stream.name if cfg.stream.name != "none" else None - reducer_name = getattr(cfg.reducer, "__name__", str(cfg.reducer)) - reducer = reducer_name if reducer_name != "latest" else None - output = cfg.output.name if cfg.output.name != "standard" else None - - return SkillInfo(name=fn.__name__, stream=stream, reducer=reducer, output=output) - - -def extract_module_info( - name: str, - inputs: dict[str, Any], - outputs: dict[str, Any], - rpcs: dict[str, Callable], # type: ignore[type-arg] -) -> ModuleInfo: - """Extract module information into a ModuleInfo structure. - - Args: - name: Module class name. - inputs: Dict of input stream name -> stream object or formatted string. - outputs: Dict of output stream name -> stream object or formatted string. - rpcs: Dict of RPC method name -> callable. - - Returns: - ModuleInfo with extracted data. - """ - - # Extract stream info - def stream_info(stream: Any, stream_name: str) -> StreamInfo: - if isinstance(stream, str): - # Pre-formatted string like "name: Type" - parse it - # Strip ANSI codes for parsing - import re - - clean = re.sub(r"\x1b\[[0-9;]*m", "", stream) - if ": " in clean: - parts = clean.split(": ", 1) - return StreamInfo(name=parts[0], type_name=parts[1]) - return StreamInfo(name=stream_name, type_name=clean) - # Instance stream object - return StreamInfo(name=stream.name, type_name=stream.type.__name__) - - input_infos = [stream_info(s, n) for n, s in inputs.items()] - output_infos = [stream_info(s, n) for n, s in outputs.items()] - - # Separate skills from regular RPCs, filtering internal ones - rpc_infos = [] - skill_infos = [] - - for rpc_name, rpc_fn in rpcs.items(): - if rpc_name in INTERNAL_RPCS: - continue - if hasattr(rpc_fn, "_skill_config"): - skill_infos.append(extract_skill_info(rpc_fn)) - else: - rpc_infos.append(extract_rpc_info(rpc_fn)) - - return ModuleInfo( - name=name, - inputs=input_infos, - outputs=output_infos, - rpcs=rpc_infos, - skills=skill_infos, - ) diff --git a/dimos/core/introspection/module/render.py b/dimos/core/introspection/module/render.py deleted file mode 100644 index 8e87a5b202..0000000000 --- a/dimos/core/introspection/module/render.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Convenience rendering functions for module introspection.""" - -from collections.abc import Callable -from typing import Any - -from dimos.core.introspection.module import ansi -from dimos.core.introspection.module.info import extract_module_info - - -def render_module_io( - name: str, - inputs: dict[str, Any], - outputs: dict[str, Any], - rpcs: dict[str, Callable], # type: ignore[type-arg] - color: bool = True, -) -> str: - """Render module IO diagram using the default (ANSI) renderer. - - Args: - name: Module class name. - inputs: Dict of input stream name -> stream object or formatted string. - outputs: Dict of output stream name -> stream object or formatted string. - rpcs: Dict of RPC method name -> callable. - color: Whether to include ANSI color codes. - - Returns: - ASCII diagram showing module inputs, outputs, RPCs, and skills. - """ - info = extract_module_info(name, inputs, outputs, rpcs) - return ansi.render(info, color=color) diff --git a/dimos/core/introspection/svg.py b/dimos/core/introspection/svg.py deleted file mode 100644 index 57b88834e0..0000000000 --- a/dimos/core/introspection/svg.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unified SVG rendering for modules and blueprints.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.core.blueprints import Blueprint - from dimos.core.introspection.blueprint.dot import LayoutAlgo - from dimos.core.introspection.module.info import ModuleInfo - - -def to_svg( - target: ModuleInfo | Blueprint, - output_path: str, - *, - layout: set[LayoutAlgo] | None = None, -) -> None: - """Render a module or blueprint to SVG. - - Dispatches to the appropriate renderer based on input type: - - ModuleInfo -> module/dot.render_svg - - Blueprint -> blueprint/dot.render_svg - - Args: - target: Either a ModuleInfo (single module) or Blueprint (blueprint graph). - output_path: Path to write the SVG file. - layout: Layout algorithms (only used for blueprints). - """ - # Avoid circular imports by importing here - from dimos.core.blueprints import Blueprint - from dimos.core.introspection.module.info import ModuleInfo - - if isinstance(target, ModuleInfo): - from dimos.core.introspection.module import dot as module_dot - - module_dot.render_svg(target, output_path) - elif isinstance(target, Blueprint): - from dimos.core.introspection.blueprint import dot as blueprint_dot - - blueprint_dot.render_svg(target, output_path, layout=layout) - else: - raise TypeError(f"Expected ModuleInfo or Blueprint, got {type(target).__name__}") diff --git a/dimos/core/introspection/utils.py b/dimos/core/introspection/utils.py deleted file mode 100644 index 166933b80c..0000000000 --- a/dimos/core/introspection/utils.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Shared utilities for introspection renderers.""" - -import hashlib -import re - -# Colors for type nodes and edges (bright, distinct, good on dark backgrounds) -TYPE_COLORS = [ - "#FF6B6B", # coral red - "#4ECDC4", # teal - "#FFE66D", # yellow - "#95E1D3", # mint - "#F38181", # salmon - "#AA96DA", # lavender - "#81C784", # green - "#64B5F6", # light blue - "#FFB74D", # orange - "#BA68C8", # purple - "#4DD0E1", # cyan - "#AED581", # lime - "#FF8A65", # deep orange - "#7986CB", # indigo - "#F06292", # pink - "#A1887F", # brown - "#90A4AE", # blue grey - "#DCE775", # lime yellow - "#4DB6AC", # teal green - "#9575CD", # deep purple - "#E57373", # light red - "#81D4FA", # sky blue - "#C5E1A5", # light green - "#FFCC80", # light orange - "#B39DDB", # light purple - "#80DEEA", # light cyan - "#FFAB91", # peach - "#CE93D8", # light violet - "#80CBC4", # light teal - "#FFF59D", # light yellow -] - -# Colors for group borders (bright, distinct, good on dark backgrounds) -GROUP_COLORS = [ - "#5C9FF0", # blue - "#FFB74D", # orange - "#81C784", # green - "#BA68C8", # purple - "#4ECDC4", # teal - "#FF6B6B", # coral - "#FFE66D", # yellow - "#7986CB", # indigo - "#F06292", # pink - "#4DB6AC", # teal green - "#9575CD", # deep purple - "#AED581", # lime - "#64B5F6", # light blue - "#FF8A65", # deep orange - "#AA96DA", # lavender -] - -# Colors for RPCs/Skills -RPC_COLOR = "#7986CB" # indigo -SKILL_COLOR = "#4ECDC4" # teal - - -def color_for_string(colors: list[str], s: str) -> str: - """Get a consistent color for a string based on its hash.""" - h = int(hashlib.md5(s.encode()).hexdigest(), 16) - return colors[h % len(colors)] - - -def sanitize_id(s: str) -> str: - """Sanitize a string to be a valid graphviz node ID.""" - return re.sub(r"[^a-zA-Z0-9_]", "_", s) diff --git a/dimos/core/module.py b/dimos/core/module.py deleted file mode 100644 index d6089a8f0a..0000000000 --- a/dimos/core/module.py +++ /dev/null @@ -1,515 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -import asyncio -from dataclasses import dataclass -from functools import partial -import inspect -import json -import sys -import threading -from typing import ( - TYPE_CHECKING, - Any, - get_args, - get_origin, - get_type_hints, - overload, -) - -from typing_extensions import TypeVar as TypeVarExtension - -if TYPE_CHECKING: - from collections.abc import Callable - - from dimos.core.introspection.module import ModuleInfo - from dimos.core.rpc_client import RPCClient - -from typing import TypeVar - -from dask.distributed import Actor, get_worker -from langchain_core.tools import tool -from reactivex.disposable import CompositeDisposable - -from dimos.core import colors -from dimos.core.core import T, rpc -from dimos.core.introspection.module import extract_module_info, render_module_io -from dimos.core.resource import Resource -from dimos.core.rpc_client import RpcCall # noqa: TC001 -from dimos.core.stream import In, Out, RemoteIn, RemoteOut, Transport -from dimos.protocol.rpc import LCMRPC, RPCSpec -from dimos.protocol.service import Configurable # type: ignore[attr-defined] -from dimos.protocol.tf import LCMTF, TFSpec -from dimos.utils.generic import classproperty - - -@dataclass(frozen=True) -class SkillInfo: - class_name: str - func_name: str - args_schema: str - - -def get_loop() -> tuple[asyncio.AbstractEventLoop, threading.Thread | None]: - # we are actually instantiating a new loop here - # to not interfere with an existing dask loop - - # try: - # # here we attempt to figure out if we are running on a dask worker - # # if so we use the dask worker _loop as ours, - # # and we register our RPC server - # worker = get_worker() - # if worker.loop: - # print("using dask worker loop") - # return worker.loop.asyncio_loop - - # except ValueError: - # ... - - try: - running_loop = asyncio.get_running_loop() - return running_loop, None - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - thr = threading.Thread(target=loop.run_forever, daemon=True) - thr.start() - return loop, thr - - -@dataclass -class ModuleConfig: - rpc_transport: type[RPCSpec] = LCMRPC - tf_transport: type[TFSpec] = LCMTF - frame_id_prefix: str | None = None - frame_id: str | None = None - - -ModuleConfigT = TypeVarExtension("ModuleConfigT", bound=ModuleConfig, default=ModuleConfig) - - -class ModuleBase(Configurable[ModuleConfigT], Resource): - _rpc: RPCSpec | None = None - _tf: TFSpec | None = None - _loop: asyncio.AbstractEventLoop | None = None - _loop_thread: threading.Thread | None - _disposables: CompositeDisposable - _bound_rpc_calls: dict[str, RpcCall] = {} - - rpc_calls: list[str] = [] - - default_config: type[ModuleConfigT] = ModuleConfig # type: ignore[assignment] - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._loop, self._loop_thread = get_loop() - self._disposables = CompositeDisposable() - # we can completely override comms protocols if we want - try: - # here we attempt to figure out if we are running on a dask worker - # if so we use the dask worker _loop as ours, - # and we register our RPC server - self.rpc = self.config.rpc_transport() - self.rpc.serve_module_rpc(self) - self.rpc.start() # type: ignore[attr-defined] - except ValueError: - ... - - @property - def frame_id(self) -> str: - base = self.config.frame_id or self.__class__.__name__ - if self.config.frame_id_prefix: - return f"{self.config.frame_id_prefix}/{base}" - return base - - @rpc - def start(self) -> None: - pass - - @rpc - def stop(self) -> None: - self._close_module() - - def _close_module(self) -> None: - self._close_rpc() - - # Save into local variables to avoid race when stopping concurrently - # (from RPC and worker shutdown) - loop_thread = getattr(self, "_loop_thread", None) - loop = getattr(self, "_loop", None) - - if loop_thread: - if loop_thread.is_alive(): - if loop: - loop.call_soon_threadsafe(loop.stop) - loop_thread.join(timeout=2) - self._loop = None - self._loop_thread = None - - if hasattr(self, "_tf") and self._tf is not None: - self._tf.stop() - self._tf = None - if hasattr(self, "_disposables"): - self._disposables.dispose() - - def _close_rpc(self) -> None: - if self.rpc: - self.rpc.stop() # type: ignore[attr-defined] - self.rpc = None # type: ignore[assignment] - - def __getstate__(self): # type: ignore[no-untyped-def] - """Exclude unpicklable runtime attributes when serializing.""" - state = self.__dict__.copy() - # Remove unpicklable attributes - state.pop("_disposables", None) - state.pop("_loop", None) - state.pop("_loop_thread", None) - state.pop("_rpc", None) - state.pop("_tf", None) - return state - - def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] - """Restore object from pickled state.""" - self.__dict__.update(state) - # Reinitialize runtime attributes - self._disposables = CompositeDisposable() - self._loop = None - self._loop_thread = None - self._rpc = None - self._tf = None - - @property - def tf(self): # type: ignore[no-untyped-def] - if self._tf is None: - # self._tf = self.config.tf_transport() - self._tf = LCMTF() - return self._tf - - @tf.setter - def tf(self, value) -> None: # type: ignore[no-untyped-def] - import warnings - - warnings.warn( - "tf is available on all modules. Call self.tf.start() to activate tf functionality. No need to assign it", - UserWarning, - stacklevel=2, - ) - - @property - def outputs(self) -> dict[str, Out]: # type: ignore[type-arg] - return { - name: s - for name, s in self.__dict__.items() - if isinstance(s, Out) and not name.startswith("_") - } - - @property - def inputs(self) -> dict[str, In]: # type: ignore[type-arg] - return { - name: s - for name, s in self.__dict__.items() - if isinstance(s, In) and not name.startswith("_") - } - - @classproperty - def rpcs(self) -> dict[str, Callable[..., Any]]: - return { - name: getattr(self, name) - for name in dir(self) - if not name.startswith("_") - and name != "rpcs" # Exclude the rpcs property itself to prevent recursion - and callable(getattr(self, name, None)) - and hasattr(getattr(self, name), "__rpc__") - } - - @rpc - def _io_instance(self, color: bool = True) -> str: - """Instance-level io() - shows actual running streams.""" - return render_module_io( - name=self.__class__.__name__, - inputs=self.inputs, - outputs=self.outputs, - rpcs=self.rpcs, - color=color, - ) - - @classmethod - def _io_class(cls, color: bool = True) -> str: - """Class-level io() - shows declared stream types from annotations.""" - hints = get_type_hints(cls) - - _yellow = colors.yellow if color else (lambda x: x) - _green = colors.green if color else (lambda x: x) - - def is_stream(hint: type, stream_type: type) -> bool: - origin = get_origin(hint) - if origin is stream_type: - return True - if isinstance(hint, type) and issubclass(hint, stream_type): - return True - return False - - def format_stream(name: str, hint: type) -> str: - args = get_args(hint) - type_name = args[0].__name__ if args else "?" - return f"{_yellow(name)}: {_green(type_name)}" - - inputs = { - name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, In) - } - outputs = { - name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, Out) - } - - return render_module_io( - name=cls.__name__, - inputs=inputs, - outputs=outputs, - rpcs=cls.rpcs, - color=color, - ) - - class _io_descriptor: - """Descriptor that makes io() work on both class and instance.""" - - def __get__( - self, obj: ModuleBase | None, objtype: type[ModuleBase] - ) -> Callable[[bool], str]: - if obj is None: - return objtype._io_class - return obj._io_instance - - io = _io_descriptor() - - @classmethod - def _module_info_class(cls) -> ModuleInfo: - """Class-level module_info() - returns ModuleInfo from annotations.""" - - hints = get_type_hints(cls) - - def is_stream(hint: type, stream_type: type) -> bool: - origin = get_origin(hint) - if origin is stream_type: - return True - if isinstance(hint, type) and issubclass(hint, stream_type): - return True - return False - - def format_stream(name: str, hint: type) -> str: - args = get_args(hint) - type_name = args[0].__name__ if args else "?" - return f"{name}: {type_name}" - - inputs = { - name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, In) - } - outputs = { - name: format_stream(name, hint) for name, hint in hints.items() if is_stream(hint, Out) - } - - return extract_module_info( - name=cls.__name__, - inputs=inputs, - outputs=outputs, - rpcs=cls.rpcs, - ) - - class _module_info_descriptor: - """Descriptor that makes module_info() work on both class and instance.""" - - def __get__( - self, obj: ModuleBase | None, objtype: type[ModuleBase] - ) -> Callable[[], ModuleInfo]: - if obj is None: - return objtype._module_info_class - # For instances, extract from actual streams - return lambda: extract_module_info( - name=obj.__class__.__name__, - inputs=obj.inputs, - outputs=obj.outputs, - rpcs=obj.rpcs, - ) - - module_info = _module_info_descriptor() - - @classproperty - def blueprint(self): # type: ignore[no-untyped-def] - # Here to prevent circular imports. - from dimos.core.blueprints import Blueprint - - return partial(Blueprint.create, self) # type: ignore[arg-type] - - @rpc - def get_rpc_method_names(self) -> list[str]: - return self.rpc_calls - - @rpc - def set_rpc_method(self, method: str, callable: RpcCall) -> None: - callable.set_rpc(self.rpc) # type: ignore[arg-type] - self._bound_rpc_calls[method] = callable - - @rpc - def set_module_ref(self, name: str, module_ref: RPCClient) -> None: - setattr(self, name, module_ref) - - @overload - def get_rpc_calls(self, method: str) -> RpcCall: ... - - @overload - def get_rpc_calls(self, method1: str, method2: str, *methods: str) -> tuple[RpcCall, ...]: ... - - def get_rpc_calls(self, *methods: str) -> RpcCall | tuple[RpcCall, ...]: # type: ignore[misc] - missing = [m for m in methods if m not in self._bound_rpc_calls] - if missing: - raise ValueError( - f"RPC methods not found. Class: {self.__class__.__name__}, RPC methods: {', '.join(missing)}" - ) - result = tuple(self._bound_rpc_calls[m] for m in methods) - return result[0] if len(result) == 1 else result - - @rpc - def get_skills(self) -> list[SkillInfo]: - skills: list[SkillInfo] = [] - for name in dir(self): - attr = getattr(self, name) - if callable(attr) and hasattr(attr, "__skill__"): - schema = json.dumps(tool(attr).args_schema.model_json_schema()) - skills.append( - SkillInfo( - class_name=self.__class__.__name__, func_name=name, args_schema=schema - ) - ) - return skills - - -class Module(ModuleBase[ModuleConfigT]): - ref: Actor - worker: int - - def __init_subclass__(cls, **kwargs: Any) -> None: - """Set class-level None attributes for In/Out type annotations. - - This is needed because Dask's Actor proxy looks up attributes on the class - (not instance) when proxying attribute access. Without class-level attributes, - the proxy would fail with AttributeError even though the instance has the attrs. - """ - super().__init_subclass__(**kwargs) - - # Get type hints for this class only (not inherited ones). - globalns = {} - for c in cls.__mro__: - if c.__module__ in sys.modules: - globalns.update(sys.modules[c.__module__].__dict__) - - try: - hints = get_type_hints(cls, globalns=globalns, include_extras=True) - except (NameError, AttributeError, TypeError): - hints = {} - - for name, ann in hints.items(): - origin = get_origin(ann) - if origin in (In, Out): - # Set class-level attribute if not already set. - if not hasattr(cls, name) or getattr(cls, name) is None: - setattr(cls, name, None) - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - self.ref = None # type: ignore[assignment] - - # Get type hints with proper namespace resolution for subclasses - # Collect namespaces from all classes in the MRO chain - import sys - - globalns = {} - for cls in self.__class__.__mro__: - if cls.__module__ in sys.modules: - globalns.update(sys.modules[cls.__module__].__dict__) - - try: - hints = get_type_hints(self.__class__, globalns=globalns, include_extras=True) - except (NameError, AttributeError, TypeError): - # If we still can't resolve hints, skip type hint processing - # This can happen with complex forward references - hints = {} - - for name, ann in hints.items(): - origin = get_origin(ann) - if origin is Out: - inner, *_ = get_args(ann) or (Any,) - stream = Out(inner, name, self) # type: ignore[var-annotated] - setattr(self, name, stream) - elif origin is In: - inner, *_ = get_args(ann) or (Any,) - stream = In(inner, name, self) # type: ignore[assignment] - setattr(self, name, stream) - super().__init__(*args, **kwargs) - - def set_ref(self, ref) -> int: # type: ignore[no-untyped-def] - worker = get_worker() - self.ref = ref - self.worker = worker.name - return worker.name # type: ignore[no-any-return] - - def __str__(self) -> str: - return f"{self.__class__.__name__}" - - @rpc - def set_transport(self, stream_name: str, transport: Transport) -> bool: # type: ignore[type-arg] - stream = getattr(self, stream_name, None) - if not stream: - raise ValueError(f"{stream_name} not found in {self.__class__.__name__}") - - if not isinstance(stream, Out) and not isinstance(stream, In): - raise TypeError(f"Output {stream_name} is not a valid stream") - - stream._transport = transport - return True - - @rpc - def configure_stream(self, stream_name: str, topic: str) -> bool: - """Configure a stream's transport by topic. Called by DockerModule for stream wiring.""" - from dimos.core.transport import pLCMTransport - - stream = getattr(self, stream_name, None) - if not isinstance(stream, (Out, In)): - return False - stream._transport = pLCMTransport(topic) - return True - - # called from remote - def connect_stream(self, input_name: str, remote_stream: RemoteOut[T]): # type: ignore[no-untyped-def] - input_stream = getattr(self, input_name, None) - if not input_stream: - raise ValueError(f"{input_name} not found in {self.__class__.__name__}") - if not isinstance(input_stream, In): - raise TypeError(f"Input {input_name} is not a valid stream") - input_stream.connection = remote_stream - - def dask_receive_msg(self, input_name: str, msg: Any) -> None: - getattr(self, input_name).transport.dask_receive_msg(msg) - - def dask_register_subscriber(self, output_name: str, subscriber: RemoteIn[T]) -> None: - getattr(self, output_name).transport.dask_register_subscriber(subscriber) - - -ModuleT = TypeVar("ModuleT", bound="Module") - - -def is_module_type(value: Any) -> bool: - try: - return inspect.isclass(value) and issubclass(value, Module) - except Exception: - return False diff --git a/dimos/core/module_coordinator.py b/dimos/core/module_coordinator.py deleted file mode 100644 index c6d975731d..0000000000 --- a/dimos/core/module_coordinator.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from concurrent.futures import ThreadPoolExecutor -import time -from typing import TYPE_CHECKING, Any - -from dimos import core -from dimos.core import DimosCluster -from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module import Module, ModuleT -from dimos.core.resource import Resource -from dimos.core.worker_manager import WorkerManager - -if TYPE_CHECKING: - from dimos.core.rpc_client import ModuleProxy - - -class ModuleCoordinator(Resource): # type: ignore[misc] - _client: DimosCluster | WorkerManager | None = None - _global_config: GlobalConfig - _n: int | None = None - _memory_limit: str = "auto" - _deployed_modules: dict[type[Module], "ModuleProxy"] - - def __init__( - self, - n: int | None = None, - cfg: GlobalConfig = global_config, - ) -> None: - self._n = n if n is not None else cfg.n_dask_workers - self._memory_limit = cfg.memory_limit - self._global_config = cfg - self._deployed_modules = {} - - def start(self) -> None: - if self._global_config.dask: - self._client = core.start(self._n, self._memory_limit) - else: - self._client = WorkerManager() - - def stop(self) -> None: - for module in reversed(self._deployed_modules.values()): - module.stop() - - self._client.close_all() # type: ignore[union-attr] - - def deploy(self, module_class: type[ModuleT], *args, **kwargs) -> "ModuleProxy": # type: ignore[no-untyped-def] - if not self._client: - raise ValueError("Trying to dimos.deploy before dask client has started") - - module: ModuleProxy = self._client.deploy(module_class, *args, **kwargs) # type: ignore[union-attr, attr-defined, assignment] - self._deployed_modules[module_class] = module - return module - - def deploy_parallel( - self, module_specs: list[tuple[type[ModuleT], tuple[Any, ...], dict[str, Any]]] - ) -> list["ModuleProxy"]: - if not self._client: - raise ValueError("Not started") - - if isinstance(self._client, WorkerManager): - modules = self._client.deploy_parallel(module_specs) - for (module_class, _, _), module in zip(module_specs, modules, strict=True): - self._deployed_modules[module_class] = module # type: ignore[assignment] - return modules # type: ignore[return-value] - else: - return [ - self.deploy(module_class, *args, **kwargs) - for module_class, args, kwargs in module_specs - ] - - def start_all_modules(self) -> None: - modules = list(self._deployed_modules.values()) - if isinstance(self._client, WorkerManager): - with ThreadPoolExecutor(max_workers=len(modules)) as executor: - list(executor.map(lambda m: m.start(), modules)) - else: - for module in modules: - module.start() - - module_list = list(self._deployed_modules.values()) - for module in modules: - if hasattr(module, "on_system_modules"): - module.on_system_modules(module_list) - - def get_instance(self, module: type[ModuleT]) -> "ModuleProxy": - return self._deployed_modules.get(module) # type: ignore[return-value, no-any-return] - - def loop(self) -> None: - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - return - finally: - self.stop() diff --git a/dimos/core/native_module.py b/dimos/core/native_module.py deleted file mode 100644 index 6a93e6453a..0000000000 --- a/dimos/core/native_module.py +++ /dev/null @@ -1,296 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""NativeModule: blueprint-integrated wrapper for native (C/C++) executables. - -A NativeModule is a thin Python Module subclass that declares In/Out ports -for blueprint wiring but delegates all real work to a managed subprocess. -The native process receives its LCM topic names via CLI args and does -pub/sub directly on the LCM multicast bus. - -Example usage:: - - @dataclass(kw_only=True) - class MyConfig(NativeModuleConfig): - executable: str = "./build/my_module" - some_param: float = 1.0 - - class MyCppModule(NativeModule): - default_config = MyConfig - pointcloud: Out[PointCloud2] - cmd_vel: In[Twist] - - # Works with autoconnect, remappings, etc. - autoconnect( - MyCppModule.blueprint(), - SomeConsumer.blueprint(), - ).build().loop() -""" - -from __future__ import annotations - -from dataclasses import dataclass, field, fields -import enum -import inspect -import json -import os -from pathlib import Path -import signal -import subprocess -import threading -from typing import IO, Any - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class LogFormat(enum.Enum): - TEXT = "text" - JSON = "json" - - -@dataclass(kw_only=True) -class NativeModuleConfig(ModuleConfig): - """Configuration for a native (C/C++) subprocess module.""" - - executable: str - build_command: str | None = None - cwd: str | None = None - extra_args: list[str] = field(default_factory=list) - extra_env: dict[str, str] = field(default_factory=dict) - shutdown_timeout: float = 10.0 - log_format: LogFormat = LogFormat.TEXT - - # Override in subclasses to exclude fields from CLI arg generation - cli_exclude: frozenset[str] = frozenset() - - def to_cli_args(self) -> list[str]: - """Auto-convert subclass config fields to CLI args. - - Iterates fields defined on the concrete subclass (not NativeModuleConfig - or its parents) and converts them to ``["--name", str(value)]`` pairs. - Skips fields whose values are ``None`` and fields in ``cli_exclude``. - """ - ignore_fields = {f.name for f in fields(NativeModuleConfig)} - args: list[str] = [] - for f in fields(self): - if f.name in ignore_fields: - continue - if f.name in self.cli_exclude: - continue - val = getattr(self, f.name) - if val is None: - continue - if isinstance(val, bool): - args.extend([f"--{f.name}", str(val).lower()]) - elif isinstance(val, list): - args.extend([f"--{f.name}", ",".join(str(v) for v in val)]) - else: - args.extend([f"--{f.name}", str(val)]) - return args - - -class NativeModule(Module[NativeModuleConfig]): - """Module that wraps a native executable as a managed subprocess. - - Subclass this, declare In/Out ports, and set ``default_config`` to a - :class:`NativeModuleConfig` subclass pointing at the executable. - - On ``start()``, the binary is launched with CLI args:: - - -- ... - - The native process should parse these args and pub/sub on the given - LCM topics directly. On ``stop()``, the process receives SIGTERM. - """ - - default_config: type[NativeModuleConfig] = NativeModuleConfig - _process: subprocess.Popen[bytes] | None = None - _watchdog: threading.Thread | None = None - _stopping: bool = False - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._resolve_paths() - - @rpc - def start(self) -> None: - if self._process is not None and self._process.poll() is None: - logger.warning("Native process already running", pid=self._process.pid) - return - - self._maybe_build() - - topics = self._collect_topics() - - cmd = [self.config.executable] - for name, topic_str in topics.items(): - cmd.extend([f"--{name}", topic_str]) - cmd.extend(self.config.to_cli_args()) - cmd.extend(self.config.extra_args) - - env = {**os.environ, **self.config.extra_env} - cwd = self.config.cwd or str(Path(self.config.executable).resolve().parent) - - logger.info("Starting native process", cmd=" ".join(cmd), cwd=cwd) - self._process = subprocess.Popen( - cmd, - env=env, - cwd=cwd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - logger.info("Native process started", pid=self._process.pid) - - self._stopping = False - self._watchdog = threading.Thread(target=self._watch_process, daemon=True) - self._watchdog.start() - - @rpc - def stop(self) -> None: - self._stopping = True - if self._process is not None and self._process.poll() is None: - logger.info("Stopping native process", pid=self._process.pid) - self._process.send_signal(signal.SIGTERM) - try: - self._process.wait(timeout=self.config.shutdown_timeout) - except subprocess.TimeoutExpired: - logger.warning( - "Native process did not exit, sending SIGKILL", pid=self._process.pid - ) - self._process.kill() - self._process.wait(timeout=5) - if self._watchdog is not None and self._watchdog is not threading.current_thread(): - self._watchdog.join(timeout=2) - self._watchdog = None - self._process = None - super().stop() - - def _watch_process(self) -> None: - """Block until the native process exits; trigger stop() if it crashed.""" - if self._process is None: - return - - stdout_t = self._start_reader(self._process.stdout, "info") - stderr_t = self._start_reader(self._process.stderr, "warning") - rc = self._process.wait() - stdout_t.join(timeout=2) - stderr_t.join(timeout=2) - - if self._stopping: - return - logger.error( - "Native process died unexpectedly", - pid=self._process.pid, - returncode=rc, - ) - self.stop() - - def _start_reader(self, stream: IO[bytes] | None, level: str) -> threading.Thread: - """Spawn a daemon thread that pipes a subprocess stream through the logger.""" - t = threading.Thread(target=self._read_log_stream, args=(stream, level), daemon=True) - t.start() - return t - - def _read_log_stream(self, stream: IO[bytes] | None, level: str) -> None: - if stream is None: - return - log_fn = getattr(logger, level) - for raw in stream: - line = raw.decode("utf-8", errors="replace").rstrip() - if not line: - continue - if self.config.log_format == LogFormat.JSON: - try: - data = json.loads(line) - event = data.pop("event", line) - log_fn(event, **data) - continue - except (json.JSONDecodeError, TypeError): - logger.warning("malformed JSON from native module", raw=line) - log_fn(line, pid=self._process.pid if self._process else None) - stream.close() - - def _resolve_paths(self) -> None: - """Resolve relative ``cwd`` and ``executable`` against the subclass's source file.""" - if self.config.cwd is not None and not Path(self.config.cwd).is_absolute(): - source_file = inspect.getfile(type(self)) - base_dir = Path(source_file).resolve().parent - self.config.cwd = str(base_dir / self.config.cwd) - if not Path(self.config.executable).is_absolute() and self.config.cwd is not None: - self.config.executable = str(Path(self.config.cwd) / self.config.executable) - - def _maybe_build(self) -> None: - """Run ``build_command`` if the executable does not exist.""" - exe = Path(self.config.executable) - if exe.exists(): - return - if self.config.build_command is None: - raise FileNotFoundError( - f"Executable not found: {exe}. " - "Set build_command in config to auto-build, or build it manually." - ) - logger.info( - "Executable not found, running build", - executable=str(exe), - build_command=self.config.build_command, - ) - proc = subprocess.Popen( - self.config.build_command, - shell=True, - cwd=self.config.cwd, - env={**os.environ, **self.config.extra_env}, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - stdout, stderr = proc.communicate() - for line in stdout.decode("utf-8", errors="replace").splitlines(): - if line.strip(): - logger.info(line) - for line in stderr.decode("utf-8", errors="replace").splitlines(): - if line.strip(): - logger.warning(line) - if proc.returncode != 0: - raise RuntimeError( - f"Build command failed (exit {proc.returncode}): {self.config.build_command}" - ) - if not exe.exists(): - raise FileNotFoundError( - f"Build command succeeded but executable still not found: {exe}" - ) - - def _collect_topics(self) -> dict[str, str]: - """Extract LCM topic strings from blueprint-assigned stream transports.""" - topics: dict[str, str] = {} - for name in list(self.inputs) + list(self.outputs): - stream = getattr(self, name, None) - if stream is None: - continue - transport = getattr(stream, "_transport", None) - if transport is None: - continue - topic = getattr(transport, "topic", None) - if topic is not None: - topics[name] = str(topic) - return topics - - -__all__ = [ - "LogFormat", - "NativeModule", - "NativeModuleConfig", -] diff --git a/dimos/core/o3dpickle.py b/dimos/core/o3dpickle.py deleted file mode 100644 index 1912ab7739..0000000000 --- a/dimos/core/o3dpickle.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import copyreg - -import numpy as np -import open3d as o3d # type: ignore[import-untyped] - - -def reduce_external(obj): # type: ignore[no-untyped-def] - # Convert Vector3dVector to numpy array for pickling - points_array = np.asarray(obj.points) - return (reconstruct_pointcloud, (points_array,)) - - -def reconstruct_pointcloud(points_array): # type: ignore[no-untyped-def] - # Create new PointCloud and assign the points - pc = o3d.geometry.PointCloud() - pc.points = o3d.utility.Vector3dVector(points_array) - return pc - - -def register_picklers() -> None: - # Register for the actual PointCloud class that gets instantiated - # We need to create a dummy PointCloud to get its actual class - _dummy_pc = o3d.geometry.PointCloud() - copyreg.pickle(_dummy_pc.__class__, reduce_external) diff --git a/dimos/core/resource.py b/dimos/core/resource.py deleted file mode 100644 index ce3f735329..0000000000 --- a/dimos/core/resource.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - - -class Resource(ABC): - @abstractmethod - def start(self) -> None: ... - - @abstractmethod - def stop(self) -> None: ... - - def dispose(self) -> None: - """ - Makes a Resource disposable - So you can do a - - from reactivex.disposable import CompositeDisposable - - disposables = CompositeDisposable() - - transport1 = LCMTransport(...) - transport2 = LCMTransport(...) - - disposables.add(transport1) - disposables.add(transport2) - - ... - - disposables.dispose() - - """ - self.stop() diff --git a/dimos/core/rpc_client.py b/dimos/core/rpc_client.py deleted file mode 100644 index 30cc4f3017..0000000000 --- a/dimos/core/rpc_client.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from typing import TYPE_CHECKING, Any - -from dimos.protocol.rpc import LCMRPC, RPCSpec -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class RpcCall: - _rpc: RPCSpec | None - _name: str - _remote_name: str - _unsub_fns: list # type: ignore[type-arg] - _stop_rpc_client: Callable[[], None] | None = None - - def __init__( - self, - original_method: Callable[..., Any] | None, - rpc: RPCSpec, - name: str, - remote_name: str, - unsub_fns: list, # type: ignore[type-arg] - stop_client: Callable[[], None] | None = None, - ) -> None: - self._rpc = rpc - self._name = name - self._remote_name = remote_name - self._unsub_fns = unsub_fns - self._stop_rpc_client = stop_client - - if original_method: - self.__doc__ = original_method.__doc__ - self.__name__ = original_method.__name__ - self.__qualname__ = f"{self.__class__.__name__}.{original_method.__name__}" - - def set_rpc(self, rpc: RPCSpec) -> None: - self._rpc = rpc - - def __call__(self, *args, **kwargs): # type: ignore[no-untyped-def] - if not self._rpc: - logger.warning("RPC client not initialized") - return None - - # For stop, use call_nowait to avoid deadlock - # (the remote side stops its RPC service before responding) - if self._name == "stop": - self._rpc.call_nowait(f"{self._remote_name}/{self._name}", (args, kwargs)) # type: ignore[arg-type] - if self._stop_rpc_client: - self._stop_rpc_client() - return None - - result, unsub_fn = self._rpc.call_sync(f"{self._remote_name}/{self._name}", (args, kwargs)) # type: ignore[arg-type] - self._unsub_fns.append(unsub_fn) - return result - - def __getstate__(self): # type: ignore[no-untyped-def] - return (self._name, self._remote_name) - - def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] - self._name, self._remote_name = state - self._unsub_fns = [] - self._rpc = None - self._stop_rpc_client = None - - -class RPCClient: - def __init__(self, actor_instance, actor_class) -> None: # type: ignore[no-untyped-def] - self.rpc = LCMRPC() - self.actor_class = actor_class - self.remote_name = actor_class.__name__ - self.actor_instance = actor_instance - self.rpcs = actor_class.rpcs.keys() - self.rpc.start() - self._unsub_fns = [] # type: ignore[var-annotated] - - def stop_rpc_client(self) -> None: - for unsub in self._unsub_fns: - try: - unsub() - except Exception: - pass - - self._unsub_fns = [] - - if self.rpc: - self.rpc.stop() - self.rpc = None # type: ignore[assignment] - - def __reduce__(self): # type: ignore[no-untyped-def] - # Return the class and the arguments needed to reconstruct the object - return ( - self.__class__, - (self.actor_instance, self.actor_class), - ) - - # passthrough - def __getattr__(self, name: str): # type: ignore[no-untyped-def] - # Check if accessing a known safe attribute to avoid recursion - if name in { - "__class__", - "__init__", - "__dict__", - "__getattr__", - "rpcs", - "remote_name", - "remote_instance", - "actor_instance", - }: - raise AttributeError(f"{name} is not found.") - - if name in self.rpcs: - original_method = getattr(self.actor_class, name, None) - return RpcCall( - original_method, - self.rpc, - name, - self.remote_name, - self._unsub_fns, - self.stop_rpc_client, - ) - - # return super().__getattr__(name) - # Try to avoid recursion by directly accessing attributes that are known - return self.actor_instance.__getattr__(name) - - -if TYPE_CHECKING: - from dimos.core.module import Module - - # the class below is only ever used for type hinting - # why? because the RPCClient instance is going to have all the methods of a Module - # but those methods/attributes are super dynamic, so the type hints can't figure that out - class ModuleProxy(RPCClient, Module): # type: ignore[misc] - def start(self) -> None: ... - def stop(self) -> None: ... diff --git a/dimos/core/stream.py b/dimos/core/stream.py deleted file mode 100644 index 77edf45417..0000000000 --- a/dimos/core/stream.py +++ /dev/null @@ -1,274 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import enum -from typing import ( - TYPE_CHECKING, - Any, - Generic, - TypeVar, -) - -from dask.distributed import Actor -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import Disposable - -import dimos.core.colors as colors -from dimos.core.resource import Resource -from dimos.utils.logging_config import setup_logger -import dimos.utils.reactive as reactive -from dimos.utils.reactive import backpressure - -if TYPE_CHECKING: - from collections.abc import Callable - - from reactivex.observable import Observable - -T = TypeVar("T") - - -logger = setup_logger() - - -class ObservableMixin(Generic[T]): - # subscribes and returns the first value it receives - # might be nicer to write without rxpy but had this snippet ready - def get_next(self, timeout: float = 10.0) -> T: - try: - return ( # type: ignore[no-any-return] - self.observable() # type: ignore[no-untyped-call] - .pipe(ops.first(), *([ops.timeout(timeout)] if timeout is not None else [])) - .run() - ) - except Exception as e: - raise Exception(f"No value received after {timeout} seconds") from e - - def hot_latest(self) -> Callable[[], T]: - return reactive.getter_streaming(self.observable()) # type: ignore[no-untyped-call] - - def pure_observable(self) -> Observable[T]: - def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - unsubscribe = self.subscribe(observer.on_next) # type: ignore[attr-defined] - return Disposable(unsubscribe) - - return rx.create(_subscribe) - - # default return is backpressured because most - # use cases will want this by default - def observable(self) -> Observable[T]: - return backpressure(self.pure_observable()) - - -class State(enum.Enum): - UNBOUND = "unbound" # descriptor defined but not bound - READY = "ready" # bound to owner but not yet connected - CONNECTED = "connected" # input bound to an output - FLOWING = "flowing" # runtime: data observed - - -class Transport(Resource, ObservableMixin[T]): - # used by local Output - def broadcast(self, selfstream: Out[T], value: T) -> None: - raise NotImplementedError - - # used by local Input - def subscribe( - self, callback: Callable[[T], Any], selfstream: Stream[T] | None = None - ) -> Callable[[], None]: - raise NotImplementedError - - def publish(self, msg: T) -> None: - self.broadcast(None, msg) # type: ignore[arg-type] - - -class Stream(Generic[T]): - _transport: Transport | None # type: ignore[type-arg] - - def __init__( - self, - type: type[T], - name: str, - owner: Any | None = None, - transport: Transport | None = None, # type: ignore[type-arg] - ) -> None: - self.name = name - self.owner = owner - self.type = type - if transport: - self._transport = transport - if not hasattr(self, "_transport"): - self._transport = None - - @property - def type_name(self) -> str: - return getattr(self.type, "__name__", repr(self.type)) - - def _color_fn(self) -> Callable[[str], str]: - if self.state == State.UNBOUND: # type: ignore[attr-defined] - return colors.orange - if self.state == State.READY: # type: ignore[attr-defined] - return colors.blue - if self.state == State.CONNECTED: # type: ignore[attr-defined] - return colors.green - return lambda s: s - - def __str__(self) -> str: - return ( - self.__class__.__name__ - + " " - + self._color_fn()(f"{self.name}[{self.type_name}]") - + " @ " - + ( - colors.orange(self.owner) # type: ignore[arg-type] - if isinstance(self.owner, Actor) - else colors.green(self.owner) # type: ignore[arg-type] - ) - + ("" if not self._transport else " via " + str(self._transport)) - ) - - -class Out(Stream[T], ObservableMixin[T]): - _transport: Transport # type: ignore[type-arg] - _subscribers: list[Callable[[T], Any]] - - def __init__(self, *argv, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*argv, **kwargs) - self._subscribers = [] - - @property - def transport(self) -> Transport[T]: - return self._transport - - @transport.setter - def transport(self, value: Transport[T]) -> None: - self._transport = value - - @property - def state(self) -> State: - return State.UNBOUND if self.owner is None else State.READY - - def __reduce__(self): # type: ignore[no-untyped-def] - if self.owner is None or not hasattr(self.owner, "ref"): - raise ValueError("Cannot serialise Out without an owner ref") - return ( - RemoteOut, - ( - self.type, - self.name, - self.owner.ref, - self._transport, - ), - ) - - def publish(self, msg: T) -> None: - if hasattr(self, "_transport") and self._transport is not None: - self._transport.broadcast(self, msg) - for cb in self._subscribers: - cb(msg) - - def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: - self._subscribers.append(cb) - - def unsubscribe() -> None: - self._subscribers.remove(cb) - - return unsubscribe - - -class RemoteStream(Stream[T]): - @property - def state(self) -> State: - return State.UNBOUND if self.owner is None else State.READY - - # this won't work but nvm - @property - def transport(self) -> Transport[T]: - return self._transport # type: ignore[return-value] - - @transport.setter - def transport(self, value: Transport[T]) -> None: - self.owner.set_transport(self.name, value).result() # type: ignore[union-attr] - self._transport = value - - -class RemoteOut(RemoteStream[T]): - def connect(self, other: RemoteIn[T]): # type: ignore[no-untyped-def] - return other.connect(self) - - def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: - return self.transport.subscribe(cb, self) - - -# representation of Input -# as views from inside of the module -class In(Stream[T], ObservableMixin[T]): - connection: RemoteOut[T] | None = None - _transport: Transport # type: ignore[type-arg] - - def __str__(self) -> str: - mystr = super().__str__() - - if not self.connection: - return mystr - - return (mystr + " ◀─").ljust(60, "─") + f" {self.connection}" - - def __reduce__(self): # type: ignore[no-untyped-def] - if self.owner is None or not hasattr(self.owner, "ref"): - raise ValueError("Cannot serialise Out without an owner ref") - return (RemoteIn, (self.type, self.name, self.owner.ref, self._transport)) - - @property - def transport(self) -> Transport[T]: - if not self._transport and self.connection: - self._transport = self.connection.transport - return self._transport - - @transport.setter - def transport(self, value: Transport[T]) -> None: - self._transport = value - - def connect(self, value: Out[T]) -> None: - value.subscribe(self.transport.publish) # type: ignore[arg-type] - - @property - def state(self) -> State: - return State.UNBOUND if self.owner is None else State.READY - - # returns unsubscribe function - def subscribe(self, cb: Callable[[T], Any]) -> Callable[[], None]: - return self.transport.subscribe(cb, self) - - -# representation of input outside of module -# used for configuring streams, setting a transport -class RemoteIn(RemoteStream[T]): - def connect(self, other: RemoteOut[T]) -> None: - return self.owner.connect_stream(self.name, other).result() # type: ignore[no-any-return, union-attr] - - # this won't work but that's ok - @property # type: ignore[misc] - def transport(self) -> Transport[T]: - return self._transport # type: ignore[return-value] - - def publish(self, msg) -> None: # type: ignore[no-untyped-def] - self.transport.broadcast(self, msg) # type: ignore[arg-type] - - @transport.setter # type: ignore[attr-defined, no-redef, untyped-decorator] - def transport(self, value: Transport[T]) -> None: - self.owner.set_transport(self.name, value).result() # type: ignore[union-attr] - self._transport = value diff --git a/dimos/core/test_blueprints.py b/dimos/core/test_blueprints.py deleted file mode 100644 index 09144054c1..0000000000 --- a/dimos/core/test_blueprints.py +++ /dev/null @@ -1,505 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -import pytest - -from dimos.core._test_future_annotations_helper import ( - FutureData, - FutureModuleIn, - FutureModuleOut, -) -from dimos.core.blueprints import ( - Blueprint, - StreamRef, - _BlueprintAtom, - autoconnect, -) -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.rpc_client import RpcCall -from dimos.core.stream import In, Out -from dimos.core.transport import LCMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.protocol import pubsub -from dimos.spec.utils import Spec - -# Disable Rerun for tests (prevents viewer spawn and gRPC flush errors) -_BUILD_WITHOUT_RERUN = { - "cli_config_overrides": {"viewer_backend": "none"}, -} - - -class Scratch: - pass - - -class Petting: - pass - - -class CatModule(Module): - pet_cat: In[Petting] - scratches: Out[Scratch] - - -class Data1: - pass - - -class Data2: - pass - - -class Data3: - pass - - -class SourceModule(Module): - color_image: Out[Data1] - - -class TargetModule(Module): - remapped_data: In[Data1] - - -class ModuleA(Module): - data1: Out[Data1] - data2: Out[Data2] - - @rpc - def get_name(self) -> str: - return "A, Module A" - - -class ModuleB(Module): - data1: In[Data1] - data2: In[Data2] - data3: Out[Data3] - - _module_a_get_name: callable = None - - @rpc - def set_ModuleA_get_name(self, callable: RpcCall) -> None: - self._module_a_get_name = callable - self._module_a_get_name.set_rpc(self.rpc) - - @rpc - def what_is_as_name(self) -> str: - if self._module_a_get_name is None: - return "ModuleA.get_name not set" - return self._module_a_get_name() - - -class ModuleC(Module): - data3: In[Data3] - - -module_a = ModuleA.blueprint -module_b = ModuleB.blueprint -module_c = ModuleC.blueprint - - -def test_get_connection_set() -> None: - assert _BlueprintAtom.create(CatModule, args=("arg1",), kwargs={"k": "v"}) == _BlueprintAtom( - module=CatModule, - streams=( - StreamRef(name="pet_cat", type=Petting, direction="in"), - StreamRef(name="scratches", type=Scratch, direction="out"), - ), - module_refs=(), - args=("arg1",), - kwargs={"k": "v"}, - ) - - -def test_autoconnect() -> None: - blueprint_set = autoconnect(module_a(), module_b()) - - assert blueprint_set == Blueprint( - blueprints=( - _BlueprintAtom( - module=ModuleA, - streams=( - StreamRef(name="data1", type=Data1, direction="out"), - StreamRef(name="data2", type=Data2, direction="out"), - ), - module_refs=(), - args=(), - kwargs={}, - ), - _BlueprintAtom( - module=ModuleB, - streams=( - StreamRef(name="data1", type=Data1, direction="in"), - StreamRef(name="data2", type=Data2, direction="in"), - StreamRef(name="data3", type=Data3, direction="out"), - ), - module_refs=(), - args=(), - kwargs={}, - ), - ) - ) - - -def test_transports() -> None: - custom_transport = LCMTransport("/custom_topic", Data1) - blueprint_set = autoconnect(module_a(), module_b()).transports( - {("data1", Data1): custom_transport} - ) - - assert ("data1", Data1) in blueprint_set.transport_map - assert blueprint_set.transport_map[("data1", Data1)] == custom_transport - - -def test_global_config() -> None: - blueprint_set = autoconnect(module_a(), module_b()).global_config(option1=True, option2=42) - - assert "option1" in blueprint_set.global_config_overrides - assert blueprint_set.global_config_overrides["option1"] is True - assert "option2" in blueprint_set.global_config_overrides - assert blueprint_set.global_config_overrides["option2"] == 42 - - -@pytest.mark.integration -def test_build_happy_path() -> None: - pubsub.lcm.autoconf() - - blueprint_set = autoconnect(module_a(), module_b(), module_c()) - - coordinator = blueprint_set.build(**_BUILD_WITHOUT_RERUN) - - try: - assert isinstance(coordinator, ModuleCoordinator) - - module_a_instance = coordinator.get_instance(ModuleA) - module_b_instance = coordinator.get_instance(ModuleB) - module_c_instance = coordinator.get_instance(ModuleC) - - assert module_a_instance is not None - assert module_b_instance is not None - assert module_c_instance is not None - - assert module_a_instance.data1.transport is not None - assert module_a_instance.data2.transport is not None - assert module_b_instance.data1.transport is not None - assert module_b_instance.data2.transport is not None - assert module_b_instance.data3.transport is not None - assert module_c_instance.data3.transport is not None - - assert module_a_instance.data1.transport.topic == module_b_instance.data1.transport.topic - assert module_a_instance.data2.transport.topic == module_b_instance.data2.transport.topic - assert module_b_instance.data3.transport.topic == module_c_instance.data3.transport.topic - - assert module_b_instance.what_is_as_name() == "A, Module A" - - finally: - coordinator.stop() - - -def test_name_conflicts_are_reported() -> None: - class ModuleA(Module): - shared_data: Out[Data1] - - class ModuleB(Module): - shared_data: In[Data2] - - blueprint_set = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) - - try: - blueprint_set._verify_no_name_conflicts() - pytest.fail("Expected ValueError to be raised") - except ValueError as e: - error_message = str(e) - assert "Blueprint cannot start because there are conflicting streams" in error_message - assert "'shared_data' has conflicting types" in error_message - assert "Data1 in ModuleA" in error_message - assert "Data2 in ModuleB" in error_message - - -def test_multiple_name_conflicts_are_reported() -> None: - class Module1(Module): - sensor_data: Out[Data1] - control_signal: Out[Data2] - - class Module2(Module): - sensor_data: In[Data2] - control_signal: In[Data3] - - blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint()) - - try: - blueprint_set._verify_no_name_conflicts() - pytest.fail("Expected ValueError to be raised") - except ValueError as e: - error_message = str(e) - assert "Blueprint cannot start because there are conflicting streams" in error_message - assert "'sensor_data' has conflicting types" in error_message - assert "'control_signal' has conflicting types" in error_message - - -def test_that_remapping_can_resolve_conflicts() -> None: - class Module1(Module): - data: Out[Data1] - - class Module2(Module): - data: Out[Data2] # Would conflict with Module1.data - - class Module3(Module): - data1: In[Data1] - data2: In[Data2] - - # Without remapping, should raise conflict error - blueprint_set = autoconnect(Module1.blueprint(), Module2.blueprint(), Module3.blueprint()) - - try: - blueprint_set._verify_no_name_conflicts() - pytest.fail("Expected ValueError due to conflict") - except ValueError as e: - assert "'data' has conflicting types" in str(e) - - # With remapping to resolve the conflict - blueprint_set_remapped = autoconnect( - Module1.blueprint(), Module2.blueprint(), Module3.blueprint() - ).remappings( - [ - (Module1, "data", "data1"), - (Module2, "data", "data2"), - ] - ) - - # Should not raise any exception after remapping - blueprint_set_remapped._verify_no_name_conflicts() - - -@pytest.mark.integration -def test_remapping() -> None: - """Test that remapping streams works correctly.""" - pubsub.lcm.autoconf() - - # Create blueprint with remapping - blueprint_set = autoconnect( - SourceModule.blueprint(), - TargetModule.blueprint(), - ).remappings( - [ - (SourceModule, "color_image", "remapped_data"), - ] - ) - - # Verify remappings are stored correctly - assert (SourceModule, "color_image") in blueprint_set.remapping_map - assert blueprint_set.remapping_map[(SourceModule, "color_image")] == "remapped_data" - - # Verify that remapped names are used in name resolution - assert ("remapped_data", Data1) in blueprint_set._all_name_types - # The original name shouldn't be in the name types since it's remapped - assert ("color_image", Data1) not in blueprint_set._all_name_types - - # Build and verify streams work - coordinator = blueprint_set.build(**_BUILD_WITHOUT_RERUN) - - try: - source_instance = coordinator.get_instance(SourceModule) - target_instance = coordinator.get_instance(TargetModule) - - assert source_instance is not None - assert target_instance is not None - - # Both should have transports set - assert source_instance.color_image.transport is not None - assert target_instance.remapped_data.transport is not None - - # They should be using the same transport (connected) - assert ( - source_instance.color_image.transport.topic - == target_instance.remapped_data.transport.topic - ) - - # The topic should be /remapped_data since that's the remapped name - assert target_instance.remapped_data.transport.topic == "/remapped_data" - - finally: - coordinator.stop() - - -def test_future_annotations_support() -> None: - """Test that modules using `from __future__ import annotations` work correctly. - - PEP 563 (future annotations) stores annotations as strings instead of actual types. - This test verifies that _BlueprintAtom.create properly resolves string annotations - to the actual In/Out types. - """ - - # Test that streams are properly extracted from modules with future annotations - out_blueprint = _BlueprintAtom.create(FutureModuleOut, args=(), kwargs={}) - assert len(out_blueprint.streams) == 1 - assert out_blueprint.streams[0] == StreamRef(name="data", type=FutureData, direction="out") - - in_blueprint = _BlueprintAtom.create(FutureModuleIn, args=(), kwargs={}) - assert len(in_blueprint.streams) == 1 - assert in_blueprint.streams[0] == StreamRef(name="data", type=FutureData, direction="in") - - -@pytest.mark.integration -def test_future_annotations_autoconnect() -> None: - """Test that autoconnect works with modules using `from __future__ import annotations`.""" - - blueprint_set = autoconnect(FutureModuleOut.blueprint(), FutureModuleIn.blueprint()) - - coordinator = blueprint_set.build(**_BUILD_WITHOUT_RERUN) - - try: - out_instance = coordinator.get_instance(FutureModuleOut) - in_instance = coordinator.get_instance(FutureModuleIn) - - assert out_instance is not None - assert in_instance is not None - - # Both should have transports set - assert out_instance.data.transport is not None - assert in_instance.data.transport is not None - - # They should be connected via the same transport - assert out_instance.data.transport.topic == in_instance.data.transport.topic - - finally: - coordinator.stop() - - -# ModuleRef / RPC tests -class CalculatorSpec(Spec, Protocol): - @rpc - def compute1(self, a: int, b: int) -> int: ... - - @rpc - def compute2(self, a: float, b: float) -> float: ... - - -class Calculator1(Module): - @rpc - def compute1(self, a: int, b: int) -> int: - return a + b - - @rpc - def compute2(self, a: float, b: float) -> float: - return a + b - - @rpc - def start(self) -> None: ... - - @rpc - def stop(self) -> None: ... - - -class Calculator2(Module): - @rpc - def compute1(self, a: int, b: int) -> int: - return a * b - - @rpc - def compute2(self, a: float, b: float) -> float: - return a * b - - @rpc - def start(self) -> None: ... - - @rpc - def stop(self) -> None: ... - - -# link to a specific module -class Mod1(Module): - stream1: In[Image] - calc: Calculator1 - - @rpc - def start(self) -> None: - _ = self.calc.compute1 - - @rpc - def stop(self) -> None: ... - - -# link to any module that implements a spec (Autoconnect will handle it) -class Mod2(Module): - stream1: In[Image] - calc: CalculatorSpec - - @rpc - def start(self) -> None: - _ = self.calc.compute1 - - @rpc - def stop(self) -> None: ... - - -@pytest.mark.integration -def test_module_ref_direct() -> None: - coordinator = autoconnect( - Calculator1.blueprint(), - Mod1.blueprint(), - ).build(**_BUILD_WITHOUT_RERUN) - - try: - mod1 = coordinator.get_instance(Mod1) - assert mod1 is not None - assert mod1.calc.compute1(2, 3) == 5 - assert mod1.calc.compute2(1.5, 2.5) == 4.0 - finally: - coordinator.stop() - - -@pytest.mark.integration -def test_module_ref_spec() -> None: - coordinator = autoconnect( - Calculator1.blueprint(), - Mod2.blueprint(), - ).build(**_BUILD_WITHOUT_RERUN) - - try: - mod2 = coordinator.get_instance(Mod2) - assert mod2 is not None - assert mod2.calc.compute1(4, 5) == 9 - assert mod2.calc.compute2(3.0, 0.5) == 3.5 - finally: - coordinator.stop() - - -@pytest.mark.integration -def test_module_ref_remap_ambiguous() -> None: - coordinator = ( - autoconnect( - Calculator1.blueprint(), - Calculator2.blueprint(), - Mod2.blueprint(), - ) - .remappings( - [ - (Mod2, "calc", Calculator1), - ] - ) - .build(**_BUILD_WITHOUT_RERUN) - ) - - try: - mod2 = coordinator.get_instance(Mod2) - assert mod2 is not None - assert mod2.calc.compute1(2, 3) == 5 - assert mod2.calc.compute2(2.0, 3.0) == 5.0 - finally: - coordinator.stop() diff --git a/dimos/core/test_core.py b/dimos/core/test_core.py deleted file mode 100644 index c229659b84..0000000000 --- a/dimos/core/test_core.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest -from reactivex.disposable import Disposable - -from dimos.core import ( - In, - LCMTransport, - Module, - Out, - pLCMTransport, - rpc, -) -from dimos.core.testing import MockRobotClient, dimos -from dimos.msgs.geometry_msgs import Vector3 -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.odometry import Odometry - -assert dimos - - -class Navigation(Module): - mov: Out[Vector3] - lidar: In[PointCloud2] - target_position: In[Vector3] - odometry: In[Odometry] - - odom_msg_count = 0 - lidar_msg_count = 0 - - @rpc - def navigate_to(self, target: Vector3) -> bool: ... - - def __init__(self) -> None: - super().__init__() - - @rpc - def start(self) -> None: - def _odom(msg) -> None: - self.odom_msg_count += 1 - print("RCV:", (time.perf_counter() - msg.pubtime) * 1000, msg) - self.mov.publish(msg.position) - - unsub = self.odometry.subscribe(_odom) - self._disposables.add(Disposable(unsub)) - - def _lidar(msg) -> None: - self.lidar_msg_count += 1 - if hasattr(msg, "pubtime"): - print("RCV:", (time.perf_counter() - msg.pubtime) * 1000, msg) - else: - print("RCV: unknown time", msg) - - unsub = self.lidar.subscribe(_lidar) - self._disposables.add(Disposable(unsub)) - - -def test_classmethods() -> None: - # Test class property access - class_rpcs = Navigation.rpcs - print("Class rpcs:", class_rpcs) - # Test instance property access - nav = Navigation() - instance_rpcs = nav.rpcs - print("Instance rpcs:", instance_rpcs) - - # Assertions - assert isinstance(class_rpcs, dict), "Class rpcs should be a dictionary" - assert isinstance(instance_rpcs, dict), "Instance rpcs should be a dictionary" - assert class_rpcs == instance_rpcs, "Class and instance rpcs should be identical" - - # Check that we have the expected RPC methods - assert "navigate_to" in class_rpcs, "navigate_to should be in rpcs" - assert "start" in class_rpcs, "start should be in rpcs" - assert len(class_rpcs) == 9 - - # Check that the values are callable - assert callable(class_rpcs["navigate_to"]), "navigate_to should be callable" - assert callable(class_rpcs["start"]), "start should be callable" - - # Check that they have the __rpc__ attribute - assert hasattr(class_rpcs["navigate_to"], "__rpc__"), ( - "navigate_to should have __rpc__ attribute" - ) - assert hasattr(class_rpcs["start"], "__rpc__"), "start should have __rpc__ attribute" - - nav._close_module() - - -@pytest.mark.module -def test_basic_deployment(dimos) -> None: - robot = dimos.deploy(MockRobotClient) - - print("\n") - print("lidar stream", robot.lidar) - print("odom stream", robot.odometry) - - nav = dimos.deploy(Navigation) - - # this one encodes proper LCM messages - robot.lidar.transport = LCMTransport("/lidar", PointCloud2) - - # odometry & mov using just a pickle over LCM - robot.odometry.transport = pLCMTransport("/odom") - nav.mov.transport = pLCMTransport("/mov") - - nav.lidar.connect(robot.lidar) - nav.odometry.connect(robot.odometry) - robot.mov.connect(nav.mov) - - robot.start() - nav.start() - - time.sleep(1) - robot.stop() - - print("robot.mov_msg_count", robot.mov_msg_count) - print("nav.odom_msg_count", nav.odom_msg_count) - print("nav.lidar_msg_count", nav.lidar_msg_count) - - assert robot.mov_msg_count >= 8 - assert nav.odom_msg_count >= 8 - assert nav.lidar_msg_count >= 8 - - dimos.shutdown() diff --git a/dimos/core/test_modules.py b/dimos/core/test_modules.py deleted file mode 100644 index d96b58af5f..0000000000 --- a/dimos/core/test_modules.py +++ /dev/null @@ -1,331 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test that all Module subclasses implement required resource management methods.""" - -import ast -import inspect -from pathlib import Path - -import pytest - -from dimos.core.module import Module - - -class ModuleVisitor(ast.NodeVisitor): - """AST visitor to find classes and their base classes.""" - - def __init__(self, filepath: str) -> None: - self.filepath = filepath - self.classes: list[ - tuple[str, list[str], set[str]] - ] = [] # (class_name, base_classes, methods) - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - """Visit a class definition.""" - # Get base class names - base_classes = [] - for base in node.bases: - if isinstance(base, ast.Name): - base_classes.append(base.id) - elif isinstance(base, ast.Attribute): - # Handle cases like dimos.core.Module - parts = [] - current = base - while isinstance(current, ast.Attribute): - parts.append(current.attr) - current = current.value - if isinstance(current, ast.Name): - parts.append(current.id) - base_classes.append(".".join(reversed(parts))) - - # Get method names defined in this class - methods = set() - for item in node.body: - if isinstance(item, ast.FunctionDef): - methods.add(item.name) - - self.classes.append((node.name, base_classes, methods)) - self.generic_visit(node) - - -def get_import_aliases(tree: ast.AST) -> dict[str, str]: - """Extract import aliases from the AST.""" - aliases = {} - - for node in ast.walk(tree): - if isinstance(node, ast.Import): - for alias in node.names: - key = alias.asname if alias.asname else alias.name - aliases[key] = alias.name - elif isinstance(node, ast.ImportFrom): - module = node.module or "" - for alias in node.names: - key = alias.asname if alias.asname else alias.name - full_name = f"{module}.{alias.name}" if module else alias.name - aliases[key] = full_name - - return aliases - - -def is_module_subclass( - base_classes: list[str], - aliases: dict[str, str], - class_hierarchy: dict[str, list[str]] | None = None, - current_module_path: str | None = None, -) -> bool: - """Check if any base class is or resolves to dimos.core.Module or its variants (recursively).""" - target_classes = { - "Module", - "ModuleBase", - "dimos.core.Module", - "dimos.core.ModuleBase", - "dimos.core.module.Module", - "dimos.core.module.ModuleBase", - } - - def find_qualified_name(base: str, context_module: str | None = None) -> str: - """Find the qualified name for a base class, using import context if available.""" - if not class_hierarchy: - return base - - # First try exact match (already fully qualified or in hierarchy) - if base in class_hierarchy: - return base - - # Check if it's in our aliases (from imports) - if base in aliases: - resolved = aliases[base] - if resolved in class_hierarchy: - return resolved - # The resolved name might be a qualified name that exists - return resolved - - # If we have a context module and base is a simple name, - # try to find it in the same module first (for local classes) - if context_module and "." not in base: - same_module_qualified = f"{context_module}.{base}" - if same_module_qualified in class_hierarchy: - return same_module_qualified - - # Otherwise return the base as-is - return base - - def check_base( - base: str, visited: set[str] | None = None, context_module: str | None = None - ) -> bool: - if visited is None: - visited = set() - - # Avoid infinite recursion - if base in visited: - return False - visited.add(base) - - # Check direct match - if base in target_classes: - return True - - # Check if it's an alias - if base in aliases: - resolved = aliases[base] - if resolved in target_classes: - return True - # Continue checking with resolved name - base = resolved - - # If we have a class hierarchy, recursively check parent classes - if class_hierarchy: - # Resolve the base class name to a qualified name - qualified_name = find_qualified_name(base, context_module) - - if qualified_name in class_hierarchy: - # Check all parent classes - for parent_base in class_hierarchy[qualified_name]: - if check_base(parent_base, visited, None): # Parent lookups don't use context - return True - - return False - - for base in base_classes: - if check_base(base, context_module=current_module_path): - return True - - return False - - -def scan_file( - filepath: Path, - class_hierarchy: dict[str, list[str]] | None = None, - root_path: Path | None = None, -) -> list[tuple[str, str, bool, bool, set[str]]]: - """ - Scan a Python file for Module subclasses. - - Returns: - List of (class_name, filepath, has_start, has_stop, forbidden_methods) - """ - forbidden_method_names = {"acquire", "release", "open", "close", "shutdown", "clean", "cleanup"} - - try: - with open(filepath, encoding="utf-8") as f: - content = f.read() - - tree = ast.parse(content, filename=str(filepath)) - aliases = get_import_aliases(tree) - - visitor = ModuleVisitor(str(filepath)) - visitor.visit(tree) - - # Get module path for this file to properly resolve base classes - current_module_path = None - if root_path: - try: - rel_path = filepath.relative_to(root_path.parent) - module_parts = list(rel_path.parts[:-1]) - if rel_path.stem != "__init__": - module_parts.append(rel_path.stem) - current_module_path = ".".join(module_parts) - except ValueError: - pass - - results = [] - for class_name, base_classes, methods in visitor.classes: - if is_module_subclass(base_classes, aliases, class_hierarchy, current_module_path): - has_start = "start" in methods - has_stop = "stop" in methods - forbidden_found = methods & forbidden_method_names - results.append((class_name, str(filepath), has_start, has_stop, forbidden_found)) - - return results - - except (SyntaxError, UnicodeDecodeError): - # Skip files that can't be parsed - return [] - - -def build_class_hierarchy(root_path: Path) -> dict[str, list[str]]: - """Build a complete class hierarchy by scanning all Python files.""" - hierarchy = {} - - for filepath in sorted(root_path.rglob("*.py")): - # Skip __pycache__ and other irrelevant directories - if "__pycache__" in filepath.parts or ".venv" in filepath.parts: - continue - - try: - with open(filepath, encoding="utf-8") as f: - content = f.read() - - tree = ast.parse(content, filename=str(filepath)) - visitor = ModuleVisitor(str(filepath)) - visitor.visit(tree) - - # Convert filepath to module path (e.g., dimos/core/module.py -> dimos.core.module) - try: - rel_path = filepath.relative_to(root_path.parent) - except ValueError: - # If we can't get relative path, skip this file - continue - - # Convert path to module notation - module_parts = list(rel_path.parts[:-1]) # Exclude filename - if rel_path.stem != "__init__": - module_parts.append(rel_path.stem) # Add filename without .py - module_name = ".".join(module_parts) - - for class_name, base_classes, _ in visitor.classes: - # Use fully qualified name as key to avoid conflicts - qualified_name = f"{module_name}.{class_name}" if module_name else class_name - hierarchy[qualified_name] = base_classes - - except (SyntaxError, UnicodeDecodeError): - # Skip files that can't be parsed - continue - - return hierarchy - - -def scan_directory(root_path: Path) -> list[tuple[str, str, bool, bool, set[str]]]: - """Scan all Python files in the directory tree.""" - # First, build the complete class hierarchy - class_hierarchy = build_class_hierarchy(root_path) - - # Then scan for Module subclasses using the complete hierarchy - results = [] - - for filepath in sorted(root_path.rglob("*.py")): - # Skip __pycache__ and other irrelevant directories - if "__pycache__" in filepath.parts or ".venv" in filepath.parts: - continue - - file_results = scan_file(filepath, class_hierarchy, root_path) - results.extend(file_results) - - return results - - -def get_all_module_subclasses(): - """Find all Module subclasses in the dimos codebase.""" - # Get the dimos package directory - dimos_file = inspect.getfile(Module) - dimos_path = Path(dimos_file).parent.parent # Go up from dimos/core/module.py to dimos/ - - results = scan_directory(dimos_path) - - # Filter out test modules and base classes - filtered_results = [] - for class_name, filepath, has_start, has_stop, forbidden_methods in results: - # Skip base module classes themselves - if class_name in ("Module", "ModuleBase"): - continue - - # Skip test-only modules (those defined in test_ files) - if "test_" in Path(filepath).name: - continue - - filtered_results.append((class_name, filepath, has_start, has_stop, forbidden_methods)) - - return filtered_results - - -@pytest.mark.parametrize( - "class_name,filepath,has_start,has_stop,forbidden_methods", - get_all_module_subclasses(), - ids=lambda val: val[0] if isinstance(val, str) else str(val), -) -def test_module_has_start_and_stop( - class_name: str, filepath, has_start, has_stop, forbidden_methods -) -> None: - """Test that Module subclasses implement start and stop methods and don't use forbidden methods.""" - # Get relative path for better error messages - try: - rel_path = Path(filepath).relative_to(Path.cwd()) - except ValueError: - rel_path = filepath - - errors = [] - - # Check for missing required methods - if not has_start: - errors.append("missing required method: start") - if not has_stop: - errors.append("missing required method: stop") - - # Check for forbidden methods - if forbidden_methods: - forbidden_list = ", ".join(sorted(forbidden_methods)) - errors.append(f"has forbidden method(s): {forbidden_list}") - - assert not errors, f"{class_name} in {rel_path} has issues:\n - " + "\n - ".join(errors) diff --git a/dimos/core/test_native_module.py b/dimos/core/test_native_module.py deleted file mode 100644 index 8af63b0bf4..0000000000 --- a/dimos/core/test_native_module.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for NativeModule: blueprint wiring, topic collection, CLI arg generation. - -Every test launches the real native_echo.py subprocess via blueprint.build(). -The echo script writes received CLI args to a temp file for assertions. -""" - -from dataclasses import dataclass -import json -from pathlib import Path -import time - -import pytest - -from dimos.core import DimosCluster -from dimos.core.blueprints import autoconnect -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.native_module import LogFormat, NativeModule, NativeModuleConfig -from dimos.core.stream import In, Out -from dimos.core.transport import LCMTransport -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.sensor_msgs.Imu import Imu -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 - -_ECHO = str(Path(__file__).parent / "tests" / "native_echo.py") - - -@pytest.fixture -def args_file(tmp_path: Path) -> str: - """Temp file path where native_echo.py writes the CLI args it received.""" - return str(tmp_path / "native_echo_args.json") - - -def read_json_file(path: str) -> dict[str, str]: - """Read and parse --key value pairs from the echo output file.""" - raw: list[str] = json.loads(Path(path).read_text()) - result = {} - i = 0 - while i < len(raw): - if raw[i].startswith("--") and i + 1 < len(raw): - result[raw[i][2:]] = raw[i + 1] - i += 2 - else: - i += 1 - return result - - -@dataclass(kw_only=True) -class StubNativeConfig(NativeModuleConfig): - executable: str = _ECHO - log_format: LogFormat = LogFormat.TEXT - output_file: str | None = None - die_after: float | None = None - some_param: float = 1.5 - - -class StubNativeModule(NativeModule): - default_config = StubNativeConfig - pointcloud: Out[PointCloud2] - imu: Out[Imu] - cmd_vel: In[Twist] - - -class StubConsumer(Module): - pointcloud: In[PointCloud2] - imu: In[Imu] - - @rpc - def start(self) -> None: - pass - - -class StubProducer(Module): - cmd_vel: Out[Twist] - - @rpc - def start(self) -> None: - pass - - -def test_process_crash_triggers_stop() -> None: - """When the native process dies unexpectedly, the watchdog calls stop().""" - mod = StubNativeModule(die_after=0.2) - mod.pointcloud.transport = LCMTransport("/pc", PointCloud2) - mod.start() - - assert mod._process is not None - pid = mod._process.pid - - # Wait for the process to die and the watchdog to call stop() - for _ in range(30): - time.sleep(0.1) - if mod._process is None: - break - - assert mod._process is None, f"Watchdog did not clean up after process {pid} died" - - -def test_manual(dimos_cluster: DimosCluster, args_file: str) -> None: - native_module = dimos_cluster.deploy( # type: ignore[attr-defined] - StubNativeModule, - some_param=2.5, - output_file=args_file, - ) - - native_module.pointcloud.transport = LCMTransport("/my/custom/lidar", PointCloud2) - native_module.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) - native_module.start() - time.sleep(1) - native_module.stop() - - assert read_json_file(args_file) == { - "cmd_vel": "/cmd_vel#geometry_msgs.Twist", - "pointcloud": "/my/custom/lidar#sensor_msgs.PointCloud2", - "output_file": args_file, - "some_param": "2.5", - } - - -@pytest.mark.heavy -def test_autoconnect(args_file: str) -> None: - """autoconnect passes correct topic args to the native subprocess.""" - blueprint = autoconnect( - StubNativeModule.blueprint( - some_param=2.5, - output_file=args_file, - ), - StubConsumer.blueprint(), - StubProducer.blueprint(), - ).transports( - { - ("pointcloud", PointCloud2): LCMTransport("/my/custom/lidar", PointCloud2), - }, - ) - - coordinator = blueprint.global_config(viewer_backend="none").build() - try: - # Validate blueprint wiring: all modules deployed - native = coordinator.get_instance(StubNativeModule) # type: ignore[type-var] - consumer = coordinator.get_instance(StubConsumer) - producer = coordinator.get_instance(StubProducer) - assert native is not None - assert consumer is not None - assert producer is not None - - # Out→In topics match between connected modules - assert native.pointcloud.transport.topic == consumer.pointcloud.transport.topic - assert native.imu.transport.topic == consumer.imu.transport.topic - assert producer.cmd_vel.transport.topic == native.cmd_vel.transport.topic - - # Custom transport was applied - assert native.pointcloud.transport.topic.topic == "/my/custom/lidar" - finally: - coordinator.stop() - - assert read_json_file(args_file) == { - "cmd_vel": "/cmd_vel#geometry_msgs.Twist", - "pointcloud": "/my/custom/lidar#sensor_msgs.PointCloud2", - "imu": "/imu#sensor_msgs.Imu", - "output_file": args_file, - "some_param": "2.5", - } diff --git a/dimos/core/test_rpcstress.py b/dimos/core/test_rpcstress.py deleted file mode 100644 index 1d09f3e210..0000000000 --- a/dimos/core/test_rpcstress.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -from dimos.core import In, Module, Out, rpc - - -class Counter(Module): - current_count: int = 0 - - count_stream: Out[int] - - def __init__(self) -> None: - super().__init__() - self.current_count = 0 - - @rpc - def increment(self): - """Increment the counter and publish the new value.""" - self.current_count += 1 - self.count_stream.publish(self.current_count) - return self.current_count - - -class CounterValidator(Module): - """Calls counter.increment() as fast as possible and validates no numbers are skipped.""" - - count_in: In[int] - - def __init__(self, increment_func) -> None: - super().__init__() - self.increment_func = increment_func - self.last_seen = 0 - self.missing_numbers = [] - self.running = False - self.call_thread = None - self.call_count = 0 - self.total_latency = 0.0 - self.call_start_time = None - self.waiting_for_response = False - - @rpc - def start(self) -> None: - """Start the validator.""" - self.count_in.subscribe(self._on_count_received) - self.running = True - self.call_thread = threading.Thread(target=self._call_loop) - self.call_thread.start() - - @rpc - def stop(self) -> None: - """Stop the validator.""" - self.running = False - if self.call_thread: - self.call_thread.join() - - def _on_count_received(self, count: int) -> None: - """Check if we received all numbers in sequence and trigger next call.""" - # Calculate round trip time - if self.call_start_time: - latency = time.time() - self.call_start_time - self.total_latency += latency - - if count != self.last_seen + 1: - for missing in range(self.last_seen + 1, count): - self.missing_numbers.append(missing) - print(f"[VALIDATOR] Missing number detected: {missing}") - self.last_seen = count - - # Signal that we can make the next call - self.waiting_for_response = False - - def _call_loop(self) -> None: - """Call increment only after receiving response from previous call.""" - while self.running: - if not self.waiting_for_response: - try: - self.waiting_for_response = True - self.call_start_time = time.time() - result = self.increment_func() - call_time = time.time() - self.call_start_time - self.call_count += 1 - if self.call_count % 100 == 0: - avg_latency = ( - self.total_latency / self.call_count if self.call_count > 0 else 0 - ) - print( - f"[VALIDATOR] Made {self.call_count} calls, last result: {result}, RPC call time: {call_time * 1000:.2f}ms, avg RTT: {avg_latency * 1000:.2f}ms" - ) - except Exception as e: - print(f"[VALIDATOR] Error calling increment: {e}") - self.waiting_for_response = False - time.sleep(0.001) # Small delay on error - else: - # Don't sleep - busy wait for maximum speed - pass - - @rpc - def get_stats(self): - """Get validation statistics.""" - avg_latency = self.total_latency / self.call_count if self.call_count > 0 else 0 - return { - "call_count": self.call_count, - "last_seen": self.last_seen, - "missing_count": len(self.missing_numbers), - "missing_numbers": self.missing_numbers[:10] if self.missing_numbers else [], - "avg_rtt_ms": avg_latency * 1000, - "calls_per_sec": self.call_count / 10.0 if self.call_count > 0 else 0, - } - - -if __name__ == "__main__": - import dimos.core as core - from dimos.core import pLCMTransport - - # Start dimos with 2 workers - client = core.start(2) - - # Deploy counter module - counter = client.deploy(Counter) - counter.count_stream.transport = pLCMTransport("/counter_stream") - - # Deploy validator module with increment function - validator = client.deploy(CounterValidator, counter.increment) - validator.count_in.transport = pLCMTransport("/counter_stream") - - # Connect validator to counter's output - validator.count_in.connect(counter.count_stream) - - # Start modules - validator.start() - - print("[MAIN] Counter and validator started. Running for 10 seconds...") - - # Test direct RPC speed for comparison - print("\n[MAIN] Testing direct RPC call speed for 1 second...") - start = time.time() - direct_count = 0 - while time.time() - start < 1.0: - counter.increment() - direct_count += 1 - print(f"[MAIN] Direct RPC calls per second: {direct_count}") - - # Run for 10 seconds - time.sleep(10) - - # Get stats before stopping - stats = validator.get_stats() - print("\n[MAIN] Final statistics:") - print(f" - Total calls made: {stats['call_count']}") - print(f" - Last number seen: {stats['last_seen']}") - print(f" - Missing numbers: {stats['missing_count']}") - print(f" - Average RTT: {stats['avg_rtt_ms']:.2f}ms") - print(f" - Calls per second: {stats['calls_per_sec']:.1f}") - if stats["missing_numbers"]: - print(f" - First missing numbers: {stats['missing_numbers']}") - - # Stop modules - validator.stop() - - # Shutdown dimos - client.shutdown() - - print("[MAIN] Test complete.") diff --git a/dimos/core/test_stream.py b/dimos/core/test_stream.py deleted file mode 100644 index 836f879b67..0000000000 --- a/dimos/core/test_stream.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -import time - -import pytest - -from dimos.core import ( - In, - LCMTransport, - Module, - rpc, -) -from dimos.core.testing import MockRobotClient, dimos -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.odometry import Odometry - -assert dimos - - -class SubscriberBase(Module): - sub1_msgs: list[Odometry] = None - sub2_msgs: list[Odometry] = None - - def __init__(self) -> None: - self.sub1_msgs = [] - self.sub2_msgs = [] - super().__init__() - - @rpc - def sub1(self) -> None: ... - - @rpc - def sub2(self) -> None: ... - - @rpc - def active_subscribers(self): - return self.odom.transport.active_subscribers - - @rpc - def sub1_msgs_len(self) -> int: - return len(self.sub1_msgs) - - @rpc - def sub2_msgs_len(self) -> int: - return len(self.sub2_msgs) - - -class ClassicSubscriber(SubscriberBase): - odom: In[Odometry] - unsub: Callable[[], None] | None = None - unsub2: Callable[[], None] | None = None - - @rpc - def sub1(self) -> None: - self.unsub = self.odom.subscribe(self.sub1_msgs.append) - - @rpc - def sub2(self) -> None: - self.unsub2 = self.odom.subscribe(self.sub2_msgs.append) - - @rpc - def stop(self) -> None: - if self.unsub: - self.unsub() - self.unsub = None - if self.unsub2: - self.unsub2() - self.unsub2 = None - - -class RXPYSubscriber(SubscriberBase): - odom: In[Odometry] - unsub: Callable[[], None] | None = None - unsub2: Callable[[], None] | None = None - - hot: Callable[[], None] | None = None - - @rpc - def sub1(self) -> None: - self.unsub = self.odom.observable().subscribe(self.sub1_msgs.append) - - @rpc - def sub2(self) -> None: - self.unsub2 = self.odom.observable().subscribe(self.sub2_msgs.append) - - @rpc - def stop(self) -> None: - if self.unsub: - self.unsub.dispose() - self.unsub = None - if self.unsub2: - self.unsub2.dispose() - self.unsub2 = None - - @rpc - def get_next(self): - return self.odom.get_next() - - @rpc - def start_hot_getter(self) -> None: - self.hot = self.odom.hot_latest() - - @rpc - def stop_hot_getter(self) -> None: - self.hot.dispose() - - @rpc - def get_hot(self): - return self.hot() - - -class SpyLCMTransport(LCMTransport): - active_subscribers: int = 0 - - def __reduce__(self): - return (SpyLCMTransport, (self.topic.topic, self.topic.lcm_type)) - - def __init__(self, topic: str, type: type, **kwargs) -> None: - super().__init__(topic, type, **kwargs) - self._subscriber_map = {} # Maps unsubscribe functions to track active subs - - def subscribe(self, selfstream: In, callback: Callable) -> Callable[[], None]: - # Call parent subscribe to get the unsubscribe function - unsubscribe_fn = super().subscribe(selfstream, callback) - - # Increment counter - self.active_subscribers += 1 - - def wrapped_unsubscribe() -> None: - # Create wrapper that decrements counter when called - if wrapped_unsubscribe in self._subscriber_map: - self.active_subscribers -= 1 - del self._subscriber_map[wrapped_unsubscribe] - unsubscribe_fn() - - # Track this subscription - self._subscriber_map[wrapped_unsubscribe] = True - - return wrapped_unsubscribe - - -@pytest.mark.parametrize("subscriber_class", [ClassicSubscriber, RXPYSubscriber]) -@pytest.mark.module -def test_subscription(dimos, subscriber_class) -> None: - robot = dimos.deploy(MockRobotClient) - - robot.lidar.transport = SpyLCMTransport("/lidar", PointCloud2) - robot.odometry.transport = SpyLCMTransport("/odom", Odometry) - - subscriber = dimos.deploy(subscriber_class) - - subscriber.odom.connect(robot.odometry) - - robot.start() - subscriber.sub1() - time.sleep(0.25) - - assert subscriber.sub1_msgs_len() > 0 - assert subscriber.sub2_msgs_len() == 0 - assert subscriber.active_subscribers() == 1 - - subscriber.sub2() - - time.sleep(0.25) - subscriber.stop() - - assert subscriber.active_subscribers() == 0 - assert subscriber.sub1_msgs_len() != 0 - assert subscriber.sub2_msgs_len() != 0 - - total_msg_n = subscriber.sub1_msgs_len() + subscriber.sub2_msgs_len() - - time.sleep(0.25) - - # ensuring no new messages have passed through - assert total_msg_n == subscriber.sub1_msgs_len() + subscriber.sub2_msgs_len() - - robot.stop() - - -@pytest.mark.module -def test_get_next(dimos) -> None: - robot = dimos.deploy(MockRobotClient) - - robot.lidar.transport = SpyLCMTransport("/lidar", PointCloud2) - robot.odometry.transport = SpyLCMTransport("/odom", Odometry) - - subscriber = dimos.deploy(RXPYSubscriber) - subscriber.odom.connect(robot.odometry) - - robot.start() - time.sleep(0.1) - - odom = subscriber.get_next() - - assert isinstance(odom, Odometry) - assert subscriber.active_subscribers() == 0 - - time.sleep(0.2) - - next_odom = subscriber.get_next() - - assert isinstance(next_odom, Odometry) - assert subscriber.active_subscribers() == 0 - - assert next_odom != odom - robot.stop() - - -@pytest.mark.module -def test_hot_getter(dimos) -> None: - robot = dimos.deploy(MockRobotClient) - - robot.lidar.transport = SpyLCMTransport("/lidar", PointCloud2) - robot.odometry.transport = SpyLCMTransport("/odom", Odometry) - - subscriber = dimos.deploy(RXPYSubscriber) - subscriber.odom.connect(robot.odometry) - - robot.start() - - # we are robust to multiple calls - subscriber.start_hot_getter() - time.sleep(0.2) - odom = subscriber.get_hot() - subscriber.stop_hot_getter() - - assert isinstance(odom, Odometry) - time.sleep(0.3) - - # there are no subs - assert subscriber.active_subscribers() == 0 - - # we can restart though - subscriber.start_hot_getter() - time.sleep(0.3) - - next_odom = subscriber.get_hot() - assert isinstance(next_odom, Odometry) - assert next_odom != odom - subscriber.stop_hot_getter() - - robot.stop() diff --git a/dimos/core/test_worker.py b/dimos/core/test_worker.py deleted file mode 100644 index 98a7c5782d..0000000000 --- a/dimos/core/test_worker.py +++ /dev/null @@ -1,153 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.core import In, Module, Out, rpc -from dimos.core.worker_manager import WorkerManager -from dimos.msgs.geometry_msgs import Vector3 - - -class SimpleModule(Module): - output: Out[Vector3] - input: In[Vector3] - - counter: int = 0 - - @rpc - def start(self) -> None: - pass - - @rpc - def increment(self) -> int: - self.counter += 1 - return self.counter - - @rpc - def get_counter(self) -> int: - return self.counter - - -class AnotherModule(Module): - value: int = 100 - - @rpc - def start(self) -> None: - pass - - @rpc - def add(self, n: int) -> int: - self.value += n - return self.value - - @rpc - def get_value(self) -> int: - return self.value - - -class ThirdModule(Module): - multiplier: int = 1 - - @rpc - def start(self) -> None: - pass - - @rpc - def multiply(self, n: int) -> int: - self.multiplier *= n - return self.multiplier - - @rpc - def get_multiplier(self) -> int: - return self.multiplier - - -@pytest.fixture -def worker_manager(): - manager = WorkerManager() - try: - yield manager - finally: - manager.close_all() - - -@pytest.mark.integration -def test_worker_manager_basic(worker_manager): - module = worker_manager.deploy(SimpleModule) - module.start() - - result = module.increment() - assert result == 1 - - result = module.increment() - assert result == 2 - - result = module.get_counter() - assert result == 2 - - module.stop() - - -@pytest.mark.integration -def test_worker_manager_multiple_different_modules(worker_manager): - module1 = worker_manager.deploy(SimpleModule) - module2 = worker_manager.deploy(AnotherModule) - - module1.start() - module2.start() - - # Each module has its own state - module1.increment() - module1.increment() - module2.add(10) - - assert module1.get_counter() == 2 - assert module2.get_value() == 110 - - # Stop modules to clean up threads - module1.stop() - module2.stop() - - -@pytest.mark.integration -def test_worker_manager_parallel_deployment(worker_manager): - modules = worker_manager.deploy_parallel( - [ - (SimpleModule, (), {}), - (AnotherModule, (), {}), - (ThirdModule, (), {}), - ] - ) - - assert len(modules) == 3 - module1, module2, module3 = modules - - # Start all modules - module1.start() - module2.start() - module3.start() - - # Each module has its own state - module1.increment() - module2.add(50) - module3.multiply(5) - - assert module1.get_counter() == 1 - assert module2.get_value() == 150 - assert module3.get_multiplier() == 5 - - # Stop modules - module1.stop() - module2.stop() - module3.stop() diff --git a/dimos/core/testing.py b/dimos/core/testing.py deleted file mode 100644 index dee25aaa45..0000000000 --- a/dimos/core/testing.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import Event, Thread -import time - -import pytest # type: ignore[import-not-found] - -from dimos.core import In, Module, Out, rpc, start -from dimos.msgs.geometry_msgs import Vector3 -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.lidar import pointcloud2_from_webrtc_lidar -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.testing import SensorReplay - - -@pytest.fixture -def dimos(): # type: ignore[no-untyped-def] - """Fixture to create a Dimos client for testing.""" - client = start(2) - yield client - client.stop() # type: ignore[attr-defined] - - -class MockRobotClient(Module): - odometry: Out[Odometry] - lidar: Out[PointCloud2] - mov: In[Vector3] - - mov_msg_count = 0 - - def mov_callback(self, msg) -> None: # type: ignore[no-untyped-def] - self.mov_msg_count += 1 - - def __init__(self) -> None: - super().__init__() - self._stop_event = Event() - self._thread = None - - @rpc - def start(self) -> None: - super().start() - - self._thread = Thread(target=self.odomloop) # type: ignore[assignment] - self._thread.start() # type: ignore[attr-defined] - self.mov.subscribe(self.mov_callback) - - @rpc - def stop(self) -> None: - self._stop_event.set() - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=1.0) - - super().stop() - - def odomloop(self) -> None: - odomdata = SensorReplay("raw_odometry_rotate_walk", autocast=Odometry.from_msg) - lidardata = SensorReplay("office_lidar", autocast=pointcloud2_from_webrtc_lidar) - - lidariter = lidardata.iterate() - self._stop_event.clear() - while not self._stop_event.is_set(): - for odom in odomdata.iterate(): - if self._stop_event.is_set(): - return - print(odom) - odom.pubtime = time.perf_counter() - self.odometry.publish(odom) - - lidarmsg = next(lidariter) - self.lidar.publish(lidarmsg) - time.sleep(0.1) diff --git a/dimos/core/tests/native_echo.py b/dimos/core/tests/native_echo.py deleted file mode 100755 index 6723b0b0d1..0000000000 --- a/dimos/core/tests/native_echo.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Echo binary for NativeModule tests. - -Parses --output_file and --die_after from CLI args, writes remaining -args as JSON to the output file, then waits for SIGTERM. -""" - -import argparse -import json -import signal -import sys -import time - -print("this message goes to stdout") -print("this message goes to stderr", file=sys.stderr) - -signal.signal(signal.SIGTERM, lambda *_: sys.exit(0)) - -parser = argparse.ArgumentParser() -parser.add_argument("--output_file", default=None) -parser.add_argument("--die_after", type=float, default=None) -args, _ = parser.parse_known_args() - -if args.output_file: - with open(args.output_file, "w") as f: - json.dump(sys.argv[1:], f) - -print("my args:", json.dumps(sys.argv[1:])) - -if args.die_after is not None: - time.sleep(args.die_after) - sys.exit(42) - -signal.pause() diff --git a/dimos/core/transport.py b/dimos/core/transport.py deleted file mode 100644 index 2586706feb..0000000000 --- a/dimos/core/transport.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import threading -from typing import ( - TYPE_CHECKING, - Any, - TypeVar, -) - -import dimos.core.colors as colors -from dimos.core.stream import In, Out, Stream, Transport -from dimos.msgs.protocol import DimosMsg - -try: - import cyclonedds as _cyclonedds # noqa: F401 - - DDS_AVAILABLE = True -except ImportError: - DDS_AVAILABLE = False -from dimos.protocol.pubsub.impl.jpeg_shm import JpegSharedMemory -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, JpegLCM, PickleLCM, Topic as LCMTopic -from dimos.protocol.pubsub.impl.rospubsub import DimosROS, ROSTopic -from dimos.protocol.pubsub.impl.shmpubsub import BytesSharedMemory, PickleSharedMemory - -if TYPE_CHECKING: - from collections.abc import Callable - -T = TypeVar("T") # type: ignore[misc] - -# TODO -# Transports need to be rewritten and simplified, -# -# there is no need for them to get a reference to "a stream" on publish/subscribe calls -# this is a legacy from dask transports. -# -# new transport should literally have 2 functions (next to start/stop) -# "send(msg)" and "receive(callback)" and that's all -# -# we can also consider pubsubs conforming directly to Transport specs -# and removing PubSubTransport glue entirely -# -# Why not ONLY pubsubs without Transport abstraction? -# -# General idea for transports (and why they exist at all) -# is that they can be * anything * like -# -# a web camera rtsp stream for Image, audio stream from mic, etc -# http binary streams, tcp connections etc - - -class PubSubTransport(Transport[T]): - topic: Any - - def __init__(self, topic: Any) -> None: - self.topic = topic - - def __str__(self) -> str: - return ( - colors.green(f"{self.__class__.__name__}(") - + colors.blue(self.topic) - + colors.green(")") - ) - - -class pLCMTransport(PubSubTransport[T]): - _started: bool = False - - def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(topic) - self.lcm = PickleLCM(**kwargs) - - def __reduce__(self): # type: ignore[no-untyped-def] - return (pLCMTransport, (self.topic,)) - - def broadcast(self, _: Out[T] | None, msg: T) -> None: - if not self._started: - self.start() - - self.lcm.publish(self.topic, msg) - - def subscribe( - self, callback: Callable[[T], Any], selfstream: Stream[T] | None = None - ) -> Callable[[], None]: - if not self._started: - self.start() - return self.lcm.subscribe(LCMTopic(self.topic), lambda msg, topic: callback(msg)) - - def start(self) -> None: - self.lcm.start() - self._started = True - - def stop(self) -> None: - self.lcm.stop() - self._started = False - - -class LCMTransport(PubSubTransport[T]): - _started: bool = False - - def __init__(self, topic: str, type: type, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(LCMTopic(topic, type)) - if not hasattr(self, "lcm"): - self.lcm = LCM(**kwargs) - - def start(self) -> None: - self.lcm.start() - self._started = True - - def stop(self) -> None: - self.lcm.stop() - self._started = False - - def __reduce__(self): # type: ignore[no-untyped-def] - return (LCMTransport, (self.topic.topic, self.topic.lcm_type)) - - def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] - if not self._started: - self.start() - - self.lcm.publish(self.topic, msg) - - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: # type: ignore[assignment, override] - if not self._started: - self.start() - return self.lcm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[return-value, arg-type] - - -class JpegLcmTransport(LCMTransport): # type: ignore[type-arg] - def __init__(self, topic: str, type: type, **kwargs) -> None: # type: ignore[no-untyped-def] - self.lcm = JpegLCM(**kwargs) # type: ignore[assignment] - super().__init__(topic, type) - - def __reduce__(self): # type: ignore[no-untyped-def] - return (JpegLcmTransport, (self.topic.topic, self.topic.lcm_type)) - - def start(self) -> None: - self.lcm.start() - self._started = True - - def stop(self) -> None: - self.lcm.stop() - self._started = False - - -class pSHMTransport(PubSubTransport[T]): - _started: bool = False - - def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(topic) - self.shm = PickleSharedMemory(**kwargs) - - def __reduce__(self): # type: ignore[no-untyped-def] - return (pSHMTransport, (self.topic,)) - - def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] - if not self._started: - self.start() - - self.shm.publish(self.topic, msg) - - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] = None) -> None: # type: ignore[assignment, override] - if not self._started: - self.start() - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[return-value] - - def start(self) -> None: - self.shm.start() - self._started = True - - def stop(self) -> None: - self.shm.stop() - self._started = False - - -class SHMTransport(PubSubTransport[T]): - _started: bool = False - - def __init__(self, topic: str, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(topic) - self.shm = BytesSharedMemory(**kwargs) - - def __reduce__(self): # type: ignore[no-untyped-def] - return (SHMTransport, (self.topic,)) - - def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] - if not self._started: - self.start() - - self.shm.publish(self.topic, msg) - - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] | None = None) -> None: # type: ignore[override] - if not self._started: - self.start() - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[arg-type, return-value] - - def start(self) -> None: - self.shm.start() - self._started = True - - def stop(self) -> None: - self.shm.stop() - self._started = False - - -class JpegShmTransport(PubSubTransport[T]): - _started: bool = False - - def __init__(self, topic: str, quality: int = 75, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(topic) - self.shm = JpegSharedMemory(quality=quality, **kwargs) - self.quality = quality - - def __reduce__(self): # type: ignore[no-untyped-def] - return (JpegShmTransport, (self.topic, self.quality)) - - def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] - if not self._started: - self.start() - - self.shm.publish(self.topic, msg) - - def subscribe(self, callback: Callable[[T], None], selfstream: In[T] | None = None) -> None: # type: ignore[override] - if not self._started: - self.start() - return self.shm.subscribe(self.topic, lambda msg, topic: callback(msg)) # type: ignore[arg-type, return-value] - - def start(self) -> None: - self.shm.start() - self._started = True - - def stop(self) -> None: - self.shm.stop() - self._started = False - - -class ROSTransport(PubSubTransport[DimosMsg]): - _ros: DimosROS | None = None - - def __init__(self, topic: str, msg_type: type[DimosMsg], **kwargs: Any) -> None: - super().__init__(ROSTopic(topic, msg_type)) - self._kwargs = kwargs - - def __reduce__(self) -> tuple[Any, ...]: - return (ROSTransport, (self.topic.topic, self.topic.msg_type)) - - def broadcast(self, _: Out[DimosMsg], msg: DimosMsg) -> None: - if self._ros is None: - self.start() - assert self._ros is not None # for type narrowing - self._ros.publish(self.topic, msg) - - def subscribe( - self, callback: Callable[[DimosMsg], Any], selfstream: Stream[DimosMsg] | None = None - ) -> Callable[[], None]: - if self._ros is None: - self.start() - assert self._ros is not None # for type narrowing - return self._ros.subscribe(self.topic, lambda msg, topic: callback(msg)) - - def start(self) -> None: - if self._ros is None: - self._ros = DimosROS(**self._kwargs) - self._ros.start() - - def stop(self) -> None: - if self._ros is not None: - self._ros.stop() - self._ros = None - - -if DDS_AVAILABLE: - from dimos.protocol.pubsub.impl.ddspubsub import DDS, Topic as DDSTopic - - class DDSTransport(PubSubTransport[T]): - def __init__(self, topic: str, type: type, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(DDSTopic(topic, type)) - self.dds = DDS(**kwargs) - self._started: bool = False - self._start_lock = threading.RLock() - - def start(self) -> None: - with self._start_lock: - if not self._started: - self.dds.start() - self._started = True - - def stop(self) -> None: - with self._start_lock: - if self._started: - self.dds.stop() - self._started = False - - def broadcast(self, _, msg) -> None: # type: ignore[no-untyped-def] - with self._start_lock: - if not self._started: - self.start() - self.dds.publish(self.topic, msg) - - def subscribe( - self, callback: Callable[[T], None], selfstream: Stream[T] | None = None - ) -> Callable[[], None]: - with self._start_lock: - if not self._started: - self.start() - return self.dds.subscribe(self.topic, lambda msg, topic: callback(msg)) - - -class ZenohTransport(PubSubTransport[T]): ... diff --git a/dimos/core/worker.py b/dimos/core/worker.py deleted file mode 100644 index d6ff71918c..0000000000 --- a/dimos/core/worker.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import multiprocessing as mp -from multiprocessing.connection import Connection -import traceback -from typing import Any - -from dimos.core.module import ModuleT -from dimos.core.rpc_client import RPCClient -from dimos.utils.actor_registry import ActorRegistry -from dimos.utils.logging_config import setup_logger -from dimos.utils.sequential_ids import SequentialIds - -logger = setup_logger() - - -class ActorFuture: - """Mimics Dask's ActorFuture - wraps a result with .result() method.""" - - def __init__(self, value: Any) -> None: - self._value = value - - def result(self, _timeout: float | None = None) -> Any: - return self._value - - -class Actor: - """Proxy that forwards method calls to the worker process.""" - - def __init__( - self, conn: Connection | None, module_class: type[ModuleT], worker_id: int - ) -> None: - self._conn = conn - self._cls = module_class - self._worker_id = worker_id - - def __reduce__(self) -> tuple[type, tuple[None, type, int]]: - """Exclude the connection when pickling - it can't be used in other processes.""" - return (Actor, (None, self._cls, self._worker_id)) - - def _send_request_to_worker(self, request: dict[str, Any]) -> Any: - if self._conn is None: - raise RuntimeError("Actor connection not available - cannot send requests") - self._conn.send(request) - response = self._conn.recv() - if response.get("error"): - if "AttributeError" in response["error"]: # TODO: better error handling - raise AttributeError(response["error"]) - raise RuntimeError(f"Worker error: {response['error']}") - return response.get("result") - - def set_ref(self, ref: Any) -> ActorFuture: - """Set the actor reference on the remote module.""" - result = self._send_request_to_worker({"type": "set_ref", "ref": ref}) - return ActorFuture(result) - - def __getattr__(self, name: str) -> Any: - """Proxy attribute access to the worker process.""" - if name.startswith("_"): - raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'") - - return self._send_request_to_worker({"type": "getattr", "name": name}) - - -# Global forkserver context. Using `forkserver` instead of `fork` because it -# avoids CUDA context corruption issues. -_forkserver_ctx: Any = None - - -def get_forkserver_context() -> Any: - global _forkserver_ctx - if _forkserver_ctx is None: - _forkserver_ctx = mp.get_context("forkserver") - return _forkserver_ctx - - -def reset_forkserver_context() -> None: - """Reset the forkserver context. Used in tests to ensure clean state.""" - global _forkserver_ctx - _forkserver_ctx = None - - -_seq_ids = SequentialIds() - - -class Worker: - def __init__( - self, - module_class: type[ModuleT], - args: tuple[Any, ...] = (), - kwargs: dict[Any, Any] | None = None, - ) -> None: - self._module_class: type[ModuleT] = module_class - self._args: tuple[Any, ...] = args - self._kwargs: dict[Any, Any] = kwargs or {} - self._process: Any = None - self._conn: Connection | None = None - self._actor: Actor | None = None - self._worker_id: int = _seq_ids.next() - self._ready: bool = False - - def start_process(self) -> None: - ctx = get_forkserver_context() - parent_conn, child_conn = ctx.Pipe() - self._conn = parent_conn - - self._process = ctx.Process( - target=_worker_entrypoint, - args=(child_conn, self._module_class, self._args, self._kwargs, self._worker_id), - daemon=True, - ) - self._process.start() - self._actor = Actor(parent_conn, self._module_class, self._worker_id) - - def wait_until_ready(self) -> None: - if self._ready: - return - if self._actor is None: - raise RuntimeError("Worker process not started") - - worker_id = self._actor.set_ref(self._actor).result() - ActorRegistry.update(str(self._actor), str(worker_id)) - self._ready = True - - logger.info( - "Deployed module.", module=self._module_class.__name__, worker_id=self._worker_id - ) - - def deploy(self) -> None: - self.start_process() - self.wait_until_ready() - - def get_instance(self) -> RPCClient: - if self._actor is None: - raise RuntimeError("Worker not deployed") - return RPCClient(self._actor, self._module_class) - - def shutdown(self) -> None: - if self._conn is not None: - try: - self._conn.send({"type": "shutdown"}) - self._conn.recv() - except (BrokenPipeError, EOFError): - pass - finally: - self._conn.close() - self._conn = None - - if self._process is not None: - self._process.join(timeout=2) - if self._process.is_alive(): - self._process.terminate() - self._process.join(timeout=1) - self._process = None - - -def _worker_entrypoint( - conn: Connection, - module_class: type[ModuleT], - args: tuple[Any, ...], - kwargs: dict[Any, Any], - worker_id: int, -) -> None: - instance = None - - try: - instance = module_class(*args, **kwargs) - instance.worker = worker_id - - _worker_loop(conn, instance, worker_id) - except Exception as e: - logger.error(f"Worker process error: {e}", exc_info=True) - finally: - if instance is not None: - try: - instance.stop() - except Exception: - logger.error("Error during worker shutdown", exc_info=True) - - -def _worker_loop(conn: Connection, instance: Any, worker_id: int) -> None: - while True: - try: - if not conn.poll(timeout=0.1): - continue - request = conn.recv() - except (EOFError, KeyboardInterrupt): - break - - response: dict[str, Any] = {} - try: - req_type = request.get("type") - - if req_type == "set_ref": - instance.ref = request.get("ref") - response["result"] = worker_id - - elif req_type == "getattr": - response["result"] = getattr(instance, request["name"]) - - elif req_type == "shutdown": - response["result"] = True - conn.send(response) - break - - else: - response["error"] = f"Unknown request type: {req_type}" - - except Exception as e: - response["error"] = f"{e.__class__.__name__}: {e}\n{traceback.format_exc()}" - - try: - conn.send(response) - except (BrokenPipeError, EOFError): - break diff --git a/dimos/core/worker_manager.py b/dimos/core/worker_manager.py deleted file mode 100644 index 175b650fd2..0000000000 --- a/dimos/core/worker_manager.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -from dimos.core.module import ModuleT -from dimos.core.rpc_client import RPCClient -from dimos.core.worker import Worker -from dimos.utils.actor_registry import ActorRegistry -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class WorkerManager: - def __init__(self) -> None: - self._workers: list[Worker] = [] - self._closed = False - - def deploy(self, module_class: type[ModuleT], *args: Any, **kwargs: Any) -> RPCClient: - if self._closed: - raise RuntimeError("WorkerManager is closed") - - worker = Worker(module_class, args=args, kwargs=kwargs) - worker.deploy() - self._workers.append(worker) - return worker.get_instance() - - def deploy_parallel( - self, module_specs: list[tuple[type[ModuleT], tuple[Any, ...], dict[Any, Any]]] - ) -> list[RPCClient]: - if self._closed: - raise RuntimeError("WorkerManager is closed") - - workers: list[Worker] = [] - for module_class, args, kwargs in module_specs: - worker = Worker(module_class, args=args, kwargs=kwargs) - worker.start_process() - workers.append(worker) - - for worker in workers: - worker.wait_until_ready() - self._workers.append(worker) - - return [worker.get_instance() for worker in workers] - - def close_all(self) -> None: - if self._closed: - return - self._closed = True - - logger.info("Shutting down all workers...") - - for worker in reversed(self._workers): - try: - worker.shutdown() - except Exception as e: - logger.error(f"Error shutting down worker: {e}", exc_info=True) - - self._workers.clear() - ActorRegistry.clear() - - logger.info("All workers shut down") diff --git a/dimos/e2e_tests/conftest.py b/dimos/e2e_tests/conftest.py deleted file mode 100644 index 46b92151e9..0000000000 --- a/dimos/e2e_tests/conftest.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Iterator - -import pytest - -from dimos.core.transport import pLCMTransport -from dimos.e2e_tests.dimos_cli_call import DimosCliCall -from dimos.e2e_tests.lcm_spy import LcmSpy -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion -from dimos.msgs.geometry_msgs.Vector3 import make_vector3 -from dimos.msgs.std_msgs.Bool import Bool - - -def _pose(x: float, y: float, theta: float) -> PoseStamped: - return PoseStamped( - position=make_vector3(x, y, 0), - orientation=Quaternion.from_euler(make_vector3(0, 0, theta)), - frame_id="map", - ) - - -@pytest.fixture -def lcm_spy() -> Iterator[LcmSpy]: - lcm_spy = LcmSpy() - lcm_spy.start() - yield lcm_spy - lcm_spy.stop() - - -@pytest.fixture -def follow_points(lcm_spy: LcmSpy): - def fun(*, points: list[tuple[float, float, float]], fail_message: str) -> None: - topic = "/goal_reached#std_msgs.Bool" - lcm_spy.save_topic(topic) - - for x, y, theta in points: - lcm_spy.publish("/goal_request#geometry_msgs.PoseStamped", _pose(x, y, theta)) - lcm_spy.wait_for_message_result( - topic, - Bool, - predicate=lambda v: bool(v), - fail_message=fail_message, - timeout=60.0, - ) - - yield fun - - -@pytest.fixture -def start_blueprint() -> Iterator[Callable[[str], DimosCliCall]]: - dimos_robot_call = DimosCliCall() - - def set_name_and_start(*demo_args: str) -> DimosCliCall: - dimos_robot_call.demo_args = list(demo_args) - dimos_robot_call.start() - return dimos_robot_call - - yield set_name_and_start - - dimos_robot_call.stop() - - -@pytest.fixture -def human_input(): - transport = pLCMTransport("/human_input") - transport.lcm.start() - - def send_human_input(message: str) -> None: - transport.publish(message) - - yield send_human_input - - transport.lcm.stop() diff --git a/dimos/e2e_tests/dimos_cli_call.py b/dimos/e2e_tests/dimos_cli_call.py deleted file mode 100644 index 2e987cf7ad..0000000000 --- a/dimos/e2e_tests/dimos_cli_call.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import signal -import subprocess -import time - - -class DimosCliCall: - process: subprocess.Popen[bytes] | None - demo_args: list[str] | None = None - - def __init__(self) -> None: - self.process = None - - def start(self) -> None: - if self.demo_args is None: - raise ValueError("Demo args must be set before starting the process.") - - args = list(self.demo_args) - if len(args) == 1: - args = ["run", *args] - - self.process = subprocess.Popen(["dimos", "--simulation", *args]) - - def stop(self) -> None: - if self.process is None: - return - - try: - # Send the kill signal (SIGTERM for graceful shutdown) - self.process.send_signal(signal.SIGTERM) - - # Record the time when we sent the kill signal - shutdown_start = time.time() - - # Wait for the process to terminate with a 30-second timeout - try: - self.process.wait(timeout=30) - shutdown_duration = time.time() - shutdown_start - - # Verify it shut down in time - assert shutdown_duration <= 30, ( - f"Process took {shutdown_duration:.2f} seconds to shut down, " - f"which exceeds the 30-second limit" - ) - except subprocess.TimeoutExpired: - # If we reach here, the process didn't terminate in 30 seconds - self.process.kill() # Force kill - self.process.wait() # Clean up - raise AssertionError( - "Process did not shut down within 30 seconds after receiving SIGTERM" - ) - - except Exception: - # Clean up if something goes wrong - if self.process.poll() is None: # Process still running - self.process.kill() - self.process.wait() - raise diff --git a/dimos/e2e_tests/lcm_spy.py b/dimos/e2e_tests/lcm_spy.py deleted file mode 100644 index 9efed09d5e..0000000000 --- a/dimos/e2e_tests/lcm_spy.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Iterator -from contextlib import contextmanager -import math -import pickle -import threading -import time -from typing import Any - -import lcm - -from dimos.msgs import DimosMsg -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.protocol.service.lcmservice import LCMService - - -class LcmSpy(LCMService): - l: lcm.LCM - messages: dict[str, list[bytes]] - _messages_lock: threading.Lock - _saved_topics: set[str] - _saved_topics_lock: threading.Lock - _topic_listeners: dict[str, list[Callable[[bytes], None]]] - _topic_listeners_lock: threading.Lock - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.l = lcm.LCM() - self.messages = {} - self._messages_lock = threading.Lock() - self._saved_topics = set() - self._saved_topics_lock = threading.Lock() - self._topic_listeners = {} - self._topic_listeners_lock = threading.Lock() - - def start(self) -> None: - super().start() - if self.l: - self.l.subscribe(".*", self.msg) - - def stop(self) -> None: - super().stop() - - def msg(self, topic: str, data: bytes) -> None: - with self._saved_topics_lock: - if topic in self._saved_topics: - with self._messages_lock: - self.messages.setdefault(topic, []).append(data) - - with self._topic_listeners_lock: - listeners = self._topic_listeners.get(topic) - if listeners: - for listener in listeners: - listener(data) - - def publish(self, topic: str, msg: Any) -> None: - self.l.publish(topic, msg.lcm_encode()) - - def save_topic(self, topic: str) -> None: - with self._saved_topics_lock: - self._saved_topics.add(topic) - - def register_topic_listener(self, topic: str, listener: Callable[[bytes], None]) -> int: - with self._topic_listeners_lock: - listeners = self._topic_listeners.setdefault(topic, []) - listener_index = len(listeners) - listeners.append(listener) - return listener_index - - def unregister_topic_listener(self, topic: str, listener_index: int) -> None: - with self._topic_listeners_lock: - listeners = self._topic_listeners[topic] - listeners.pop(listener_index) - - @contextmanager - def topic_listener(self, topic: str, listener: Callable[[bytes], None]) -> Iterator[None]: - listener_index = self.register_topic_listener(topic, listener) - try: - yield - finally: - self.unregister_topic_listener(topic, listener_index) - - def wait_until( - self, - *, - condition: Callable[[], bool], - timeout: float, - error_message: str, - poll_interval: float = 0.1, - ) -> None: - start_time = time.time() - while time.time() - start_time < timeout: - if condition(): - return - time.sleep(poll_interval) - raise TimeoutError(error_message) - - def wait_for_saved_topic(self, topic: str, timeout: float = 30.0) -> None: - def condition() -> bool: - with self._messages_lock: - return topic in self.messages - - self.wait_until( - condition=condition, - timeout=timeout, - error_message=f"Timeout waiting for topic {topic}", - ) - - def wait_for_saved_topic_content( - self, topic: str, content_contains: bytes, timeout: float = 30.0 - ) -> None: - def condition() -> bool: - with self._messages_lock: - return any(content_contains in msg for msg in self.messages.get(topic, [])) - - self.wait_until( - condition=condition, - timeout=timeout, - error_message=f"Timeout waiting for '{topic}' to contain '{content_contains!r}'", - ) - - def wait_for_message_pickle_result( - self, - topic: str, - predicate: Callable[[Any], bool], - fail_message: str, - timeout: float = 30.0, - ) -> None: - event = threading.Event() - - def listener(msg: bytes) -> None: - data = pickle.loads(msg) - if predicate(data["res"]): - event.set() - - with self.topic_listener(topic, listener): - self.wait_until( - condition=event.is_set, - timeout=timeout, - error_message=fail_message, - ) - - def wait_for_message_result( - self, - topic: str, - type: type[DimosMsg], - predicate: Callable[[Any], bool], - fail_message: str, - timeout: float = 30.0, - ) -> None: - event = threading.Event() - - def listener(msg: bytes) -> None: - data = type.lcm_decode(msg) - if predicate(data): - event.set() - - with self.topic_listener(topic, listener): - self.wait_until( - condition=event.is_set, - timeout=timeout, - error_message=fail_message, - ) - - def wait_until_odom_position( - self, x: float, y: float, threshold: float = 1, timeout: float = 60 - ) -> None: - def predicate(msg: PoseStamped) -> bool: - pos = msg.position - distance = math.sqrt((pos.x - x) ** 2 + (pos.y - y) ** 2) - return distance < threshold - - self.wait_for_message_result( - "/odom#geometry_msgs.PoseStamped", - PoseStamped, - predicate, - f"Failed to get to position x={x}, y={y}", - timeout, - ) diff --git a/dimos/e2e_tests/test_control_coordinator.py b/dimos/e2e_tests/test_control_coordinator.py deleted file mode 100644 index f6e520831d..0000000000 --- a/dimos/e2e_tests/test_control_coordinator.py +++ /dev/null @@ -1,255 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""End-to-end tests for the ControlCoordinator. - -These tests start a real coordinator process and communicate via LCM/RPC. -Unlike unit tests, these verify the full system integration. -""" - -import os -import time - -import pytest - -from dimos.control.coordinator import ControlCoordinator -from dimos.core.rpc_client import RPCClient -from dimos.msgs.sensor_msgs import JointState -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryPoint, TrajectoryState - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM doesn't work in CI.") -@pytest.mark.e2e -class TestControlCoordinatorE2E: - """End-to-end tests for ControlCoordinator.""" - - def test_coordinator_starts_and_responds_to_rpc(self, lcm_spy, start_blueprint) -> None: - """Test that coordinator starts and responds to RPC queries.""" - # Save topics we care about (LCM topics include type suffix) - joint_state_topic = "/coordinator/joint_state#sensor_msgs.JointState" - lcm_spy.save_topic(joint_state_topic) - lcm_spy.save_topic("/rpc/ControlCoordinator/list_joints/res") - lcm_spy.save_topic("/rpc/ControlCoordinator/list_tasks/res") - - # Start the mock coordinator blueprint - start_blueprint("coordinator-mock") - - # Wait for joint state to be published (proves tick loop is running) - lcm_spy.wait_for_saved_topic(joint_state_topic) - - # Create RPC client and query - client = RPCClient(None, ControlCoordinator) - try: - # Test list_joints RPC - joints = client.list_joints() - assert joints is not None - assert len(joints) == 7 # Mock arm has 7 DOF - assert "arm_joint1" in joints - - # Test list_tasks RPC - tasks = client.list_tasks() - assert tasks is not None - assert "traj_arm" in tasks - - # Test list_hardware RPC - hardware = client.list_hardware() - assert hardware is not None - assert "arm" in hardware - finally: - client.stop_rpc_client() - - def test_coordinator_executes_trajectory(self, lcm_spy, start_blueprint) -> None: - """Test that coordinator executes a trajectory via RPC.""" - # Save topics - lcm_spy.save_topic("/coordinator/joint_state#sensor_msgs.JointState") - - # Start coordinator - start_blueprint("coordinator-mock") - - # Wait for it to be ready - lcm_spy.wait_for_saved_topic("/coordinator/joint_state#sensor_msgs.JointState") - - # Create RPC client - client = RPCClient(None, ControlCoordinator) - try: - # Get initial joint positions - initial_positions = client.get_joint_positions() - assert initial_positions is not None - - # Create a simple trajectory - trajectory = JointTrajectory( - joint_names=[f"arm_joint{i + 1}" for i in range(7)], - points=[ - TrajectoryPoint( - time_from_start=0.0, - positions=[0.0] * 7, - velocities=[0.0] * 7, - ), - TrajectoryPoint( - time_from_start=0.5, - positions=[0.1] * 7, - velocities=[0.0] * 7, - ), - ], - ) - - # Execute trajectory via task_invoke - result = client.task_invoke("traj_arm", "execute", {"trajectory": trajectory}) - assert result is True - - # Poll for completion - timeout = 5.0 - start_time = time.time() - completed = False - - while time.time() - start_time < timeout: - state = client.task_invoke("traj_arm", "get_state") - if state is not None and state == TrajectoryState.COMPLETED: - completed = True - break - time.sleep(0.1) - - assert completed, "Trajectory did not complete within timeout" - finally: - client.stop_rpc_client() - - def test_coordinator_joint_state_published(self, lcm_spy, start_blueprint) -> None: - """Test that joint state messages are published at expected rate.""" - joint_state_topic = "/coordinator/joint_state#sensor_msgs.JointState" - lcm_spy.save_topic(joint_state_topic) - - # Start coordinator - start_blueprint("coordinator-mock") - - # Wait for initial message - lcm_spy.wait_for_saved_topic(joint_state_topic) - - # Collect messages for 1 second - time.sleep(1.0) - - # Check we received messages (should be ~100 at 100Hz) - with lcm_spy._messages_lock: - message_count = len(lcm_spy.messages.get(joint_state_topic, [])) - - # Allow some tolerance (at least 50 messages in 1 second) - assert message_count >= 50, f"Expected ~100 messages, got {message_count}" - - # Decode a message to verify structure - with lcm_spy._messages_lock: - raw_msg = lcm_spy.messages[joint_state_topic][0] - - joint_state = JointState.lcm_decode(raw_msg) - assert len(joint_state.name) == 7 - assert len(joint_state.position) == 7 - assert "arm_joint1" in joint_state.name - - def test_coordinator_cancel_trajectory(self, lcm_spy, start_blueprint) -> None: - """Test that a running trajectory can be cancelled.""" - lcm_spy.save_topic("/coordinator/joint_state#sensor_msgs.JointState") - - # Start coordinator - start_blueprint("coordinator-mock") - lcm_spy.wait_for_saved_topic("/coordinator/joint_state#sensor_msgs.JointState") - - client = RPCClient(None, ControlCoordinator) - try: - # Create a long trajectory (5 seconds) - trajectory = JointTrajectory( - joint_names=[f"arm_joint{i + 1}" for i in range(7)], - points=[ - TrajectoryPoint( - time_from_start=0.0, - positions=[0.0] * 7, - velocities=[0.0] * 7, - ), - TrajectoryPoint( - time_from_start=5.0, - positions=[1.0] * 7, - velocities=[0.0] * 7, - ), - ], - ) - - # Start trajectory via task_invoke - result = client.task_invoke("traj_arm", "execute", {"trajectory": trajectory}) - assert result is True - - # Wait a bit then cancel - time.sleep(0.5) - cancel_result = client.task_invoke("traj_arm", "cancel") - assert cancel_result is True - - # Check status is ABORTED - state = client.task_invoke("traj_arm", "get_state") - assert state is not None - assert state == TrajectoryState.ABORTED - finally: - client.stop_rpc_client() - - def test_dual_arm_coordinator(self, lcm_spy, start_blueprint) -> None: - """Test dual-arm coordinator with independent trajectories.""" - lcm_spy.save_topic("/coordinator/joint_state#sensor_msgs.JointState") - - # Start dual-arm mock coordinator - start_blueprint("coordinator-dual-mock") - lcm_spy.wait_for_saved_topic("/coordinator/joint_state#sensor_msgs.JointState") - - client = RPCClient(None, ControlCoordinator) - try: - # Verify both arms present - joints = client.list_joints() - assert "left_arm_joint1" in joints - assert "right_arm_joint1" in joints - - tasks = client.list_tasks() - assert "traj_left" in tasks - assert "traj_right" in tasks - - # Create trajectories for both arms - left_trajectory = JointTrajectory( - joint_names=[f"left_arm_joint{i + 1}" for i in range(7)], - points=[ - TrajectoryPoint(time_from_start=0.0, positions=[0.0] * 7), - TrajectoryPoint(time_from_start=0.5, positions=[0.2] * 7), - ], - ) - - right_trajectory = JointTrajectory( - joint_names=[f"right_arm_joint{i + 1}" for i in range(6)], - points=[ - TrajectoryPoint(time_from_start=0.0, positions=[0.0] * 6), - TrajectoryPoint(time_from_start=0.5, positions=[0.3] * 6), - ], - ) - - # Execute both via task_invoke - assert ( - client.task_invoke("traj_left", "execute", {"trajectory": left_trajectory}) is True - ) - assert ( - client.task_invoke("traj_right", "execute", {"trajectory": right_trajectory}) - is True - ) - - # Wait for completion - time.sleep(1.0) - - # Both should complete - left_state = client.task_invoke("traj_left", "get_state") - right_state = client.task_invoke("traj_right", "get_state") - - assert left_state == TrajectoryState.COMPLETED - assert right_state == TrajectoryState.COMPLETED - finally: - client.stop_rpc_client() diff --git a/dimos/e2e_tests/test_dimos_cli_e2e.py b/dimos/e2e_tests/test_dimos_cli_e2e.py deleted file mode 100644 index ede0ec7a3a..0000000000 --- a/dimos/e2e_tests/test_dimos_cli_e2e.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import pytest - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM spy doesn't work in CI.") -@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") -@pytest.mark.e2e -def test_dimos_skills(lcm_spy, start_blueprint, human_input) -> None: - lcm_spy.save_topic("/agent") - lcm_spy.save_topic("/rpc/Agent/on_system_modules/res") - lcm_spy.save_topic("/rpc/DemoCalculatorSkill/sum_numbers/req") - lcm_spy.save_topic("/rpc/DemoCalculatorSkill/sum_numbers/res") - - start_blueprint("run", "demo-skill") - - lcm_spy.wait_for_saved_topic("/rpc/Agent/on_system_modules/res") - - human_input("what is 52983 + 587237") - - lcm_spy.wait_for_saved_topic_content("/agent", b"640220") - - assert "/rpc/DemoCalculatorSkill/sum_numbers/req" in lcm_spy.messages - assert "/rpc/DemoCalculatorSkill/sum_numbers/res" in lcm_spy.messages diff --git a/dimos/e2e_tests/test_person_follow.py b/dimos/e2e_tests/test_person_follow.py deleted file mode 100644 index abb9cfb4fa..0000000000 --- a/dimos/e2e_tests/test_person_follow.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Generator -import os -import threading -import time - -import pytest - -from dimos.e2e_tests.dimos_cli_call import DimosCliCall -from dimos.e2e_tests.lcm_spy import LcmSpy -from dimos.simulation.mujoco.person_on_track import PersonTrackPublisher - -StartPersonTrack = Callable[[list[tuple[float, float]]], None] - - -@pytest.fixture -def start_person_track() -> Generator[StartPersonTrack, None, None]: - publisher: PersonTrackPublisher | None = None - stop_event = threading.Event() - thread: threading.Thread | None = None - - def start(track: list[tuple[float, float]]) -> None: - nonlocal publisher, thread - publisher = PersonTrackPublisher(track) - - def run_person_track() -> None: - while not stop_event.is_set(): - publisher.tick() - time.sleep(1 / 60) - - thread = threading.Thread(target=run_person_track, daemon=True) - thread.start() - - yield start - - stop_event.set() - if thread is not None: - thread.join(timeout=1.0) - if publisher is not None: - publisher.stop() - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM spy doesn't work in CI.") -@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") -@pytest.mark.mujoco -def test_person_follow( - lcm_spy: LcmSpy, - start_blueprint: Callable[[str], DimosCliCall], - human_input: Callable[[str], None], - start_person_track: StartPersonTrack, -) -> None: - start_blueprint("--mujoco-start-pos", "-6.18 0.96", "run", "unitree-go2-agentic") - - lcm_spy.save_topic("/rpc/Agent/on_system_modules/res") - lcm_spy.wait_for_saved_topic("/rpc/Agent/on_system_modules/res", timeout=120.0) - - time.sleep(5) - - start_person_track( - [ - (-2.60, 1.28), - (4.80, 0.21), - (4.14, -6.0), - (0.59, -3.79), - (-3.35, -0.51), - ] - ) - human_input("follow the person in beige pants") - - lcm_spy.wait_until_odom_position(4.2, -3, threshold=1.5) diff --git a/dimos/e2e_tests/test_simulation_module.py b/dimos/e2e_tests/test_simulation_module.py deleted file mode 100644 index 6c15f62056..0000000000 --- a/dimos/e2e_tests/test_simulation_module.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""End-to-end tests for the simulation module.""" - -import os - -import pytest - -from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState - - -def _positions_within_tolerance( - positions: list[float], - target: list[float], - tolerance: float, -) -> bool: - if len(positions) < len(target): - return False - return all(abs(positions[i] - target[i]) <= tolerance for i in range(len(target))) - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM doesn't work in CI.") -@pytest.mark.e2e -class TestSimulationModuleE2E: - def test_xarm7_joint_state_published(self, lcm_spy, start_blueprint) -> None: - joint_state_topic = "/xarm/joint_states#sensor_msgs.JointState" - lcm_spy.save_topic(joint_state_topic) - - start_blueprint("xarm7-trajectory-sim") - lcm_spy.wait_for_saved_topic(joint_state_topic, timeout=15.0) - - with lcm_spy._messages_lock: - raw_joint_state = lcm_spy.messages[joint_state_topic][0] - - joint_state = JointState.lcm_decode(raw_joint_state) - assert len(joint_state.name) == 8 - assert len(joint_state.position) == 8 - - def test_xarm7_robot_state_published(self, lcm_spy, start_blueprint) -> None: - robot_state_topic = "/xarm/robot_state#sensor_msgs.RobotState" - lcm_spy.save_topic(robot_state_topic) - - start_blueprint("xarm7-trajectory-sim") - lcm_spy.wait_for_saved_topic(robot_state_topic, timeout=15.0) - - with lcm_spy._messages_lock: - raw_robot_state = lcm_spy.messages[robot_state_topic][0] - - robot_state = RobotState.lcm_decode(raw_robot_state) - assert robot_state.mt_able in (0, 1) - - def test_xarm7_joint_command_updates_joint_state(self, lcm_spy, start_blueprint) -> None: - joint_state_topic = "/xarm/joint_states#sensor_msgs.JointState" - joint_command_topic = "/xarm/joint_position_command#sensor_msgs.JointCommand" - lcm_spy.save_topic(joint_state_topic) - - start_blueprint("xarm7-trajectory-sim") - lcm_spy.wait_for_saved_topic(joint_state_topic, timeout=15.0) - - target_positions = [0.2, -0.2, 0.1, -0.1, 0.15, -0.15, 0.05] - lcm_spy.publish(joint_command_topic, JointCommand(positions=target_positions)) - - tolerance = 0.03 - lcm_spy.wait_for_message_result( - joint_state_topic, - JointState, - predicate=lambda msg: _positions_within_tolerance( - list(msg.position), - target_positions, - tolerance, - ), - fail_message=("joint_state did not reach commanded positions within tolerance"), - timeout=10.0, - ) diff --git a/dimos/e2e_tests/test_spatial_memory.py b/dimos/e2e_tests/test_spatial_memory.py deleted file mode 100644 index 8b03a9915c..0000000000 --- a/dimos/e2e_tests/test_spatial_memory.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -import math -import os -import time - -import pytest - -from dimos.e2e_tests.dimos_cli_call import DimosCliCall -from dimos.e2e_tests.lcm_spy import LcmSpy - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM spy doesn't work in CI.") -@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") -@pytest.mark.mujoco -def test_spatial_memory_navigation( - lcm_spy: LcmSpy, - start_blueprint: Callable[[str], DimosCliCall], - human_input: Callable[[str], None], - follow_points: Callable[..., None], -) -> None: - start_blueprint("run", "unitree-go2-agentic") - - lcm_spy.save_topic("/rpc/Agent/on_system_modules/res") - lcm_spy.wait_for_saved_topic("/rpc/Agent/on_system_modules/res", timeout=120.0) - - time.sleep(5) - - follow_points( - points=[ - # Navigate to the bookcase. - (1, 1, 0), - (4, 1, 0), - (4.2, -1.1, -math.pi / 2), - (4.2, -3, -math.pi / 2), - (4.2, -5, -math.pi / 2), - # Move away, until it's not visible. - (1, 1, math.pi / 2), - ], - fail_message="Failed to get to the bookcase.", - ) - - time.sleep(5) - - human_input("go to the bookcase") - - lcm_spy.wait_until_odom_position(4.2, -5, threshold=2.0) diff --git a/dimos/environment/__init__.py b/dimos/environment/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/environment/environment.py b/dimos/environment/environment.py deleted file mode 100644 index ba1923b765..0000000000 --- a/dimos/environment/environment.py +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -import numpy as np - - -class Environment(ABC): - def __init__(self) -> None: - self.environment_type = None - self.graph = None - - @abstractmethod - def label_objects(self) -> list[str]: - """ - Label all objects in the environment. - - Returns: - A list of string labels representing the objects in the environment. - """ - pass - - @abstractmethod - def get_visualization(self, format_type): # type: ignore[no-untyped-def] - """Return different visualization formats like images, NERFs, or other 3D file types.""" - pass - - @abstractmethod - def generate_segmentations( # type: ignore[no-untyped-def] - self, model: str | None = None, objects: list[str] | None = None, *args, **kwargs - ) -> list[np.ndarray]: # type: ignore[type-arg] - """ - Generate object segmentations of objects[] using neural methods. - - Args: - model (str, optional): The string of the desired segmentation model (SegmentAnything, etc.) - objects (list[str], optional): The list of strings of the specific objects to segment. - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - - Returns: - list of numpy.ndarray: A list where each element is a numpy array - representing a binary mask for a segmented area of an object in the environment. - - Note: - The specific arguments and their usage will depend on the subclass implementation. - """ - pass - - @abstractmethod - def get_segmentations(self) -> list[np.ndarray]: # type: ignore[type-arg] - """ - Get segmentations using a method like 'segment anything'. - - Returns: - list of numpy.ndarray: A list where each element is a numpy array - representing a binary mask for a segmented area of an object in the environment. - """ - pass - - @abstractmethod - def generate_point_cloud(self, object: str | None = None, *args, **kwargs) -> np.ndarray: # type: ignore[no-untyped-def, type-arg] - """ - Generate a point cloud for the entire environment or a specific object. - - Args: - object (str, optional): The string of the specific object to get the point cloud for. - If None, returns the point cloud for the entire environment. - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - - Returns: - np.ndarray: A numpy array representing the generated point cloud. - Shape: (n, 3) where n is the number of points and each point is [x, y, z]. - - Note: - The specific arguments and their usage will depend on the subclass implementation. - """ - pass - - @abstractmethod - def get_point_cloud(self, object: str | None = None) -> np.ndarray: # type: ignore[type-arg] - """ - Return point clouds of the entire environment or a specific object. - - Args: - object (str, optional): The string of the specific object to get the point cloud for. If None, returns the point cloud for the entire environment. - - Returns: - np.ndarray: A numpy array representing the point cloud. - Shape: (n, 3) where n is the number of points and each point is [x, y, z]. - """ - pass - - @abstractmethod - def generate_depth_map( # type: ignore[no-untyped-def] - self, - stereo: bool | None = None, - monocular: bool | None = None, - model: str | None = None, - *args, - **kwargs, - ) -> np.ndarray: # type: ignore[type-arg] - """ - Generate a depth map using monocular or stereo camera methods. - - Args: - stereo (bool, optional): Whether to stereo camera is avaliable for ground truth depth map generation. - monocular (bool, optional): Whether to use monocular camera for neural depth map generation. - model (str, optional): The string of the desired monocular depth model (Metric3D, ZoeDepth, etc.) - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - - Returns: - np.ndarray: A 2D numpy array representing the generated depth map. - Shape: (height, width) where each value represents the depth - at that pixel location. - - Note: - The specific arguments and their usage will depend on the subclass implementation. - """ - pass - - @abstractmethod - def get_depth_map(self) -> np.ndarray: # type: ignore[type-arg] - """ - Return a depth map of the environment. - - Returns: - np.ndarray: A 2D numpy array representing the depth map. - Shape: (height, width) where each value represents the depth - at that pixel location. Typically, closer objects have smaller - values and farther objects have larger values. - - Note: - The exact range and units of the depth values may vary depending on the - specific implementation and the sensor or method used to generate the depth map. - """ - pass - - def initialize_from_images(self, images): # type: ignore[no-untyped-def] - """Initialize the environment from a set of image frames or video.""" - raise NotImplementedError("This method is not implemented for this environment type.") - - def initialize_from_file(self, file_path): # type: ignore[no-untyped-def] - """Initialize the environment from a spatial file type. - - Supported file types include: - - GLTF/GLB (GL Transmission Format) - - FBX (Filmbox) - - OBJ (Wavefront Object) - - USD/USDA/USDC (Universal Scene Description) - - STL (Stereolithography) - - COLLADA (DAE) - - Alembic (ABC) - - PLY (Polygon File Format) - - 3DS (3D Studio) - - VRML/X3D (Virtual Reality Modeling Language) - - Args: - file_path (str): Path to the spatial file. - - Raises: - NotImplementedError: If the method is not implemented for this environment type. - """ - raise NotImplementedError("This method is not implemented for this environment type.") diff --git a/dimos/exceptions/__init__.py b/dimos/exceptions/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/exceptions/agent_memory_exceptions.py b/dimos/exceptions/agent_memory_exceptions.py deleted file mode 100644 index eec80be83c..0000000000 --- a/dimos/exceptions/agent_memory_exceptions.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import traceback - - -class AgentMemoryError(Exception): - """ - Base class for all exceptions raised by AgentMemory operations. - All custom exceptions related to AgentMemory should inherit from this class. - - Args: - message (str): Human-readable message describing the error. - """ - - def __init__(self, message: str = "Error in AgentMemory operation") -> None: - super().__init__(message) - - -class AgentMemoryConnectionError(AgentMemoryError): - """ - Exception raised for errors attempting to connect to the database. - This includes failures due to network issues, authentication errors, or incorrect connection parameters. - - Args: - message (str): Human-readable message describing the error. - cause (Exception, optional): Original exception, if any, that led to this error. - """ - - def __init__(self, message: str = "Failed to connect to the database", cause=None) -> None: # type: ignore[no-untyped-def] - super().__init__(message) - if cause: - self.cause = cause - self.traceback = traceback.format_exc() if cause else None - - def __str__(self) -> str: - return f"{self.message}\nCaused by: {self.cause!r}" if self.cause else self.message # type: ignore[attr-defined] - - -class UnknownConnectionTypeError(AgentMemoryConnectionError): - """ - Exception raised when an unknown or unsupported connection type is specified during AgentMemory setup. - - Args: - message (str): Human-readable message explaining that an unknown connection type was used. - """ - - def __init__( - self, message: str = "Unknown connection type used in AgentMemory connection" - ) -> None: - super().__init__(message) - - -class DataRetrievalError(AgentMemoryError): - """ - Exception raised for errors retrieving data from the database. - This could occur due to query failures, timeouts, or corrupt data issues. - - Args: - message (str): Human-readable message describing the data retrieval error. - """ - - def __init__( - self, message: str = "Error in retrieving data during AgentMemory operation" - ) -> None: - super().__init__(message) - - -class DataNotFoundError(DataRetrievalError): - """ - Exception raised when the requested data is not found in the database. - This is used when a query completes successfully but returns no result for the specified identifier. - - Args: - vector_id (int or str): The identifier for the vector that was not found. - message (str, optional): Human-readable message providing more detail. If not provided, a default message is generated. - """ - - def __init__(self, vector_id, message=None) -> None: # type: ignore[no-untyped-def] - message = message or f"Requested data for vector ID {vector_id} was not found." - super().__init__(message) - self.vector_id = vector_id diff --git a/dimos/hardware/__init__.py b/dimos/hardware/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/hardware/end_effectors/__init__.py b/dimos/hardware/end_effectors/__init__.py deleted file mode 100644 index 9a7aa9759a..0000000000 --- a/dimos/hardware/end_effectors/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .end_effector import EndEffector - -__all__ = ["EndEffector"] diff --git a/dimos/hardware/end_effectors/end_effector.py b/dimos/hardware/end_effectors/end_effector.py deleted file mode 100644 index e958261b91..0000000000 --- a/dimos/hardware/end_effectors/end_effector.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class EndEffector: - def __init__(self, effector_type=None) -> None: # type: ignore[no-untyped-def] - self.effector_type = effector_type - - def get_effector_type(self): # type: ignore[no-untyped-def] - return self.effector_type diff --git a/dimos/hardware/manipulators/README.md b/dimos/hardware/manipulators/README.md deleted file mode 100644 index 60d3c94567..0000000000 --- a/dimos/hardware/manipulators/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Manipulator Drivers - -This module provides manipulator arm drivers: Protocol-only with injectable adapters. - -## Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Driver (Module) │ -│ - Owns threading (control loop, monitor loop) │ -│ - Publishes joint_state, robot_state │ -│ - Subscribes to joint_position_command, joint_velocity_cmd │ -│ - Exposes RPC methods (move_joint, enable_servos, etc.) │ -└─────────────────────┬───────────────────────────────────────┘ - │ uses -┌─────────────────────▼───────────────────────────────────────┐ -│ Adapter (implements Protocol) │ -│ - Handles SDK communication │ -│ - Unit conversions (radians ↔ vendor units) │ -│ - Swappable: XArmAdapter, PiperAdapter, MockAdapter │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Key Benefits - -- **Testable**: Inject `MockAdapter` for unit tests without hardware -- **Flexible**: Each arm controls its own threading/timing -- **Simple**: No ABC inheritance required - just implement the Protocol -- **Type-safe**: Full type checking via `ManipulatorAdapter` Protocol - -## Directory Structure - -``` -manipulators/ -├── spec.py # ManipulatorAdapter Protocol + shared types -├── registry.py # Adapter registry with auto-discovery -├── mock/ -│ └── adapter.py # MockAdapter for testing -├── xarm/ -│ ├── adapter.py # XArmAdapter (SDK wrapper) -└── piper/ - ├── adapter.py # PiperAdapter (SDK wrapper) -``` - -## Quick Start - -### Using a Driver Directly - -```python -from dimos.hardware.manipulators.xarm import XArm - -arm = XArm(ip="192.168.1.185", dof=6) -arm.start() -arm.enable_servos() -arm.move_joint([0, 0, 0, 0, 0, 0]) -arm.stop() -``` - -### Using Blueprints - -```python -from dimos.hardware.manipulators.xarm.blueprints import xarm_trajectory - -coordinator = xarm_trajectory.build() -coordinator.loop() -``` - -### Testing Without Hardware - -```python -from dimos.hardware.manipulators.mock import MockAdapter -from dimos.hardware.manipulators.xarm import XArm - -arm = XArm(adapter=MockAdapter(dof=6)) -arm.start() # No hardware needed! -arm.move_joint([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) -``` - -## Adding a New Arm - -1. **Create the adapter** (`adapter.py`): - -```python -class MyArmAdapter: # No inheritance needed - just match the Protocol - def __init__(self, ip: str = "192.168.1.100", dof: int = 6) -> None: - self._ip = ip - self._dof = dof - - def connect(self) -> bool: ... - def disconnect(self) -> None: ... - def read_joint_positions(self) -> list[float]: ... - def write_joint_positions(self, positions: list[float], velocity: float = 1.0) -> bool: ... - # ... implement other Protocol methods -``` - -2. **Create the driver** (`arm.py`): - -```python -from dimos.core import Module, ModuleConfig, In, Out, rpc -from .adapter import MyArmAdapter - -class MyArm(Module[MyArmConfig]): - joint_state: Out[JointState] - robot_state: Out[RobotState] - joint_position_command: In[JointCommand] - - def __init__(self, adapter=None, **kwargs): - super().__init__(**kwargs) - self.adapter = adapter or MyArmAdapter( - ip=self.config.ip, - dof=self.config.dof, - ) - # ... setup control loops -``` - -3. **Create blueprints** (`blueprints.py`) for common configurations. - -## ManipulatorAdapter Protocol - -All adapters must implement these core methods: - -| Category | Methods | -|----------|---------| -| Connection | `connect()`, `disconnect()`, `is_connected()` | -| Info | `get_info()`, `get_dof()`, `get_limits()` | -| State | `read_joint_positions()`, `read_joint_velocities()`, `read_joint_efforts()` | -| Motion | `write_joint_positions()`, `write_joint_velocities()`, `write_stop()` | -| Servo | `write_enable()`, `read_enabled()`, `write_clear_errors()` | -| Mode | `set_control_mode()`, `get_control_mode()` | - -Optional methods (return `None`/`False` if unsupported): -- `read_cartesian_position()`, `write_cartesian_position()` -- `read_gripper_position()`, `write_gripper_position()` -- `read_force_torque()` - -## Unit Conventions - -All adapters convert to/from SI units: - -| Quantity | Unit | -|----------|------| -| Angles | radians | -| Angular velocity | rad/s | -| Torque | Nm | -| Position | meters | -| Force | Newtons | diff --git a/dimos/hardware/manipulators/__init__.py b/dimos/hardware/manipulators/__init__.py deleted file mode 100644 index 58986c9211..0000000000 --- a/dimos/hardware/manipulators/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Manipulator drivers for robotic arms. - -Architecture: Protocol-based adapters for different manipulator hardware. -- spec.py: ManipulatorAdapter Protocol and shared types -- xarm/: XArm adapter -- piper/: Piper adapter -- mock/: Mock adapter for testing - -Usage: - >>> from dimos.hardware.manipulators.xarm import XArm - >>> arm = XArm(ip="192.168.1.185") - >>> arm.start() - >>> arm.enable_servos() - >>> arm.move_joint([0, 0, 0, 0, 0, 0]) - -Testing: - >>> from dimos.hardware.manipulators.xarm import XArm - >>> from dimos.hardware.manipulators.mock import MockAdapter - >>> arm = XArm(adapter=MockAdapter()) - >>> arm.start() # No hardware needed! -""" - -from dimos.hardware.manipulators.spec import ( - ControlMode, - DriverStatus, - JointLimits, - ManipulatorAdapter, - ManipulatorInfo, -) - -__all__ = [ - "ControlMode", - "DriverStatus", - "JointLimits", - "ManipulatorAdapter", - "ManipulatorInfo", -] diff --git a/dimos/hardware/manipulators/mock/__init__.py b/dimos/hardware/manipulators/mock/__init__.py deleted file mode 100644 index 63be6f7e98..0000000000 --- a/dimos/hardware/manipulators/mock/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Mock adapter for testing manipulator drivers without hardware. - -Usage: - >>> from dimos.hardware.manipulators.xarm import XArm - >>> from dimos.hardware.manipulators.mock import MockAdapter - >>> arm = XArm(adapter=MockAdapter()) - >>> arm.start() # No hardware needed! - >>> arm.move_joint([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) - >>> assert arm.adapter.read_joint_positions() == [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] -""" - -from dimos.hardware.manipulators.mock.adapter import MockAdapter - -__all__ = ["MockAdapter"] diff --git a/dimos/hardware/manipulators/mock/adapter.py b/dimos/hardware/manipulators/mock/adapter.py deleted file mode 100644 index ff299669f7..0000000000 --- a/dimos/hardware/manipulators/mock/adapter.py +++ /dev/null @@ -1,261 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Mock adapter for testing - no hardware required. - -Usage: - >>> from dimos.hardware.manipulators.xarm import XArm - >>> from dimos.hardware.manipulators.mock import MockAdapter - >>> arm = XArm(adapter=MockAdapter()) - >>> arm.start() # No hardware! -""" - -from __future__ import annotations - -import math -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.hardware.manipulators.registry import AdapterRegistry - -from dimos.hardware.manipulators.spec import ( - ControlMode, - JointLimits, - ManipulatorInfo, -) - - -class MockAdapter: - """Fake adapter for unit tests. - - Implements ManipulatorAdapter protocol with in-memory state. - Useful for: - - Unit testing driver logic without hardware - - Integration testing with predictable behavior - - Development without physical robot - """ - - def __init__(self, dof: int = 6, **_: object) -> None: - self._dof = dof - self._positions = [0.0] * dof - self._velocities = [0.0] * dof - self._efforts = [0.0] * dof - self._enabled = False - self._connected = False - self._control_mode = ControlMode.POSITION - self._cartesian_position: dict[str, float] = { - "x": 0.3, - "y": 0.0, - "z": 0.3, - "roll": 0.0, - "pitch": 0.0, - "yaw": 0.0, - } - self._gripper_position: float = 0.0 - self._error_code: int = 0 - self._error_message: str = "" - - # ========================================================================= - # Connection - # ========================================================================= - - def connect(self) -> bool: - """Simulate connection.""" - self._connected = True - return True - - def disconnect(self) -> None: - """Simulate disconnection.""" - self._connected = False - - def is_connected(self) -> bool: - """Check mock connection status.""" - return self._connected - - # ========================================================================= - # Info - # ========================================================================= - - def get_info(self) -> ManipulatorInfo: - """Return mock info.""" - return ManipulatorInfo( - vendor="Mock", - model="MockArm", - dof=self._dof, - firmware_version="1.0.0", - serial_number="MOCK-001", - ) - - def get_dof(self) -> int: - """Return DOF.""" - return self._dof - - def get_limits(self) -> JointLimits: - """Return mock joint limits.""" - return JointLimits( - position_lower=[-math.pi] * self._dof, - position_upper=[math.pi] * self._dof, - velocity_max=[1.0] * self._dof, - ) - - # ========================================================================= - # Control Mode - # ========================================================================= - - def set_control_mode(self, mode: ControlMode) -> bool: - """Set mock control mode.""" - self._control_mode = mode - return True - - def get_control_mode(self) -> ControlMode: - """Get mock control mode.""" - return self._control_mode - - # ========================================================================= - # State Reading - # ========================================================================= - - def read_joint_positions(self) -> list[float]: - """Return mock joint positions.""" - return self._positions.copy() - - def read_joint_velocities(self) -> list[float]: - """Return mock joint velocities.""" - return self._velocities.copy() - - def read_joint_efforts(self) -> list[float]: - """Return mock joint efforts.""" - return self._efforts.copy() - - def read_state(self) -> dict[str, int]: - """Return mock state.""" - # Use index of control mode as int (0=position, 1=velocity, etc.) - mode_int = list(ControlMode).index(self._control_mode) - return { - "state": 0 if self._enabled else 1, - "mode": mode_int, - } - - def read_error(self) -> tuple[int, str]: - """Return mock error.""" - return self._error_code, self._error_message - - # ========================================================================= - # Motion Control - # ========================================================================= - - def write_joint_positions( - self, - positions: list[float], - velocity: float = 1.0, - ) -> bool: - """Set mock joint positions (instant move).""" - if len(positions) != self._dof: - return False - self._positions = list(positions) - return True - - def write_joint_velocities(self, velocities: list[float]) -> bool: - """Set mock joint velocities.""" - if len(velocities) != self._dof: - return False - self._velocities = list(velocities) - return True - - def write_stop(self) -> bool: - """Stop mock motion.""" - self._velocities = [0.0] * self._dof - return True - - # ========================================================================= - # Servo Control - # ========================================================================= - - def write_enable(self, enable: bool) -> bool: - """Enable/disable mock servos.""" - self._enabled = enable - return True - - def read_enabled(self) -> bool: - """Check mock servo state.""" - return self._enabled - - def write_clear_errors(self) -> bool: - """Clear mock errors.""" - self._error_code = 0 - self._error_message = "" - return True - - # ========================================================================= - # Cartesian Control (Optional) - # ========================================================================= - - def read_cartesian_position(self) -> dict[str, float] | None: - """Return mock cartesian position.""" - return self._cartesian_position.copy() - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - """Set mock cartesian position.""" - self._cartesian_position.update(pose) - return True - - # ========================================================================= - # Gripper (Optional) - # ========================================================================= - - def read_gripper_position(self) -> float | None: - """Return mock gripper position.""" - return self._gripper_position - - def write_gripper_position(self, position: float) -> bool: - """Set mock gripper position.""" - self._gripper_position = position - return True - - # ========================================================================= - # Force/Torque (Optional) - # ========================================================================= - - def read_force_torque(self) -> list[float] | None: - """Return mock F/T sensor data (not supported in mock).""" - return None - - # ========================================================================= - # Test Helpers (not part of Protocol) - # ========================================================================= - - def set_error(self, code: int, message: str) -> None: - """Inject an error for testing error handling.""" - self._error_code = code - self._error_message = message - - def set_positions(self, positions: list[float]) -> None: - """Set positions directly for testing.""" - self._positions = list(positions) - - def set_efforts(self, efforts: list[float]) -> None: - """Set efforts directly for testing.""" - self._efforts = list(efforts) - - -def register(registry: AdapterRegistry) -> None: - """Register this adapter with the registry.""" - registry.register("mock", MockAdapter) - - -__all__ = ["MockAdapter"] diff --git a/dimos/hardware/manipulators/piper/__init__.py b/dimos/hardware/manipulators/piper/__init__.py deleted file mode 100644 index bfeb89b1c0..0000000000 --- a/dimos/hardware/manipulators/piper/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Piper manipulator hardware adapter. - -Usage: - >>> from dimos.hardware.manipulators.piper import PiperAdapter - >>> adapter = PiperAdapter(can_port="can0") - >>> adapter.connect() - >>> positions = adapter.read_joint_positions() -""" - -from dimos.hardware.manipulators.piper.adapter import PiperAdapter - -__all__ = ["PiperAdapter"] diff --git a/dimos/hardware/manipulators/piper/adapter.py b/dimos/hardware/manipulators/piper/adapter.py deleted file mode 100644 index 68b5769a95..0000000000 --- a/dimos/hardware/manipulators/piper/adapter.py +++ /dev/null @@ -1,528 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Piper adapter - implements ManipulatorAdapter protocol. - -SDK Units: angles=0.001 degrees (millidegrees), distance=mm -DimOS Units: angles=radians, distance=meters -""" - -from __future__ import annotations - -import math -import time -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from dimos.hardware.manipulators.registry import AdapterRegistry - -from dimos.hardware.manipulators.spec import ( - ControlMode, - JointLimits, - ManipulatorAdapter, - ManipulatorInfo, -) - -# Unit conversion constants -# Piper uses 0.001 degrees (millidegrees) for angles -RAD_TO_MILLIDEG = 57295.7795 # radians -> millidegrees -MILLIDEG_TO_RAD = 1.0 / RAD_TO_MILLIDEG # millidegrees -> radians -MM_TO_M = 0.001 # mm -> meters - -# Hardware specs -GRIPPER_MAX_OPENING_M = 0.08 # Max gripper opening in meters - -# Default configurable parameters -DEFAULT_GRIPPER_SPEED = 1000 - - -class PiperAdapter(ManipulatorAdapter): - """Piper-specific adapter. - - Implements ManipulatorAdapter protocol via duck typing. - No inheritance required - just matching method signatures. - - Unit conversions: - - Angles: Piper uses 0.001 degrees, we use radians - - Velocities: Piper uses internal units, we use rad/s - """ - - def __init__( - self, - address: str = "can0", - dof: int = 6, - gripper_speed: int = DEFAULT_GRIPPER_SPEED, - **_: object, - ) -> None: - if dof != 6: - raise ValueError(f"PiperAdapter only supports 6 DOF (got {dof})") - self._can_port = address - self._dof = dof - self._gripper_speed = gripper_speed - self._sdk: Any = None - self._connected: bool = False - self._enabled: bool = False - self._control_mode: ControlMode = ControlMode.POSITION - - # ========================================================================= - # Connection - # ========================================================================= - - def connect(self) -> bool: - """Connect to Piper via CAN bus.""" - try: - from piper_sdk import C_PiperInterface_V2 - - self._sdk = C_PiperInterface_V2( - can_name=self._can_port, - judge_flag=True, # Enable safety checks - can_auto_init=True, # Let SDK handle CAN initialization - dh_is_offset=False, - ) - - # Connect to CAN port - self._sdk.ConnectPort(piper_init=True, start_thread=True) - - # Wait for initialization - time.sleep(0.025) - - # Check connection by trying to get status - status = self._sdk.GetArmStatus() - if status is not None: - self._connected = True - print(f"Piper connected via CAN port {self._can_port}") - return True - else: - print(f"ERROR: Failed to connect to Piper on {self._can_port} - no status received") - return False - - except ImportError: - print("ERROR: Piper SDK not installed. Please install piper_sdk") - return False - except Exception as e: - print(f"ERROR: Failed to connect to Piper on {self._can_port}: {e}") - return False - - def disconnect(self) -> None: - """Disconnect from Piper.""" - if self._sdk: - try: - if self._enabled: - self._sdk.DisablePiper() - self._enabled = False - self._sdk.DisconnectPort() - except Exception: - pass - finally: - self._sdk = None - self._connected = False - - def is_connected(self) -> bool: - """Check if connected to Piper.""" - if not self._connected or not self._sdk: - return False - - try: - status = self._sdk.GetArmStatus() - return status is not None - except Exception: - return False - - # ========================================================================= - # Info - # ========================================================================= - - def get_info(self) -> ManipulatorInfo: - """Get Piper information.""" - firmware_version = None - if self._sdk: - try: - firmware_version = self._sdk.GetPiperFirmwareVersion() - except Exception: - pass - - return ManipulatorInfo( - vendor="Agilex", - model="Piper", - dof=self._dof, - firmware_version=firmware_version, - ) - - def get_dof(self) -> int: - """Get degrees of freedom.""" - return self._dof - - def get_limits(self) -> JointLimits: - """Get joint limits.""" - # Piper joint limits (approximate, in radians) - lower = [-3.14, -2.35, -2.35, -3.14, -2.35, -3.14] - upper = [3.14, 2.35, 2.35, 3.14, 2.35, 3.14] - max_vel = [math.pi] * self._dof # ~180 deg/s - - return JointLimits( - position_lower=lower, - position_upper=upper, - velocity_max=max_vel, - ) - - # ========================================================================= - # Control Mode - # ========================================================================= - - def set_control_mode(self, mode: ControlMode) -> bool: - """Set Piper control mode via MotionCtrl_2.""" - if not self._sdk: - return False - - # Piper move modes: 0x01=position, 0x02=velocity - # SERVO_POSITION uses position mode for high-freq streaming - move_mode = 0x01 # Default position mode - if mode == ControlMode.VELOCITY: - move_mode = 0x02 - - try: - self._sdk.MotionCtrl_2( - ctrl_mode=0x01, # CAN control mode - move_mode=move_mode, - move_spd_rate_ctrl=50, # Speed rate (0-100) - is_mit_mode=0x00, # Not MIT mode - ) - self._control_mode = mode - return True - except Exception: - return False - - def get_control_mode(self) -> ControlMode: - """Get current control mode.""" - return self._control_mode - - # ========================================================================= - # State Reading - # ========================================================================= - - def read_joint_positions(self) -> list[float]: - """Read joint positions (Piper units -> radians).""" - if not self._sdk: - raise RuntimeError("Not connected") - - joint_msgs = self._sdk.GetArmJointMsgs() - if not joint_msgs or not joint_msgs.joint_state: - raise RuntimeError("Failed to read joint positions") - - js = joint_msgs.joint_state - return [ - js.joint_1 * MILLIDEG_TO_RAD, - js.joint_2 * MILLIDEG_TO_RAD, - js.joint_3 * MILLIDEG_TO_RAD, - js.joint_4 * MILLIDEG_TO_RAD, - js.joint_5 * MILLIDEG_TO_RAD, - js.joint_6 * MILLIDEG_TO_RAD, - ] - - def read_joint_velocities(self) -> list[float]: - """Read joint velocities. - - Note: Piper doesn't provide real-time velocity feedback. - Returns zeros. For velocity estimation, use finite differences. - """ - return [0.0] * self._dof - - def read_joint_efforts(self) -> list[float]: - """Read joint efforts/torques. - - Note: Piper doesn't provide torque feedback by default. - """ - return [0.0] * self._dof - - def read_state(self) -> dict[str, int]: - """Read robot state.""" - if not self._sdk: - return {"state": 0, "mode": 0} - - try: - status = self._sdk.GetArmStatus() - if status and status.arm_status: - arm_status = status.arm_status - error_code = getattr(arm_status, "err_code", 0) - state = 2 if error_code != 0 else 0 # 2=error, 0=idle - return { - "state": state, - "mode": 0, # Piper doesn't expose mode - "error_code": error_code, - } - except Exception: - pass - - return {"state": 0, "mode": 0} - - def read_error(self) -> tuple[int, str]: - """Read error code and message.""" - if not self._sdk: - return 0, "" - - try: - status = self._sdk.GetArmStatus() - if status and status.arm_status: - error_code = getattr(status.arm_status, "err_code", 0) - if error_code == 0: - return 0, "" - - # Piper error codes - error_map = { - 1: "Communication error", - 2: "Motor error", - 3: "Encoder error", - 4: "Overtemperature", - 5: "Overcurrent", - 6: "Joint limit error", - 7: "Emergency stop", - 8: "Power error", - } - return error_code, error_map.get(error_code, f"Unknown error {error_code}") - except Exception: - pass - - return 0, "" - - # ========================================================================= - # Motion Control (Joint Space) - # ========================================================================= - - def write_joint_positions( - self, - positions: list[float], - velocity: float = 1.0, - ) -> bool: - """Write joint positions (radians -> Piper units). - - Args: - positions: Target positions in radians - velocity: Speed as fraction of max (0-1) - """ - if not self._sdk: - return False - - # Convert radians to Piper units (0.001 degrees) - piper_joints = [round(rad * RAD_TO_MILLIDEG) for rad in positions] - - # Set speed rate if not full speed - if velocity < 1.0: - speed_rate = int(velocity * 100) - try: - self._sdk.MotionCtrl_2( - ctrl_mode=0x01, - move_mode=0x01, - move_spd_rate_ctrl=speed_rate, - is_mit_mode=0x00, - ) - except Exception: - pass - - try: - self._sdk.JointCtrl( - piper_joints[0], - piper_joints[1], - piper_joints[2], - piper_joints[3], - piper_joints[4], - piper_joints[5], - ) - return True - except Exception as e: - print(f"Piper joint control error: {e}") - return False - - def write_joint_velocities(self, velocities: list[float]) -> bool: - """Write joint velocities. - - Note: Piper doesn't have native velocity control at SDK level. - Returns False - the driver should implement this via position integration. - """ - return False - - def write_stop(self) -> bool: - """Emergency stop.""" - if not self._sdk: - return False - - try: - if hasattr(self._sdk, "EmergencyStop"): - self._sdk.EmergencyStop() - return True - except Exception: - pass - - # Fallback: disable arm - return self.write_enable(False) - - # ========================================================================= - # Servo Control - # ========================================================================= - - def write_enable(self, enable: bool) -> bool: - """Enable or disable servos.""" - if not self._sdk: - return False - - try: - if enable: - # Enable with retries (500ms max) - attempts = 0 - max_attempts = 50 - success = False - while attempts < max_attempts: - if self._sdk.EnablePiper(): - success = True - break - time.sleep(0.01) - attempts += 1 - - if success: - self._enabled = True - # Set control mode - self._sdk.MotionCtrl_2( - ctrl_mode=0x01, - move_mode=0x01, - move_spd_rate_ctrl=30, - is_mit_mode=0x00, - ) - return True - return False - else: - self._sdk.DisablePiper() - self._enabled = False - return True - except Exception: - return False - - def read_enabled(self) -> bool: - """Check if servos are enabled.""" - return self._enabled - - def write_clear_errors(self) -> bool: - """Clear error state.""" - if not self._sdk: - return False - - try: - if hasattr(self._sdk, "ClearError"): - self._sdk.ClearError() - return True - except Exception: - pass - - # Alternative: disable and re-enable - self.write_enable(False) - time.sleep(0.1) - return self.write_enable(True) - - # ========================================================================= - # Cartesian Control (Optional) - # ========================================================================= - - def read_cartesian_position(self) -> dict[str, float] | None: - """Read end-effector pose. - - Note: Piper may not support direct cartesian feedback. - Returns None if not available. - """ - if not self._sdk: - return None - - try: - if hasattr(self._sdk, "GetArmEndPoseMsgs"): - pose_msgs = self._sdk.GetArmEndPoseMsgs() - if pose_msgs and pose_msgs.end_pose: - ep = pose_msgs.end_pose - return { - "x": ep.X_axis * MM_TO_M, - "y": ep.Y_axis * MM_TO_M, - "z": ep.Z_axis * MM_TO_M, - "roll": ep.RX_axis * MILLIDEG_TO_RAD, - "pitch": ep.RY_axis * MILLIDEG_TO_RAD, - "yaw": ep.RZ_axis * MILLIDEG_TO_RAD, - } - except Exception: - pass - - return None - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - """Write end-effector pose. - - Note: Piper may not support direct cartesian control. - """ - # Cartesian control not commonly supported in Piper SDK - return False - - # ========================================================================= - # Gripper (Optional) - # ========================================================================= - - def read_gripper_position(self) -> float | None: - """Read gripper position (percentage -> meters).""" - if not self._sdk: - return None - - try: - if hasattr(self._sdk, "GetArmGripperMsgs"): - gripper_msgs = self._sdk.GetArmGripperMsgs() - if gripper_msgs and gripper_msgs.gripper_state: - # Piper gripper position is 0-100 percentage - pos: float = gripper_msgs.gripper_state.grippers_angle - return (pos / 100.0) * GRIPPER_MAX_OPENING_M - except Exception: - pass - - return None - - def write_gripper_position(self, position: float) -> bool: - """Write gripper position (meters -> percentage).""" - if not self._sdk: - return False - - try: - if hasattr(self._sdk, "GripperCtrl"): - # Convert meters to percentage (0-100) - percentage = int((position / GRIPPER_MAX_OPENING_M) * 100) - percentage = max(0, min(100, percentage)) - self._sdk.GripperCtrl(percentage, self._gripper_speed, 0x01, 0) - return True - except Exception: - pass - - return False - - # ========================================================================= - # Force/Torque Sensor (Optional) - # ========================================================================= - - def read_force_torque(self) -> list[float] | None: - """Read F/T sensor data. - - Note: Piper doesn't typically have F/T sensor. - """ - return None - - -def register(registry: AdapterRegistry) -> None: - """Register this adapter with the registry.""" - registry.register("piper", PiperAdapter) - - -__all__ = ["PiperAdapter"] diff --git a/dimos/hardware/manipulators/registry.py b/dimos/hardware/manipulators/registry.py deleted file mode 100644 index 65dbe74b50..0000000000 --- a/dimos/hardware/manipulators/registry.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Adapter registry with auto-discovery. - -Automatically discovers and registers manipulator adapters from subpackages. -Each adapter provides a `register()` function in its adapter.py module. - -Usage: - from dimos.hardware.manipulators.registry import adapter_registry - - # Create an adapter by name - adapter = adapter_registry.create("xarm", ip="192.168.1.185", dof=6) - adapter = adapter_registry.create("piper", can_port="can0", dof=6) - adapter = adapter_registry.create("mock", dof=7) - - # List available adapters - print(adapter_registry.available()) # ["mock", "piper", "xarm"] -""" - -from __future__ import annotations - -import importlib -import logging -import pkgutil -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from dimos.hardware.manipulators.spec import ManipulatorAdapter - -logger = logging.getLogger(__name__) - - -class AdapterRegistry: - """Registry for manipulator adapters with auto-discovery.""" - - def __init__(self) -> None: - self._adapters: dict[str, type[ManipulatorAdapter]] = {} - - def register(self, name: str, cls: type[ManipulatorAdapter]) -> None: - """Register an adapter class.""" - self._adapters[name.lower()] = cls - - def create(self, name: str, **kwargs: Any) -> ManipulatorAdapter: - """Create an adapter instance by name. - - Args: - name: Adapter name (e.g., "xarm", "piper", "mock") - **kwargs: Arguments passed to adapter constructor - - Returns: - Configured adapter instance - - Raises: - KeyError: If adapter name is not found - """ - key = name.lower() - if key not in self._adapters: - raise KeyError(f"Unknown adapter: {name}. Available: {self.available()}") - - return self._adapters[key](**kwargs) - - def available(self) -> list[str]: - """List available adapter names.""" - return sorted(self._adapters.keys()) - - def discover(self) -> None: - """Discover and register adapters from subpackages. - - Can be called multiple times to pick up newly added adapters. - """ - import dimos.hardware.manipulators as pkg - - for _, name, ispkg in pkgutil.iter_modules(pkg.__path__): - if not ispkg: - continue - try: - module = importlib.import_module(f"dimos.hardware.manipulators.{name}.adapter") - if hasattr(module, "register"): - module.register(self) - except ImportError as e: - logger.debug(f"Skipping adapter {name}: {e}") - - -adapter_registry = AdapterRegistry() -adapter_registry.discover() - -__all__ = ["AdapterRegistry", "adapter_registry"] diff --git a/dimos/hardware/manipulators/spec.py b/dimos/hardware/manipulators/spec.py deleted file mode 100644 index ff4d38c54f..0000000000 --- a/dimos/hardware/manipulators/spec.py +++ /dev/null @@ -1,261 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Manipulator specifications: Protocol and shared types. - -This file defines: -1. Shared enums and dataclasses used by all arms -2. ManipulatorAdapter Protocol that adapters must implement - -Note: No ABC for drivers. Each arm implements its own driver -with full control over threading and logic. -""" - -from dataclasses import dataclass -from enum import Enum -from typing import Protocol, runtime_checkable - -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 - -# ============================================================================ -# SHARED TYPES -# ============================================================================ - - -class DriverStatus(Enum): - """Status returned by driver operations.""" - - DISCONNECTED = "disconnected" - CONNECTED = "connected" - ENABLED = "enabled" - MOVING = "moving" - ERROR = "error" - - -class ControlMode(Enum): - """Control modes for manipulator.""" - - POSITION = "position" # Planned position control (slower, smoother) - SERVO_POSITION = "servo_position" # High-freq joint position streaming (100Hz+) - VELOCITY = "velocity" - TORQUE = "torque" - CARTESIAN = "cartesian" - CARTESIAN_VELOCITY = "cartesian_velocity" - IMPEDANCE = "impedance" - - -@dataclass -class ManipulatorInfo: - """Information about the manipulator.""" - - vendor: str - model: str - dof: int - firmware_version: str | None = None - serial_number: str | None = None - - -@dataclass -class JointLimits: - """Joint position and velocity limits.""" - - position_lower: list[float] # radians - position_upper: list[float] # radians - velocity_max: list[float] # rad/s - - -def default_base_transform() -> Transform: - """Default identity transform for arm mounting.""" - return Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - - -# ============================================================================ -# ADAPTER PROTOCOL -# ============================================================================ - - -@runtime_checkable -class ManipulatorAdapter(Protocol): - """Protocol for hardware-specific IO. - - Implement this per vendor SDK. All methods use SI units: - - Angles: radians - - Angular velocity: rad/s - - Torque: Nm - - Position: meters - - Force: Newtons - """ - - # --- Connection --- - - def connect(self) -> bool: - """Connect to hardware. Returns True on success.""" - ... - - def disconnect(self) -> None: - """Disconnect from hardware.""" - ... - - def is_connected(self) -> bool: - """Check if connected.""" - ... - - # --- Info --- - - def get_info(self) -> ManipulatorInfo: - """Get manipulator info (vendor, model, DOF).""" - ... - - def get_dof(self) -> int: - """Get degrees of freedom.""" - ... - - def get_limits(self) -> JointLimits: - """Get joint limits.""" - ... - - # --- Control Mode --- - - def set_control_mode(self, mode: ControlMode) -> bool: - """Set control mode (position, velocity, torque, cartesian, etc). - - Args: - mode: Target control mode - - Returns: - True if mode switch successful, False otherwise - - Note: Some arms (like XArm) may accept commands in any mode, - while others (like Piper) require explicit mode switching. - """ - ... - - def get_control_mode(self) -> ControlMode: - """Get current control mode. - - Returns: - Current control mode - """ - ... - - # --- State Reading --- - - def read_joint_positions(self) -> list[float]: - """Read current joint positions (radians).""" - ... - - def read_joint_velocities(self) -> list[float]: - """Read current joint velocities (rad/s).""" - ... - - def read_joint_efforts(self) -> list[float]: - """Read current joint efforts (Nm).""" - ... - - def read_state(self) -> dict[str, int]: - """Read robot state (mode, state code, etc).""" - ... - - def read_error(self) -> tuple[int, str]: - """Read error code and message. (0, '') means no error.""" - ... - - # --- Motion Control (Joint Space) --- - - def write_joint_positions( - self, - positions: list[float], - velocity: float = 1.0, - ) -> bool: - """Command joint positions (radians). Returns success.""" - ... - - def write_joint_velocities(self, velocities: list[float]) -> bool: - """Command joint velocities (rad/s). Returns success.""" - ... - - def write_stop(self) -> bool: - """Stop all motion immediately.""" - ... - - # --- Servo Control --- - - def write_enable(self, enable: bool) -> bool: - """Enable or disable servos. Returns success.""" - ... - - def read_enabled(self) -> bool: - """Check if servos are enabled.""" - ... - - def write_clear_errors(self) -> bool: - """Clear error state. Returns success.""" - ... - - # --- Optional: Cartesian Control --- - # Return None/False if not supported - - def read_cartesian_position(self) -> dict[str, float] | None: - """Read end-effector pose. - - Returns: - Dict with keys: x, y, z (meters), roll, pitch, yaw (radians) - None if not supported - """ - ... - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - """Command end-effector pose. - - Args: - pose: Dict with keys: x, y, z (meters), roll, pitch, yaw (radians) - velocity: Speed as fraction of max (0-1) - - Returns: - True if command accepted, False if not supported - """ - ... - - # --- Optional: Gripper --- - - def read_gripper_position(self) -> float | None: - """Read gripper position (meters). None if no gripper.""" - ... - - def write_gripper_position(self, position: float) -> bool: - """Command gripper position. False if no gripper.""" - ... - - # --- Optional: Force/Torque Sensor --- - - def read_force_torque(self) -> list[float] | None: - """Read F/T sensor [fx, fy, fz, tx, ty, tz]. None if no sensor.""" - ... - - -__all__ = [ - "ControlMode", - "DriverStatus", - "JointLimits", - "ManipulatorAdapter", - "ManipulatorInfo", - "default_base_transform", -] diff --git a/dimos/hardware/manipulators/xarm/__init__.py b/dimos/hardware/manipulators/xarm/__init__.py deleted file mode 100644 index 8bcab667c1..0000000000 --- a/dimos/hardware/manipulators/xarm/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""XArm manipulator hardware adapter. - -Usage: - >>> from dimos.hardware.manipulators.xarm import XArmAdapter - >>> adapter = XArmAdapter(ip="192.168.1.185", dof=6) - >>> adapter.connect() - >>> positions = adapter.read_joint_positions() -""" - -from dimos.hardware.manipulators.xarm.adapter import XArmAdapter - -__all__ = ["XArmAdapter"] diff --git a/dimos/hardware/manipulators/xarm/adapter.py b/dimos/hardware/manipulators/xarm/adapter.py deleted file mode 100644 index dd9f764031..0000000000 --- a/dimos/hardware/manipulators/xarm/adapter.py +++ /dev/null @@ -1,379 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""XArm adapter - implements ManipulatorAdapter protocol. - -SDK Units: angles=degrees, distance=mm, velocity=deg/s -DimOS Units: angles=radians, distance=meters, velocity=rad/s -""" - -from __future__ import annotations - -import math -from typing import TYPE_CHECKING - -from xarm.wrapper import XArmAPI - -if TYPE_CHECKING: - from dimos.hardware.manipulators.registry import AdapterRegistry - -from dimos.hardware.manipulators.spec import ( - ControlMode, - JointLimits, - ManipulatorAdapter, - ManipulatorInfo, -) - -# Unit conversion constants -MM_TO_M = 0.001 -M_TO_MM = 1000.0 -MAX_CARTESIAN_SPEED_MM = 500.0 # Max cartesian speed in mm/s - -# XArm mode codes -_XARM_MODE_POSITION = 0 -_XARM_MODE_SERVO_CARTESIAN = 1 -_XARM_MODE_JOINT_VELOCITY = 4 -_XARM_MODE_CARTESIAN_VELOCITY = 5 -_XARM_MODE_JOINT_TORQUE = 6 - - -class XArmAdapter(ManipulatorAdapter): - """XArm-specific adapter. - - Implements ManipulatorAdapter protocol via duck typing. - No inheritance required - just matching method signatures. - """ - - def __init__(self, address: str, dof: int = 6, **_: object) -> None: - if not address: - raise ValueError("address (IP) is required for XArmAdapter") - self._ip = address - self._dof = dof - self._arm: XArmAPI | None = None - self._control_mode: ControlMode = ControlMode.POSITION - - # ========================================================================= - # Connection - # ========================================================================= - - def connect(self) -> bool: - """Connect to XArm via TCP/IP.""" - try: - self._arm = XArmAPI(self._ip) - self._arm.connect() - - if not self._arm.connected: - print(f"ERROR: XArm at {self._ip} not reachable (connected=False)") - return False - - # Initialize to servo mode for high-frequency control - self._arm.set_mode(_XARM_MODE_SERVO_CARTESIAN) # Mode 1 = servo mode - self._arm.set_state(0) - self._control_mode = ControlMode.SERVO_POSITION - - return True - except Exception as e: - print(f"ERROR: Failed to connect to XArm at {self._ip}: {e}") - return False - - def disconnect(self) -> None: - """Disconnect from XArm.""" - if self._arm: - self._arm.disconnect() - self._arm = None - - def is_connected(self) -> bool: - """Check if connected to XArm.""" - return self._arm is not None and self._arm.connected - - # ========================================================================= - # Info - # ========================================================================= - - def get_info(self) -> ManipulatorInfo: - """Get XArm information.""" - return ManipulatorInfo( - vendor="UFACTORY", - model=f"xArm{self._dof}", - dof=self._dof, - ) - - def get_dof(self) -> int: - """Get degrees of freedom.""" - return self._dof - - def get_limits(self) -> JointLimits: - """Get joint limits (default XArm limits).""" - # XArm typical joint limits (varies by joint, using conservative values) - limit = 2 * math.pi - return JointLimits( - position_lower=[-limit] * self._dof, - position_upper=[limit] * self._dof, - velocity_max=[math.pi] * self._dof, # ~180 deg/s - ) - - # ========================================================================= - # Control Mode - # ========================================================================= - - def set_control_mode(self, mode: ControlMode) -> bool: - """Set XArm control mode. - - Note: XArm is flexible and often accepts commands without explicit - mode switching, but some operations require the correct mode. - """ - if not self._arm: - return False - - mode_map = { - ControlMode.POSITION: _XARM_MODE_POSITION, - ControlMode.SERVO_POSITION: _XARM_MODE_SERVO_CARTESIAN, # Mode 1 for high-freq - ControlMode.VELOCITY: _XARM_MODE_JOINT_VELOCITY, - ControlMode.TORQUE: _XARM_MODE_JOINT_TORQUE, - ControlMode.CARTESIAN: _XARM_MODE_SERVO_CARTESIAN, - ControlMode.CARTESIAN_VELOCITY: _XARM_MODE_CARTESIAN_VELOCITY, - } - - xarm_mode = mode_map.get(mode) - if xarm_mode is None: - return False - - code = self._arm.set_mode(xarm_mode) - if code == 0: - self._arm.set_state(0) - self._control_mode = mode - return True - return False - - def get_control_mode(self) -> ControlMode: - """Get current control mode.""" - return self._control_mode - - # ========================================================================= - # State Reading - # ========================================================================= - - def read_joint_positions(self) -> list[float]: - """Read joint positions (degrees -> radians).""" - if not self._arm: - raise RuntimeError("Not connected") - - _, angles = self._arm.get_servo_angle() - if not angles: - raise RuntimeError("Failed to read joint positions") - return [math.radians(a) for a in angles[: self._dof]] - - def read_joint_velocities(self) -> list[float]: - """Read joint velocities. - - Note: XArm doesn't provide real-time velocity feedback directly. - Returns zeros. For velocity estimation, use finite differences - on positions in the driver. - """ - return [0.0] * self._dof - - def read_joint_efforts(self) -> list[float]: - """Read joint torques in Nm.""" - if not self._arm: - return [0.0] * self._dof - - code, torques = self._arm.get_joints_torque() - if code == 0 and torques: - return list(torques[: self._dof]) - return [0.0] * self._dof - - def read_state(self) -> dict[str, int]: - """Read robot state.""" - if not self._arm: - return {"state": 0, "mode": 0} - - return { - "state": self._arm.state, - "mode": self._arm.mode, - } - - def read_error(self) -> tuple[int, str]: - """Read error code and message.""" - if not self._arm: - return 0, "" - - code = self._arm.error_code - if code == 0: - return 0, "" - return code, f"XArm error {code}" - - # ========================================================================= - # Motion Control (Joint Space) - # ========================================================================= - - def write_joint_positions( - self, - positions: list[float], - velocity: float = 1.0, - ) -> bool: - """Write joint positions for servo mode (radians -> degrees). - - Uses set_servo_angle_j() for high-frequency servo control. - Requires mode 1 (servo mode) to be active. - - Args: - positions: Target positions in radians - velocity: Speed as fraction of max (0-1) - not used in servo mode - """ - if not self._arm: - return False - - # Convert radians to degrees - angles = [math.degrees(p) for p in positions] - - # Use set_servo_angle_j for high-frequency servo control (100Hz+) - # This only executes the last instruction, suitable for real-time control - code: int = self._arm.set_servo_angle_j(angles, speed=100, mvacc=500) - return code == 0 - - def write_joint_velocities(self, velocities: list[float]) -> bool: - """Write joint velocities (rad/s -> deg/s). - - Note: Requires velocity mode to be active. - """ - if not self._arm: - return False - - # Convert rad/s to deg/s - speeds = [math.degrees(v) for v in velocities] - code: int = self._arm.vc_set_joint_velocity(speeds) - return code == 0 - - def write_stop(self) -> bool: - """Emergency stop.""" - if not self._arm: - return False - code: int = self._arm.emergency_stop() - return code == 0 - - # ========================================================================= - # Servo Control - # ========================================================================= - - def write_enable(self, enable: bool) -> bool: - """Enable or disable servos.""" - if not self._arm: - return False - code: int = self._arm.motion_enable(enable=enable) - return code == 0 - - def read_enabled(self) -> bool: - """Check if servos are enabled.""" - if not self._arm: - return False - # XArm state 0 = ready/enabled - state: int = self._arm.state - return state == 0 - - def write_clear_errors(self) -> bool: - """Clear error state.""" - if not self._arm: - return False - code: int = self._arm.clean_error() - return code == 0 - - # ========================================================================= - # Cartesian Control (Optional) - # ========================================================================= - - def read_cartesian_position(self) -> dict[str, float] | None: - """Read end-effector pose (mm -> meters, degrees -> radians).""" - if not self._arm: - return None - - _, pose = self._arm.get_position() - if pose and len(pose) >= 6: - return { - "x": pose[0] * MM_TO_M, - "y": pose[1] * MM_TO_M, - "z": pose[2] * MM_TO_M, - "roll": math.radians(pose[3]), - "pitch": math.radians(pose[4]), - "yaw": math.radians(pose[5]), - } - return None - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - """Write end-effector pose (meters -> mm, radians -> degrees).""" - if not self._arm: - return False - - code: int = self._arm.set_position( - x=pose.get("x", 0) * M_TO_MM, - y=pose.get("y", 0) * M_TO_MM, - z=pose.get("z", 0) * M_TO_MM, - roll=math.degrees(pose.get("roll", 0)), - pitch=math.degrees(pose.get("pitch", 0)), - yaw=math.degrees(pose.get("yaw", 0)), - speed=velocity * MAX_CARTESIAN_SPEED_MM, - wait=False, - ) - return code == 0 - - # ========================================================================= - # Gripper (Optional) - # ========================================================================= - - def read_gripper_position(self) -> float | None: - """Read gripper position (mm -> meters).""" - if not self._arm: - return None - - result = self._arm.get_gripper_position() - code: int = result[0] - pos: float | None = result[1] - if code == 0 and pos is not None: - return pos * MM_TO_M - return None - - def write_gripper_position(self, position: float) -> bool: - """Write gripper position (meters -> mm).""" - if not self._arm: - return False - - self._arm.set_gripper_enable(True) - pos_mm = position * M_TO_MM - code: int = self._arm.set_gripper_position(pos_mm, wait=True) - return code == 0 - - # ========================================================================= - # Force/Torque Sensor (Optional) - # ========================================================================= - - def read_force_torque(self) -> list[float] | None: - """Read F/T sensor data if available.""" - if not self._arm: - return None - - code, ft = self._arm.get_ft_sensor_data() - if code == 0 and ft: - return list(ft) - return None - - -def register(registry: AdapterRegistry) -> None: - """Register this adapter with the registry.""" - registry.register("xarm", XArmAdapter) - - -__all__ = ["XArmAdapter"] diff --git a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py deleted file mode 100644 index 9161185d50..0000000000 --- a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera.py +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -import logging -import sys -import threading -import time - -import numpy as np - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import Out -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.utils.logging_config import setup_logger - -# Add system path for gi module if needed -if "/usr/lib/python3/dist-packages" not in sys.path: - sys.path.insert(0, "/usr/lib/python3/dist-packages") - -import gi # type: ignore[import-not-found, import-untyped] - -gi.require_version("Gst", "1.0") -gi.require_version("GstApp", "1.0") -from gi.repository import GLib, Gst # type: ignore[import-not-found, import-untyped] - -logger = setup_logger(level=logging.INFO) - -Gst.init(None) - - -@dataclass -class Config(ModuleConfig): - frame_id: str = "camera" - - -class GstreamerCameraModule(Module): - """Module that captures frames from a remote camera using GStreamer TCP with absolute timestamps.""" - - default_config = Config - config: Config - - video: Out[Image] - - def __init__( # type: ignore[no-untyped-def] - self, - host: str = "localhost", - port: int = 5000, - timestamp_offset: float = 0.0, - reconnect_interval: float = 5.0, - *args, - **kwargs, - ) -> None: - """Initialize the GStreamer TCP camera module. - - Args: - host: TCP server host to connect to - port: TCP server port - frame_id: Frame ID for the published images - timestamp_offset: Offset to add to timestamps (useful for clock synchronization) - reconnect_interval: Seconds to wait before attempting reconnection - """ - self.host = host - self.port = port - self.timestamp_offset = timestamp_offset - self.reconnect_interval = reconnect_interval - - self.pipeline = None - self.appsink = None - self.main_loop = None - self.main_loop_thread = None - self.running = False - self.should_reconnect = False - self.frame_count = 0 - self.last_log_time = time.time() - self.reconnect_timer_id = None - super().__init__(**kwargs) - - @rpc - def start(self) -> None: - if self.running: - logger.warning("GStreamer camera module is already running") - return - - super().start() - - self.should_reconnect = True - self._connect() - - @rpc - def stop(self) -> None: - self.should_reconnect = False - self._cleanup_reconnect_timer() - - if not self.running: - return - - self.running = False - - if self.pipeline: - self.pipeline.set_state(Gst.State.NULL) - - if self.main_loop: - self.main_loop.quit() - - # Only join the thread if we're not calling from within it - if self.main_loop_thread and self.main_loop_thread != threading.current_thread(): - self.main_loop_thread.join(timeout=2.0) - - super().stop() - - def _connect(self) -> None: - if not self.should_reconnect: - return - - try: - self._create_pipeline() # type: ignore[no-untyped-call] - self._start_pipeline() # type: ignore[no-untyped-call] - self.running = True - logger.info(f"GStreamer TCP camera module connected to {self.host}:{self.port}") - except Exception as e: - logger.error(f"Failed to connect to {self.host}:{self.port}: {e}") - self._schedule_reconnect() - - def _cleanup_reconnect_timer(self) -> None: - if self.reconnect_timer_id: - GLib.source_remove(self.reconnect_timer_id) - self.reconnect_timer_id = None - - def _schedule_reconnect(self) -> None: - if not self.should_reconnect: - return - - self._cleanup_reconnect_timer() - logger.info(f"Scheduling reconnect in {self.reconnect_interval} seconds...") - self.reconnect_timer_id = GLib.timeout_add_seconds( - int(self.reconnect_interval), self._reconnect_timeout - ) - - def _reconnect_timeout(self) -> bool: - self.reconnect_timer_id = None - if self.should_reconnect: - logger.info("Attempting to reconnect...") - self._connect() - return False # Don't repeat the timeout - - def _handle_disconnect(self) -> None: - if not self.should_reconnect: - return - - self.running = False - - if self.pipeline: - self.pipeline.set_state(Gst.State.NULL) - self.pipeline = None - - self.appsink = None - - logger.warning(f"Disconnected from {self.host}:{self.port}") - self._schedule_reconnect() - - def _create_pipeline(self): # type: ignore[no-untyped-def] - # TCP client source with Matroska demuxer to extract absolute timestamps - pipeline_str = f""" - tcpclientsrc host={self.host} port={self.port} ! - matroskademux name=demux ! - h264parse ! - avdec_h264 ! - videoconvert ! - video/x-raw,format=BGR ! - appsink name=sink emit-signals=true sync=false max-buffers=1 drop=true - """ - - try: - self.pipeline = Gst.parse_launch(pipeline_str) - self.appsink = self.pipeline.get_by_name("sink") # type: ignore[attr-defined] - self.appsink.connect("new-sample", self._on_new_sample) # type: ignore[attr-defined] - except Exception as e: - logger.error(f"Failed to create GStreamer pipeline: {e}") - raise - - def _start_pipeline(self): # type: ignore[no-untyped-def] - """Start the GStreamer pipeline and main loop.""" - self.main_loop = GLib.MainLoop() - - # Start the pipeline - ret = self.pipeline.set_state(Gst.State.PLAYING) # type: ignore[attr-defined] - if ret == Gst.StateChangeReturn.FAILURE: - logger.error("Unable to set the pipeline to playing state") - raise RuntimeError("Failed to start GStreamer pipeline") - - # Run the main loop in a separate thread - self.main_loop_thread = threading.Thread(target=self._run_main_loop) # type: ignore[assignment] - self.main_loop_thread.daemon = True # type: ignore[attr-defined] - self.main_loop_thread.start() # type: ignore[attr-defined] - - # Set up bus message handling - bus = self.pipeline.get_bus() # type: ignore[attr-defined] - bus.add_signal_watch() - bus.connect("message", self._on_bus_message) - - def _run_main_loop(self) -> None: - try: - self.main_loop.run() # type: ignore[attr-defined] - except Exception as e: - logger.error(f"Main loop error: {e}") - - def _on_bus_message(self, bus, message) -> None: # type: ignore[no-untyped-def] - t = message.type - - if t == Gst.MessageType.EOS: - logger.info("End of stream - server disconnected") - self._handle_disconnect() - elif t == Gst.MessageType.ERROR: - err, debug = message.parse_error() - logger.error(f"GStreamer error: {err}, {debug}") - self._handle_disconnect() - elif t == Gst.MessageType.WARNING: - warn, debug = message.parse_warning() - logger.warning(f"GStreamer warning: {warn}, {debug}") - elif t == Gst.MessageType.STATE_CHANGED: - if message.src == self.pipeline: - _old_state, new_state, _pending_state = message.parse_state_changed() - if new_state == Gst.State.PLAYING: - logger.info("Pipeline is now playing - connected to TCP server") - - def _on_new_sample(self, appsink): # type: ignore[no-untyped-def] - """Handle new video samples from the appsink.""" - sample = appsink.emit("pull-sample") - if sample is None: - return Gst.FlowReturn.OK - - buffer = sample.get_buffer() - caps = sample.get_caps() - - # Extract video format information - struct = caps.get_structure(0) - width = struct.get_value("width") - height = struct.get_value("height") - - # Get the absolute timestamp from the buffer - # Matroska preserves the absolute timestamps we set in the sender - if buffer.pts != Gst.CLOCK_TIME_NONE: - # Convert nanoseconds to seconds and add offset - # This is the absolute time from when the frame was captured - timestamp = (buffer.pts / 1e9) + self.timestamp_offset - - # Skip frames with invalid timestamps (before year 2000) - # This filters out initial gray frames with relative timestamps - year_2000_timestamp = 946684800.0 # January 1, 2000 00:00:00 UTC - if timestamp < year_2000_timestamp: - logger.debug(f"Skipping frame with invalid timestamp: {timestamp:.6f}") - return Gst.FlowReturn.OK - - else: - return Gst.FlowReturn.OK - - # Map the buffer to access the data - success, map_info = buffer.map(Gst.MapFlags.READ) - if not success: - logger.error("Failed to map buffer") - return Gst.FlowReturn.ERROR - - try: - # Convert buffer data to numpy array - # The videoconvert element outputs BGR format - data = np.frombuffer(map_info.data, dtype=np.uint8) - - # Reshape to image dimensions - # For BGR format, we have 3 channels - image_array = data.reshape((height, width, 3)) - - # Create an Image message with the absolute timestamp - image_msg = Image( - data=image_array.copy(), # Make a copy to ensure data persistence - format=ImageFormat.BGR, - frame_id=self.frame_id, - ts=timestamp, - ) - - # Publish the image - if self.video and self.running: - self.video.publish(image_msg) - - # Log statistics periodically - self.frame_count += 1 - current_time = time.time() - if current_time - self.last_log_time >= 5.0: - fps = self.frame_count / (current_time - self.last_log_time) - logger.debug( - f"Receiving frames - FPS: {fps:.1f}, Resolution: {width}x{height}, " - f"Absolute timestamp: {timestamp:.6f}" - ) - self.frame_count = 0 - self.last_log_time = current_time - - except Exception as e: - logger.error(f"Error processing frame: {e}") - - finally: - buffer.unmap(map_info) - - return Gst.FlowReturn.OK diff --git a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py deleted file mode 100755 index cc0e3424a5..0000000000 --- a/dimos/hardware/sensors/camera/gstreamer/gstreamer_camera_test_script.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import logging -import time - -from dimos import core -from dimos.hardware.sensors.camera.gstreamer.gstreamer_camera import GstreamerCameraModule -from dimos.msgs.sensor_msgs import Image -from dimos.protocol import pubsub - -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) - - -def main() -> None: - parser = argparse.ArgumentParser(description="Test script for GStreamer TCP camera module") - - # Network options - parser.add_argument( - "--host", default="localhost", help="TCP server host to connect to (default: localhost)" - ) - parser.add_argument("--port", type=int, default=5000, help="TCP server port (default: 5000)") - - # Camera options - parser.add_argument( - "--frame-id", - default="zed_camera", - help="Frame ID for published images (default: zed_camera)", - ) - parser.add_argument( - "--reconnect-interval", - type=float, - default=5.0, - help="Seconds to wait before attempting reconnection (default: 5.0)", - ) - - # Logging options - parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") - - args = parser.parse_args() - - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Initialize LCM - pubsub.lcm.autoconf() # type: ignore[attr-defined] - - # Start dimos - logger.info("Starting dimos...") - dimos = core.start(8) - - # Deploy the GStreamer camera module - logger.info(f"Deploying GStreamer TCP camera module (connecting to {args.host}:{args.port})...") - camera = dimos.deploy( # type: ignore[attr-defined] - GstreamerCameraModule, - host=args.host, - port=args.port, - frame_id=args.frame_id, - reconnect_interval=args.reconnect_interval, - ) - - # Set up LCM transport for the video output - camera.video.transport = core.LCMTransport("/zed/video", Image) - - # Counter for received frames - frame_count = [0] - last_log_time = [time.time()] - first_timestamp = [None] - - def on_frame(msg) -> None: # type: ignore[no-untyped-def] - frame_count[0] += 1 - current_time = time.time() - - # Capture first timestamp to show absolute timestamps are preserved - if first_timestamp[0] is None: - first_timestamp[0] = msg.ts - logger.info(f"First frame absolute timestamp: {msg.ts:.6f}") - - # Log stats every 2 seconds - if current_time - last_log_time[0] >= 2.0: - fps = frame_count[0] / (current_time - last_log_time[0]) - timestamp_delta = msg.ts - first_timestamp[0] - logger.info( - f"Received {frame_count[0]} frames - FPS: {fps:.1f} - " - f"Resolution: {msg.width}x{msg.height} - " - f"Timestamp: {msg.ts:.3f} (delta: {timestamp_delta:.3f}s)" - ) - frame_count[0] = 0 - last_log_time[0] = current_time - - # Subscribe to video output for monitoring - camera.video.subscribe(on_frame) - - # Start the camera - logger.info("Starting GStreamer camera...") - camera.start() - - logger.info("GStreamer TCP camera module is running. Press Ctrl+C to stop.") - logger.info(f"Connecting to TCP server at {args.host}:{args.port}") - logger.info("Publishing frames to LCM topic: /zed/video") - logger.info("") - logger.info("To start the sender on the camera machine, run:") - logger.info( - f" python3 dimos/hardware/gstreamer_sender.py --device /dev/video0 --host 0.0.0.0 --port {args.port}" - ) - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("Shutting down...") - camera.stop() - logger.info("Stopped.") - - -if __name__ == "__main__": - main() diff --git a/dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py b/dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py deleted file mode 100755 index 4aee200419..0000000000 --- a/dimos/hardware/sensors/camera/gstreamer/gstreamer_sender.py +++ /dev/null @@ -1,359 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -import logging -import signal -import sys -import time - -# Add system path for gi module if needed -if "/usr/lib/python3/dist-packages" not in sys.path: - sys.path.insert(0, "/usr/lib/python3/dist-packages") - -import gi # type: ignore[import-not-found, import-untyped] - -gi.require_version("Gst", "1.0") -gi.require_version("GstVideo", "1.0") -from gi.repository import GLib, Gst # type: ignore[import-not-found, import-untyped] - -# Initialize GStreamer -Gst.init(None) - -# Setup logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger("gstreamer_tcp_sender") - - -class GStreamerTCPSender: - def __init__( - self, - device: str = "/dev/video0", - width: int = 2560, - height: int = 720, - framerate: int = 60, - format_str: str = "YUY2", - bitrate: int = 5000, - host: str = "0.0.0.0", - port: int = 5000, - single_camera: bool = False, - ) -> None: - """Initialize the GStreamer TCP sender. - - Args: - device: Video device path - width: Video width in pixels - height: Video height in pixels - framerate: Frame rate in fps - format_str: Video format - bitrate: H264 encoding bitrate in kbps - host: Host to listen on (0.0.0.0 for all interfaces) - port: TCP port for listening - single_camera: If True, crop to left half (for stereo cameras) - """ - self.device = device - self.width = width - self.height = height - self.framerate = framerate - self.format = format_str - self.bitrate = bitrate - self.host = host - self.port = port - self.single_camera = single_camera - - self.pipeline = None - self.videosrc = None - self.encoder = None - self.mux = None - self.main_loop = None - self.running = False - self.start_time = None - self.frame_count = 0 - - def create_pipeline(self): # type: ignore[no-untyped-def] - """Create the GStreamer pipeline with TCP server sink.""" - - # Create pipeline - self.pipeline = Gst.Pipeline.new("tcp-sender-pipeline") - - # Create elements - self.videosrc = Gst.ElementFactory.make("v4l2src", "source") - self.videosrc.set_property("device", self.device) # type: ignore[attr-defined] - self.videosrc.set_property("do-timestamp", True) # type: ignore[attr-defined] - logger.info(f"Using camera device: {self.device}") - - # Create caps filter for video format - capsfilter = Gst.ElementFactory.make("capsfilter", "capsfilter") - caps = Gst.Caps.from_string( - f"video/x-raw,width={self.width},height={self.height}," - f"format={self.format},framerate={self.framerate}/1" - ) - capsfilter.set_property("caps", caps) - - # Video converter - videoconvert = Gst.ElementFactory.make("videoconvert", "convert") - - # Crop element for single camera mode - videocrop = None - if self.single_camera: - videocrop = Gst.ElementFactory.make("videocrop", "crop") - # Crop to left half: for 2560x720 stereo, get left 1280x720 - videocrop.set_property("left", 0) - videocrop.set_property("right", self.width // 2) # Remove right half - videocrop.set_property("top", 0) - videocrop.set_property("bottom", 0) - - # H264 encoder - self.encoder = Gst.ElementFactory.make("x264enc", "encoder") - self.encoder.set_property("tune", "zerolatency") # type: ignore[attr-defined] - self.encoder.set_property("bitrate", self.bitrate) # type: ignore[attr-defined] - self.encoder.set_property("key-int-max", 30) # type: ignore[attr-defined] - - # H264 parser - h264parse = Gst.ElementFactory.make("h264parse", "parser") - - # Use matroskamux which preserves timestamps better - self.mux = Gst.ElementFactory.make("matroskamux", "mux") - self.mux.set_property("streamable", True) # type: ignore[attr-defined] - self.mux.set_property("writing-app", "gstreamer-tcp-sender") # type: ignore[attr-defined] - - # TCP server sink - tcpserversink = Gst.ElementFactory.make("tcpserversink", "sink") - tcpserversink.set_property("host", self.host) - tcpserversink.set_property("port", self.port) - tcpserversink.set_property("sync", False) - - # Add elements to pipeline - self.pipeline.add(self.videosrc) # type: ignore[attr-defined] - self.pipeline.add(capsfilter) # type: ignore[attr-defined] - self.pipeline.add(videoconvert) # type: ignore[attr-defined] - if videocrop: - self.pipeline.add(videocrop) # type: ignore[attr-defined] - self.pipeline.add(self.encoder) # type: ignore[attr-defined] - self.pipeline.add(h264parse) # type: ignore[attr-defined] - self.pipeline.add(self.mux) # type: ignore[attr-defined] - self.pipeline.add(tcpserversink) # type: ignore[attr-defined] - - # Link elements - if not self.videosrc.link(capsfilter): # type: ignore[attr-defined] - raise RuntimeError("Failed to link source to capsfilter") - if not capsfilter.link(videoconvert): - raise RuntimeError("Failed to link capsfilter to videoconvert") - - # Link through crop if in single camera mode - if videocrop: - if not videoconvert.link(videocrop): - raise RuntimeError("Failed to link videoconvert to videocrop") - if not videocrop.link(self.encoder): - raise RuntimeError("Failed to link videocrop to encoder") - else: - if not videoconvert.link(self.encoder): - raise RuntimeError("Failed to link videoconvert to encoder") - - if not self.encoder.link(h264parse): # type: ignore[attr-defined] - raise RuntimeError("Failed to link encoder to h264parse") - if not h264parse.link(self.mux): - raise RuntimeError("Failed to link h264parse to mux") - if not self.mux.link(tcpserversink): # type: ignore[attr-defined] - raise RuntimeError("Failed to link mux to tcpserversink") - - # Add probe to inject absolute timestamps - # Place probe after crop (if present) or after videoconvert - if videocrop: - probe_element = videocrop - else: - probe_element = videoconvert - probe_pad = probe_element.get_static_pad("src") - probe_pad.add_probe(Gst.PadProbeType.BUFFER, self._inject_absolute_timestamp, None) - - # Set up bus message handling - bus = self.pipeline.get_bus() # type: ignore[attr-defined] - bus.add_signal_watch() - bus.connect("message", self._on_bus_message) - - def _inject_absolute_timestamp(self, pad, info, user_data): # type: ignore[no-untyped-def] - buffer = info.get_buffer() - if buffer: - absolute_time = time.time() - absolute_time_ns = int(absolute_time * 1e9) - - # Set both PTS and DTS to the absolute time - # This will be preserved by matroskamux - buffer.pts = absolute_time_ns - buffer.dts = absolute_time_ns - - self.frame_count += 1 - return Gst.PadProbeReturn.OK - - def _on_bus_message(self, bus, message) -> None: # type: ignore[no-untyped-def] - t = message.type - - if t == Gst.MessageType.EOS: - logger.info("End of stream") - self.stop() - elif t == Gst.MessageType.ERROR: - err, debug = message.parse_error() - logger.error(f"Pipeline error: {err}, {debug}") - self.stop() - elif t == Gst.MessageType.WARNING: - warn, debug = message.parse_warning() - logger.warning(f"Pipeline warning: {warn}, {debug}") - elif t == Gst.MessageType.STATE_CHANGED: - if message.src == self.pipeline: - old_state, new_state, _pending_state = message.parse_state_changed() - logger.debug( - f"Pipeline state changed: {old_state.value_nick} -> {new_state.value_nick}" - ) - - def start(self): # type: ignore[no-untyped-def] - if self.running: - logger.warning("Sender is already running") - return - - logger.info("Creating TCP pipeline with absolute timestamps...") - self.create_pipeline() # type: ignore[no-untyped-call] - - logger.info("Starting pipeline...") - ret = self.pipeline.set_state(Gst.State.PLAYING) # type: ignore[attr-defined] - if ret == Gst.StateChangeReturn.FAILURE: - logger.error("Failed to start pipeline") - raise RuntimeError("Failed to start GStreamer pipeline") - - self.running = True - self.start_time = time.time() # type: ignore[assignment] - self.frame_count = 0 - - logger.info("TCP video sender started:") - logger.info(f" Source: {self.device}") - if self.single_camera: - output_width = self.width // 2 - logger.info(f" Input Resolution: {self.width}x{self.height} @ {self.framerate}fps") - logger.info( - f" Output Resolution: {output_width}x{self.height} @ {self.framerate}fps (left camera only)" - ) - else: - logger.info(f" Resolution: {self.width}x{self.height} @ {self.framerate}fps") - logger.info(f" Bitrate: {self.bitrate} kbps") - logger.info(f" TCP Server: {self.host}:{self.port}") - logger.info(" Container: Matroska (preserves absolute timestamps)") - logger.info(" Waiting for client connections...") - - self.main_loop = GLib.MainLoop() - try: - self.main_loop.run() # type: ignore[attr-defined] - except KeyboardInterrupt: - logger.info("Interrupted by user") - finally: - self.stop() - - def stop(self) -> None: - if not self.running: - return - - self.running = False - - if self.pipeline: - logger.info("Stopping pipeline...") - self.pipeline.set_state(Gst.State.NULL) - - if self.main_loop and self.main_loop.is_running(): - self.main_loop.quit() - - if self.frame_count > 0 and self.start_time: - elapsed = time.time() - self.start_time - avg_fps = self.frame_count / elapsed - logger.info(f"Total frames sent: {self.frame_count}, Average FPS: {avg_fps:.1f}") - - logger.info("TCP video sender stopped") - - -def main() -> None: - parser = argparse.ArgumentParser( - description="GStreamer TCP video sender with absolute timestamps" - ) - - # Video source options - parser.add_argument( - "--device", default="/dev/video0", help="Video device path (default: /dev/video0)" - ) - - # Video format options - parser.add_argument("--width", type=int, default=2560, help="Video width (default: 2560)") - parser.add_argument("--height", type=int, default=720, help="Video height (default: 720)") - parser.add_argument("--framerate", type=int, default=15, help="Frame rate in fps (default: 15)") - parser.add_argument("--format", default="YUY2", help="Video format (default: YUY2)") - - # Encoding options - parser.add_argument( - "--bitrate", type=int, default=5000, help="H264 bitrate in kbps (default: 5000)" - ) - - # Network options - parser.add_argument( - "--host", - default="0.0.0.0", - help="Host to listen on (default: 0.0.0.0 for all interfaces)", - ) - parser.add_argument("--port", type=int, default=5000, help="TCP port (default: 5000)") - - # Camera options - parser.add_argument( - "--single-camera", - action="store_true", - help="Extract left camera only from stereo feed (crops 2560x720 to 1280x720)", - ) - - # Logging options - parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") - - args = parser.parse_args() - - if args.verbose: - logging.getLogger().setLevel(logging.DEBUG) - - # Create and start sender - sender = GStreamerTCPSender( - device=args.device, - width=args.width, - height=args.height, - framerate=args.framerate, - format_str=args.format, - bitrate=args.bitrate, - host=args.host, - port=args.port, - single_camera=args.single_camera, - ) - - # Handle signals gracefully - def signal_handler(sig, frame) -> None: # type: ignore[no-untyped-def] - logger.info(f"Received signal {sig}, shutting down...") - sender.stop() - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - try: - sender.start() # type: ignore[no-untyped-call] - except Exception as e: - logger.error(f"Failed to start sender: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/dimos/hardware/sensors/camera/module.py b/dimos/hardware/sensors/camera/module.py deleted file mode 100644 index 11821d4724..0000000000 --- a/dimos/hardware/sensors/camera/module.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from dataclasses import dataclass, field -import time -from typing import Any - -import reactivex as rx - -from dimos.agents.annotation import skill -from dimos.core.blueprints import autoconnect -from dimos.core.core import rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import Out -from dimos.hardware.sensors.camera.spec import CameraHardware -from dimos.hardware.sensors.camera.webcam import Webcam -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier -from dimos.spec import perception -from dimos.visualization.rerun.bridge import rerun_bridge - - -def default_transform() -> Transform: - return Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ) - - -@dataclass -class CameraModuleConfig(ModuleConfig): - frame_id: str = "camera_link" - transform: Transform | None = field(default_factory=default_transform) - hardware: Callable[[], CameraHardware[Any]] | CameraHardware[Any] = Webcam - frequency: float = 0.0 # Hz, 0 means no limit - - -class CameraModule(Module[CameraModuleConfig], perception.Camera): - color_image: Out[Image] - camera_info: Out[CameraInfo] - - hardware: CameraHardware[Any] - - config: CameraModuleConfig - default_config = CameraModuleConfig - _global_config: GlobalConfig - - def __init__(self, *args: Any, cfg: GlobalConfig = global_config, **kwargs: Any) -> None: - self._global_config = cfg - self._latest_image: Image | None = None - super().__init__(*args, **kwargs) - - @rpc - def start(self) -> None: - super().start() - - if callable(self.config.hardware): - self.hardware = self.config.hardware() - else: - self.hardware = self.config.hardware - - stream = self.hardware.image_stream() - - if self.config.frequency > 0: - stream = stream.pipe(sharpness_barrier(self.config.frequency)) - - def on_image(image: Image) -> None: - self.color_image.publish(image) - self._latest_image = image - - self._disposables.add( - stream.subscribe(on_image), - ) - - self._disposables.add( - rx.interval(1.0).subscribe(lambda _: self.publish_metadata()), - ) - - def publish_metadata(self) -> None: - camera_info = self.hardware.camera_info.with_ts(time.time()) - self.camera_info.publish(camera_info) - - if not self.config.transform: - return - - camera_link = self.config.transform - camera_link.ts = camera_info.ts - - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=camera_link.ts, - ) - - self.tf.publish(camera_link, camera_optical) - - @skill - def take_a_picture(self) -> Image: - """Grabs and returns the latest image from the camera.""" - if self._latest_image is None: - raise RuntimeError("No image received from camera yet.") - return self._latest_image - - def stop(self) -> None: - if self.hardware and hasattr(self.hardware, "stop"): - self.hardware.stop() - super().stop() - - -camera_module = CameraModule.blueprint - -demo_camera = autoconnect( - camera_module(), - rerun_bridge(), -) - -__all__ = ["CameraModule", "camera_module"] diff --git a/dimos/hardware/sensors/camera/realsense/README.md b/dimos/hardware/sensors/camera/realsense/README.md deleted file mode 100644 index 665833047e..0000000000 --- a/dimos/hardware/sensors/camera/realsense/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# RealSense SDK Install - -1) Install the Intel RealSense SDK: - - https://github.com/IntelRealSense/librealsense - -2) Install the Python bindings: - ```bash - pip install pyrealsense2 - ``` diff --git a/dimos/hardware/sensors/camera/realsense/__init__.py b/dimos/hardware/sensors/camera/realsense/__init__.py deleted file mode 100644 index 58f519a12e..0000000000 --- a/dimos/hardware/sensors/camera/realsense/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.hardware.sensors.camera.realsense.camera import ( - RealSenseCamera, - RealSenseCameraConfig, - realsense_camera, - ) - -__all__ = ["RealSenseCamera", "RealSenseCameraConfig", "realsense_camera"] - - -def __getattr__(name: str) -> object: - if name in __all__: - from dimos.hardware.sensors.camera.realsense.camera import ( - RealSenseCamera, - RealSenseCameraConfig, - realsense_camera, - ) - - globals().update( - RealSenseCamera=RealSenseCamera, - RealSenseCameraConfig=RealSenseCameraConfig, - realsense_camera=realsense_camera, - ) - return globals()[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/dimos/hardware/sensors/camera/realsense/camera.py b/dimos/hardware/sensors/camera/realsense/camera.py deleted file mode 100644 index 4ff0ccf6c4..0000000000 --- a/dimos/hardware/sensors/camera/realsense/camera.py +++ /dev/null @@ -1,487 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import atexit -from dataclasses import dataclass, field -import threading -import time -from typing import TYPE_CHECKING - -import cv2 -import numpy as np -import reactivex as rx -from scipy.spatial.transform import Rotation # type: ignore[import-untyped] - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera.spec import ( - OPTICAL_ROTATION, - DepthCameraConfig, - DepthCameraHardware, -) -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.spec import perception -from dimos.utils.reactive import backpressure - -if TYPE_CHECKING: - import pyrealsense2 as rs # type: ignore[import-not-found] - - from dimos.core.stream import Out - - -def default_base_transform() -> Transform: - """Default identity transform for camera mounting.""" - return Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - - -@dataclass -class RealSenseCameraConfig(ModuleConfig, DepthCameraConfig): - width: int = 848 - height: int = 480 - fps: int = 15 - camera_name: str = "camera" - base_frame_id: str = "base_link" - base_transform: Transform | None = field(default_factory=default_base_transform) - align_depth_to_color: bool = True - enable_depth: bool = True - enable_pointcloud: bool = False - pointcloud_fps: float = 5.0 - camera_info_fps: float = 1.0 - serial_number: str | None = None - - -class RealSenseCamera(DepthCameraHardware, Module, perception.DepthCamera): - color_image: Out[Image] - depth_image: Out[Image] - pointcloud: Out[PointCloud2] - camera_info: Out[CameraInfo] - depth_camera_info: Out[CameraInfo] - - config: RealSenseCameraConfig - default_config = RealSenseCameraConfig - - @property - def _camera_link(self) -> str: - return f"{self.config.camera_name}_link" - - @property - def _color_frame(self) -> str: - return f"{self.config.camera_name}_color_frame" - - @property - def _color_optical_frame(self) -> str: - return f"{self.config.camera_name}_color_optical_frame" - - @property - def _depth_frame(self) -> str: - return f"{self.config.camera_name}_depth_frame" - - @property - def _depth_optical_frame(self) -> str: - return f"{self.config.camera_name}_depth_optical_frame" - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._pipeline: rs.pipeline | None = None - self._profile: rs.pipeline_profile | None = None - self._align: rs.align | None = None - self._running = False - self._thread: threading.Thread | None = None - self._color_camera_info: CameraInfo | None = None - self._depth_camera_info: CameraInfo | None = None - self._depth_scale: float = 0.001 - self._color_to_depth_extrinsics: rs.extrinsics | None = None - # Pointcloud generation state - self._latest_color_img: Image | None = None - self._latest_depth_img: Image | None = None - self._pointcloud_lock = threading.Lock() - - @rpc - def start(self) -> None: - import pyrealsense2 as rs # type: ignore[import-not-found] - - self._pipeline = rs.pipeline() - config = rs.config() - - if self.config.serial_number: - config.enable_device(self.config.serial_number) - - config.enable_stream( - rs.stream.color, - self.config.width, - self.config.height, - rs.format.bgr8, - self.config.fps, - ) - - if self.config.enable_depth: - config.enable_stream( - rs.stream.depth, - self.config.width, - self.config.height, - rs.format.z16, - self.config.fps, - ) - - self._profile = self._pipeline.start(config) - - if self.config.enable_depth: - depth_sensor = self._profile.get_device().first_depth_sensor() - self._depth_scale = depth_sensor.get_depth_scale() - - if self.config.align_depth_to_color and self.config.enable_depth: - self._align = rs.align(rs.stream.color) - - self._build_camera_info() - self._get_extrinsics() - - self._running = True - self._thread = threading.Thread(target=self._capture_loop, daemon=True) - self._thread.start() - - if self.config.enable_pointcloud and self.config.enable_depth: - interval_sec = 1.0 / self.config.pointcloud_fps - self._disposables.add( - backpressure(rx.interval(interval_sec)).subscribe( - on_next=lambda _: self._generate_pointcloud(), - on_error=lambda e: print(f"Pointcloud error: {e}"), - ) - ) - - interval_sec = 1.0 / self.config.camera_info_fps - self._disposables.add( - rx.interval(interval_sec).subscribe( - on_next=lambda _: self._publish_camera_info(), - on_error=lambda e: print(f"CameraInfo error: {e}"), - ) - ) - - def _publish_camera_info(self) -> None: - ts = time.time() - if self._color_camera_info: - self._color_camera_info.ts = ts - self.camera_info.publish(self._color_camera_info) - if self._depth_camera_info: - self._depth_camera_info.ts = ts - self.depth_camera_info.publish(self._depth_camera_info) - - def _build_camera_info(self) -> None: - import pyrealsense2 as rs # type: ignore[import-not-found] - - if self._profile is None: - return - - # Color camera info - color_stream = self._profile.get_stream(rs.stream.color).as_video_stream_profile() - color_intrinsics = color_stream.get_intrinsics() - self._color_camera_info = self._intrinsics_to_camera_info( - color_intrinsics, self._color_optical_frame - ) - - # Depth camera info - if self.config.enable_depth: - if self.config.align_depth_to_color: - # When aligned to color, depth uses color intrinsics and frame - self._depth_camera_info = self._intrinsics_to_camera_info( - color_intrinsics, self._color_optical_frame - ) - else: - depth_stream = self._profile.get_stream(rs.stream.depth).as_video_stream_profile() - depth_intrinsics = depth_stream.get_intrinsics() - self._depth_camera_info = self._intrinsics_to_camera_info( - depth_intrinsics, self._depth_optical_frame - ) - - def _intrinsics_to_camera_info(self, intrinsics: rs.intrinsics, frame_id: str) -> CameraInfo: - import pyrealsense2 as rs # type: ignore[import-not-found] - - fx, fy = intrinsics.fx, intrinsics.fy - cx, cy = intrinsics.ppx, intrinsics.ppy - - K = [fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0] - P = [fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0] - D = list(intrinsics.coeffs) if intrinsics.coeffs else [] - - distortion_model = { - rs.distortion.none: "", - rs.distortion.modified_brown_conrady: "plumb_bob", - rs.distortion.inverse_brown_conrady: "plumb_bob", - rs.distortion.ftheta: "equidistant", - rs.distortion.brown_conrady: "plumb_bob", - rs.distortion.kannala_brandt4: "equidistant", - }.get(intrinsics.model, "") - - return CameraInfo( - height=intrinsics.height, - width=intrinsics.width, - distortion_model=distortion_model, - D=D, - K=K, - P=P, - frame_id=frame_id, - ) - - def _get_extrinsics(self) -> None: - import pyrealsense2 as rs # type: ignore[import-not-found] - - if self._profile is None or not self.config.enable_depth: - return - - depth_stream = self._profile.get_stream(rs.stream.depth) - color_stream = self._profile.get_stream(rs.stream.color) - self._color_to_depth_extrinsics = color_stream.get_extrinsics_to(depth_stream) - - def _extrinsics_to_transform( - self, - extrinsics: rs.extrinsics, - frame_id: str, - child_frame_id: str, - ts: float, - ) -> Transform: - rotation_matrix = np.array(extrinsics.rotation).reshape(3, 3) - quat = Rotation.from_matrix(rotation_matrix).as_quat() # [x, y, z, w] - return Transform( - translation=Vector3(*extrinsics.translation), - rotation=Quaternion(quat[0], quat[1], quat[2], quat[3]), - frame_id=frame_id, - child_frame_id=child_frame_id, - ts=ts, - ) - - def _capture_loop(self) -> None: - while self._running and self._pipeline is not None: - try: - frames = self._pipeline.wait_for_frames(timeout_ms=1000) - except (RuntimeError, AttributeError): - # Pipeline stopped or None - exit loop - break - - ts = time.time() - - if self._align is not None: - frames = self._align.process(frames) - - color_frame = frames.get_color_frame() - depth_frame = frames.get_depth_frame() if self.config.enable_depth else None - - # Process color - color_img = None - if color_frame: - color_data = np.asanyarray(color_frame.get_data()) - color_data = cv2.cvtColor(color_data, cv2.COLOR_BGR2RGB) - color_img = Image( - data=color_data, - format=ImageFormat.RGB, - frame_id=self._color_optical_frame, - ts=ts, - ) - self.color_image.publish(color_img) - - # Process depth - depth_img = None - if depth_frame: - depth_data = np.asanyarray(depth_frame.get_data()) - # When aligned, depth is in color optical frame - depth_frame_id = ( - self._color_optical_frame - if self.config.align_depth_to_color - else self._depth_optical_frame - ) - depth_img = Image( - data=depth_data, - format=ImageFormat.DEPTH16, - frame_id=depth_frame_id, - ts=ts, - ) - self.depth_image.publish(depth_img) - - # Store latest images for pointcloud generation - if self.config.enable_pointcloud and color_img is not None and depth_img is not None: - with self._pointcloud_lock: - self._latest_color_img = color_img - self._latest_depth_img = depth_img - - # Publish TF - self._publish_tf(ts) - - def _publish_tf(self, ts: float) -> None: - transforms = [] - - # base_link -> camera_link (user-provided mounting transform) - if self.config.base_transform is not None: - base_to_camera = Transform( - translation=self.config.base_transform.translation, - rotation=self.config.base_transform.rotation, - frame_id=self.config.base_frame_id, - child_frame_id=self._camera_link, - ts=ts, - ) - transforms.append(base_to_camera) - - # camera_link -> camera_depth_frame (identity, depth is at camera_link origin) - camera_link_to_depth = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id=self._camera_link, - child_frame_id=self._depth_frame, - ts=ts, - ) - transforms.append(camera_link_to_depth) - - # camera_depth_frame -> camera_depth_optical_frame - depth_to_depth_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=OPTICAL_ROTATION, - frame_id=self._depth_frame, - child_frame_id=self._depth_optical_frame, - ts=ts, - ) - transforms.append(depth_to_depth_optical) - - color_tf = self._extrinsics_to_transform( - self._color_to_depth_extrinsics, - self._camera_link, - self._color_frame, - ts, - ) - # Invert the transform since extrinsics are color->depth - color_tf = color_tf.inverse() - color_tf.frame_id = self._camera_link - color_tf.child_frame_id = self._color_frame - color_tf.ts = ts - transforms.append(color_tf) - - # camera_color_frame -> camera_color_optical_frame - color_to_color_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=OPTICAL_ROTATION, - frame_id=self._color_frame, - child_frame_id=self._color_optical_frame, - ts=ts, - ) - transforms.append(color_to_color_optical) - - self.tf.publish(*transforms) - - def _generate_pointcloud(self) -> None: - """Generate and publish pointcloud from latest images (called by rx.interval).""" - with self._pointcloud_lock: - color_img = self._latest_color_img - depth_img = self._latest_depth_img - - if color_img is None or depth_img is None or self._color_camera_info is None: - return - - try: - pcd = PointCloud2.from_rgbd( - color_image=color_img, - depth_image=depth_img, - camera_info=self._color_camera_info, - depth_scale=self._depth_scale, - ) - pcd = pcd.voxel_downsample(0.005) - self.pointcloud.publish(pcd) - except Exception as e: - print(f"Pointcloud generation error: {e}") - - @rpc - def stop(self) -> None: - self._running = False - - # Stop pipeline first to unblock wait_for_frames() - if self._pipeline: - try: - self._pipeline.stop() - except Exception: - pass # Pipeline might already be stopped - self._pipeline = None - - # Now join the thread (should exit quickly since pipeline is stopped) - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=2.0) - if self._thread.is_alive(): - # Force thread termination by clearing reference - self._thread = None - - self._profile = None - self._align = None - self._color_to_depth_extrinsics = None - self._latest_color_img = None - self._latest_depth_img = None - super().stop() - - @rpc - def get_color_camera_info(self) -> CameraInfo | None: - return self._color_camera_info - - @rpc - def get_depth_camera_info(self) -> CameraInfo | None: - return self._depth_camera_info - - @rpc - def get_depth_scale(self) -> float: - return self._depth_scale - - -def main() -> None: - dimos = ModuleCoordinator(n=2) - dimos.start() - - camera = dimos.deploy(RealSenseCamera, enable_pointcloud=True, pointcloud_fps=5.0) # type: ignore[type-var] - foxglove_bridge = FoxgloveBridge() - foxglove_bridge.start() - - camera.color_image.transport = LCMTransport("/camera/color", Image) - camera.depth_image.transport = LCMTransport("/camera/depth", Image) - camera.pointcloud.transport = LCMTransport("/camera/pointcloud", PointCloud2) - camera.camera_info.transport = LCMTransport("/camera/color_info", CameraInfo) - camera.depth_camera_info.transport = LCMTransport("/camera/depth_info", CameraInfo) - - def cleanup() -> None: - try: - dimos.stop() - except Exception: - pass - - atexit.register(cleanup) - dimos.start_all_modules() - - try: - while True: - time.sleep(0.1) - except (KeyboardInterrupt, SystemExit): - pass - finally: - atexit.unregister(cleanup) - cleanup() - - -if __name__ == "__main__": - main() - - -realsense_camera = RealSenseCamera.blueprint - -__all__ = ["RealSenseCamera", "RealSenseCameraConfig", "realsense_camera"] diff --git a/dimos/hardware/sensors/camera/realsense/handeyeout_xarm6/calibration.json b/dimos/hardware/sensors/camera/realsense/handeyeout_xarm6/calibration.json deleted file mode 100644 index 2591a2f04a..0000000000 --- a/dimos/hardware/sensors/camera/realsense/handeyeout_xarm6/calibration.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "transform_id": "EEF_TO_COLOR_OPT", - "parent_frame": "eef", - "child_frame": "camera_color_optical_frame", - "direction": "T_parent_child", - "units": { - "translation": "meters", - "angles": "radians" - }, - "translation_m": { - "x": 0.067052239, - "y": -0.0311387575, - "z": 0.021611456 - }, - "rotation_rpy_rad": { - "roll": -0.004202176, - "pitch": -0.00848499, - "yaw": 1.5898775, - "convention": "R = Rz(yaw) * Ry(pitch) * Rx(roll)" - }, - "rotation_quat_wxyz": { - "w": 0.7003270047, - "x": 0.0015569323, - "y": -0.0044709112, - "z": 0.7138064706 - }, - "notes": [ - "This is an extrinsic transform from end-effector frame to RealSense color optical frame.", - "Keep quaternion as the source of truth; use RPY mainly for debugging/printing." - ] -} diff --git a/dimos/hardware/sensors/camera/spec.py b/dimos/hardware/sensors/camera/spec.py deleted file mode 100644 index 23fd1a076e..0000000000 --- a/dimos/hardware/sensors/camera/spec.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from typing import Generic, Protocol, TypeVar - -from reactivex.observable import Observable - -from dimos.msgs.geometry_msgs import Quaternion, Transform -from dimos.msgs.sensor_msgs import CameraInfo -from dimos.msgs.sensor_msgs.Image import Image -from dimos.protocol.service import Configurable # type: ignore[attr-defined] - -OPTICAL_ROTATION = Quaternion(-0.5, 0.5, -0.5, 0.5) - - -class CameraConfig(Protocol): - frame_id_prefix: str | None - width: int - height: int - fps: int | float - - -CameraConfigT = TypeVar("CameraConfigT", bound=CameraConfig) - - -class CameraHardware(ABC, Configurable[CameraConfigT], Generic[CameraConfigT]): - @abstractmethod - def image_stream(self) -> Observable[Image]: - pass - - @property - @abstractmethod - def camera_info(self) -> CameraInfo: - pass - - -class DepthCameraConfig(CameraConfig): - """Protocol for depth camera configuration.""" - - camera_name: str - base_frame_id: str - base_transform: Transform | None - align_depth_to_color: bool - enable_depth: bool - enable_pointcloud: bool - pointcloud_fps: float - camera_info_fps: float - - -class DepthCameraHardware(ABC): - """Abstract class for depth camera modules (RealSense, ZED, etc.).""" - - config: DepthCameraConfig - - @abstractmethod - def get_color_camera_info(self) -> CameraInfo | None: - """Get color camera intrinsics.""" - pass - - @abstractmethod - def get_depth_camera_info(self) -> CameraInfo | None: - """Get depth camera intrinsics.""" - pass - - @abstractmethod - def get_depth_scale(self) -> float: - """Get the depth scale factor (meters per unit).""" - pass - - @property - @abstractmethod - def _camera_link(self) -> str: - pass - - @property - @abstractmethod - def _color_frame(self) -> str: - pass - - @property - @abstractmethod - def _color_optical_frame(self) -> str: - pass - - @property - @abstractmethod - def _depth_frame(self) -> str: - pass - - @property - @abstractmethod - def _depth_optical_frame(self) -> str: - pass diff --git a/dimos/hardware/sensors/camera/test_webcam.py b/dimos/hardware/sensors/camera/test_webcam.py deleted file mode 100644 index e40a73acc9..0000000000 --- a/dimos/hardware/sensors/camera/test_webcam.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos import core -from dimos.hardware.sensors.camera import zed -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.hardware.sensors.camera.webcam import Webcam -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image - - -@pytest.fixture -def dimos(): - dimos_instance = core.start(1) - yield dimos_instance - dimos_instance.stop() - - -@pytest.mark.tool -def test_streaming_single(dimos) -> None: - camera = dimos.deploy( - CameraModule, - transform=Transform( - translation=Vector3(0.05, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - camera_index=0, - fps=0.0, # full speed but set something to test sharpness barrier - camera_info=zed.CameraInfo.SingleWebcam, - ), - ) - - camera.color_image.transport = core.LCMTransport("/color_image", Image) - camera.camera_info.transport = core.LCMTransport("/camera_info", CameraInfo) - camera.start() - - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - camera.stop() - dimos.stop() diff --git a/dimos/hardware/sensors/camera/webcam.py b/dimos/hardware/sensors/camera/webcam.py deleted file mode 100644 index 51199624fe..0000000000 --- a/dimos/hardware/sensors/camera/webcam.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass, field -from functools import cache -import threading -import time -from typing import Literal - -import cv2 -from reactivex import create -from reactivex.observable import Observable - -from dimos.hardware.sensors.camera.spec import CameraConfig, CameraHardware -from dimos.msgs.sensor_msgs import CameraInfo, Image -from dimos.msgs.sensor_msgs.Image import ImageFormat -from dimos.utils.reactive import backpressure - - -@dataclass -class WebcamConfig(CameraConfig): - camera_index: int = 0 # /dev/videoN - width: int = 640 - height: int = 480 - fps: float = 15.0 - camera_info: CameraInfo = field(default_factory=CameraInfo) - frame_id_prefix: str | None = None - stereo_slice: Literal["left", "right"] | None = None # For stereo cameras - - -class Webcam(CameraHardware[WebcamConfig]): - default_config = WebcamConfig - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._capture = None - self._capture_thread = None - self._stop_event = threading.Event() - self._observer = None - - @cache - def image_stream(self) -> Observable[Image]: - """Create an observable that starts/stops camera on subscription""" - - def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - # Store the observer so emit() can use it - self._observer = observer - - # Start the camera when someone subscribes - try: - self.start() # type: ignore[no-untyped-call] - except Exception as e: - observer.on_error(e) - return - - # Return a dispose function to stop camera when unsubscribed - def dispose() -> None: - self._observer = None - self.stop() - - return dispose - - return backpressure(create(subscribe)) - - def start(self): # type: ignore[no-untyped-def] - if self._capture_thread and self._capture_thread.is_alive(): - return - - # Open the video capture - self._capture = cv2.VideoCapture(self.config.camera_index) # type: ignore[assignment] - if not self._capture.isOpened(): # type: ignore[attr-defined] - raise RuntimeError(f"Failed to open camera {self.config.camera_index}") - - # Set camera properties - self._capture.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.width) # type: ignore[attr-defined] - self._capture.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.height) # type: ignore[attr-defined] - - # Clear stop event and start the capture thread - self._stop_event.clear() - self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) # type: ignore[assignment] - self._capture_thread.start() # type: ignore[attr-defined] - - def stop(self) -> None: - """Stop capturing frames""" - # Signal thread to stop - self._stop_event.set() - - # Wait for thread to finish - if self._capture_thread and self._capture_thread.is_alive(): - timeout = 0.1 if self.config.fps <= 0 else (1.0 / self.config.fps) + 0.1 - self._capture_thread.join(timeout=timeout) - - # Release the capture - if self._capture: - self._capture.release() - self._capture = None - - def _frame(self, frame: str): # type: ignore[no-untyped-def] - if not self.config.frame_id_prefix: - return frame - else: - return f"{self.config.frame_id_prefix}/{frame}" - - def capture_frame(self) -> Image: - # Read frame - ret, frame = self._capture.read() # type: ignore[attr-defined] - if not ret: - raise RuntimeError(f"Failed to read frame from camera {self.config.camera_index}") - - # Convert BGR to RGB (OpenCV uses BGR by default) - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # Create Image message - # Using Image.from_numpy() since it's designed for numpy arrays - # Setting format to RGB since we converted from BGR->RGB above - image = Image.from_numpy( - frame_rgb, - format=ImageFormat.RGB, # We converted to RGB above - frame_id=self._frame("camera_optical"), # Standard frame ID for camera images - ts=time.time(), # Current timestamp - ) - - if self.config.stereo_slice in ("left", "right"): - half_width = image.width // 2 - if self.config.stereo_slice == "left": - image = image.crop(0, 0, half_width, image.height) - else: - image = image.crop(half_width, 0, half_width, image.height) - - return image - - def _capture_loop(self) -> None: - """Capture frames at the configured frequency""" - frame_interval = 0.0 if self.config.fps <= 0 else 1.0 / self.config.fps - next_frame_time = time.time() - - while self._capture and not self._stop_event.is_set(): - image = self.capture_frame() - - # Emit the image to the observer only if not stopping - if self._observer and not self._stop_event.is_set(): - self._observer.on_next(image) - - # Wait for next frame time or until stopped - if frame_interval <= 0: - continue - next_frame_time += frame_interval - sleep_time = next_frame_time - time.time() - if sleep_time > 0: - # Use event.wait so we can be interrupted by stop - if self._stop_event.wait(timeout=sleep_time): - break # Stop was requested - else: - # We're running behind, reset timing - next_frame_time = time.time() - - @property - def camera_info(self) -> CameraInfo: - return self.config.camera_info - - def emit(self, image: Image) -> None: ... diff --git a/dimos/hardware/sensors/camera/zed/__init__.py b/dimos/hardware/sensors/camera/zed/__init__.py deleted file mode 100644 index f8e73273bf..0000000000 --- a/dimos/hardware/sensors/camera/zed/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ZED camera hardware interfaces.""" - -from pathlib import Path - -from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider - -# Check if ZED SDK is available -try: - import pyzed.sl as sl # noqa: F401 - - HAS_ZED_SDK = True -except ImportError: - HAS_ZED_SDK = False - -# Only import ZED classes if SDK is available -if HAS_ZED_SDK: - from dimos.hardware.sensors.camera.zed.camera import ZEDCamera, ZEDModule, zed_camera -else: - # Provide stub classes when SDK is not available - class ZEDCamera: # type: ignore[no-redef] - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - raise ImportError( - "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." - ) - - class ZEDModule: # type: ignore[no-redef] - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - raise ImportError( - "ZED SDK not installed. Please install pyzed package to use ZED camera functionality." - ) - - def zed_camera(*args: object, **kwargs: object) -> None: # type: ignore[no-redef] - raise ModuleNotFoundError( - "ZED SDK not installed. Please install pyzed package to use ZED camera functionality.", - name="pyzed", - ) - - -# Set up camera calibration provider (always available) -CALIBRATION_DIR = Path(__file__).parent -CameraInfo = CalibrationProvider(CALIBRATION_DIR) - -__all__ = [ - "HAS_ZED_SDK", - "CameraInfo", - "ZEDCamera", - "ZEDModule", - "zed_camera", -] diff --git a/dimos/hardware/sensors/camera/zed/camera.py b/dimos/hardware/sensors/camera/zed/camera.py deleted file mode 100644 index 171b706ff9..0000000000 --- a/dimos/hardware/sensors/camera/zed/camera.py +++ /dev/null @@ -1,536 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import atexit -from dataclasses import dataclass, field -import threading -import time -from typing import TYPE_CHECKING - -import cv2 -import pyzed.sl as sl -import reactivex as rx - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera.spec import ( - OPTICAL_ROTATION, - DepthCameraConfig, - DepthCameraHardware, -) -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.spec import perception -from dimos.utils.reactive import backpressure - -if TYPE_CHECKING: - from dimos.core.stream import Out - - -def default_base_transform() -> Transform: - """Default identity transform for camera mounting.""" - return Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - - -@dataclass -class ZEDCameraConfig(ModuleConfig, DepthCameraConfig): - width: int = 1280 - height: int = 720 - fps: int = 15 - camera_name: str = "camera" - base_frame_id: str = "base_link" - base_transform: Transform | None = field(default_factory=default_base_transform) - align_depth_to_color: bool = True - enable_depth: bool = True - enable_pointcloud: bool = False - pointcloud_fps: float = 5.0 - camera_info_fps: float = 1.0 - camera_id: int = 0 - serial_number: int | str | None = None - resolution: str | None = None - depth_mode: str | sl.DEPTH_MODE = "NEURAL" - enable_fill_mode: bool = False - enable_tracking: bool = True - enable_imu_fusion: bool = True - enable_pose_smoothing: bool = True - enable_area_memory: bool = False - set_floor_as_origin: bool = True - world_frame: str = "world" - - -class ZEDCamera(DepthCameraHardware, Module, perception.DepthCamera): - color_image: Out[Image] - depth_image: Out[Image] - pointcloud: Out[PointCloud2] - camera_info: Out[CameraInfo] - depth_camera_info: Out[CameraInfo] - - config: ZEDCameraConfig - default_config = ZEDCameraConfig - - @property - def _camera_link(self) -> str: - return f"{self.config.camera_name}_link" - - @property - def _color_frame(self) -> str: - return f"{self.config.camera_name}_color_frame" - - @property - def _color_optical_frame(self) -> str: - return f"{self.config.camera_name}_color_optical_frame" - - @property - def _depth_frame(self) -> str: - return f"{self.config.camera_name}_depth_frame" - - @property - def _depth_optical_frame(self) -> str: - return f"{self.config.camera_name}_depth_optical_frame" - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._zed: sl.Camera | None = None - self._init_params: sl.InitParameters | None = None - self._runtime_params: sl.RuntimeParameters | None = None - self._running = False - self._thread: threading.Thread | None = None - self._color_camera_info: CameraInfo | None = None - self._depth_camera_info: CameraInfo | None = None - self._depth_scale: float = 1.0 - self._camera_link_to_color_extrinsics: sl.Transform - self._latest_color_img: Image | None = None - self._latest_depth_img: Image | None = None - self._pointcloud_lock = threading.Lock() - self._image_left: sl.Mat | None = None - self._depth_map: sl.Mat | None = None - self._pose: sl.Pose | None = None - self._tracking_enabled = False - self._stream_width = self.config.width - self._stream_height = self.config.height - self._sl_camera_info: sl.CameraInformation | None = None - - def _publish_camera_info(self) -> None: - ts = time.time() - if self._color_camera_info: - self._color_camera_info.ts = ts - self.camera_info.publish(self._color_camera_info) - if self._depth_camera_info: - self._depth_camera_info.ts = ts - self.depth_camera_info.publish(self._depth_camera_info) - - @rpc - def start(self) -> None: - self._zed = sl.Camera() - self._init_params = sl.InitParameters() - if self.config.resolution: - self._init_params.camera_resolution = getattr(sl.RESOLUTION, self.config.resolution) - else: - self._init_params.camera_resolution = sl.RESOLUTION.HD720 - self._init_params.camera_fps = self.config.fps - if isinstance(self.config.depth_mode, sl.DEPTH_MODE): - self._init_params.depth_mode = self.config.depth_mode - else: - self._init_params.depth_mode = getattr(sl.DEPTH_MODE, self.config.depth_mode) - self._init_params.coordinate_system = sl.COORDINATE_SYSTEM.RIGHT_HANDED_Z_UP_X_FWD - self._init_params.coordinate_units = sl.UNIT.METER - if self.config.serial_number is not None: - self._init_params.set_from_serial_number(int(self.config.serial_number)) - else: - self._init_params.set_from_camera_id(self.config.camera_id) - - err = self._zed.open(self._init_params) - if err != sl.ERROR_CODE.SUCCESS: - self._zed = None - raise RuntimeError(f"Failed to open ZED camera: {err}") - - self._runtime_params = sl.RuntimeParameters() - self._runtime_params.enable_fill_mode = self.config.enable_fill_mode - self._image_left = sl.Mat() - self._depth_map = sl.Mat() - self._pose = sl.Pose() - - self._sl_camera_info = self._zed.get_camera_information() - if self._sl_camera_info is not None: - self._stream_width = self._sl_camera_info.camera_configuration.resolution.width - self._stream_height = self._sl_camera_info.camera_configuration.resolution.height - - self._build_camera_info() - self._get_extrinsics() - - if self.config.enable_tracking: - self._enable_tracking() - - interval_sec = 1.0 / self.config.camera_info_fps - self._disposables.add( - rx.interval(interval_sec).subscribe( - on_next=lambda _: self._publish_camera_info(), - on_error=lambda e: print(f"CameraInfo error: {e}"), - ) - ) - - self._running = True - self._thread = threading.Thread(target=self._capture_loop, daemon=True) - self._thread.start() - - if self.config.enable_pointcloud and self.config.enable_depth: - interval_sec = 1.0 / self.config.pointcloud_fps - self._disposables.add( - backpressure(rx.interval(interval_sec)).subscribe( - on_next=lambda _: self._generate_pointcloud(), - on_error=lambda e: print(f"Pointcloud error: {e}"), - ) - ) - - def _build_camera_info(self) -> None: - if self._sl_camera_info is None: - return - calib = self._sl_camera_info.camera_configuration.calibration_parameters - left_cam = calib.left_cam - - self._color_camera_info = self._intrinsics_to_camera_info( - left_cam, self._color_optical_frame - ) - - if self.config.enable_depth: - depth_frame = ( - self._color_optical_frame - if self.config.align_depth_to_color - else self._depth_optical_frame - ) - self._depth_camera_info = self._intrinsics_to_camera_info(left_cam, depth_frame) - - def _intrinsics_to_camera_info( - self, intrinsics: sl.CameraParameters, frame_id: str - ) -> CameraInfo: - fx, fy = intrinsics.fx, intrinsics.fy - cx, cy = intrinsics.cx, intrinsics.cy - - K = [fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0] - P = [fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0] - D = list(intrinsics.disto) - - return CameraInfo( - height=self._stream_height, - width=self._stream_width, - distortion_model="plumb_bob", - D=D, - K=K, - P=P, - frame_id=frame_id, - ) - - def _get_extrinsics(self) -> None: - if self._sl_camera_info is None: - return - sensors_config = self._sl_camera_info.sensors_configuration - # camera_imu_transform gives the transform from IMU (body center) to left camera - self._camera_link_to_color_extrinsics = sensors_config.camera_imu_transform - - def _extrinsics_to_transform( - self, - extrinsics: sl.Transform, - frame_id: str, - child_frame_id: str, - ts: float, - ) -> Transform: - translation = extrinsics.get_translation().get() - quat = extrinsics.get_orientation().get() # [x, y, z, w] - return Transform( - translation=Vector3(*translation), - rotation=Quaternion(quat[0], quat[1], quat[2], quat[3]), - frame_id=frame_id, - child_frame_id=child_frame_id, - ts=ts, - ) - - def _enable_tracking(self) -> None: - if self._zed is None: - return - tracking_params = sl.PositionalTrackingParameters() - tracking_params.enable_area_memory = self.config.enable_area_memory - tracking_params.enable_pose_smoothing = self.config.enable_pose_smoothing - tracking_params.enable_imu_fusion = self.config.enable_imu_fusion - tracking_params.set_floor_as_origin = self.config.set_floor_as_origin - err = self._zed.enable_positional_tracking(tracking_params) - if err != sl.ERROR_CODE.SUCCESS: - print(f"Failed to enable positional tracking: {err}") - self._tracking_enabled = False - return - self._tracking_enabled = True - - def _capture_loop(self) -> None: - while self._running and self._zed is not None: - try: - err = self._zed.grab(self._runtime_params) - except Exception: - break - - if err != sl.ERROR_CODE.SUCCESS: - if not self._running: - break - time.sleep(0.001) - continue - - ts = time.time() - - color_img = None - if self._image_left is not None: - self._zed.retrieve_image(self._image_left, sl.VIEW.LEFT) - color_data = self._image_left.get_data() - if color_data.ndim == 3 and color_data.shape[2] == 4: - color_data = color_data[:, :, :3] - color_data = cv2.cvtColor(color_data, cv2.COLOR_BGR2RGB) - color_img = Image( - data=color_data, - format=ImageFormat.RGB, - frame_id=self._color_optical_frame, - ts=ts, - ) - self.color_image.publish(color_img) - - depth_img = None - if self.config.enable_depth and self._depth_map is not None: - self._zed.retrieve_measure(self._depth_map, sl.MEASURE.DEPTH) - depth_data = self._depth_map.get_data() - if depth_data.ndim == 3: - depth_data = depth_data[:, :, 0] - depth_frame_id = ( - self._color_optical_frame - if self.config.align_depth_to_color - else self._depth_optical_frame - ) - depth_img = Image( - data=depth_data, - format=ImageFormat.DEPTH, - frame_id=depth_frame_id, - ts=ts, - ) - self.depth_image.publish(depth_img) - - if self.config.enable_pointcloud and color_img is not None and depth_img is not None: - with self._pointcloud_lock: - self._latest_color_img = color_img - self._latest_depth_img = depth_img - - self._publish_tf(ts) - - def _tracking_transform(self, ts: float) -> Transform | None: - if not self._tracking_enabled or self._zed is None or self._pose is None: - return None - state = self._zed.get_position(self._pose, sl.REFERENCE_FRAME.WORLD) - if state != sl.POSITIONAL_TRACKING_STATE.OK: - return None - - translation = self._pose.get_translation().get().tolist() - rotation = self._pose.get_orientation().get().tolist() - world_to_camera = Transform( - translation=Vector3(*translation), - rotation=Quaternion(*rotation), - frame_id=self.config.world_frame, - child_frame_id=self._camera_link, - ts=ts, - ) - if self.config.base_transform is None: - return world_to_camera - - base_to_camera = Transform( - translation=self.config.base_transform.translation, - rotation=self.config.base_transform.rotation, - frame_id=self.config.base_frame_id, - child_frame_id=self._camera_link, - ts=ts, - ) - camera_to_base = base_to_camera.inverse() - world_to_base = world_to_camera + camera_to_base - world_to_base.frame_id = self.config.world_frame - world_to_base.child_frame_id = self.config.base_frame_id - world_to_base.ts = ts - return world_to_base - - def _publish_tf(self, ts: float) -> None: - transforms = [] - - if self.config.base_transform is not None: - base_to_camera = Transform( - translation=self.config.base_transform.translation, - rotation=self.config.base_transform.rotation, - frame_id=self.config.base_frame_id, - child_frame_id=self._camera_link, - ts=ts, - ) - transforms.append(base_to_camera) - - # camera_imu_transform is IMU -> left_camera (coordinate transform), - # we need to invert to get the pose of left camera in camera_link frame - camera_link_to_depth = self._extrinsics_to_transform( - self._camera_link_to_color_extrinsics, - self._camera_link, - self._depth_frame, - ts, - ).inverse() - camera_link_to_depth.frame_id = self._camera_link - camera_link_to_depth.child_frame_id = self._depth_frame - transforms.append(camera_link_to_depth) - - depth_to_depth_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=OPTICAL_ROTATION, - frame_id=self._depth_frame, - child_frame_id=self._depth_optical_frame, - ts=ts, - ) - transforms.append(depth_to_depth_optical) - - color_tf = self._extrinsics_to_transform( - self._camera_link_to_color_extrinsics, - self._camera_link, - self._color_frame, - ts, - ).inverse() - color_tf.frame_id = self._camera_link - color_tf.child_frame_id = self._color_frame - transforms.append(color_tf) - - color_to_color_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=OPTICAL_ROTATION, - frame_id=self._color_frame, - child_frame_id=self._color_optical_frame, - ts=ts, - ) - transforms.append(color_to_color_optical) - - tracking_tf = self._tracking_transform(ts) - if tracking_tf is not None: - transforms.append(tracking_tf) - - self.tf.publish(*transforms) - - def _generate_pointcloud(self) -> None: - with self._pointcloud_lock: - color_img = self._latest_color_img - depth_img = self._latest_depth_img - - if color_img is None or depth_img is None or self._color_camera_info is None: - return - - try: - pcd = PointCloud2.from_rgbd( - color_image=color_img, - depth_image=depth_img, - camera_info=self._color_camera_info, - depth_scale=self._depth_scale, - ) - pcd = pcd.voxel_downsample(0.005) - self.pointcloud.publish(pcd) - except Exception as e: - print(f"Pointcloud generation error: {e}") - - @rpc - def stop(self) -> None: - self._running = False - - if self._zed: - if self._tracking_enabled: - try: - self._zed.disable_positional_tracking() - except Exception: - pass - try: - self._zed.close() - except Exception: - pass - self._zed = None - - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=2.0) - if self._thread.is_alive(): - self._thread = None - - self._color_camera_info = None - self._depth_camera_info = None - self._latest_color_img = None - self._latest_depth_img = None - self._image_left = None - self._depth_map = None - self._pose = None - self._sl_camera_info = None - self._tracking_enabled = False - super().stop() - - @rpc - def get_color_camera_info(self) -> CameraInfo | None: - return self._color_camera_info - - @rpc - def get_depth_camera_info(self) -> CameraInfo | None: - return self._depth_camera_info - - @rpc - def get_depth_scale(self) -> float: - return self._depth_scale - - -def main() -> None: - dimos = ModuleCoordinator(n=2) - dimos.start() - - camera = dimos.deploy(ZEDCamera, enable_pointcloud=True, pointcloud_fps=5.0) # type: ignore[type-var] - foxglove_bridge = FoxgloveBridge() - foxglove_bridge.start() - - camera.color_image.transport = LCMTransport("/camera/color", Image) - camera.depth_image.transport = LCMTransport("/camera/depth", Image) - camera.pointcloud.transport = LCMTransport("/camera/pointcloud", PointCloud2) - camera.camera_info.transport = LCMTransport("/camera/color_info", CameraInfo) - camera.depth_camera_info.transport = LCMTransport("/camera/depth_info", CameraInfo) - - def cleanup() -> None: - try: - dimos.stop() - except Exception: - pass - - atexit.register(cleanup) - dimos.start_all_modules() - - try: - while True: - time.sleep(0.1) - except (KeyboardInterrupt, SystemExit): - pass - finally: - atexit.unregister(cleanup) - cleanup() - - -if __name__ == "__main__": - main() - - -ZEDModule = ZEDCamera -zed_camera = ZEDCamera.blueprint - -__all__ = ["ZEDCamera", "ZEDCameraConfig", "ZEDModule", "zed_camera"] diff --git a/dimos/hardware/sensors/camera/zed/single_webcam.yaml b/dimos/hardware/sensors/camera/zed/single_webcam.yaml deleted file mode 100644 index 1ce9457559..0000000000 --- a/dimos/hardware/sensors/camera/zed/single_webcam.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# for cv2.VideoCapture and cutting only half of the frame -image_width: 640 -image_height: 376 -camera_name: zed_webcam_single -camera_matrix: - rows: 3 - cols: 3 - data: [379.45267, 0. , 302.43516, - 0. , 380.67871, 228.00954, - 0. , 0. , 1. ] -distortion_model: plumb_bob -distortion_coefficients: - rows: 1 - cols: 5 - data: [-0.309435, 0.092185, -0.009059, 0.003708, 0.000000] -rectification_matrix: - rows: 3 - cols: 3 - data: [1., 0., 0., - 0., 1., 0., - 0., 0., 1.] -projection_matrix: - rows: 3 - cols: 4 - data: [291.12888, 0. , 304.94086, 0. , - 0. , 347.95022, 231.8885 , 0. , - 0. , 0. , 1. , 0. ] diff --git a/dimos/hardware/sensors/camera/zed/test_zed.py b/dimos/hardware/sensors/camera/zed/test_zed.py deleted file mode 100644 index 2d912553c6..0000000000 --- a/dimos/hardware/sensors/camera/zed/test_zed.py +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo - - -def test_zed_import_and_calibration_access() -> None: - """Test that zed module can be imported and calibrations accessed.""" - # Import zed module from camera - from dimos.hardware.sensors.camera import zed - - # Test that CameraInfo is accessible - assert hasattr(zed, "CameraInfo") - - # Test snake_case access - camera_info_snake = zed.CameraInfo.single_webcam - assert isinstance(camera_info_snake, CameraInfo) - assert camera_info_snake.width == 640 - assert camera_info_snake.height == 376 - assert camera_info_snake.distortion_model == "plumb_bob" - - # Test PascalCase access - camera_info_pascal = zed.CameraInfo.SingleWebcam - assert isinstance(camera_info_pascal, CameraInfo) - assert camera_info_pascal.width == 640 - assert camera_info_pascal.height == 376 - - # Verify both access methods return the same cached object - assert camera_info_snake is camera_info_pascal - - print("✓ ZED import and calibration access test passed!") diff --git a/dimos/hardware/sensors/fake_zed_module.py b/dimos/hardware/sensors/fake_zed_module.py deleted file mode 100644 index ec5613077d..0000000000 --- a/dimos/hardware/sensors/fake_zed_module.py +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -FakeZEDModule - Replays recorded ZED data for testing without hardware. -""" - -from dataclasses import dataclass -import functools -import logging - -from dimos_lcm.sensor_msgs import CameraInfo -import numpy as np - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import Out -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.protocol.tf import TF -from dimos.utils.logging_config import setup_logger -from dimos.utils.testing import TimedSensorReplay - -logger = setup_logger(level=logging.INFO) - - -@dataclass -class FakeZEDModuleConfig(ModuleConfig): - frame_id: str = "zed_camera" - - -class FakeZEDModule(Module[FakeZEDModuleConfig]): - """ - Fake ZED module that replays recorded data instead of real camera. - """ - - # Define LCM outputs (same as ZEDModule) - color_image: Out[Image] - depth_image: Out[Image] - camera_info: Out[CameraInfo] - pose: Out[PoseStamped] - - default_config = FakeZEDModuleConfig - config: FakeZEDModuleConfig - - def __init__(self, recording_path: str, **kwargs: object) -> None: - """ - Initialize FakeZEDModule with recording path. - - Args: - recording_path: Path to recorded data directory - """ - super().__init__(**kwargs) - - self.recording_path = recording_path - self._running = False - - # Initialize TF publisher - self.tf = TF() - - logger.info(f"FakeZEDModule initialized with recording: {self.recording_path}") - - @functools.cache - def _get_color_stream(self): # type: ignore[no-untyped-def] - """Get cached color image stream.""" - logger.info(f"Loading color image stream from {self.recording_path}/color") - - def image_autocast(x): # type: ignore[no-untyped-def] - """Convert raw numpy array to Image.""" - if isinstance(x, np.ndarray): - return Image(data=x, format=ImageFormat.RGB) - elif isinstance(x, Image): - return x - return x - - color_replay = TimedSensorReplay(f"{self.recording_path}/color", autocast=image_autocast) - return color_replay.stream() - - @functools.cache - def _get_depth_stream(self): # type: ignore[no-untyped-def] - """Get cached depth image stream.""" - logger.info(f"Loading depth image stream from {self.recording_path}/depth") - - def depth_autocast(x): # type: ignore[no-untyped-def] - """Convert raw numpy array to depth Image.""" - if isinstance(x, np.ndarray): - # Depth images are float32 - return Image(data=x, format=ImageFormat.DEPTH) - elif isinstance(x, Image): - return x - return x - - depth_replay = TimedSensorReplay(f"{self.recording_path}/depth", autocast=depth_autocast) - return depth_replay.stream() - - @functools.cache - def _get_pose_stream(self): # type: ignore[no-untyped-def] - """Get cached pose stream.""" - logger.info(f"Loading pose stream from {self.recording_path}/pose") - - def pose_autocast(x): # type: ignore[no-untyped-def] - """Convert raw pose dict to PoseStamped.""" - if isinstance(x, dict): - import time - - return PoseStamped( - position=x.get("position", [0, 0, 0]), - orientation=x.get("rotation", [0, 0, 0, 1]), - ts=time.time(), - ) - elif isinstance(x, PoseStamped): - return x - return x - - pose_replay = TimedSensorReplay(f"{self.recording_path}/pose", autocast=pose_autocast) - return pose_replay.stream() - - @functools.cache - def _get_camera_info_stream(self): # type: ignore[no-untyped-def] - """Get cached camera info stream.""" - logger.info(f"Loading camera info stream from {self.recording_path}/camera_info") - - def camera_info_autocast(x): # type: ignore[no-untyped-def] - """Convert raw camera info dict to CameraInfo message.""" - if isinstance(x, dict): - # Extract calibration parameters - left_cam = x.get("left_cam", {}) - resolution = x.get("resolution", {}) - - # Create CameraInfo message - header = Header(self.frame_id) - - # Create camera matrix K (3x3) - K = [ - left_cam.get("fx", 0), - 0, - left_cam.get("cx", 0), - 0, - left_cam.get("fy", 0), - left_cam.get("cy", 0), - 0, - 0, - 1, - ] - - # Distortion coefficients - D = [ - left_cam.get("k1", 0), - left_cam.get("k2", 0), - left_cam.get("p1", 0), - left_cam.get("p2", 0), - left_cam.get("k3", 0), - ] - - # Identity rotation matrix - R = [1, 0, 0, 0, 1, 0, 0, 0, 1] - - # Projection matrix P (3x4) - P = [ - left_cam.get("fx", 0), - 0, - left_cam.get("cx", 0), - 0, - 0, - left_cam.get("fy", 0), - left_cam.get("cy", 0), - 0, - 0, - 0, - 1, - 0, - ] - - return CameraInfo( - D_length=len(D), - header=header, - height=resolution.get("height", 0), - width=resolution.get("width", 0), - distortion_model="plumb_bob", - D=D, - K=K, - R=R, - P=P, - binning_x=0, - binning_y=0, - ) - elif isinstance(x, CameraInfo): - return x - return x - - info_replay = TimedSensorReplay( - f"{self.recording_path}/camera_info", autocast=camera_info_autocast - ) - return info_replay.stream() - - @rpc - def start(self) -> None: - """Start replaying recorded data.""" - super().start() - - if self._running: - logger.warning("FakeZEDModule already running") - return - - logger.info("Starting FakeZEDModule replay...") - - self._running = True - - # Subscribe to all streams and publish - try: - # Color image stream - unsub = self._get_color_stream().subscribe( - lambda msg: self.color_image.publish(msg) if self._running else None - ) - self._disposables.add(unsub) - logger.info("Started color image replay stream") - except Exception as e: - logger.warning(f"Color image stream not available: {e}") - - try: - # Depth image stream - unsub = self._get_depth_stream().subscribe( - lambda msg: self.depth_image.publish(msg) if self._running else None - ) - self._disposables.add(unsub) - logger.info("Started depth image replay stream") - except Exception as e: - logger.warning(f"Depth image stream not available: {e}") - - try: - # Pose stream - unsub = self._get_pose_stream().subscribe( - lambda msg: self._publish_pose(msg) if self._running else None - ) - self._disposables.add(unsub) - logger.info("Started pose replay stream") - except Exception as e: - logger.warning(f"Pose stream not available: {e}") - - try: - # Camera info stream - unsub = self._get_camera_info_stream().subscribe( - lambda msg: self.camera_info.publish(msg) if self._running else None - ) - self._disposables.add(unsub) - logger.info("Started camera info replay stream") - except Exception as e: - logger.warning(f"Camera info stream not available: {e}") - - logger.info("FakeZEDModule replay started") - - @rpc - def stop(self) -> None: - if not self._running: - return - - self._running = False - - super().stop() - - def _publish_pose(self, msg) -> None: # type: ignore[no-untyped-def] - """Publish pose and TF transform.""" - if msg: - self.pose.publish(msg) - - # Publish TF transform from world to camera - import time - - from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 - - transform = Transform( - translation=Vector3(*msg.position), - rotation=Quaternion(*msg.orientation), - frame_id="world", - child_frame_id=self.frame_id, - ts=time.time(), - ) - self.tf.publish(transform) diff --git a/dimos/hardware/sensors/lidar/__init__.py b/dimos/hardware/sensors/lidar/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/hardware/sensors/lidar/common/dimos_native_module.hpp b/dimos/hardware/sensors/lidar/common/dimos_native_module.hpp deleted file mode 100644 index cdd5d85914..0000000000 --- a/dimos/hardware/sensors/lidar/common/dimos_native_module.hpp +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// Lightweight header-only helper for dimos NativeModule C++ binaries. -// Parses -- CLI args passed by the Python NativeModule wrapper. - -#pragma once - -#include -#include -#include -#include - -#include "std_msgs/Header.hpp" -#include "std_msgs/Time.hpp" - -namespace dimos { - -class NativeModule { -public: - NativeModule(int argc, char** argv) { - for (int i = 1; i < argc; ++i) { - std::string arg(argv[i]); - if (arg.size() > 2 && arg[0] == '-' && arg[1] == '-' && i + 1 < argc) { - args_[arg.substr(2)] = argv[++i]; - } - } - } - - /// Get the full LCM channel string for a declared port. - /// Format is "#", e.g. "/pointcloud#sensor_msgs.PointCloud2". - /// This is the exact channel name used by Python LCMTransport subscribers. - const std::string& topic(const std::string& port) const { - auto it = args_.find(port); - if (it == args_.end()) { - throw std::runtime_error("NativeModule: no topic for port '" + port + "'"); - } - return it->second; - } - - /// Get a string arg value, or a default if not present. - std::string arg(const std::string& key, const std::string& default_val = "") const { - auto it = args_.find(key); - return it != args_.end() ? it->second : default_val; - } - - /// Get a float arg value, or a default if not present. - float arg_float(const std::string& key, float default_val = 0.0f) const { - auto it = args_.find(key); - return it != args_.end() ? std::stof(it->second) : default_val; - } - - /// Get an int arg value, or a default if not present. - int arg_int(const std::string& key, int default_val = 0) const { - auto it = args_.find(key); - return it != args_.end() ? std::stoi(it->second) : default_val; - } - - /// Check if a port/arg was provided. - bool has(const std::string& key) const { - return args_.count(key) > 0; - } - -private: - std::map args_; -}; - -/// Convert seconds (double) to a ROS-style Time message. -inline std_msgs::Time time_from_seconds(double t) { - std_msgs::Time ts; - ts.sec = static_cast(t); - ts.nsec = static_cast((t - ts.sec) * 1e9); - return ts; -} - -/// Build a stamped Header with auto-incrementing sequence number. -inline std_msgs::Header make_header(const std::string& frame_id, double ts) { - static std::atomic seq{0}; - std_msgs::Header h; - h.seq = seq.fetch_add(1, std::memory_order_relaxed); - h.stamp = time_from_seconds(ts); - h.frame_id = frame_id; - return h; -} - -} // namespace dimos diff --git a/dimos/hardware/sensors/lidar/common/livox_sdk_config.hpp b/dimos/hardware/sensors/lidar/common/livox_sdk_config.hpp deleted file mode 100644 index d7101c850e..0000000000 --- a/dimos/hardware/sensors/lidar/common/livox_sdk_config.hpp +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// Shared Livox SDK2 configuration utilities for dimos native modules. -// Used by both mid360_native and fastlio2_native. - -#pragma once - -#include -#include - -#include -#include - -#include -#include -#include - -namespace livox_common { - -// Gravity constant for converting accelerometer data from g to m/s^2 -inline constexpr double GRAVITY_MS2 = 9.80665; - -// Livox data_type values (not provided as named constants in SDK2 header) -inline constexpr uint8_t DATA_TYPE_IMU = 0x00; -inline constexpr uint8_t DATA_TYPE_CARTESIAN_HIGH = 0x01; -inline constexpr uint8_t DATA_TYPE_CARTESIAN_LOW = 0x02; - -// SDK network port configuration for Livox Mid-360 -struct SdkPorts { - int cmd_data = 56100; - int push_msg = 56200; - int point_data = 56300; - int imu_data = 56400; - int log_data = 56500; - int host_cmd_data = 56101; - int host_push_msg = 56201; - int host_point_data = 56301; - int host_imu_data = 56401; - int host_log_data = 56501; -}; - -// Write Livox SDK JSON config to an in-memory file (memfd_create). -// Returns {fd, path} — caller must close(fd) after LivoxLidarSdkInit reads it. -inline std::pair write_sdk_config(const std::string& host_ip, - const std::string& lidar_ip, - const SdkPorts& ports) { - int fd = memfd_create("livox_sdk_config", 0); - if (fd < 0) { - perror("memfd_create"); - return {-1, ""}; - } - - FILE* fp = fdopen(fd, "w"); - if (!fp) { - perror("fdopen"); - close(fd); - return {-1, ""}; - } - - fprintf(fp, - "{\n" - " \"MID360\": {\n" - " \"lidar_net_info\": {\n" - " \"cmd_data_port\": %d,\n" - " \"push_msg_port\": %d,\n" - " \"point_data_port\": %d,\n" - " \"imu_data_port\": %d,\n" - " \"log_data_port\": %d\n" - " },\n" - " \"host_net_info\": [\n" - " {\n" - " \"host_ip\": \"%s\",\n" - " \"multicast_ip\": \"224.1.1.5\",\n" - " \"cmd_data_port\": %d,\n" - " \"push_msg_port\": %d,\n" - " \"point_data_port\": %d,\n" - " \"imu_data_port\": %d,\n" - " \"log_data_port\": %d\n" - " }\n" - " ]\n" - " }\n" - "}\n", - ports.cmd_data, ports.push_msg, ports.point_data, - ports.imu_data, ports.log_data, - host_ip.c_str(), - ports.host_cmd_data, ports.host_push_msg, ports.host_point_data, - ports.host_imu_data, ports.host_log_data); - fflush(fp); // flush but don't fclose — that would close fd - - char path[64]; - snprintf(path, sizeof(path), "/proc/self/fd/%d", fd); - return {fd, path}; -} - -// Initialize Livox SDK from in-memory config. -// Returns true on success. Handles fd lifecycle internally. -inline bool init_livox_sdk(const std::string& host_ip, - const std::string& lidar_ip, - const SdkPorts& ports) { - auto [fd, path] = write_sdk_config(host_ip, lidar_ip, ports); - if (fd < 0) { - fprintf(stderr, "Error: failed to write SDK config\n"); - return false; - } - - bool ok = LivoxLidarSdkInit(path.c_str(), host_ip.c_str()); - close(fd); - - if (!ok) { - fprintf(stderr, "Error: LivoxLidarSdkInit failed\n"); - } - return ok; -} - -} // namespace livox_common diff --git a/dimos/hardware/sensors/lidar/fastlio2/__init__.py b/dimos/hardware/sensors/lidar/fastlio2/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/avia.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/avia.yaml deleted file mode 100644 index 8447b64658..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/avia.yaml +++ /dev/null @@ -1,35 +0,0 @@ -common: - lid_topic: "/livox/lidar" - imu_topic: "/livox/imu" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 1 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 6 - blind: 4 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 90 - det_range: 450.0 - extrinsic_est_en: false # true: enable the online estimation of IMU-LiDAR extrinsic - extrinsic_T: [ 0.04165, 0.02326, -0.0284 ] - extrinsic_R: [ 1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/horizon.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/horizon.yaml deleted file mode 100644 index 43db0c3bff..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/horizon.yaml +++ /dev/null @@ -1,35 +0,0 @@ -common: - lid_topic: "/livox/lidar" - imu_topic: "/livox/imu" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 1 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 6 - blind: 4 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 100 - det_range: 260.0 - extrinsic_est_en: true # true: enable the online estimation of IMU-LiDAR extrinsic - extrinsic_T: [ 0.05512, 0.02226, -0.0297 ] - extrinsic_R: [ 1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/marsim.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/marsim.yaml deleted file mode 100644 index ad6c89121a..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/marsim.yaml +++ /dev/null @@ -1,35 +0,0 @@ -common: - lid_topic: "/quad0_pcl_render_node/sensor_cloud" - imu_topic: "/quad_0/imu" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 4 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 4 - blind: 0.5 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 90 - det_range: 50.0 - extrinsic_est_en: false # true: enable the online estimation of IMU-LiDAR extrinsic - extrinsic_T: [ -0.0, -0.0, 0.0 ] - extrinsic_R: [ 1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/mid360.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/mid360.yaml deleted file mode 100644 index 512047ee48..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/mid360.yaml +++ /dev/null @@ -1,35 +0,0 @@ -common: - lid_topic: "/livox/lidar" - imu_topic: "/livox/imu" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 1 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 4 - blind: 0.5 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 360 - det_range: 100.0 - extrinsic_est_en: false # true: enable the online estimation of IMU-LiDAR extrinsic - extrinsic_T: [ -0.011, -0.02329, 0.04412 ] - extrinsic_R: [ 1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/ouster64.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/ouster64.yaml deleted file mode 100644 index 9d891bbeba..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/ouster64.yaml +++ /dev/null @@ -1,36 +0,0 @@ -common: - lid_topic: "/os_cloud_node/points" - imu_topic: "/os_cloud_node/imu" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 3 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 64 - timestamp_unit: 3 # 0-second, 1-milisecond, 2-microsecond, 3-nanosecond. - blind: 4 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 180 - det_range: 150.0 - extrinsic_est_en: false # true: enable the online estimation of IMU-LiDAR extrinsic - extrinsic_T: [ 0.0, 0.0, 0.0 ] - extrinsic_R: [1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/config/velodyne.yaml b/dimos/hardware/sensors/lidar/fastlio2/config/velodyne.yaml deleted file mode 100644 index 450eda48b8..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/config/velodyne.yaml +++ /dev/null @@ -1,37 +0,0 @@ -common: - lid_topic: "/velodyne_points" - imu_topic: "/imu/data" - time_sync_en: false # ONLY turn on when external time synchronization is really not possible - time_offset_lidar_to_imu: 0.0 # Time offset between lidar and IMU calibrated by other algorithms, e.g. LI-Init (can be found in README). - # This param will take effect no matter what time_sync_en is. So if the time offset is not known exactly, please set as 0.0 - -preprocess: - lidar_type: 2 # 1 for Livox serials LiDAR, 2 for Velodyne LiDAR, 3 for ouster LiDAR, - scan_line: 32 - scan_rate: 10 # only need to be set for velodyne, unit: Hz, - timestamp_unit: 2 # the unit of time/t field in the PointCloud2 rostopic: 0-second, 1-milisecond, 2-microsecond, 3-nanosecond. - blind: 2 - -mapping: - acc_cov: 0.1 - gyr_cov: 0.1 - b_acc_cov: 0.0001 - b_gyr_cov: 0.0001 - fov_degree: 180 - det_range: 100.0 - extrinsic_est_en: false # true: enable the online estimation of IMU-LiDAR extrinsic, - extrinsic_T: [ 0, 0, 0.28] - extrinsic_R: [ 1, 0, 0, - 0, 1, 0, - 0, 0, 1] - -publish: - path_en: false - scan_publish_en: true # false: close all the point cloud output - dense_publish_en: true # false: low down the points number in a global-frame point clouds scan. - scan_bodyframe_pub_en: true # true: output the point cloud scans in IMU-body-frame - -pcd_save: - pcd_save_en: true - interval: -1 # how many LiDAR frames saved in each pcd file; - # -1 : all frames will be saved in ONE pcd file, may lead to memory crash when having too much frames. diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/CMakeLists.txt b/dimos/hardware/sensors/lidar/fastlio2/cpp/CMakeLists.txt deleted file mode 100644 index 39f9f90443..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/CMakeLists.txt +++ /dev/null @@ -1,117 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(fastlio2_native CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3") - -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/result" CACHE PATH "" FORCE) -endif() - -# OpenMP for parallel processing -find_package(OpenMP QUIET) -if(OpenMP_CXX_FOUND) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") -endif() - -# MP defines (same logic as FAST-LIO) -message("CPU architecture: ${CMAKE_SYSTEM_PROCESSOR}") -if(CMAKE_SYSTEM_PROCESSOR MATCHES "(x86)|(X86)|(amd64)|(AMD64)") - include(ProcessorCount) - ProcessorCount(N) - if(N GREATER 4) - add_definitions(-DMP_EN -DMP_PROC_NUM=3) - elseif(N GREATER 3) - add_definitions(-DMP_EN -DMP_PROC_NUM=2) - else() - add_definitions(-DMP_PROC_NUM=1) - endif() -else() - add_definitions(-DMP_PROC_NUM=1) -endif() - -# Fetch dependencies -include(FetchContent) - -# FAST-LIO-NON-ROS (pass -DFASTLIO_DIR= or auto-fetched from GitHub) -if(NOT FASTLIO_DIR) - message(STATUS "FASTLIO_DIR not set, fetching FAST-LIO-NON-ROS from GitHub...") - FetchContent_Declare(fast_lio - GIT_REPOSITORY https://github.com/leshy/FAST-LIO-NON-ROS.git - GIT_TAG dimos-integration - GIT_SHALLOW TRUE - ) - FetchContent_MakeAvailable(fast_lio) - set(FASTLIO_DIR ${fast_lio_SOURCE_DIR}) -endif() - -# dimos-lcm C++ message headers -FetchContent_Declare(dimos_lcm - GIT_REPOSITORY https://github.com/dimensionalOS/dimos-lcm.git - GIT_TAG main - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(dimos_lcm) - -# LCM -find_package(PkgConfig REQUIRED) -pkg_check_modules(LCM REQUIRED lcm) - -# Eigen3 -find_package(Eigen3 REQUIRED) - -# PCL (only components we need — avoid full PCL which drags in VTK via io) -find_package(PCL 1.8 REQUIRED COMPONENTS common filters) - -# yaml-cpp (FAST-LIO config parsing — standard YAML format) -find_package(yaml-cpp REQUIRED) - -# Livox SDK2 (from nix or /usr/local fallback) -find_library(LIVOX_SDK livox_lidar_sdk_shared) -if(NOT LIVOX_SDK) - message(FATAL_ERROR "Livox SDK2 not found. Available via nix flake in lidar/livox/") -endif() -get_filename_component(LIVOX_SDK_LIB_DIR ${LIVOX_SDK} DIRECTORY) -get_filename_component(LIVOX_SDK_PREFIX ${LIVOX_SDK_LIB_DIR} DIRECTORY) -set(LIVOX_SDK_INCLUDE_DIR ${LIVOX_SDK_PREFIX}/include) - -add_executable(fastlio2_native - main.cpp - ${FASTLIO_DIR}/src/preprocess.cpp - ${FASTLIO_DIR}/include/ikd-Tree/ikd_Tree.cpp -) - -# Shared Livox common headers (livox_sdk_config.hpp etc.) -if(NOT LIVOX_COMMON_DIR) - set(LIVOX_COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../common) -endif() - -target_include_directories(fastlio2_native PRIVATE - ${FASTLIO_DIR}/include - ${FASTLIO_DIR}/src - ${dimos_lcm_SOURCE_DIR}/generated/cpp_lcm_msgs - ${LCM_INCLUDE_DIRS} - ${EIGEN3_INCLUDE_DIR} - ${PCL_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR} - ${LIVOX_COMMON_DIR} - ${LIVOX_SDK_INCLUDE_DIR} -) - -target_link_libraries(fastlio2_native PRIVATE - ${LCM_LIBRARIES} - ${LIVOX_SDK} - ${PCL_LIBRARIES} - yaml-cpp::yaml-cpp -) - -if(OpenMP_CXX_FOUND) - target_link_libraries(fastlio2_native PRIVATE OpenMP::OpenMP_CXX) -endif() - -target_link_directories(fastlio2_native PRIVATE - ${LCM_LIBRARY_DIRS} -) - -install(TARGETS fastlio2_native DESTINATION bin) diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/README.md b/dimos/hardware/sensors/lidar/fastlio2/cpp/README.md deleted file mode 100644 index da6e7c8803..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/README.md +++ /dev/null @@ -1,109 +0,0 @@ -# FAST-LIO2 Native Module (C++) - -Real-time LiDAR SLAM using FAST-LIO2 with integrated Livox Mid-360 driver. -Binds Livox SDK2 directly into FAST-LIO-NON-ROS: SDK callbacks feed -CustomMsg/Imu to FastLio, which performs EKF-LOAM SLAM. Registered -(world-frame) point clouds and odometry are published on LCM. - -## Build - -### Nix (recommended) - -```bash -cd dimos/hardware/sensors/lidar/fastlio2/cpp -nix build .#fastlio2_native -``` - -Binary lands at `result/bin/fastlio2_native`. - -The flake pulls Livox SDK2 from the livox sub-flake and -[FAST-LIO-NON-ROS](https://github.com/leshy/FAST-LIO-NON-ROS) from GitHub -automatically. - -### Native (CMake) - -Requires: -- CMake >= 3.14 -- [LCM](https://lcm-proj.github.io/) (`pacman -S lcm` or build from source) -- [Livox SDK2](https://github.com/Livox-SDK/Livox-SDK2) installed to `/usr/local` -- Eigen3, PCL (common, filters), yaml-cpp, Boost, OpenMP -- [FAST-LIO-NON-ROS](https://github.com/leshy/FAST-LIO-NON-ROS) checked out locally - -```bash -cd dimos/hardware/sensors/lidar/fastlio2/cpp -cmake -B build -DFASTLIO_DIR=$HOME/coding/FAST-LIO-NON-ROS -cmake --build build -j$(nproc) -cmake --install build -``` - -Binary lands at `result/bin/fastlio2_native` (same location as nix). - -If `-DFASTLIO_DIR` is omitted, CMake auto-fetches FAST-LIO-NON-ROS from GitHub. - -## Network setup - -The Mid-360 communicates over USB ethernet. Configure the interface: - -```bash -sudo nmcli con add type ethernet ifname usbeth0 con-name livox-mid360 \ - ipv4.addresses 192.168.1.5/24 ipv4.method manual -sudo nmcli con up livox-mid360 -``` - -This persists across reboots. The lidar defaults to `192.168.1.155`. - -## Usage - -Normally launched by `FastLio2` via the NativeModule framework: - -```python -from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2 -from dimos.core.blueprints import autoconnect - -autoconnect( - FastLio2.blueprint(host_ip="192.168.1.5"), - SomeConsumer.blueprint(), -).build().loop() -``` - -### Manual invocation (for debugging) - -```bash -./result/bin/fastlio2_native \ - --lidar '/pointcloud#sensor_msgs.PointCloud2' \ - --odometry '/odometry#nav_msgs.Odometry' \ - --host_ip 192.168.1.5 \ - --lidar_ip 192.168.1.155 \ - --config_path ../config/mid360.yaml -``` - -Topic strings must include the `#type` suffix -- this is the actual LCM channel -name used by dimos subscribers. - -For full vis: -```sh -rerun-bridge -``` - -For LCM traffic: -```sh -lcm-spy -``` - -## Configuration - -FAST-LIO2 config files live in `config/`. The YAML config controls filter -parameters, EKF tuning, and point cloud processing settings. - -## File overview - -| File | Description | -|---------------------------|--------------------------------------------------------------| -| `main.cpp` | Livox SDK2 + FAST-LIO2 integration, EKF SLAM, LCM publishing | -| `cloud_filter.hpp` | Point cloud filtering (range, voxel downsampling) | -| `voxel_map.hpp` | Global voxel map accumulation | -| `dimos_native_module.hpp` | Reusable header for parsing NativeModule CLI args | -| `config/` | FAST-LIO2 YAML configuration files | -| `flake.nix` | Nix flake for hermetic builds | -| `CMakeLists.txt` | Build config, fetches dimos-lcm headers automatically | -| `../module.py` | Python NativeModule wrapper (`FastLio2`) | diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/cloud_filter.hpp b/dimos/hardware/sensors/lidar/fastlio2/cpp/cloud_filter.hpp deleted file mode 100644 index 352ba9bef5..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/cloud_filter.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// Point cloud filtering utilities: voxel grid downsampling and -// statistical outlier removal using PCL. - -#ifndef CLOUD_FILTER_HPP_ -#define CLOUD_FILTER_HPP_ - -#include -#include -#include -#include - -struct CloudFilterConfig { - float voxel_size = 0.1f; - int sor_mean_k = 50; - float sor_stddev = 1.0f; -}; - -/// Apply voxel grid downsample + statistical outlier removal in-place. -/// Returns the filtered cloud (new allocation). -template -typename pcl::PointCloud::Ptr filter_cloud( - const typename pcl::PointCloud::Ptr& input, - const CloudFilterConfig& cfg) { - - if (!input || input->empty()) return input; - - // Voxel grid downsample - typename pcl::PointCloud::Ptr voxelized(new pcl::PointCloud()); - pcl::VoxelGrid vg; - vg.setInputCloud(input); - vg.setLeafSize(cfg.voxel_size, cfg.voxel_size, cfg.voxel_size); - vg.filter(*voxelized); - - // Statistical outlier removal - if (cfg.sor_mean_k > 0 && voxelized->size() > static_cast(cfg.sor_mean_k)) { - typename pcl::PointCloud::Ptr cleaned(new pcl::PointCloud()); - pcl::StatisticalOutlierRemoval sor; - sor.setInputCloud(voxelized); - sor.setMeanK(cfg.sor_mean_k); - sor.setStddevMulThresh(cfg.sor_stddev); - sor.filter(*cleaned); - return cleaned; - } - - return voxelized; -} - -#endif diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/config/mid360.json b/dimos/hardware/sensors/lidar/fastlio2/cpp/config/mid360.json deleted file mode 100644 index ff6cc6dbf6..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/config/mid360.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "common": { - "time_sync_en": false, - "time_offset_lidar_to_imu": 0.0, - "msr_freq": 50.0, - "main_freq": 5000.0 - }, - "preprocess": { - "lidar_type": 1, - "scan_line": 1, - "blind": 1 - }, - "mapping": { - "acc_cov": 0.1, - "gyr_cov": 0.1, - "b_acc_cov": 0.0001, - "b_gyr_cov": 0.0001, - "fov_degree": 360, - "det_range": 100.0, - "extrinsic_est_en": true, - "extrinsic_T": [ - 0.04165, - 0.02326, - -0.0284 - ], - "extrinsic_R": [ - 1.0, - 0.0, - 0.0, - 0.0, - 1.0, - 0.0, - 0.0, - 0.0, - 1.0 - ] - } -} diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.lock b/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.lock deleted file mode 100644 index 2636f00ada..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.lock +++ /dev/null @@ -1,135 +0,0 @@ -{ - "nodes": { - "dimos-lcm": { - "flake": false, - "locked": { - "lastModified": 1769774949, - "narHash": "sha256-icRK7jerqNlwK1WZBrnIP04I2WozzFqTD7qsmnPxQuo=", - "owner": "dimensionalOS", - "repo": "dimos-lcm", - "rev": "0aa72b7b1bd3a65f50f5c03485ee9b728df56afe", - "type": "github" - }, - "original": { - "owner": "dimensionalOS", - "ref": "main", - "repo": "dimos-lcm", - "type": "github" - } - }, - "dimos-lcm_2": { - "flake": false, - "locked": { - "lastModified": 1769774949, - "narHash": "sha256-icRK7jerqNlwK1WZBrnIP04I2WozzFqTD7qsmnPxQuo=", - "owner": "dimensionalOS", - "repo": "dimos-lcm", - "rev": "0aa72b7b1bd3a65f50f5c03485ee9b728df56afe", - "type": "github" - }, - "original": { - "owner": "dimensionalOS", - "ref": "main", - "repo": "dimos-lcm", - "type": "github" - } - }, - "fast-lio": { - "flake": false, - "locked": { - "lastModified": 1770976391, - "narHash": "sha256-OjSHk6qs3oCZ7XNjDyq4/K/Rb1VhqyADtra2q3F8V5U=", - "owner": "leshy", - "repo": "FAST-LIO-NON-ROS", - "rev": "47606ac6bbafcae9231936b4662b94c84fe87339", - "type": "github" - }, - "original": { - "owner": "leshy", - "ref": "dimos-integration", - "repo": "FAST-LIO-NON-ROS", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "livox-sdk": { - "inputs": { - "dimos-lcm": "dimos-lcm_2", - "flake-utils": [ - "flake-utils" - ], - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "path": "../../livox/cpp", - "type": "path" - }, - "original": { - "path": "../../livox/cpp", - "type": "path" - }, - "parent": [] - }, - "nixpkgs": { - "locked": { - "lastModified": 1770841267, - "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "dimos-lcm": "dimos-lcm", - "fast-lio": "fast-lio", - "flake-utils": "flake-utils", - "livox-sdk": "livox-sdk", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.nix b/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.nix deleted file mode 100644 index 7a58aceb76..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/flake.nix +++ /dev/null @@ -1,59 +0,0 @@ -{ - description = "FAST-LIO2 + Livox Mid-360 native module"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - livox-sdk.url = "path:../../livox/cpp"; - livox-sdk.inputs.nixpkgs.follows = "nixpkgs"; - livox-sdk.inputs.flake-utils.follows = "flake-utils"; - dimos-lcm = { - url = "github:dimensionalOS/dimos-lcm/main"; - flake = false; - }; - fast-lio = { - url = "github:leshy/FAST-LIO-NON-ROS/dimos-integration"; - flake = false; - }; - }; - - outputs = { self, nixpkgs, flake-utils, livox-sdk, dimos-lcm, fast-lio, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - livox-sdk2 = livox-sdk.packages.${system}.livox-sdk2; - - livox-common = ../../common; - - fastlio2_native = pkgs.stdenv.mkDerivation { - pname = "fastlio2_native"; - version = "0.1.0"; - - src = ./.; - - nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ]; - buildInputs = [ - livox-sdk2 - pkgs.lcm - pkgs.glib - pkgs.eigen - pkgs.pcl - pkgs.yaml-cpp - pkgs.boost - pkgs.llvmPackages.openmp - ]; - - cmakeFlags = [ - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" - "-DFETCHCONTENT_SOURCE_DIR_DIMOS_LCM=${dimos-lcm}" - "-DFASTLIO_DIR=${fast-lio}" - "-DLIVOX_COMMON_DIR=${livox-common}" - ]; - }; - in { - packages = { - default = fastlio2_native; - inherit fastlio2_native; - }; - }); -} diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/main.cpp b/dimos/hardware/sensors/lidar/fastlio2/cpp/main.cpp deleted file mode 100644 index 60b8d9cdb2..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/main.cpp +++ /dev/null @@ -1,522 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// FAST-LIO2 + Livox Mid-360 native module for dimos NativeModule framework. -// -// Binds Livox SDK2 directly into FAST-LIO-NON-ROS: SDK callbacks feed -// CustomMsg/Imu to FastLio, which performs EKF-LOAM SLAM. Registered -// (world-frame) point clouds and odometry are published on LCM. -// -// Usage: -// ./fastlio2_native \ -// --lidar '/lidar#sensor_msgs.PointCloud2' \ -// --odometry '/odometry#nav_msgs.Odometry' \ -// --config_path /path/to/mid360.yaml \ -// --host_ip 192.168.1.5 --lidar_ip 192.168.1.155 - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "livox_sdk_config.hpp" - -#include "cloud_filter.hpp" -#include "dimos_native_module.hpp" -#include "voxel_map.hpp" - -// dimos LCM message headers -#include "geometry_msgs/Quaternion.hpp" -#include "geometry_msgs/Vector3.hpp" -#include "nav_msgs/Odometry.hpp" -#include "sensor_msgs/Imu.hpp" -#include "sensor_msgs/PointCloud2.hpp" -#include "sensor_msgs/PointField.hpp" - -// FAST-LIO (header-only core, compiled sources linked via CMake) -#include "fast_lio.hpp" - -using livox_common::GRAVITY_MS2; -using livox_common::DATA_TYPE_IMU; -using livox_common::DATA_TYPE_CARTESIAN_HIGH; -using livox_common::DATA_TYPE_CARTESIAN_LOW; - -// --------------------------------------------------------------------------- -// Global state -// --------------------------------------------------------------------------- - -static std::atomic g_running{true}; -static lcm::LCM* g_lcm = nullptr; -static FastLio* g_fastlio = nullptr; - -static std::string g_lidar_topic; -static std::string g_odometry_topic; -static std::string g_map_topic; -static std::string g_frame_id = "map"; -static std::string g_child_frame_id = "body"; -static float g_frequency = 10.0f; - -// Frame accumulator (Livox SDK raw → CustomMsg) -static std::mutex g_pc_mutex; -static std::vector g_accumulated_points; -static uint64_t g_frame_start_ns = 0; -static bool g_frame_has_timestamp = false; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -static uint64_t get_timestamp_ns(const LivoxLidarEthernetPacket* pkt) { - uint64_t ns = 0; - std::memcpy(&ns, pkt->timestamp, sizeof(uint64_t)); - return ns; -} - -using dimos::time_from_seconds; -using dimos::make_header; - -// --------------------------------------------------------------------------- -// Publish lidar (world-frame point cloud) -// --------------------------------------------------------------------------- - -static void publish_lidar(PointCloudXYZI::Ptr cloud, double timestamp, - const std::string& topic = "") { - const std::string& chan = topic.empty() ? g_lidar_topic : topic; - if (!g_lcm || !cloud || cloud->empty() || chan.empty()) return; - - int num_points = static_cast(cloud->size()); - - sensor_msgs::PointCloud2 pc; - pc.header = make_header(g_frame_id, timestamp); - pc.height = 1; - pc.width = num_points; - pc.is_bigendian = 0; - pc.is_dense = 1; - - // Fields: x, y, z, intensity (float32 each) - pc.fields_length = 4; - pc.fields.resize(4); - - auto make_field = [](const std::string& name, int32_t offset) { - sensor_msgs::PointField f; - f.name = name; - f.offset = offset; - f.datatype = sensor_msgs::PointField::FLOAT32; - f.count = 1; - return f; - }; - - pc.fields[0] = make_field("x", 0); - pc.fields[1] = make_field("y", 4); - pc.fields[2] = make_field("z", 8); - pc.fields[3] = make_field("intensity", 12); - - pc.point_step = 16; - pc.row_step = pc.point_step * num_points; - - pc.data_length = pc.row_step; - pc.data.resize(pc.data_length); - - for (int i = 0; i < num_points; ++i) { - float* dst = reinterpret_cast(pc.data.data() + i * 16); - dst[0] = cloud->points[i].x; - dst[1] = cloud->points[i].y; - dst[2] = cloud->points[i].z; - dst[3] = cloud->points[i].intensity; - } - - g_lcm->publish(chan, &pc); -} - -// --------------------------------------------------------------------------- -// Publish odometry -// --------------------------------------------------------------------------- - -static void publish_odometry(const custom_messages::Odometry& odom, double timestamp) { - if (!g_lcm) return; - - nav_msgs::Odometry msg; - msg.header = make_header(g_frame_id, timestamp); - msg.child_frame_id = g_child_frame_id; - - // Pose - msg.pose.pose.position.x = odom.pose.pose.position.x; - msg.pose.pose.position.y = odom.pose.pose.position.y; - msg.pose.pose.position.z = odom.pose.pose.position.z; - msg.pose.pose.orientation.x = odom.pose.pose.orientation.x; - msg.pose.pose.orientation.y = odom.pose.pose.orientation.y; - msg.pose.pose.orientation.z = odom.pose.pose.orientation.z; - msg.pose.pose.orientation.w = odom.pose.pose.orientation.w; - - // Covariance (fixed-size double[36]) - for (int i = 0; i < 36; ++i) { - msg.pose.covariance[i] = odom.pose.covariance[i]; - } - - // Twist (zero — FAST-LIO doesn't output velocity directly) - msg.twist.twist.linear.x = 0; - msg.twist.twist.linear.y = 0; - msg.twist.twist.linear.z = 0; - msg.twist.twist.angular.x = 0; - msg.twist.twist.angular.y = 0; - msg.twist.twist.angular.z = 0; - std::memset(msg.twist.covariance, 0, sizeof(msg.twist.covariance)); - - g_lcm->publish(g_odometry_topic, &msg); -} - -// --------------------------------------------------------------------------- -// Livox SDK callbacks -// --------------------------------------------------------------------------- - -static void on_point_cloud(const uint32_t /*handle*/, const uint8_t /*dev_type*/, - LivoxLidarEthernetPacket* data, void* /*client_data*/) { - if (!g_running.load() || data == nullptr) return; - - uint64_t ts_ns = get_timestamp_ns(data); - uint16_t dot_num = data->dot_num; - - std::lock_guard lock(g_pc_mutex); - - if (!g_frame_has_timestamp) { - g_frame_start_ns = ts_ns; - g_frame_has_timestamp = true; - } - - if (data->data_type == DATA_TYPE_CARTESIAN_HIGH) { - auto* pts = reinterpret_cast(data->data); - for (uint16_t i = 0; i < dot_num; ++i) { - custom_messages::CustomPoint cp; - cp.x = static_cast(pts[i].x) / 1000.0; // mm → m - cp.y = static_cast(pts[i].y) / 1000.0; - cp.z = static_cast(pts[i].z) / 1000.0; - cp.reflectivity = pts[i].reflectivity; - cp.tag = pts[i].tag; - cp.line = 0; // Mid-360: non-repetitive, single "line" - cp.offset_time = static_cast(ts_ns - g_frame_start_ns); - g_accumulated_points.push_back(cp); - } - } else if (data->data_type == DATA_TYPE_CARTESIAN_LOW) { - auto* pts = reinterpret_cast(data->data); - for (uint16_t i = 0; i < dot_num; ++i) { - custom_messages::CustomPoint cp; - cp.x = static_cast(pts[i].x) / 100.0; // cm → m - cp.y = static_cast(pts[i].y) / 100.0; - cp.z = static_cast(pts[i].z) / 100.0; - cp.reflectivity = pts[i].reflectivity; - cp.tag = pts[i].tag; - cp.line = 0; - cp.offset_time = static_cast(ts_ns - g_frame_start_ns); - g_accumulated_points.push_back(cp); - } - } -} - -static void on_imu_data(const uint32_t /*handle*/, const uint8_t /*dev_type*/, - LivoxLidarEthernetPacket* data, void* /*client_data*/) { - if (!g_running.load() || data == nullptr || !g_fastlio) return; - - double ts = static_cast(get_timestamp_ns(data)) / 1e9; - auto* imu_pts = reinterpret_cast(data->data); - uint16_t dot_num = data->dot_num; - - for (uint16_t i = 0; i < dot_num; ++i) { - auto imu_msg = boost::make_shared(); - imu_msg->header.stamp = custom_messages::Time().fromSec(ts); - imu_msg->header.seq = 0; - imu_msg->header.frame_id = "livox_frame"; - - imu_msg->orientation.x = 0.0; - imu_msg->orientation.y = 0.0; - imu_msg->orientation.z = 0.0; - imu_msg->orientation.w = 1.0; - for (int j = 0; j < 9; ++j) - imu_msg->orientation_covariance[j] = 0.0; - - imu_msg->angular_velocity.x = static_cast(imu_pts[i].gyro_x); - imu_msg->angular_velocity.y = static_cast(imu_pts[i].gyro_y); - imu_msg->angular_velocity.z = static_cast(imu_pts[i].gyro_z); - for (int j = 0; j < 9; ++j) - imu_msg->angular_velocity_covariance[j] = 0.0; - - imu_msg->linear_acceleration.x = static_cast(imu_pts[i].acc_x) * GRAVITY_MS2; - imu_msg->linear_acceleration.y = static_cast(imu_pts[i].acc_y) * GRAVITY_MS2; - imu_msg->linear_acceleration.z = static_cast(imu_pts[i].acc_z) * GRAVITY_MS2; - for (int j = 0; j < 9; ++j) - imu_msg->linear_acceleration_covariance[j] = 0.0; - - g_fastlio->feed_imu(imu_msg); - } -} - -static void on_info_change(const uint32_t handle, const LivoxLidarInfo* info, - void* /*client_data*/) { - if (info == nullptr) return; - - char sn[17] = {}; - std::memcpy(sn, info->sn, 16); - char ip[17] = {}; - std::memcpy(ip, info->lidar_ip, 16); - - printf("[fastlio2] Device connected: handle=%u type=%u sn=%s ip=%s\n", - handle, info->dev_type, sn, ip); - - SetLivoxLidarWorkMode(handle, kLivoxLidarNormal, nullptr, nullptr); - EnableLivoxLidarImuData(handle, nullptr, nullptr); -} - -// --------------------------------------------------------------------------- -// Signal handling -// --------------------------------------------------------------------------- - -static void signal_handler(int /*sig*/) { - g_running.store(false); -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -int main(int argc, char** argv) { - dimos::NativeModule mod(argc, argv); - - // Required: LCM topics for output ports - g_lidar_topic = mod.has("lidar") ? mod.topic("lidar") : ""; - g_odometry_topic = mod.has("odometry") ? mod.topic("odometry") : ""; - g_map_topic = mod.has("global_map") ? mod.topic("global_map") : ""; - - if (g_lidar_topic.empty() && g_odometry_topic.empty()) { - fprintf(stderr, "Error: at least one of --lidar or --odometry is required\n"); - return 1; - } - - // FAST-LIO config path - std::string config_path = mod.arg("config_path", ""); - if (config_path.empty()) { - fprintf(stderr, "Error: --config_path is required\n"); - return 1; - } - - // FAST-LIO internal processing rates - double msr_freq = mod.arg_float("msr_freq", 50.0f); - double main_freq = mod.arg_float("main_freq", 5000.0f); - - // Livox hardware config - std::string host_ip = mod.arg("host_ip", "192.168.1.5"); - std::string lidar_ip = mod.arg("lidar_ip", "192.168.1.155"); - g_frequency = mod.arg_float("frequency", 10.0f); - g_frame_id = mod.arg("frame_id", "map"); - g_child_frame_id = mod.arg("child_frame_id", "body"); - float pointcloud_freq = mod.arg_float("pointcloud_freq", 5.0f); - float odom_freq = mod.arg_float("odom_freq", 50.0f); - CloudFilterConfig filter_cfg; - filter_cfg.voxel_size = mod.arg_float("voxel_size", 0.1f); - filter_cfg.sor_mean_k = mod.arg_int("sor_mean_k", 50); - filter_cfg.sor_stddev = mod.arg_float("sor_stddev", 1.0f); - float map_voxel_size = mod.arg_float("map_voxel_size", 0.1f); - float map_max_range = mod.arg_float("map_max_range", 100.0f); - float map_freq = mod.arg_float("map_freq", 0.0f); - - // SDK network ports (defaults from SdkPorts struct in livox_sdk_config.hpp) - livox_common::SdkPorts ports; - const livox_common::SdkPorts port_defaults; - ports.cmd_data = mod.arg_int("cmd_data_port", port_defaults.cmd_data); - ports.push_msg = mod.arg_int("push_msg_port", port_defaults.push_msg); - ports.point_data = mod.arg_int("point_data_port", port_defaults.point_data); - ports.imu_data = mod.arg_int("imu_data_port", port_defaults.imu_data); - ports.log_data = mod.arg_int("log_data_port", port_defaults.log_data); - ports.host_cmd_data = mod.arg_int("host_cmd_data_port", port_defaults.host_cmd_data); - ports.host_push_msg = mod.arg_int("host_push_msg_port", port_defaults.host_push_msg); - ports.host_point_data = mod.arg_int("host_point_data_port", port_defaults.host_point_data); - ports.host_imu_data = mod.arg_int("host_imu_data_port", port_defaults.host_imu_data); - ports.host_log_data = mod.arg_int("host_log_data_port", port_defaults.host_log_data); - - printf("[fastlio2] Starting FAST-LIO2 + Livox Mid-360 native module\n"); - printf("[fastlio2] lidar topic: %s\n", - g_lidar_topic.empty() ? "(disabled)" : g_lidar_topic.c_str()); - printf("[fastlio2] odometry topic: %s\n", - g_odometry_topic.empty() ? "(disabled)" : g_odometry_topic.c_str()); - printf("[fastlio2] global_map topic: %s\n", - g_map_topic.empty() ? "(disabled)" : g_map_topic.c_str()); - printf("[fastlio2] config: %s\n", config_path.c_str()); - printf("[fastlio2] host_ip: %s lidar_ip: %s frequency: %.1f Hz\n", - host_ip.c_str(), lidar_ip.c_str(), g_frequency); - printf("[fastlio2] pointcloud_freq: %.1f Hz odom_freq: %.1f Hz\n", - pointcloud_freq, odom_freq); - printf("[fastlio2] voxel_size: %.3f sor_mean_k: %d sor_stddev: %.1f\n", - filter_cfg.voxel_size, filter_cfg.sor_mean_k, filter_cfg.sor_stddev); - if (!g_map_topic.empty()) - printf("[fastlio2] map_voxel_size: %.3f map_max_range: %.1f map_freq: %.1f Hz\n", - map_voxel_size, map_max_range, map_freq); - - // Signal handlers - signal(SIGTERM, signal_handler); - signal(SIGINT, signal_handler); - - // Init LCM - lcm::LCM lcm; - if (!lcm.good()) { - fprintf(stderr, "Error: LCM init failed\n"); - return 1; - } - g_lcm = &lcm; - - // Init FAST-LIO with config - printf("[fastlio2] Initializing FAST-LIO...\n"); - FastLio fast_lio(config_path, msr_freq, main_freq); - g_fastlio = &fast_lio; - printf("[fastlio2] FAST-LIO initialized.\n"); - - // Init Livox SDK (in-memory config, no temp files) - if (!livox_common::init_livox_sdk(host_ip, lidar_ip, ports)) { - return 1; - } - - // Register SDK callbacks - SetLivoxLidarPointCloudCallBack(on_point_cloud, nullptr); - SetLivoxLidarImuDataCallback(on_imu_data, nullptr); - SetLivoxLidarInfoChangeCallback(on_info_change, nullptr); - - // Start SDK - if (!LivoxLidarSdkStart()) { - fprintf(stderr, "Error: LivoxLidarSdkStart failed\n"); - LivoxLidarSdkUninit(); - return 1; - } - - printf("[fastlio2] SDK started, waiting for device...\n"); - - // Main loop - auto frame_interval = std::chrono::microseconds( - static_cast(1e6 / g_frequency)); - auto last_emit = std::chrono::steady_clock::now(); - const double process_period_ms = 1000.0 / main_freq; - - // Rate limiters for output publishing - auto pc_interval = std::chrono::microseconds( - static_cast(1e6 / pointcloud_freq)); - auto odom_interval = std::chrono::microseconds( - static_cast(1e6 / odom_freq)); - auto last_pc_publish = std::chrono::steady_clock::now(); - auto last_odom_publish = std::chrono::steady_clock::now(); - - // Global voxel map (only if map topic is configured AND map_freq > 0) - std::unique_ptr global_map; - std::chrono::microseconds map_interval{0}; - auto last_map_publish = std::chrono::steady_clock::now(); - if (!g_map_topic.empty() && map_freq > 0.0f) { - global_map = std::make_unique(map_voxel_size, map_max_range); - map_interval = std::chrono::microseconds( - static_cast(1e6 / map_freq)); - } - - while (g_running.load()) { - auto loop_start = std::chrono::high_resolution_clock::now(); - - // At frame rate: build CustomMsg from accumulated points and feed to FAST-LIO - auto now = std::chrono::steady_clock::now(); - if (now - last_emit >= frame_interval) { - std::vector points; - uint64_t frame_start = 0; - - { - std::lock_guard lock(g_pc_mutex); - if (!g_accumulated_points.empty()) { - points.swap(g_accumulated_points); - frame_start = g_frame_start_ns; - g_frame_has_timestamp = false; - } - } - - if (!points.empty()) { - // Build CustomMsg - auto lidar_msg = boost::make_shared(); - lidar_msg->header.seq = 0; - lidar_msg->header.stamp = custom_messages::Time().fromSec( - static_cast(frame_start) / 1e9); - lidar_msg->header.frame_id = "livox_frame"; - lidar_msg->timebase = frame_start; - lidar_msg->lidar_id = 0; - for (int i = 0; i < 3; i++) - lidar_msg->rsvd[i] = 0; - lidar_msg->point_num = static_cast(points.size()); - lidar_msg->points = std::move(points); - - fast_lio.feed_lidar(lidar_msg); - } - - last_emit = now; - } - - // Run FAST-LIO processing step (high frequency) - fast_lio.process(); - - // Check for new results and accumulate/publish (rate-limited) - auto pose = fast_lio.get_pose(); - if (!pose.empty() && (pose[0] != 0.0 || pose[1] != 0.0 || pose[2] != 0.0)) { - double ts = std::chrono::duration( - std::chrono::system_clock::now().time_since_epoch()).count(); - - auto world_cloud = fast_lio.get_world_cloud(); - if (world_cloud && !world_cloud->empty()) { - auto filtered = filter_cloud(world_cloud, filter_cfg); - - // Per-scan publish at pointcloud_freq - if (!g_lidar_topic.empty() && now - last_pc_publish >= pc_interval) { - publish_lidar(filtered, ts); - last_pc_publish = now; - } - - // Global map: insert, prune, and publish at map_freq - if (global_map) { - global_map->insert(filtered); - - if (now - last_map_publish >= map_interval) { - global_map->prune( - static_cast(pose[0]), - static_cast(pose[1]), - static_cast(pose[2])); - auto map_cloud = global_map->to_cloud(); - publish_lidar(map_cloud, ts, g_map_topic); - last_map_publish = now; - } - } - } - - // Publish odometry (rate-limited to odom_freq) - if (!g_odometry_topic.empty() && (now - last_odom_publish >= odom_interval)) { - publish_odometry(fast_lio.get_odometry(), ts); - last_odom_publish = now; - } - } - - // Handle LCM messages - lcm.handleTimeout(0); - - // Rate control (~5kHz processing) - auto loop_end = std::chrono::high_resolution_clock::now(); - auto elapsed_ms = std::chrono::duration(loop_end - loop_start).count(); - if (elapsed_ms < process_period_ms) { - std::this_thread::sleep_for(std::chrono::microseconds( - static_cast((process_period_ms - elapsed_ms) * 1000))); - } - } - - // Cleanup - printf("[fastlio2] Shutting down...\n"); - g_fastlio = nullptr; - LivoxLidarSdkUninit(); - g_lcm = nullptr; - - printf("[fastlio2] Done.\n"); - return 0; -} diff --git a/dimos/hardware/sensors/lidar/fastlio2/cpp/voxel_map.hpp b/dimos/hardware/sensors/lidar/fastlio2/cpp/voxel_map.hpp deleted file mode 100644 index a50740cd04..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/cpp/voxel_map.hpp +++ /dev/null @@ -1,297 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// Efficient global voxel map using a hash map. -// Supports O(1) insert/update, distance-based pruning, and -// raycasting-based free space clearing via Amanatides & Woo 3D DDA. -// FOV is discovered dynamically from incoming point cloud data. - -#ifndef VOXEL_MAP_HPP_ -#define VOXEL_MAP_HPP_ - -#include -#include -#include - -#include -#include - -struct VoxelKey { - int32_t x, y, z; - bool operator==(const VoxelKey& o) const { return x == o.x && y == o.y && z == o.z; } -}; - -struct VoxelKeyHash { - size_t operator()(const VoxelKey& k) const { - // Fast spatial hash — large primes reduce collisions for grid coords - size_t h = static_cast(k.x) * 73856093u; - h ^= static_cast(k.y) * 19349669u; - h ^= static_cast(k.z) * 83492791u; - return h; - } -}; - -struct Voxel { - float x, y, z; // running centroid - float intensity; - uint32_t count; // points merged into this voxel - uint8_t miss_count; // consecutive scans where a ray passed through without hitting -}; - -/// Config for raycast-based free space clearing. -struct RaycastConfig { - int subsample = 4; // raycast every Nth point - int max_misses = 3; // erase after this many consecutive misses - float fov_margin_rad = 0.035f; // ~2° safety margin added to discovered FOV -}; - -class VoxelMap { -public: - explicit VoxelMap(float voxel_size, float max_range = 100.0f) - : voxel_size_(voxel_size), max_range_(max_range) { - map_.reserve(500000); - } - - /// Insert a point cloud into the map, merging into existing voxels. - /// Resets miss_count for hit voxels. - template - void insert(const typename pcl::PointCloud::Ptr& cloud) { - if (!cloud) return; - float inv = 1.0f / voxel_size_; - for (const auto& pt : cloud->points) { - VoxelKey key{ - static_cast(std::floor(pt.x * inv)), - static_cast(std::floor(pt.y * inv)), - static_cast(std::floor(pt.z * inv))}; - - auto it = map_.find(key); - if (it != map_.end()) { - // Running average update - auto& v = it->second; - float n = static_cast(v.count); - float n1 = n + 1.0f; - v.x = (v.x * n + pt.x) / n1; - v.y = (v.y * n + pt.y) / n1; - v.z = (v.z * n + pt.z) / n1; - v.intensity = (v.intensity * n + pt.intensity) / n1; - v.count++; - v.miss_count = 0; - } else { - map_.emplace(key, Voxel{pt.x, pt.y, pt.z, pt.intensity, 1, 0}); - } - } - } - - /// Cast rays from sensor origin through each point in the cloud. - /// Discovers the sensor FOV from the cloud's elevation angle range, - /// then marks intermediate voxels as missed and erases those exceeding - /// the miss threshold within the discovered FOV. - /// - /// Orientation quaternion (qx,qy,qz,qw) is body→world. - template - void raycast_clear(float ox, float oy, float oz, - float qx, float qy, float qz, float qw, - const typename pcl::PointCloud::Ptr& cloud, - const RaycastConfig& cfg) { - if (!cloud || cloud->empty() || cfg.max_misses <= 0) return; - - // Phase 0: discover FOV from this scan's elevation angles in sensor-local frame - update_fov(ox, oy, oz, qx, qy, qz, qw, cloud); - - // Skip raycasting until we have a valid FOV (need at least a few scans) - if (!fov_valid_) return; - - float inv = 1.0f / voxel_size_; - int n_pts = static_cast(cloud->size()); - float fov_up = fov_up_ + cfg.fov_margin_rad; - float fov_down = fov_down_ - cfg.fov_margin_rad; - - // Phase 1: walk rays, increment miss_count for intermediate voxels - for (int i = 0; i < n_pts; i += cfg.subsample) { - const auto& pt = cloud->points[i]; - raycast_single(ox, oy, oz, pt.x, pt.y, pt.z, inv); - } - - // Phase 2: erase voxels that exceeded miss threshold and are within FOV - for (auto it = map_.begin(); it != map_.end();) { - if (it->second.miss_count > static_cast(cfg.max_misses)) { - if (in_sensor_fov(ox, oy, oz, qx, qy, qz, qw, - it->second.x, it->second.y, it->second.z, - fov_up, fov_down)) { - it = map_.erase(it); - continue; - } - } - ++it; - } - } - - /// Remove voxels farther than max_range from the given position. - void prune(float px, float py, float pz) { - float r2 = max_range_ * max_range_; - for (auto it = map_.begin(); it != map_.end();) { - float dx = it->second.x - px; - float dy = it->second.y - py; - float dz = it->second.z - pz; - if (dx * dx + dy * dy + dz * dz > r2) - it = map_.erase(it); - else - ++it; - } - } - - /// Export all voxel centroids as a point cloud. - template - typename pcl::PointCloud::Ptr to_cloud() const { - typename pcl::PointCloud::Ptr cloud( - new pcl::PointCloud(map_.size(), 1)); - size_t i = 0; - for (const auto& [key, v] : map_) { - auto& pt = cloud->points[i++]; - pt.x = v.x; - pt.y = v.y; - pt.z = v.z; - pt.intensity = v.intensity; - } - return cloud; - } - - size_t size() const { return map_.size(); } - void clear() { map_.clear(); } - void set_max_range(float r) { max_range_ = r; } - float fov_up_deg() const { return fov_up_ * 180.0f / static_cast(M_PI); } - float fov_down_deg() const { return fov_down_ * 180.0f / static_cast(M_PI); } - bool fov_valid() const { return fov_valid_; } - -private: - std::unordered_map map_; - float voxel_size_; - float max_range_; - - // Dynamically discovered sensor FOV (accumulated over scans) - float fov_up_ = -static_cast(M_PI); // start narrow, expand from data - float fov_down_ = static_cast(M_PI); - int fov_scan_count_ = 0; - bool fov_valid_ = false; - static constexpr int FOV_WARMUP_SCANS = 5; // require N scans before trusting FOV - - /// Update discovered FOV from a scan's elevation angles in sensor-local frame. - template - void update_fov(float ox, float oy, float oz, - float qx, float qy, float qz, float qw, - const typename pcl::PointCloud::Ptr& cloud) { - // Inverse quaternion for world→sensor rotation - float nqx = -qx, nqy = -qy, nqz = -qz; - - for (const auto& pt : cloud->points) { - float wx = pt.x - ox, wy = pt.y - oy, wz = pt.z - oz; - - // Rotate to sensor-local frame - float tx = 2.0f * (nqy * wz - nqz * wy); - float ty = 2.0f * (nqz * wx - nqx * wz); - float tz = 2.0f * (nqx * wy - nqy * wx); - float lx = wx + qw * tx + (nqy * tz - nqz * ty); - float ly = wy + qw * ty + (nqz * tx - nqx * tz); - float lz = wz + qw * tz + (nqx * ty - nqy * tx); - - float horiz_dist = std::sqrt(lx * lx + ly * ly); - if (horiz_dist < 1e-6f) continue; - float elevation = std::atan2(lz, horiz_dist); - - if (elevation > fov_up_) fov_up_ = elevation; - if (elevation < fov_down_) fov_down_ = elevation; - } - - if (++fov_scan_count_ >= FOV_WARMUP_SCANS && !fov_valid_) { - fov_valid_ = true; - printf("[voxel_map] FOV discovered: [%.1f, %.1f] deg\n", - fov_down_deg(), fov_up_deg()); - } - } - - /// Amanatides & Woo 3D DDA: walk from (ox,oy,oz) to (px,py,pz), - /// incrementing miss_count for all intermediate voxels. - void raycast_single(float ox, float oy, float oz, - float px, float py, float pz, float inv) { - float dx = px - ox, dy = py - oy, dz = pz - oz; - float len = std::sqrt(dx * dx + dy * dy + dz * dz); - if (len < 1e-6f) return; - dx /= len; dy /= len; dz /= len; - - int32_t cx = static_cast(std::floor(ox * inv)); - int32_t cy = static_cast(std::floor(oy * inv)); - int32_t cz = static_cast(std::floor(oz * inv)); - int32_t ex = static_cast(std::floor(px * inv)); - int32_t ey = static_cast(std::floor(py * inv)); - int32_t ez = static_cast(std::floor(pz * inv)); - - int sx = (dx >= 0) ? 1 : -1; - int sy = (dy >= 0) ? 1 : -1; - int sz = (dz >= 0) ? 1 : -1; - - // tMax: parametric distance along ray to next voxel boundary per axis - // tDelta: parametric distance to cross one full voxel per axis - float tMaxX = (std::abs(dx) < 1e-10f) ? 1e30f - : (((dx > 0 ? cx + 1 : cx) * voxel_size_ - ox) / dx); - float tMaxY = (std::abs(dy) < 1e-10f) ? 1e30f - : (((dy > 0 ? cy + 1 : cy) * voxel_size_ - oy) / dy); - float tMaxZ = (std::abs(dz) < 1e-10f) ? 1e30f - : (((dz > 0 ? cz + 1 : cz) * voxel_size_ - oz) / dz); - - float tDeltaX = (std::abs(dx) < 1e-10f) ? 1e30f : std::abs(voxel_size_ / dx); - float tDeltaY = (std::abs(dy) < 1e-10f) ? 1e30f : std::abs(voxel_size_ / dy); - float tDeltaZ = (std::abs(dz) < 1e-10f) ? 1e30f : std::abs(voxel_size_ / dz); - - // Walk through voxels (skip endpoint — it was hit) - int max_steps = static_cast(len * inv) + 3; // safety bound - for (int step = 0; step < max_steps; ++step) { - if (cx == ex && cy == ey && cz == ez) break; // reached endpoint - - VoxelKey key{cx, cy, cz}; - auto it = map_.find(key); - if (it != map_.end() && it->second.miss_count < 255) { - it->second.miss_count++; - } - - // Step to next voxel on the axis with smallest tMax - if (tMaxX < tMaxY && tMaxX < tMaxZ) { - cx += sx; tMaxX += tDeltaX; - } else if (tMaxY < tMaxZ) { - cy += sy; tMaxY += tDeltaY; - } else { - cz += sz; tMaxZ += tDeltaZ; - } - } - } - - /// Check if a voxel centroid falls within the sensor's vertical FOV. - /// Rotates the vector (sensor→voxel) into sensor-local frame using the - /// inverse of the body→world quaternion, then checks elevation angle. - static bool in_sensor_fov(float ox, float oy, float oz, - float qx, float qy, float qz, float qw, - float vx, float vy, float vz, - float fov_up_rad, float fov_down_rad) { - // Vector from sensor origin to voxel in world frame - float wx = vx - ox, wy = vy - oy, wz = vz - oz; - - // Rotate by quaternion inverse (conjugate): q* = (-qx,-qy,-qz,qw) - float nqx = -qx, nqy = -qy, nqz = -qz; - // t = 2 * cross(q.xyz, v) - float tx = 2.0f * (nqy * wz - nqz * wy); - float ty = 2.0f * (nqz * wx - nqx * wz); - float tz = 2.0f * (nqx * wy - nqy * wx); - // v' = v + qw * t + cross(q.xyz, t) - float lx = wx + qw * tx + (nqy * tz - nqz * ty); - float ly = wy + qw * ty + (nqz * tx - nqx * tz); - float lz = wz + qw * tz + (nqx * ty - nqy * tx); - - // Elevation angle in sensor-local frame - float horiz_dist = std::sqrt(lx * lx + ly * ly); - if (horiz_dist < 1e-6f) return true; // directly above/below, treat as in FOV - float elevation = std::atan2(lz, horiz_dist); - - return elevation >= fov_down_rad && elevation <= fov_up_rad; - } -}; - -#endif diff --git a/dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py b/dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py deleted file mode 100644 index 05801729e3..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/fastlio_blueprints.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2 -from dimos.mapping.voxels import VoxelGridMapper -from dimos.visualization.rerun.bridge import rerun_bridge - -voxel_size = 0.05 - -mid360_fastlio = autoconnect( - FastLio2.blueprint(voxel_size=voxel_size, map_voxel_size=voxel_size, map_freq=-1), - rerun_bridge( - visual_override={ - "world/lidar": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"), - } - ), -).global_config(n_dask_workers=2, robot_model="mid360_fastlio2") - -mid360_fastlio_voxels = autoconnect( - FastLio2.blueprint(), - VoxelGridMapper.blueprint(publish_interval=1.0, voxel_size=voxel_size, carve_columns=False), - rerun_bridge( - visual_override={ - "world/global_map": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"), - "world/lidar": None, - } - ), -).global_config(n_dask_workers=3, robot_model="mid360_fastlio2_voxels") - -mid360_fastlio_voxels_native = autoconnect( - FastLio2.blueprint(voxel_size=voxel_size, map_voxel_size=voxel_size, map_freq=3.0), - rerun_bridge( - visual_override={ - "world/lidar": None, - "world/global_map": lambda grid: grid.to_rerun(voxel_size=voxel_size, mode="boxes"), - } - ), -).global_config(n_dask_workers=2, robot_model="mid360_fastlio2") diff --git a/dimos/hardware/sensors/lidar/fastlio2/module.py b/dimos/hardware/sensors/lidar/fastlio2/module.py deleted file mode 100644 index ee9a0783a0..0000000000 --- a/dimos/hardware/sensors/lidar/fastlio2/module.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Python NativeModule wrapper for the FAST-LIO2 + Livox Mid-360 binary. - -Binds Livox SDK2 directly into FAST-LIO-NON-ROS for real-time LiDAR SLAM. -Outputs registered (world-frame) point clouds and odometry with covariance. - -Usage:: - - from dimos.hardware.sensors.lidar.fastlio2.module import FastLio2 - from dimos.core.blueprints import autoconnect - - autoconnect( - FastLio2.blueprint(host_ip="192.168.1.5"), - SomeConsumer.blueprint(), - ).build().loop() -""" - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import TYPE_CHECKING - -from dimos.core import Out # noqa: TC001 -from dimos.core.native_module import NativeModule, NativeModuleConfig -from dimos.hardware.sensors.lidar.livox.ports import ( - SDK_CMD_DATA_PORT, - SDK_HOST_CMD_DATA_PORT, - SDK_HOST_IMU_DATA_PORT, - SDK_HOST_LOG_DATA_PORT, - SDK_HOST_POINT_DATA_PORT, - SDK_HOST_PUSH_MSG_PORT, - SDK_IMU_DATA_PORT, - SDK_LOG_DATA_PORT, - SDK_POINT_DATA_PORT, - SDK_PUSH_MSG_PORT, -) -from dimos.msgs.nav_msgs.Odometry import Odometry # noqa: TC001 -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 # noqa: TC001 -from dimos.spec import mapping, perception - -_CONFIG_DIR = Path(__file__).parent / "config" - - -@dataclass(kw_only=True) -class FastLio2Config(NativeModuleConfig): - """Config for the FAST-LIO2 + Livox Mid-360 native module.""" - - cwd: str | None = "cpp" - executable: str = "result/bin/fastlio2_native" - build_command: str | None = "nix build .#fastlio2_native" - - # Livox SDK hardware config - host_ip: str = "192.168.1.5" - lidar_ip: str = "192.168.1.155" - frequency: float = 10.0 - - # Frame IDs for output messages - frame_id: str = "map" - child_frame_id: str = "body" - - # FAST-LIO internal processing rates - msr_freq: float = 50.0 - main_freq: float = 5000.0 - - # Output publish rates (Hz) - pointcloud_freq: float = 10.0 - odom_freq: float = 30.0 - - # Point cloud filtering - voxel_size: float = 0.1 - sor_mean_k: int = 50 - sor_stddev: float = 1.0 - - # Global voxel map (disabled when map_freq <= 0) - map_freq: float = 0.0 - map_voxel_size: float = 0.1 - map_max_range: float = 100.0 - - # FAST-LIO YAML config (relative to config/ dir, or absolute path) - # C++ binary reads YAML directly via yaml-cpp - config: str = "mid360.yaml" - - # SDK port configuration (see livox/ports.py for defaults) - cmd_data_port: int = SDK_CMD_DATA_PORT - push_msg_port: int = SDK_PUSH_MSG_PORT - point_data_port: int = SDK_POINT_DATA_PORT - imu_data_port: int = SDK_IMU_DATA_PORT - log_data_port: int = SDK_LOG_DATA_PORT - host_cmd_data_port: int = SDK_HOST_CMD_DATA_PORT - host_push_msg_port: int = SDK_HOST_PUSH_MSG_PORT - host_point_data_port: int = SDK_HOST_POINT_DATA_PORT - host_imu_data_port: int = SDK_HOST_IMU_DATA_PORT - host_log_data_port: int = SDK_HOST_LOG_DATA_PORT - - # Resolved in __post_init__, passed as --config_path to the binary - config_path: str | None = None - - # config is not a CLI arg (config_path is) - cli_exclude: frozenset[str] = frozenset({"config"}) - - def __post_init__(self) -> None: - if self.config_path is None: - path = Path(self.config) - if not path.is_absolute(): - path = _CONFIG_DIR / path - self.config_path = str(path.resolve()) - - -class FastLio2(NativeModule, perception.Lidar, perception.Odometry, mapping.GlobalPointcloud): - """FAST-LIO2 SLAM module with integrated Livox Mid-360 driver. - - Ports: - lidar (Out[PointCloud2]): World-frame registered point cloud. - odometry (Out[Odometry]): Pose with covariance at LiDAR scan rate. - global_map (Out[PointCloud2]): Global voxel map (optional, enable via map_freq > 0). - """ - - default_config: type[FastLio2Config] = FastLio2Config # type: ignore[assignment] - lidar: Out[PointCloud2] - odometry: Out[Odometry] - global_map: Out[PointCloud2] - - -fastlio2_module = FastLio2.blueprint - -__all__ = [ - "FastLio2", - "FastLio2Config", - "fastlio2_module", -] - -# Verify protocol port compliance (mypy will flag missing ports) -if TYPE_CHECKING: - FastLio2() diff --git a/dimos/hardware/sensors/lidar/livox/__init__.py b/dimos/hardware/sensors/lidar/livox/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/hardware/sensors/lidar/livox/cpp/CMakeLists.txt b/dimos/hardware/sensors/lidar/livox/cpp/CMakeLists.txt deleted file mode 100644 index b6641a2fc6..0000000000 --- a/dimos/hardware/sensors/lidar/livox/cpp/CMakeLists.txt +++ /dev/null @@ -1,57 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(livox_mid360_native CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/result" CACHE PATH "" FORCE) -endif() - -# Fetch dimos-lcm for C++ message headers -include(FetchContent) -FetchContent_Declare(dimos_lcm - GIT_REPOSITORY https://github.com/dimensionalOS/dimos-lcm.git - GIT_TAG main - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(dimos_lcm) - -# Find LCM -find_package(PkgConfig REQUIRED) -pkg_check_modules(LCM REQUIRED lcm) - -# Livox SDK2 (from nix or /usr/local fallback) -find_library(LIVOX_SDK livox_lidar_sdk_shared) -if(NOT LIVOX_SDK) - message(FATAL_ERROR "Livox SDK2 not found. Available via nix flake in lidar/livox/") -endif() -get_filename_component(LIVOX_SDK_LIB_DIR ${LIVOX_SDK} DIRECTORY) -get_filename_component(LIVOX_SDK_PREFIX ${LIVOX_SDK_LIB_DIR} DIRECTORY) -set(LIVOX_SDK_INCLUDE_DIR ${LIVOX_SDK_PREFIX}/include) - -add_executable(mid360_native main.cpp) - -# Shared Livox common headers (livox_sdk_config.hpp etc.) -if(NOT LIVOX_COMMON_DIR) - set(LIVOX_COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../common) -endif() - -target_include_directories(mid360_native PRIVATE - ${dimos_lcm_SOURCE_DIR}/generated/cpp_lcm_msgs - ${LCM_INCLUDE_DIRS} - ${CMAKE_CURRENT_SOURCE_DIR} - ${LIVOX_COMMON_DIR} - ${LIVOX_SDK_INCLUDE_DIR} -) - -target_link_libraries(mid360_native PRIVATE - ${LCM_LIBRARIES} - ${LIVOX_SDK} -) - -target_link_directories(mid360_native PRIVATE - ${LCM_LIBRARY_DIRS} -) - -install(TARGETS mid360_native DESTINATION bin) diff --git a/dimos/hardware/sensors/lidar/livox/cpp/README.md b/dimos/hardware/sensors/lidar/livox/cpp/README.md deleted file mode 100644 index 4db5248ce1..0000000000 --- a/dimos/hardware/sensors/lidar/livox/cpp/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# Livox Mid-360 Native Module (C++) - -Native C++ driver for the Livox Mid-360 LiDAR. Publishes PointCloud2 and IMU -data directly on LCM, bypassing Python for minimal latency. - -## Build - -### Nix (recommended) - -```bash -cd dimos/hardware/sensors/lidar/livox/cpp -nix build .#mid360_native -``` - -Binary lands at `result/bin/mid360_native`. - -To build just the Livox SDK2 library: - -```bash -nix build .#livox-sdk2 -``` - -### Native (CMake) - -Requires: -- CMake >= 3.14 -- [LCM](https://lcm-proj.github.io/) (`pacman -S lcm` or build from source) -- [Livox SDK2](https://github.com/Livox-SDK/Livox-SDK2) installed to `/usr/local` - -Installing Livox SDK2 manually: - -```bash -cd ~/src -git clone https://github.com/Livox-SDK/Livox-SDK2.git -cd Livox-SDK2 && mkdir build && cd build -cmake .. && make -j$(nproc) -sudo make install -``` - -Then build: - -```bash -cd dimos/hardware/sensors/lidar/livox/cpp -cmake -B build -cmake --build build -j$(nproc) -cmake --install build -``` - -Binary lands at `result/bin/mid360_native` (same location as nix). - -CMake automatically fetches [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm) -for the C++ message headers on first configure. - -## Network setup - -The Mid-360 communicates over USB ethernet. Configure the interface: - -```bash -sudo nmcli con add type ethernet ifname usbeth0 con-name livox-mid360 \ - ipv4.addresses 192.168.1.5/24 ipv4.method manual -sudo nmcli con up livox-mid360 -``` - -This persists across reboots. The lidar defaults to `192.168.1.155`. - -## Usage - -Normally launched by `Mid360` via the NativeModule framework: - -```python -from dimos.hardware.sensors.lidar.livox.module import Mid360 -from dimos.core.blueprints import autoconnect - -autoconnect( - Mid360.blueprint(host_ip="192.168.1.5"), - SomeConsumer.blueprint(), -).build().loop() -``` - -### Manual invocation (for debugging) - -```bash -./result/bin/mid360_native \ - --pointcloud '/pointcloud#sensor_msgs.PointCloud2' \ - --imu '/imu#sensor_msgs.Imu' \ - --host_ip 192.168.1.5 \ - --lidar_ip 192.168.1.155 \ - --frequency 10 -``` - -Topic strings must include the `#type` suffix -- this is the actual LCM channel -name used by dimos subscribers. - -View data in another terminal: - -For full vis: -```sh -rerun-bridge -``` - -For LCM traffic: -```sh -lcm-spy -``` - -## File overview - -| File | Description | -|---------------------------|----------------------------------------------------------| -| `main.cpp` | Livox SDK2 callbacks, frame accumulation, LCM publishing | -| `dimos_native_module.hpp` | Reusable header for parsing NativeModule CLI args | -| `flake.nix` | Nix flake for hermetic builds | -| `CMakeLists.txt` | Build config, fetches dimos-lcm headers automatically | -| `../module.py` | Python NativeModule wrapper (`Mid360`) | diff --git a/dimos/hardware/sensors/lidar/livox/cpp/flake.lock b/dimos/hardware/sensors/lidar/livox/cpp/flake.lock deleted file mode 100644 index 58e8252be8..0000000000 --- a/dimos/hardware/sensors/lidar/livox/cpp/flake.lock +++ /dev/null @@ -1,79 +0,0 @@ -{ - "nodes": { - "dimos-lcm": { - "flake": false, - "locked": { - "lastModified": 1769774949, - "narHash": "sha256-icRK7jerqNlwK1WZBrnIP04I2WozzFqTD7qsmnPxQuo=", - "owner": "dimensionalOS", - "repo": "dimos-lcm", - "rev": "0aa72b7b1bd3a65f50f5c03485ee9b728df56afe", - "type": "github" - }, - "original": { - "owner": "dimensionalOS", - "ref": "main", - "repo": "dimos-lcm", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1770841267, - "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "dimos-lcm": "dimos-lcm", - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/dimos/hardware/sensors/lidar/livox/cpp/flake.nix b/dimos/hardware/sensors/lidar/livox/cpp/flake.nix deleted file mode 100644 index eeb06b33a6..0000000000 --- a/dimos/hardware/sensors/lidar/livox/cpp/flake.nix +++ /dev/null @@ -1,71 +0,0 @@ -{ - description = "Livox SDK2 and Mid-360 native module"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - dimos-lcm = { - url = "github:dimensionalOS/dimos-lcm/main"; - flake = false; - }; - }; - - outputs = { self, nixpkgs, flake-utils, dimos-lcm, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - - livox-sdk2 = pkgs.stdenv.mkDerivation rec { - pname = "livox-sdk2"; - version = "1.2.5"; - - src = pkgs.fetchFromGitHub { - owner = "Livox-SDK"; - repo = "Livox-SDK2"; - rev = "v${version}"; - hash = "sha256-NGscO/vLiQ17yQJtdPyFzhhMGE89AJ9kTL5cSun/bpU="; - }; - - nativeBuildInputs = [ pkgs.cmake ]; - - cmakeFlags = [ - "-DBUILD_SHARED_LIBS=ON" - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" - ]; - - preConfigure = '' - substituteInPlace CMakeLists.txt \ - --replace-fail "add_subdirectory(samples)" "" - sed -i '1i #include ' sdk_core/comm/define.h - sed -i '1i #include ' sdk_core/logger_handler/file_manager.h - ''; - }; - - livox-common = ../../common; - - mid360_native = pkgs.stdenv.mkDerivation { - pname = "mid360_native"; - version = "0.1.0"; - - src = ./.; - - nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ]; - buildInputs = [ livox-sdk2 pkgs.lcm pkgs.glib ]; - - cmakeFlags = [ - "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" - "-DFETCHCONTENT_SOURCE_DIR_DIMOS_LCM=${dimos-lcm}" - "-DLIVOX_COMMON_DIR=${livox-common}" - ]; - }; - in { - packages = { - default = mid360_native; - inherit livox-sdk2 mid360_native; - }; - - devShells.default = pkgs.mkShell { - buildInputs = [ livox-sdk2 ]; - }; - }); -} diff --git a/dimos/hardware/sensors/lidar/livox/cpp/main.cpp b/dimos/hardware/sensors/lidar/livox/cpp/main.cpp deleted file mode 100644 index cdf083ef3b..0000000000 --- a/dimos/hardware/sensors/lidar/livox/cpp/main.cpp +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2026 Dimensional Inc. -// SPDX-License-Identifier: Apache-2.0 -// -// Livox Mid-360 native module for dimos NativeModule framework. -// -// Publishes PointCloud2 and Imu messages on LCM topics received via CLI args. -// Usage: ./mid360_native --lidar --imu [--host_ip ] [--lidar_ip ] ... - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "livox_sdk_config.hpp" - -#include "dimos_native_module.hpp" - -#include "geometry_msgs/Quaternion.hpp" -#include "geometry_msgs/Vector3.hpp" -#include "sensor_msgs/Imu.hpp" -#include "sensor_msgs/PointCloud2.hpp" -#include "sensor_msgs/PointField.hpp" - -using livox_common::GRAVITY_MS2; -using livox_common::DATA_TYPE_IMU; -using livox_common::DATA_TYPE_CARTESIAN_HIGH; -using livox_common::DATA_TYPE_CARTESIAN_LOW; - -// --------------------------------------------------------------------------- -// Global state -// --------------------------------------------------------------------------- - -static std::atomic g_running{true}; -static lcm::LCM* g_lcm = nullptr; -static std::string g_lidar_topic; -static std::string g_imu_topic; -static std::string g_frame_id = "lidar_link"; -static std::string g_imu_frame_id = "imu_link"; -static float g_frequency = 10.0f; - -// Frame accumulator -static std::mutex g_pc_mutex; -static std::vector g_accumulated_xyz; // interleaved x,y,z -static std::vector g_accumulated_intensity; // per-point intensity -static double g_frame_timestamp = 0.0; -static bool g_frame_has_timestamp = false; - -// --------------------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------------------- - -static double get_timestamp_ns(const LivoxLidarEthernetPacket* pkt) { - uint64_t ns = 0; - std::memcpy(&ns, pkt->timestamp, sizeof(uint64_t)); - return static_cast(ns); -} - -using dimos::time_from_seconds; -using dimos::make_header; - -// --------------------------------------------------------------------------- -// Build and publish PointCloud2 -// --------------------------------------------------------------------------- - -static void publish_pointcloud(const std::vector& xyz, - const std::vector& intensity, - double timestamp) { - if (!g_lcm || xyz.empty()) return; - - int num_points = static_cast(xyz.size()) / 3; - - sensor_msgs::PointCloud2 pc; - pc.header = make_header(g_frame_id, timestamp); - pc.height = 1; - pc.width = num_points; - pc.is_bigendian = 0; - pc.is_dense = 1; - - // Fields: x, y, z (float32), intensity (float32) - pc.fields_length = 4; - pc.fields.resize(4); - - auto make_field = [](const std::string& name, int32_t offset) { - sensor_msgs::PointField f; - f.name = name; - f.offset = offset; - f.datatype = sensor_msgs::PointField::FLOAT32; - f.count = 1; - return f; - }; - - pc.fields[0] = make_field("x", 0); - pc.fields[1] = make_field("y", 4); - pc.fields[2] = make_field("z", 8); - pc.fields[3] = make_field("intensity", 12); - - pc.point_step = 16; // 4 floats * 4 bytes - pc.row_step = pc.point_step * num_points; - - // Pack point data - pc.data_length = pc.row_step; - pc.data.resize(pc.data_length); - - for (int i = 0; i < num_points; ++i) { - float* dst = reinterpret_cast(pc.data.data() + i * 16); - dst[0] = xyz[i * 3 + 0]; - dst[1] = xyz[i * 3 + 1]; - dst[2] = xyz[i * 3 + 2]; - dst[3] = intensity[i]; - } - - g_lcm->publish(g_lidar_topic, &pc); -} - -// --------------------------------------------------------------------------- -// SDK callbacks -// --------------------------------------------------------------------------- - -static void on_point_cloud(const uint32_t /*handle*/, const uint8_t /*dev_type*/, - LivoxLidarEthernetPacket* data, void* /*client_data*/) { - if (!g_running.load() || data == nullptr) return; - - double ts_ns = get_timestamp_ns(data); - double ts = ts_ns / 1e9; - uint16_t dot_num = data->dot_num; - - std::lock_guard lock(g_pc_mutex); - - if (!g_frame_has_timestamp) { - g_frame_timestamp = ts; - g_frame_has_timestamp = true; - } - - if (data->data_type == DATA_TYPE_CARTESIAN_HIGH) { - auto* pts = reinterpret_cast(data->data); - for (uint16_t i = 0; i < dot_num; ++i) { - // Livox high-precision coordinates are in mm, convert to meters - g_accumulated_xyz.push_back(static_cast(pts[i].x) / 1000.0f); - g_accumulated_xyz.push_back(static_cast(pts[i].y) / 1000.0f); - g_accumulated_xyz.push_back(static_cast(pts[i].z) / 1000.0f); - g_accumulated_intensity.push_back(static_cast(pts[i].reflectivity) / 255.0f); - } - } else if (data->data_type == DATA_TYPE_CARTESIAN_LOW) { - auto* pts = reinterpret_cast(data->data); - for (uint16_t i = 0; i < dot_num; ++i) { - // Livox low-precision coordinates are in cm, convert to meters - g_accumulated_xyz.push_back(static_cast(pts[i].x) / 100.0f); - g_accumulated_xyz.push_back(static_cast(pts[i].y) / 100.0f); - g_accumulated_xyz.push_back(static_cast(pts[i].z) / 100.0f); - g_accumulated_intensity.push_back(static_cast(pts[i].reflectivity) / 255.0f); - } - } -} - -static void on_imu_data(const uint32_t /*handle*/, const uint8_t /*dev_type*/, - LivoxLidarEthernetPacket* data, void* /*client_data*/) { - if (!g_running.load() || data == nullptr || !g_lcm) return; - if (g_imu_topic.empty()) return; - - double ts = get_timestamp_ns(data) / 1e9; - auto* imu_pts = reinterpret_cast(data->data); - uint16_t dot_num = data->dot_num; - - for (uint16_t i = 0; i < dot_num; ++i) { - sensor_msgs::Imu msg; - msg.header = make_header(g_imu_frame_id, ts); - - // Orientation unknown — set to identity with high covariance - msg.orientation.x = 0.0; - msg.orientation.y = 0.0; - msg.orientation.z = 0.0; - msg.orientation.w = 1.0; - msg.orientation_covariance[0] = -1.0; // indicates unknown - - msg.angular_velocity.x = static_cast(imu_pts[i].gyro_x); - msg.angular_velocity.y = static_cast(imu_pts[i].gyro_y); - msg.angular_velocity.z = static_cast(imu_pts[i].gyro_z); - - msg.linear_acceleration.x = static_cast(imu_pts[i].acc_x) * GRAVITY_MS2; - msg.linear_acceleration.y = static_cast(imu_pts[i].acc_y) * GRAVITY_MS2; - msg.linear_acceleration.z = static_cast(imu_pts[i].acc_z) * GRAVITY_MS2; - - g_lcm->publish(g_imu_topic, &msg); - } -} - -static void on_info_change(const uint32_t handle, const LivoxLidarInfo* info, - void* /*client_data*/) { - if (info == nullptr) return; - - char sn[17] = {}; - std::memcpy(sn, info->sn, 16); - char ip[17] = {}; - std::memcpy(ip, info->lidar_ip, 16); - - printf("[mid360] Device connected: handle=%u type=%u sn=%s ip=%s\n", - handle, info->dev_type, sn, ip); - - // Set to normal work mode - SetLivoxLidarWorkMode(handle, kLivoxLidarNormal, nullptr, nullptr); - - // Enable IMU - if (!g_imu_topic.empty()) { - EnableLivoxLidarImuData(handle, nullptr, nullptr); - } -} - -// --------------------------------------------------------------------------- -// Signal handling -// --------------------------------------------------------------------------- - -static void signal_handler(int /*sig*/) { - g_running.store(false); -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -int main(int argc, char** argv) { - dimos::NativeModule mod(argc, argv); - - // Required: LCM topics for ports - g_lidar_topic = mod.has("lidar") ? mod.topic("lidar") : ""; - g_imu_topic = mod.has("imu") ? mod.topic("imu") : ""; - - if (g_lidar_topic.empty()) { - fprintf(stderr, "Error: --lidar is required\n"); - return 1; - } - - // Optional config args - std::string host_ip = mod.arg("host_ip", "192.168.1.5"); - std::string lidar_ip = mod.arg("lidar_ip", "192.168.1.155"); - g_frequency = mod.arg_float("frequency", 10.0f); - g_frame_id = mod.arg("frame_id", "lidar_link"); - g_imu_frame_id = mod.arg("imu_frame_id", "imu_link"); - - // SDK network ports (defaults from SdkPorts struct in livox_sdk_config.hpp) - livox_common::SdkPorts ports; - const livox_common::SdkPorts port_defaults; - ports.cmd_data = mod.arg_int("cmd_data_port", port_defaults.cmd_data); - ports.push_msg = mod.arg_int("push_msg_port", port_defaults.push_msg); - ports.point_data = mod.arg_int("point_data_port", port_defaults.point_data); - ports.imu_data = mod.arg_int("imu_data_port", port_defaults.imu_data); - ports.log_data = mod.arg_int("log_data_port", port_defaults.log_data); - ports.host_cmd_data = mod.arg_int("host_cmd_data_port", port_defaults.host_cmd_data); - ports.host_push_msg = mod.arg_int("host_push_msg_port", port_defaults.host_push_msg); - ports.host_point_data = mod.arg_int("host_point_data_port", port_defaults.host_point_data); - ports.host_imu_data = mod.arg_int("host_imu_data_port", port_defaults.host_imu_data); - ports.host_log_data = mod.arg_int("host_log_data_port", port_defaults.host_log_data); - - printf("[mid360] Starting native Livox Mid-360 module\n"); - printf("[mid360] lidar topic: %s\n", g_lidar_topic.c_str()); - printf("[mid360] imu topic: %s\n", g_imu_topic.empty() ? "(disabled)" : g_imu_topic.c_str()); - printf("[mid360] host_ip: %s lidar_ip: %s frequency: %.1f Hz\n", - host_ip.c_str(), lidar_ip.c_str(), g_frequency); - - // Signal handlers - signal(SIGTERM, signal_handler); - signal(SIGINT, signal_handler); - - // Init LCM - lcm::LCM lcm; - if (!lcm.good()) { - fprintf(stderr, "Error: LCM init failed\n"); - return 1; - } - g_lcm = &lcm; - - // Init Livox SDK (in-memory config, no temp files) - if (!livox_common::init_livox_sdk(host_ip, lidar_ip, ports)) { - return 1; - } - - // Register callbacks - SetLivoxLidarPointCloudCallBack(on_point_cloud, nullptr); - if (!g_imu_topic.empty()) { - SetLivoxLidarImuDataCallback(on_imu_data, nullptr); - } - SetLivoxLidarInfoChangeCallback(on_info_change, nullptr); - - // Start SDK - if (!LivoxLidarSdkStart()) { - fprintf(stderr, "Error: LivoxLidarSdkStart failed\n"); - LivoxLidarSdkUninit(); - return 1; - } - - printf("[mid360] SDK started, waiting for device...\n"); - - // Main loop: periodically emit accumulated point clouds - auto frame_interval = std::chrono::microseconds( - static_cast(1e6 / g_frequency)); - auto last_emit = std::chrono::steady_clock::now(); - - while (g_running.load()) { - // Handle LCM (for any subscriptions, though we mostly publish) - lcm.handleTimeout(10); // 10ms timeout - - auto now = std::chrono::steady_clock::now(); - if (now - last_emit >= frame_interval) { - // Swap out the accumulated data - std::vector xyz; - std::vector intensity; - double ts = 0.0; - - { - std::lock_guard lock(g_pc_mutex); - if (!g_accumulated_xyz.empty()) { - xyz.swap(g_accumulated_xyz); - intensity.swap(g_accumulated_intensity); - ts = g_frame_timestamp; - g_frame_has_timestamp = false; - } - } - - if (!xyz.empty()) { - publish_pointcloud(xyz, intensity, ts); - } - - last_emit = now; - } - } - - // Cleanup - printf("[mid360] Shutting down...\n"); - LivoxLidarSdkUninit(); - g_lcm = nullptr; - - printf("[mid360] Done.\n"); - return 0; -} diff --git a/dimos/hardware/sensors/lidar/livox/livox_blueprints.py b/dimos/hardware/sensors/lidar/livox/livox_blueprints.py deleted file mode 100644 index 0cda912b73..0000000000 --- a/dimos/hardware/sensors/lidar/livox/livox_blueprints.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.lidar.livox.module import Mid360 -from dimos.visualization.rerun.bridge import rerun_bridge - -mid360 = autoconnect( - Mid360.blueprint(), - rerun_bridge(), -).global_config(n_dask_workers=2, robot_model="mid360") diff --git a/dimos/hardware/sensors/lidar/livox/module.py b/dimos/hardware/sensors/lidar/livox/module.py deleted file mode 100644 index 672968a0eb..0000000000 --- a/dimos/hardware/sensors/lidar/livox/module.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Python NativeModule wrapper for the C++ Livox Mid-360 driver. - -Usage:: - from dimos.hardware.sensors.lidar.livox.module import Mid360 - from dimos.core.blueprints import autoconnect - - autoconnect( - Mid360.blueprint(host_ip="192.168.1.5"), - SomeConsumer.blueprint(), - ).build().loop() -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from dimos.core import Out # noqa: TC001 -from dimos.core.native_module import NativeModule, NativeModuleConfig -from dimos.hardware.sensors.lidar.livox.ports import ( - SDK_CMD_DATA_PORT, - SDK_HOST_CMD_DATA_PORT, - SDK_HOST_IMU_DATA_PORT, - SDK_HOST_LOG_DATA_PORT, - SDK_HOST_POINT_DATA_PORT, - SDK_HOST_PUSH_MSG_PORT, - SDK_IMU_DATA_PORT, - SDK_LOG_DATA_PORT, - SDK_POINT_DATA_PORT, - SDK_PUSH_MSG_PORT, -) -from dimos.msgs.sensor_msgs.Imu import Imu # noqa: TC001 -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 # noqa: TC001 -from dimos.spec import perception - - -@dataclass(kw_only=True) -class Mid360Config(NativeModuleConfig): - """Config for the C++ Mid-360 native module.""" - - cwd: str | None = "cpp" - executable: str = "result/bin/mid360_native" - build_command: str | None = "nix build .#mid360_native" - - host_ip: str = "192.168.1.5" - lidar_ip: str = "192.168.1.155" - frequency: float = 10.0 - enable_imu: bool = True - frame_id: str = "lidar_link" - imu_frame_id: str = "imu_link" - - # SDK port configuration (see livox/ports.py for defaults) - cmd_data_port: int = SDK_CMD_DATA_PORT - push_msg_port: int = SDK_PUSH_MSG_PORT - point_data_port: int = SDK_POINT_DATA_PORT - imu_data_port: int = SDK_IMU_DATA_PORT - log_data_port: int = SDK_LOG_DATA_PORT - host_cmd_data_port: int = SDK_HOST_CMD_DATA_PORT - host_push_msg_port: int = SDK_HOST_PUSH_MSG_PORT - host_point_data_port: int = SDK_HOST_POINT_DATA_PORT - host_imu_data_port: int = SDK_HOST_IMU_DATA_PORT - host_log_data_port: int = SDK_HOST_LOG_DATA_PORT - - -class Mid360(NativeModule, perception.Lidar, perception.IMU): - """Livox Mid-360 LiDAR module backed by a native C++ binary. - - Ports: - lidar (Out[PointCloud2]): Point cloud frames at configured frequency. - imu (Out[Imu]): IMU data at ~200 Hz (if enabled). - """ - - config: Mid360Config - default_config = Mid360Config - - lidar: Out[PointCloud2] - imu: Out[Imu] - - -mid360_module = Mid360.blueprint - -__all__ = [ - "Mid360", - "Mid360Config", - "mid360_module", -] - -# Verify protocol port compliance (mypy will flag missing ports) -if TYPE_CHECKING: - Mid360() diff --git a/dimos/hardware/sensors/lidar/livox/ports.py b/dimos/hardware/sensors/lidar/livox/ports.py deleted file mode 100644 index 9ad83251d6..0000000000 --- a/dimos/hardware/sensors/lidar/livox/ports.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Default Livox SDK2 network port constants. - -These match the defaults in ``common/livox_sdk_config.hpp`` (``SdkPorts``). -Both the Mid-360 driver and FAST-LIO2 modules reference this single source -so port numbers are defined in one place on the Python side. -""" - -SDK_CMD_DATA_PORT = 56100 -SDK_PUSH_MSG_PORT = 56200 -SDK_POINT_DATA_PORT = 56300 -SDK_IMU_DATA_PORT = 56400 -SDK_LOG_DATA_PORT = 56500 -SDK_HOST_CMD_DATA_PORT = 56101 -SDK_HOST_PUSH_MSG_PORT = 56201 -SDK_HOST_POINT_DATA_PORT = 56301 -SDK_HOST_IMU_DATA_PORT = 56401 -SDK_HOST_LOG_DATA_PORT = 56501 diff --git a/dimos/manipulation/__init__.py b/dimos/manipulation/__init__.py deleted file mode 100644 index 3ed1863092..0000000000 --- a/dimos/manipulation/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Manipulation module for robot arm motion planning and control.""" - -from dimos.manipulation.manipulation_module import ( - ManipulationModule, - ManipulationModuleConfig, - ManipulationState, - manipulation_module, -) - -__all__ = [ - "ManipulationModule", - "ManipulationModuleConfig", - "ManipulationState", - "manipulation_module", -] diff --git a/dimos/manipulation/control/__init__.py b/dimos/manipulation/control/__init__.py deleted file mode 100644 index ec85660eb3..0000000000 --- a/dimos/manipulation/control/__init__.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Manipulation Control Modules - -Hardware-agnostic controllers for robotic manipulation tasks. - -Submodules: -- servo_control: Real-time servo-level controllers (Cartesian motion control) -- trajectory_controller: Trajectory planning and execution -""" - -# Re-export from servo_control for backwards compatibility -from dimos.manipulation.control.servo_control import ( - CartesianMotionController, - CartesianMotionControllerConfig, - cartesian_motion_controller, -) - -# Re-export from trajectory_controller -from dimos.manipulation.control.trajectory_controller import ( - JointTrajectoryController, - JointTrajectoryControllerConfig, - joint_trajectory_controller, -) - -__all__ = [ - # Servo control - "CartesianMotionController", - "CartesianMotionControllerConfig", - # Trajectory control - "JointTrajectoryController", - "JointTrajectoryControllerConfig", - "cartesian_motion_controller", - "joint_trajectory_controller", -] diff --git a/dimos/manipulation/control/coordinator_client.py b/dimos/manipulation/control/coordinator_client.py deleted file mode 100644 index 4e277fae97..0000000000 --- a/dimos/manipulation/control/coordinator_client.py +++ /dev/null @@ -1,713 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Interactive client for the ControlCoordinator. - -Interfaces with a running ControlCoordinator via RPC to: -- Query hardware and task status -- Plan and execute trajectories on single or multiple arms -- Monitor execution progress - -Usage: - # Terminal 1: Start the coordinator - dimos run coordinator-mock # Single arm - dimos run coordinator-dual-mock # Dual arm - - # Terminal 2: Run this client - python -m dimos.manipulation.control.coordinator_client - python -m dimos.manipulation.control.coordinator_client --task traj_left - python -m dimos.manipulation.control.coordinator_client --task traj_right - -How it works: - 1. Connects to ControlCoordinator via LCM RPC - 2. Queries available hardware/tasks/joints - 3. You add waypoints (joint positions) - 4. Generates trajectory with trapezoidal velocity profile - 5. Sends trajectory to coordinator via execute_trajectory() RPC - 6. Coordinator's tick loop executes it at 100Hz -""" - -from __future__ import annotations - -import math -import sys -import time -from typing import TYPE_CHECKING, Any - -from dimos.control.coordinator import ControlCoordinator -from dimos.core.rpc_client import RPCClient -from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( - JointTrajectoryGenerator, -) - -if TYPE_CHECKING: - from dimos.msgs.trajectory_msgs import JointTrajectory - - -class CoordinatorClient: - """ - RPC client for the ControlCoordinator. - - Connects to a running coordinator and provides methods to: - - Query state (joints, tasks, hardware) - - Execute trajectories on any task - - Monitor progress - - Example: - client = CoordinatorClient() - - # Query state - print(client.list_hardware()) # ['left_arm', 'right_arm'] - print(client.list_tasks()) # ['traj_left', 'traj_right'] - - # Setup for a task - client.select_task("traj_left") - - # Get current position and create trajectory - current = client.get_current_positions() - target = [0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] - trajectory = client.generate_trajectory([current, target]) - - # Execute - client.execute_trajectory("traj_left", trajectory) - """ - - def __init__(self) -> None: - """Initialize connection to coordinator via RPC.""" - self._rpc = RPCClient(None, ControlCoordinator) - - # Per-task state - self._current_task: str | None = None - self._task_joints: dict[str, list[str]] = {} # task_name -> joint_names - self._generators: dict[str, JointTrajectoryGenerator] = {} # task_name -> generator - - def stop(self) -> None: - """Stop the RPC client.""" - self._rpc.stop_rpc_client() - - # ========================================================================= - # Query methods (RPC calls) - # ========================================================================= - - def list_hardware(self) -> list[str]: - """List all hardware IDs.""" - return self._rpc.list_hardware() or [] - - def list_joints(self) -> list[str]: - """List all joint names across all hardware.""" - return self._rpc.list_joints() or [] - - def list_tasks(self) -> list[str]: - """List all task names.""" - return self._rpc.list_tasks() or [] - - def get_active_tasks(self) -> list[str]: - """Get currently active task names.""" - return self._rpc.get_active_tasks() or [] - - def get_joint_positions(self) -> dict[str, float]: - """Get current joint positions for all joints.""" - return self._rpc.get_joint_positions() or {} - - def get_trajectory_status(self, task_name: str) -> dict[str, Any]: - """Get status of a trajectory task via task_invoke.""" - result = self._rpc.task_invoke(task_name, "get_state", {}) - if result is not None: - return {"state": int(result), "task": task_name} - return {} - - # ========================================================================= - # Trajectory execution (via task_invoke) - # ========================================================================= - - def execute_trajectory(self, task_name: str, trajectory: JointTrajectory) -> bool: - """Execute a trajectory on a task via task_invoke.""" - result = self._rpc.task_invoke(task_name, "execute", {"trajectory": trajectory}) - return bool(result) - - def cancel_trajectory(self, task_name: str) -> bool: - """Cancel an active trajectory via task_invoke.""" - result = self._rpc.task_invoke(task_name, "cancel", {}) - return bool(result) - - # ========================================================================= - # Task selection and setup - # ========================================================================= - - def select_task(self, task_name: str) -> bool: - """ - Select a task and setup its trajectory generator. - - This queries the coordinator to find which joints the task controls, - then creates a trajectory generator for those joints. - """ - tasks = self.list_tasks() - if task_name not in tasks: - print(f"Task '{task_name}' not found. Available: {tasks}") - return False - - self._current_task = task_name - - # Get joints for this task (infer from task name pattern) - # e.g., "traj_left" -> joints starting with "left_arm_" (hardware_id based naming) - # e.g., "traj_arm" -> joints starting with "arm_" - all_joints = self.list_joints() - - # Try to infer hardware_id from task name - if "_" in task_name: - suffix = task_name.split("_", 1)[1] # "traj_left" -> "left" - # Try both patterns: exact suffix (e.g., "arm_") and with "_arm" suffix (e.g., "left_arm_") - task_joints = [j for j in all_joints if j.startswith(suffix + "_")] - if not task_joints: - # Try with "_arm" suffix for dual-arm setups (left -> left_arm) - task_joints = [j for j in all_joints if j.startswith(suffix + "_arm_")] - else: - task_joints = all_joints - - if not task_joints: - # Fallback: use all joints - task_joints = all_joints - - self._task_joints[task_name] = task_joints - - # Create generator if not exists - if task_name not in self._generators: - self._generators[task_name] = JointTrajectoryGenerator( - num_joints=len(task_joints), - max_velocity=1.0, - max_acceleration=2.0, - points_per_segment=50, - ) - - return True - - def get_task_joints(self, task_name: str | None = None) -> list[str]: - """Get joint names for a task.""" - task = task_name or self._current_task - if task is None: - return [] - return self._task_joints.get(task, []) - - def get_current_positions(self, task_name: str | None = None) -> list[float] | None: - """Get current joint positions for a task as a list.""" - task = task_name or self._current_task - if task is None: - return None - - joints = self._task_joints.get(task, []) - if not joints: - return None - - positions = self.get_joint_positions() - if not positions: - return None - - return [positions.get(j, 0.0) for j in joints] - - def generate_trajectory( - self, waypoints: list[list[float]], task_name: str | None = None - ) -> JointTrajectory | None: - """Generate trajectory from waypoints using trapezoidal velocity profile.""" - task = task_name or self._current_task - if task is None: - print("Error: No task selected") - return None - - generator = self._generators.get(task) - if generator is None: - print(f"Error: No generator for task '{task}'. Call select_task() first.") - return None - - return generator.generate(waypoints) - - def set_velocity_limit(self, velocity: float, task_name: str | None = None) -> None: - """Set max velocity for trajectory generation.""" - task = task_name or self._current_task - if task and task in self._generators: - gen = self._generators[task] - gen.set_limits(velocity, gen.max_acceleration) - - def set_acceleration_limit(self, acceleration: float, task_name: str | None = None) -> None: - """Set max acceleration for trajectory generation.""" - task = task_name or self._current_task - if task and task in self._generators: - gen = self._generators[task] - gen.set_limits(gen.max_velocity, acceleration) - - -# ============================================================================= -# Interactive CLI -# ============================================================================= - - -def parse_joint_input(line: str, num_joints: int) -> list[float] | None: - """Parse joint positions from user input (degrees by default, 'r' suffix for radians).""" - parts = line.strip().split() - if len(parts) != num_joints: - return None - - positions = [] - for part in parts: - try: - if part.endswith("r"): - positions.append(float(part[:-1])) - else: - positions.append(math.radians(float(part))) - except ValueError: - return None - - return positions - - -def format_positions(positions: list[float], as_degrees: bool = True) -> str: - """Format positions for display.""" - if as_degrees: - return "[" + ", ".join(f"{math.degrees(p):.1f}" for p in positions) + "] deg" - return "[" + ", ".join(f"{p:.3f}" for p in positions) + "] rad" - - -def preview_waypoints(waypoints: list[list[float]], joint_names: list[str]) -> None: - """Show waypoints list.""" - if not waypoints: - print("No waypoints") - return - - print(f"\nWaypoints ({len(waypoints)}):") - print("-" * 70) - - # Header with joint names (truncated) - headers = [j.split("_")[-1][:6] for j in joint_names] # e.g., "joint1" -> "joint1" - header_str = " ".join(f"{h:>7}" for h in headers) - print(f" # | {header_str} (degrees)") - print("-" * 70) - - for i, joints in enumerate(waypoints): - deg = [f"{math.degrees(j):7.1f}" for j in joints] - print(f" {i + 1:2} | {' '.join(deg)}") - print("-" * 70) - - -def preview_trajectory(trajectory: JointTrajectory, joint_names: list[str]) -> None: - """Show generated trajectory preview.""" - headers = [j.split("_")[-1][:6] for j in joint_names] - header_str = " ".join(f"{h:>7}" for h in headers) - - print("\n" + "=" * 70) - print("GENERATED TRAJECTORY") - print("=" * 70) - print(f"Duration: {trajectory.duration:.3f}s") - print(f"Points: {len(trajectory.points)}") - print("-" * 70) - print(f"{'Time':>6} | {header_str} (degrees)") - print("-" * 70) - - num_samples = min(10, max(len(trajectory.points) // 10, 5)) - for i in range(num_samples + 1): - t = (i / num_samples) * trajectory.duration - q_ref, _ = trajectory.sample(t) - q_deg = [f"{math.degrees(q):7.1f}" for q in q_ref] - print(f"{t:6.2f} | {' '.join(q_deg)}") - - print("=" * 70) - - -def wait_for_completion(client: CoordinatorClient, task_name: str, timeout: float = 60.0) -> bool: - """Wait for trajectory to complete by polling task state. - - TrajectoryState is an IntEnum: IDLE=0, EXECUTING=1, COMPLETED=2, ABORTED=3, FAULT=4. - """ - start = time.time() - _STATE_NAMES = {0: "IDLE", 1: "EXECUTING", 2: "COMPLETED", 3: "ABORTED", 4: "FAULT"} - - while time.time() - start < timeout: - status = client.get_trajectory_status(task_name) - if not status: - print("\nCould not get trajectory status") - return False - - state_val = status.get("state") - state_name = _STATE_NAMES.get(state_val, f"UNKNOWN({state_val})") # type: ignore[arg-type] - - if state_val in (0, 2): # IDLE or COMPLETED - print(f"\nTrajectory finished: {state_name}") - return True - if state_val in (3, 4): # ABORTED or FAULT - print(f"\nTrajectory failed: {state_name}") - return False - # state_val == 1 means EXECUTING, keep polling - elapsed = time.time() - start - print(f"\r Executing... ({elapsed:.1f}s)", end="", flush=True) - time.sleep(0.1) - - print("\nTimeout waiting for trajectory") - return False - - -class CoordinatorShell: - """IPython shell interface for coordinator control.""" - - def __init__(self, client: CoordinatorClient, initial_task: str) -> None: - self._client = client - self._current_task = initial_task - self._waypoints: list[list[float]] = [] - self._generated_trajectory: JointTrajectory | None = None - - if not client.select_task(initial_task): - raise ValueError(f"Failed to select task: {initial_task}") - - def _joints(self) -> list[str]: - return self._client.get_task_joints(self._current_task) - - def _num_joints(self) -> int: - return len(self._joints()) - - def help(self) -> None: - """Show available commands.""" - print("\nCoordinator Client Commands:") - print("=" * 60) - print("Waypoint Commands:") - print(" here() - Add current position as waypoint") - print(" add(j1, j2, ...) - Add waypoint (degrees)") - print(" waypoints() - List all waypoints") - print(" delete(n) - Delete waypoint n") - print(" clear() - Clear all waypoints") - print("\nTrajectory Commands:") - print(" preview() - Preview generated trajectory") - print(" run() - Execute trajectory") - print(" status() - Show task status") - print(" cancel() - Cancel active trajectory") - print("\nMulti-Arm Commands:") - print(" tasks() - List all tasks") - print(" switch('task_name') - Switch to different task") - print(" hw() - List hardware") - print(" joints() - List joints for current task") - print("\nSettings:") - print(" current() - Show current joint positions") - print(" vel(value) - Set max velocity (rad/s)") - print(" accel(value) - Set max acceleration (rad/s^2)") - print("=" * 60) - - def here(self) -> None: - """Add current position as waypoint.""" - positions = self._client.get_current_positions(self._current_task) - if positions: - self._waypoints.append(positions) - self._generated_trajectory = None - print(f"Added waypoint {len(self._waypoints)}: {format_positions(positions)}") - else: - print("Could not get current positions") - - def add(self, *joints: float) -> None: - """Add waypoint with specified joint values (in degrees).""" - num_joints = self._num_joints() - if len(joints) != num_joints: - print(f"Need {num_joints} joint values, got {len(joints)}") - return - - rad_joints = [math.radians(j) for j in joints] - self._waypoints.append(rad_joints) - self._generated_trajectory = None - print(f"Added waypoint {len(self._waypoints)}: {format_positions(rad_joints)}") - - def waypoints(self) -> None: - """List all waypoints.""" - preview_waypoints(self._waypoints, self._joints()) - - def delete(self, index: int) -> None: - """Delete a waypoint by index (1-based).""" - idx = index - 1 - if 0 <= idx < len(self._waypoints): - self._waypoints.pop(idx) - self._generated_trajectory = None - print(f"Deleted waypoint {index}") - else: - print(f"Invalid index (1-{len(self._waypoints)})") - - def clear(self) -> None: - """Clear all waypoints.""" - self._waypoints.clear() - self._generated_trajectory = None - print("Cleared waypoints") - - def preview(self) -> None: - """Preview generated trajectory.""" - if len(self._waypoints) < 2: - print("Need at least 2 waypoints") - return - try: - self._generated_trajectory = self._client.generate_trajectory( - self._waypoints, self._current_task - ) - if self._generated_trajectory: - preview_trajectory(self._generated_trajectory, self._joints()) - except Exception as e: - print(f"Error: {e}") - - def run(self) -> None: - """Execute trajectory.""" - if len(self._waypoints) < 2: - print("Need at least 2 waypoints") - return - - if self._generated_trajectory is None: - self._generated_trajectory = self._client.generate_trajectory( - self._waypoints, self._current_task - ) - - if self._generated_trajectory is None: - print("Failed to generate trajectory") - return - - preview_trajectory(self._generated_trajectory, self._joints()) - confirm = input("\nExecute? [y/N]: ").strip().lower() - if confirm == "y": - if self._client.execute_trajectory(self._current_task, self._generated_trajectory): - print("Trajectory started...") - wait_for_completion(self._client, self._current_task) - else: - print("Failed to start trajectory") - - def status(self) -> None: - """Show task status.""" - _STATE_NAMES = {0: "IDLE", 1: "EXECUTING", 2: "COMPLETED", 3: "ABORTED", 4: "FAULT"} - status = self._client.get_trajectory_status(self._current_task) - state_val = status.get("state") - state_name = _STATE_NAMES.get(state_val, f"UNKNOWN({state_val})") # type: ignore[arg-type] - print(f"\nTask: {self._current_task}") - print(f" State: {state_name} ({state_val})") - - def cancel(self) -> None: - """Cancel active trajectory.""" - if self._client.cancel_trajectory(self._current_task): - print("Cancelled") - else: - print("Cancel failed") - - def tasks(self) -> None: - """List all tasks.""" - all_tasks = self._client.list_tasks() - active = self._client.get_active_tasks() - print("\nTasks:") - for t in all_tasks: - marker = "* " if t == self._current_task else " " - active_marker = " [ACTIVE]" if t in active else "" - t_joints = self._client.get_task_joints(t) - joint_count = len(t_joints) if t_joints else "?" - print(f"{marker}{t} ({joint_count} joints){active_marker}") - - def switch(self, task_name: str) -> None: - """Switch to a different task.""" - if self._client.select_task(task_name): - self._current_task = task_name - self._waypoints.clear() - self._generated_trajectory = None - joints = self._joints() - print(f"Switched to {self._current_task} ({len(joints)} joints)") - print(f"Joints: {', '.join(joints)}") - else: - print(f"Failed to switch to {task_name}") - - def hw(self) -> None: - """List hardware.""" - hardware = self._client.list_hardware() - print(f"\nHardware: {', '.join(hardware)}") - - def joints(self) -> None: - """List joints for current task.""" - joints = self._joints() - print(f"\nJoints for {self._current_task}:") - for i, j in enumerate(joints): - pos = self._client.get_joint_positions().get(j, 0.0) - print(f" {i + 1}. {j}: {math.degrees(pos):.1f} deg") - - def current(self) -> None: - """Show current joint positions.""" - positions = self._client.get_current_positions(self._current_task) - if positions: - print(f"Current: {format_positions(positions)}") - else: - print("Could not get positions") - - def vel(self, value: float | None = None) -> None: - """Set or show max velocity (rad/s).""" - if value is None: - gen = self._client._generators.get(self._current_task) - if gen: - print(f"Max velocity: {gen.max_velocity[0]:.2f} rad/s") - return - - if value <= 0: - print("Velocity must be positive") - return - - self._client.set_velocity_limit(value, self._current_task) - self._generated_trajectory = None - print(f"Max velocity: {value:.2f} rad/s") - - def accel(self, value: float | None = None) -> None: - """Set or show max acceleration (rad/s^2).""" - if value is None: - gen = self._client._generators.get(self._current_task) - if gen: - print(f"Max acceleration: {gen.max_acceleration[0]:.2f} rad/s^2") - return - - if value <= 0: - print("Acceleration must be positive") - return - - self._client.set_acceleration_limit(value, self._current_task) - self._generated_trajectory = None - print(f"Max acceleration: {value:.2f} rad/s^2") - - -def interactive_mode(client: CoordinatorClient, initial_task: str) -> None: - """Start IPython interactive mode.""" - import IPython - - shell = CoordinatorShell(client, initial_task) - - print("\n" + "=" * 60) - print(f"Coordinator Client (IPython) - Task: {initial_task}") - print("=" * 60) - print(f"Joints: {', '.join(shell._joints())}") - print("\nType help() for available commands") - print("=" * 60 + "\n") - - IPython.start_ipython( # type: ignore[no-untyped-call] - argv=[], - user_ns={ - "help": shell.help, - "here": shell.here, - "add": shell.add, - "waypoints": shell.waypoints, - "delete": shell.delete, - "clear": shell.clear, - "preview": shell.preview, - "run": shell.run, - "status": shell.status, - "cancel": shell.cancel, - "tasks": shell.tasks, - "switch": shell.switch, - "hw": shell.hw, - "joints": shell.joints, - "current": shell.current, - "vel": shell.vel, - "accel": shell.accel, - "client": client, - "shell": shell, - }, - ) - - -def _run_client(client: CoordinatorClient, task: str, vel: float, accel: float) -> int: - """Run the client with the given configuration.""" - try: - hardware = client.list_hardware() - tasks = client.list_tasks() - - if not hardware: - print("\nWarning: No hardware found. Is the coordinator running?") - print("Start with: dimos run coordinator-mock") - response = input("Continue anyway? [y/N]: ").strip().lower() - if response != "y": - return 0 - else: - print(f"Hardware: {', '.join(hardware)}") - print(f"Tasks: {', '.join(tasks)}") - - except Exception as e: - print(f"\nConnection error: {e}") - print("Make sure coordinator is running: dimos run coordinator-mock") - return 1 - - if task not in tasks and tasks: - print(f"\nTask '{task}' not found.") - print(f"Available: {', '.join(tasks)}") - task = tasks[0] - print(f"Using '{task}'") - - if client.select_task(task): - client.set_velocity_limit(vel, task) - client.set_acceleration_limit(accel, task) - - interactive_mode(client, task) - return 0 - - -def main() -> int: - """Main entry point.""" - import argparse - - parser = argparse.ArgumentParser( - description="Interactive client for ControlCoordinator", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Single arm (with coordinator-mock running) - python -m dimos.manipulation.control.coordinator_client - - # Dual arm - control left arm - python -m dimos.manipulation.control.coordinator_client --task traj_left - - # Dual arm - control right arm - python -m dimos.manipulation.control.coordinator_client --task traj_right - """, - ) - parser.add_argument( - "--task", - type=str, - default="traj_arm", - help="Initial task to control (default: traj_arm)", - ) - parser.add_argument( - "--vel", - type=float, - default=1.0, - help="Max velocity in rad/s (default: 1.0)", - ) - parser.add_argument( - "--accel", - type=float, - default=2.0, - help="Max acceleration in rad/s^2 (default: 2.0)", - ) - args = parser.parse_args() - - print("\n" + "=" * 70) - print("Coordinator Client") - print("=" * 70) - print("\nConnecting to ControlCoordinator via RPC...") - - client = CoordinatorClient() - try: - return _run_client(client, args.task, args.vel, args.accel) - finally: - client.stop() - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n\nInterrupted") - sys.exit(0) - except Exception as e: - print(f"\nError: {e}") - import traceback - - traceback.print_exc() - sys.exit(1) diff --git a/dimos/manipulation/control/dual_trajectory_setter.py b/dimos/manipulation/control/dual_trajectory_setter.py deleted file mode 100644 index 4f8a8802e1..0000000000 --- a/dimos/manipulation/control/dual_trajectory_setter.py +++ /dev/null @@ -1,542 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Dual-Arm Interactive Trajectory Publisher. - -Interactive terminal UI for creating joint trajectories for two arms independently. -Supports running trajectories on left arm, right arm, or both simultaneously. - -Workflow: -1. Add waypoints to left or right arm (or both) -2. Generator applies trapezoidal velocity profiles -3. Preview the generated trajectories -4. Run on left, right, or both arms - -Use with xarm-dual-trajectory blueprint running in another terminal. -""" - -from dataclasses import dataclass -import math -import sys -import time - -from dimos import core -from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( - JointTrajectoryGenerator, -) -from dimos.msgs.sensor_msgs import JointState -from dimos.msgs.trajectory_msgs import JointTrajectory - - -@dataclass -class ArmState: - """State for a single arm.""" - - name: str - num_joints: int | None = None - latest_joint_state: JointState | None = None - generator: JointTrajectoryGenerator | None = None - waypoints: list[list[float]] | None = None - generated_trajectory: JointTrajectory | None = None - - def __post_init__(self) -> None: - self.waypoints = [] - - -class DualTrajectorySetter: - """ - Creates and publishes JointTrajectory for dual-arm setups. - - Manages two arms independently with separate waypoints and trajectories. - Supports running trajectories on one or both arms. - """ - - def __init__( - self, - left_joint_topic: str = "/xarm/left/joint_states", - right_joint_topic: str = "/xarm/right/joint_states", - left_trajectory_topic: str = "/xarm/left/trajectory", - right_trajectory_topic: str = "/xarm/right/trajectory", - ): - """ - Initialize the dual trajectory setter. - - Args: - left_joint_topic: Topic for left arm joint states - right_joint_topic: Topic for right arm joint states - left_trajectory_topic: Topic to publish left arm trajectories - right_trajectory_topic: Topic to publish right arm trajectories - """ - # Arm states - self.left = ArmState(name="left") - self.right = ArmState(name="right") - - # Publishers for trajectories - self.left_trajectory_pub: core.LCMTransport[JointTrajectory] = core.LCMTransport( - left_trajectory_topic, JointTrajectory - ) - self.right_trajectory_pub: core.LCMTransport[JointTrajectory] = core.LCMTransport( - right_trajectory_topic, JointTrajectory - ) - - # Subscribers for joint states - self.left_joint_sub: core.LCMTransport[JointState] = core.LCMTransport( - left_joint_topic, JointState - ) - self.right_joint_sub: core.LCMTransport[JointState] = core.LCMTransport( - right_joint_topic, JointState - ) - - print("DualTrajectorySetter initialized") - print(f" Left arm: {left_joint_topic} -> {left_trajectory_topic}") - print(f" Right arm: {right_joint_topic} -> {right_trajectory_topic}") - - def start(self) -> bool: - """Start subscribing to joint states.""" - self.left_joint_sub.subscribe(self._on_left_joint_state) - self.right_joint_sub.subscribe(self._on_right_joint_state) - print(" Waiting for joint states...") - - # Wait for both arms - left_ready = False - right_ready = False - - for _ in range(50): # 5 second timeout - if not left_ready and self.left.latest_joint_state is not None: - self.left.num_joints = len(self.left.latest_joint_state.position) - self.left.generator = JointTrajectoryGenerator( - num_joints=self.left.num_joints, - max_velocity=1.0, - max_acceleration=2.0, - points_per_segment=50, - ) - print(f" Left arm ready ({self.left.num_joints} joints)") - left_ready = True - - if not right_ready and self.right.latest_joint_state is not None: - self.right.num_joints = len(self.right.latest_joint_state.position) - self.right.generator = JointTrajectoryGenerator( - num_joints=self.right.num_joints, - max_velocity=1.0, - max_acceleration=2.0, - points_per_segment=50, - ) - print(f" Right arm ready ({self.right.num_joints} joints)") - right_ready = True - - if left_ready and right_ready: - return True - - time.sleep(0.1) - - if not left_ready: - print(" Warning: Left arm not responding") - if not right_ready: - print(" Warning: Right arm not responding") - - return left_ready or right_ready - - def _on_left_joint_state(self, msg: JointState) -> None: - """Callback for left arm joint state.""" - self.left.latest_joint_state = msg - - def _on_right_joint_state(self, msg: JointState) -> None: - """Callback for right arm joint state.""" - self.right.latest_joint_state = msg - - def get_current_joints(self, arm: ArmState) -> list[float] | None: - """Get current joint positions for an arm.""" - if arm.latest_joint_state is None or arm.num_joints is None: - return None - return list(arm.latest_joint_state.position[: arm.num_joints]) - - def generate_trajectory(self, arm: ArmState) -> JointTrajectory | None: - """Generate trajectory for an arm from its waypoints.""" - if arm.generator is None or not arm.waypoints or len(arm.waypoints) < 2: - return None - return arm.generator.generate(arm.waypoints) - - def publish_trajectory(self, arm: ArmState, trajectory: JointTrajectory) -> None: - """Publish trajectory to an arm.""" - if arm.name == "left": - self.left_trajectory_pub.broadcast(None, trajectory) - else: - self.right_trajectory_pub.broadcast(None, trajectory) - print( - f" Published to {arm.name}: {len(trajectory.points)} points, " - f"duration={trajectory.duration:.2f}s" - ) - - -def parse_joint_input(line: str, num_joints: int) -> list[float] | None: - """Parse joint positions from user input (degrees by default, 'r' suffix for radians).""" - parts = line.strip().split() - if len(parts) != num_joints: - return None - - positions = [] - for part in parts: - try: - if part.endswith("r"): - positions.append(float(part[:-1])) - else: - positions.append(math.radians(float(part))) - except ValueError: - return None - - return positions - - -def preview_waypoints(arm: ArmState) -> None: - """Show waypoints for an arm.""" - if not arm.waypoints or arm.num_joints is None: - print(f" {arm.name.upper()}: No waypoints") - return - - joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(arm.num_joints)]) - line_width = 6 + 3 + arm.num_joints * 8 + 10 - - print(f"\n{arm.name.upper()} Waypoints ({len(arm.waypoints)}):") - print("-" * line_width) - print(f" # | {joint_headers} (degrees)") - print("-" * line_width) - for i, joints in enumerate(arm.waypoints): - deg = [f"{math.degrees(j):7.1f}" for j in joints] - print(f" {i + 1:2} | {' '.join(deg)}") - print("-" * line_width) - - -def preview_trajectory(arm: ArmState) -> None: - """Show generated trajectory for an arm.""" - if arm.generated_trajectory is None or arm.num_joints is None: - print(f" {arm.name.upper()}: No trajectory") - return - - traj = arm.generated_trajectory - joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(arm.num_joints)]) - line_width = 9 + 3 + arm.num_joints * 8 + 10 - - print(f"\n{'=' * line_width}") - print(f"{arm.name.upper()} TRAJECTORY") - print(f"{'=' * line_width}") - print(f"Duration: {traj.duration:.3f}s | Points: {len(traj.points)}") - print("-" * line_width) - print(f"{'Time':>6} | {joint_headers} (degrees)") - print("-" * line_width) - - num_samples = min(10, max(len(traj.points) // 10, 5)) - for i in range(num_samples + 1): - t = (i / num_samples) * traj.duration - q_ref, _ = traj.sample(t) - q_deg = [f"{math.degrees(q):7.1f}" for q in q_ref] - print(f"{t:6.2f} | {' '.join(q_deg)}") - - print("-" * line_width) - - -def interactive_mode(setter: DualTrajectorySetter) -> None: - """Interactive mode for creating dual-arm trajectories.""" - left = setter.left - right = setter.right - - print("\n" + "=" * 80) - print("Dual-Arm Interactive Trajectory Setter") - print("=" * 80) - - if left.num_joints: - print(f" Left arm: {left.num_joints} joints") - else: - print(" Left arm: NOT CONNECTED") - - if right.num_joints: - print(f" Right arm: {right.num_joints} joints") - else: - print(" Right arm: NOT CONNECTED") - - print("\nCommands:") - print(" left add ... - Add waypoint to left arm (degrees)") - print(" right add ... - Add waypoint to right arm (degrees)") - print(" left here - Add current position as waypoint (left)") - print(" right here - Add current position as waypoint (right)") - print(" left current - Show current left arm joints") - print(" right current - Show current right arm joints") - print(" left list - List left arm waypoints") - print(" right list - List right arm waypoints") - print(" left delete - Delete waypoint n from left") - print(" right delete - Delete waypoint n from right") - print(" left clear - Clear left arm waypoints") - print(" right clear - Clear right arm waypoints") - print(" preview - Preview both trajectories") - print(" run left - Run trajectory on left arm only") - print(" run right - Run trajectory on right arm only") - print(" run both - Run trajectories on both arms") - print(" vel - Set max velocity (rad/s)") - print(" quit - Exit") - print("=" * 80) - - try: - while True: - left_wp = len(left.waypoints) if left.waypoints else 0 - right_wp = len(right.waypoints) if right.waypoints else 0 - prompt = f"[L:{left_wp} R:{right_wp}] > " - line = input(prompt).strip() - - if not line: - continue - - parts = line.split() - cmd = parts[0].lower() - - # Determine which arm (if applicable) - arm: ArmState | None = None - if cmd in ("left", "l"): - arm = left - parts = parts[1:] # Remove arm selector - cmd = parts[0].lower() if parts else "" - elif cmd in ("right", "r"): - arm = right - parts = parts[1:] - cmd = parts[0].lower() if parts else "" - - # ARM-SPECIFIC COMMANDS - if arm is not None: - if arm.num_joints is None: - print(f" {arm.name.upper()} arm not connected") - continue - - # ADD waypoint - if cmd == "add" and len(parts) >= arm.num_joints + 1: - joints = parse_joint_input( - " ".join(parts[1 : arm.num_joints + 1]), arm.num_joints - ) - if joints: - arm.waypoints.append(joints) # type: ignore[union-attr] - arm.generated_trajectory = None - deg = [f"{math.degrees(j):.1f}" for j in joints] - print( - f" {arm.name.upper()} waypoint {len(arm.waypoints)}: [{', '.join(deg)}] deg" # type: ignore[arg-type] - ) - else: - print(f" Invalid values (need {arm.num_joints} in degrees)") - - # HERE - add current position - elif cmd == "here": - joints = setter.get_current_joints(arm) - if joints: - arm.waypoints.append(joints) # type: ignore[union-attr] - arm.generated_trajectory = None - deg = [f"{math.degrees(j):.1f}" for j in joints] - print( - f" {arm.name.upper()} waypoint {len(arm.waypoints)}: [{', '.join(deg)}] deg" # type: ignore[arg-type] - ) - else: - print(" No joint state available") - - # CURRENT - elif cmd == "current": - joints = setter.get_current_joints(arm) - if joints: - deg = [f"{math.degrees(j):.1f}" for j in joints] - print(f" {arm.name.upper()}: [{', '.join(deg)}] deg") - else: - print(" No joint state available") - - # LIST - elif cmd == "list": - preview_waypoints(arm) - - # DELETE - elif cmd == "delete" and len(parts) >= 2: - try: - idx = int(parts[1]) - 1 - if arm.waypoints and 0 <= idx < len(arm.waypoints): - arm.waypoints.pop(idx) - arm.generated_trajectory = None - print(f" Deleted {arm.name} waypoint {idx + 1}") - else: - wp_count = len(arm.waypoints) if arm.waypoints else 0 - print(f" Invalid index (1-{wp_count})") - except ValueError: - print(" Invalid index") - - # CLEAR - elif cmd == "clear": - if arm.waypoints: - arm.waypoints.clear() - arm.generated_trajectory = None - print(f" {arm.name.upper()} waypoints cleared") - - else: - print(f" Unknown command for {arm.name}: {cmd}") - - # GLOBAL COMMANDS - elif cmd == "preview": - # Generate trajectories if needed - for a in [left, right]: - if a.waypoints and len(a.waypoints) >= 2: - try: - a.generated_trajectory = setter.generate_trajectory(a) - except Exception as e: - print(f" Error generating {a.name} trajectory: {e}") - a.generated_trajectory = None - - preview_trajectory(left) - preview_trajectory(right) - - elif cmd == "run" and len(parts) >= 2: - target = parts[1].lower() - - # Determine which arms to run - arms_to_run: list[ArmState] = [] - if target in ("left", "l"): - arms_to_run = [left] - elif target in ("right", "r"): - arms_to_run = [right] - elif target == "both": - arms_to_run = [left, right] - else: - print(" Usage: run left|right|both") - continue - - # Generate trajectories if needed - for a in arms_to_run: - if not a.waypoints or len(a.waypoints) < 2: - print(f" {a.name.upper()}: Need at least 2 waypoints") - continue - - if a.generated_trajectory is None: - try: - a.generated_trajectory = setter.generate_trajectory(a) - except Exception as e: - print(f" Error generating {a.name} trajectory: {e}") - continue - - # Preview and confirm - valid_arms = [a for a in arms_to_run if a.generated_trajectory is not None] - if not valid_arms: - print(" No valid trajectories to run") - continue - - for a in valid_arms: - preview_trajectory(a) - - arm_names = ", ".join(a.name.upper() for a in valid_arms) - confirm = input(f"\n Run on {arm_names}? [y/N]: ").strip().lower() - if confirm == "y": - print("\n Publishing trajectories...") - for a in valid_arms: - if a.generated_trajectory: - setter.publish_trajectory(a, a.generated_trajectory) - - elif cmd == "vel" and len(parts) >= 3: - arm_name = parts[1].lower() - target_arm: ArmState | None = ( - left - if arm_name in ("left", "l") - else right - if arm_name in ("right", "r") - else None - ) - if target_arm is None or target_arm.generator is None: - print(" Usage: vel left|right ") - continue - try: - vel = float(parts[2]) - if vel <= 0: - print(" Velocity must be positive") - else: - target_arm.generator.set_limits(vel, target_arm.generator.max_acceleration) - target_arm.generated_trajectory = None - print(f" {target_arm.name.upper()} max velocity: {vel:.2f} rad/s") - except ValueError: - print(" Invalid velocity value") - - elif cmd in ("quit", "exit", "q"): - break - - else: - print(f" Unknown command: {cmd}") - - except KeyboardInterrupt: - print("\n\nExiting...") - - -def main() -> int: - """Main entry point.""" - import argparse - - parser = argparse.ArgumentParser(description="Dual-Arm Interactive Trajectory Setter") - parser.add_argument( - "--left-joint-topic", - type=str, - default="/xarm/left/joint_states", - help="Left arm joint state topic", - ) - parser.add_argument( - "--right-joint-topic", - type=str, - default="/xarm/right/joint_states", - help="Right arm joint state topic", - ) - parser.add_argument( - "--left-trajectory-topic", - type=str, - default="/xarm/left/trajectory", - help="Left arm trajectory topic", - ) - parser.add_argument( - "--right-trajectory-topic", - type=str, - default="/xarm/right/trajectory", - help="Right arm trajectory topic", - ) - args = parser.parse_args() - - print("\n" + "=" * 80) - print("Dual-Arm Trajectory Setter") - print("=" * 80) - print("\nRun 'dimos run xarm-dual-trajectory' in another terminal first!") - print("=" * 80) - - setter = DualTrajectorySetter( - left_joint_topic=args.left_joint_topic, - right_joint_topic=args.right_joint_topic, - left_trajectory_topic=args.left_trajectory_topic, - right_trajectory_topic=args.right_trajectory_topic, - ) - - if not setter.start(): - print("\nWarning: Could not connect to both arms") - response = input("Continue anyway? [y/N]: ").strip().lower() - if response != "y": - return 0 - - interactive_mode(setter) - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n\nInterrupted by user") - sys.exit(0) - except Exception as e: - print(f"\nError: {e}") - import traceback - - traceback.print_exc() - sys.exit(1) diff --git a/dimos/manipulation/control/servo_control/README.md b/dimos/manipulation/control/servo_control/README.md deleted file mode 100644 index fb11fdb2a4..0000000000 --- a/dimos/manipulation/control/servo_control/README.md +++ /dev/null @@ -1,477 +0,0 @@ -# Cartesian Motion Controller - -Hardware-agnostic Cartesian space motion controller for robotic manipulators. - -## Overview - -The `CartesianMotionController` provides closed-loop Cartesian pose tracking by: -1. **Subscribing** to target poses (PoseStamped) -2. **Computing** Cartesian error (position + orientation) -3. **Generating** velocity commands using PID control -4. **Converting** to joint space via IK -5. **Publishing** joint commands to the hardware driver - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ TargetSetter (Interactive CLI) │ -│ - User inputs target positions │ -│ - Preserves orientation when left blank │ -└───────────────────────┬─────────────────────────────────────┘ - │ PoseStamped (/target_pose) - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ CartesianMotionController │ -│ - Computes FK (current pose) │ -│ - Computes Cartesian error │ -│ - PID control → Cartesian velocity │ -│ - Integrates velocity → next desired pose │ -│ - Computes IK → target joint angles │ -│ - Publishes current pose for feedback │ -└──────────┬────────────────────────────────┬─────────────────┘ - │ JointCommand │ PoseStamped - │ │ (current_pose) - ▼ ▼ -┌─────────────────────────────────┐ (back to TargetSetter -│ Hardware Driver (xArm, etc.) │ for orientation preservation) -│ - 100Hz control loop │ -│ - Sends commands to robot │ -│ - Publishes JointState │ -└─────────────────────────────────┘ - │ JointState - │ (feedback) - ▼ - (back to controller) -``` - -## Key Features - -### ✓ Hardware Agnostic -- Works with **any** arm driver implementing `ArmDriverSpec` protocol -- Only requires `get_inverse_kinematics()` and `get_forward_kinematics()` RPC methods -- Supports xArm, Piper, UR, Franka, or custom arms - -### ✓ PID-Based Control -- Separate PIDs for position (X, Y, Z) and orientation (roll, pitch, yaw) -- Configurable gains and velocity limits -- Smooth, stable motion with damping - -### ✓ Safety Features -- Configurable position/orientation error limits -- Automatic emergency stop on excessive errors -- Command timeout detection -- Convergence monitoring - -### ✓ Flexible Input -- RPC method: `set_target_pose(position, orientation, frame_id)` -- Topic subscription: `target_pose` (PoseStamped messages) -- Supports both Euler angles and quaternions - -## Usage - -### Basic Example - -```python -from dimos.hardware.manipulators.xarm import XArmDriver, XArmDriverConfig -from dimos.manipulation.control import CartesianMotionController, CartesianMotionControllerConfig - -# 1. Create hardware driver -arm_driver = XArmDriver(config=XArmDriverConfig(ip_address="192.168.1.235")) - -# 2. Create Cartesian controller (hardware-agnostic!) -controller = CartesianMotionController( - arm_driver=arm_driver, - config=CartesianMotionControllerConfig( - control_frequency=20.0, - position_kp=1.0, - max_linear_velocity=0.15, # m/s - ) -) - -# 3. Set up topic connections (shared memory) -from dimos.core.transport import pSHMTransport - -transport_joint_state = pSHMTransport("joint_state") -transport_joint_cmd = pSHMTransport("joint_cmd") - -arm_driver.joint_state.connection = transport_joint_state -controller.joint_state.connection = transport_joint_state -controller.joint_position_command.connection = transport_joint_cmd -arm_driver.joint_position_command.connection = transport_joint_cmd - -# 4. Start modules -arm_driver.start() -controller.start() - -# 5. Send Cartesian goal (move 10cm in X) -controller.set_target_pose( - position=[0.3, 0.0, 0.5], # xyz in meters - orientation=[0, 0, 0], # roll, pitch, yaw in radians - frame_id="world" -) - -# 6. Wait for convergence -while not controller.is_converged(): - time.sleep(0.1) - -print("Target reached!") -``` - -### Using Quaternions - -```python -from dimos.msgs.geometry_msgs import Quaternion - -# Create quaternion (identity rotation) -quat = Quaternion(x=0, y=0, z=0, w=1) - -controller.set_target_pose( - position=[0.4, 0.1, 0.6], - orientation=[quat.x, quat.y, quat.z, quat.w], # 4-element list -) -``` - -### Using PoseStamped Messages - -```python -from dimos.msgs.geometry_msgs import PoseStamped - -# Create target pose -target = PoseStamped( - frame_id="world", - position=[0.3, 0.2, 0.5], - orientation=[0, 0, 0, 1] # quaternion -) - -# Option 1: Via RPC -controller.set_target_pose( - position=list(target.position), - orientation=list(target.orientation) -) - -# Option 2: Via topic (if connected) -controller.target_pose.publish(target) -``` - -### Using the TargetSetter Tool - -The `TargetSetter` is an interactive CLI tool that makes it easy to manually send target poses to the controller. It provides a user-friendly interface for testing and teleoperation. - -**Key Features:** -- **Interactive terminal UI** - prompts for x, y, z coordinates -- **Orientation preservation** - automatically uses current orientation when left blank -- **Live feedback** - subscribes to controller's current pose -- **Simple workflow** - just enter coordinates and press Enter - -**Setup:** - -```python -# Terminal 1: Start the controller (as shown in Basic Example above) -arm_driver = XArmDriver(config=XArmDriverConfig(ip_address="192.168.1.235")) -controller = CartesianMotionController(arm_driver=arm_driver) - -# Set up LCM transports for target_pose and current_pose -from dimos.core import LCMTransport -controller.target_pose.connection = LCMTransport("/target_pose", PoseStamped) -controller.current_pose.connection = LCMTransport("/xarm/current_pose", PoseStamped) - -arm_driver.start() -controller.start() - -# Terminal 2: Run the target setter -python -m dimos.manipulation.control.target_setter -``` - -**Usage Example:** - -``` -================================================================================ -Interactive Target Setter -================================================================================ -Mode: WORLD FRAME (absolute coordinates) - -Enter target coordinates (Ctrl+C to quit) -================================================================================ - --------------------------------------------------------------------------------- - -Enter target position (in meters): - x (m): 0.3 - y (m): 0.0 - z (m): 0.5 - -Enter orientation (in degrees, leave blank to preserve current orientation): - roll (°): - pitch (°): - yaw (°): - -✓ Published target (preserving current orientation): - Position: x=0.3000m, y=0.0000m, z=0.5000m - Orientation: roll=0.0°, pitch=0.0°, yaw=0.0° -``` - -**How It Works:** - -1. **TargetSetter** subscribes to `/xarm/current_pose` from the controller -2. User enters target position (x, y, z) in meters -3. User can optionally enter orientation (roll, pitch, yaw) in degrees -4. If orientation is left blank (0, 0, 0), TargetSetter uses the current orientation from the controller -5. TargetSetter publishes the target pose to `/target_pose` topic -6. **CartesianMotionController** receives the target and tracks it - -**Benefits:** - -- **No orientation math** - just move positions without worrying about quaternions -- **Safe testing** - manually verify each move before sending -- **Quick iteration** - test different positions interactively -- **Educational** - see the controller respond in real-time - -## Configuration - -```python -@dataclass -class CartesianMotionControllerConfig: - # Control loop - control_frequency: float = 20.0 # Hz (recommend 10-50Hz) - command_timeout: float = 1.0 # seconds - - # PID gains (position) - position_kp: float = 1.0 # m/s per meter of error - position_ki: float = 0.0 # Integral gain - position_kd: float = 0.1 # Derivative gain (damping) - - # PID gains (orientation) - orientation_kp: float = 2.0 # rad/s per radian of error - orientation_ki: float = 0.0 - orientation_kd: float = 0.2 - - # Safety limits - max_linear_velocity: float = 0.2 # m/s - max_angular_velocity: float = 1.0 # rad/s - max_position_error: float = 0.5 # m (emergency stop threshold) - max_orientation_error: float = 1.57 # rad (~90°) - - # Convergence - position_tolerance: float = 0.001 # m (1mm) - orientation_tolerance: float = 0.01 # rad (~0.57°) - - # Control mode - velocity_control_mode: bool = True # Use velocity-based control -``` - -## Hardware Abstraction - -The controller uses the **Protocol pattern** for hardware abstraction: - -```python -# spec.py -class ArmDriverSpec(Protocol): - # Required RPC methods - def get_inverse_kinematics(self, pose: list[float]) -> tuple[int, list[float] | None]: ... - def get_forward_kinematics(self, angles: list[float]) -> tuple[int, list[float] | None]: ... - - # Required topics - joint_state: Out[JointState] - robot_state: Out[RobotState] - joint_position_command: In[JointCommand] -``` - -**Any driver implementing this protocol works with the controller!** - -### Adding a New Arm - -1. Implement `ArmDriverSpec` protocol: - ```python - class MyArmDriver(Module): - @rpc - def get_inverse_kinematics(self, pose: list[float]) -> tuple[int, list[float] | None]: - # Your IK implementation - return (0, joint_angles) - - @rpc - def get_forward_kinematics(self, angles: list[float]) -> tuple[int, list[float] | None]: - # Your FK implementation - return (0, tcp_pose) - ``` - -2. Use with controller: - ```python - my_driver = MyArmDriver() - controller = CartesianMotionController(arm_driver=my_driver) - ``` - -**That's it! No changes to the controller needed.** - -## RPC Methods - -### Control Methods - -```python -@rpc -def set_target_pose( - position: list[float], # [x, y, z] in meters - orientation: list[float], # [qx, qy, qz, qw] or [roll, pitch, yaw] - frame_id: str = "world" -) -> None -``` - -```python -@rpc -def clear_target() -> None -``` - -### Query Methods - -```python -@rpc -def get_current_pose() -> Optional[Pose] -``` - -```python -@rpc -def is_converged() -> bool -``` - -## Topics - -### Inputs (Subscriptions) - -| Topic | Type | Description | -|-------|------|-------------| -| `joint_state` | `JointState` | Current joint positions/velocities (from driver) | -| `robot_state` | `RobotState` | Robot status (from driver) | -| `target_pose` | `PoseStamped` | Desired TCP pose (from planner) | - -### Outputs (Publications) - -| Topic | Type | Description | -|-------|------|-------------| -| `joint_position_command` | `JointCommand` | Target joint angles (to driver) | -| `cartesian_velocity` | `Twist` | Debug: Cartesian velocity commands | -| `current_pose` | `PoseStamped` | Current TCP pose (for TargetSetter and other tools) | - -## Control Algorithm - -``` -1. Read current joint state from driver -2. Compute FK: joint angles → TCP pose -3. Compute error: e = target_pose - current_pose -4. PID control: velocity = PID(e, dt) -5. Integrate: next_pose = current_pose + velocity * dt -6. Compute IK: next_pose → target_joints -7. Publish target_joints to driver -``` - -### Why This Works - -- **Outer loop (Cartesian)**: Runs at 10-50Hz, computes IK -- **Inner loop (Joint)**: Driver runs at 100Hz, executes smoothly -- **Decoupling**: Separates high-level planning from low-level control - -## Tuning Guide - -### Conservative (Safe) -```python -config = CartesianMotionControllerConfig( - control_frequency=10.0, - position_kp=0.5, - max_linear_velocity=0.1, # Slow! -) -``` - -### Moderate (Recommended) -```python -config = CartesianMotionControllerConfig( - control_frequency=20.0, - position_kp=1.0, - position_kd=0.1, - max_linear_velocity=0.15, -) -``` - -### Aggressive (Fast) -```python -config = CartesianMotionControllerConfig( - control_frequency=50.0, - position_kp=2.0, - position_kd=0.2, - max_linear_velocity=0.3, -) -``` - -### Tips - -- **Increase Kp**: Faster response, but may oscillate -- **Increase Kd**: More damping, smoother motion -- **Increase Ki**: Eliminates steady-state error (usually not needed) -- **Lower frequency**: Less CPU load, smoother -- **Higher frequency**: Faster response, more accurate - -## Extending - -### Next Steps (Phase 2+) - -1. **Trajectory Following**: Add waypoint tracking - ```python - controller.follow_trajectory(waypoints: list[Pose], duration: float) - ``` - -2. **Collision Avoidance**: Integrate with planning - ```python - controller.set_collision_checker(checker: CollisionChecker) - ``` - -3. **Impedance Control**: Add force/torque feedback - ```python - controller.set_impedance(stiffness: float, damping: float) - ``` - -4. **Visual Servoing**: Integrate with perception - ```python - controller.track_object(object_id: int) - ``` - -## Troubleshooting - -### Controller not moving -- Check `arm_driver` is started and publishing `joint_state` -- Verify topic connections are set up -- Check robot is in correct mode (servo mode for xArm) - -### Oscillation / Instability -- Reduce `position_kp` or `orientation_kp` -- Increase `position_kd` or `orientation_kd` -- Lower `control_frequency` - -### IK failures -- Target pose may be unreachable -- Check joint limits -- Verify pose is within workspace -- Check singularity avoidance - -### Not converging -- Increase `position_tolerance` / `orientation_tolerance` -- Check for workspace limits -- Increase `max_linear_velocity` - -## Files - -``` -dimos/manipulation/control/ -├── __init__.py # Module exports -├── cartesian_motion_controller.py # Main controller -├── target_setter.py # Interactive target pose publisher -├── example_cartesian_control.py # Usage example -└── README.md # This file -``` - -## Related Modules - -- [xarm_driver.py](../../hardware/manipulators/xarm/xarm_driver.py) - Hardware driver for xArm -- [spec.py](../../hardware/manipulators/xarm/spec.py) - Protocol specification -- [simple_controller.py](../../utils/simple_controller.py) - PID implementation - -## License - -Copyright 2025 Dimensional Inc. - Apache 2.0 License diff --git a/dimos/manipulation/control/servo_control/__init__.py b/dimos/manipulation/control/servo_control/__init__.py deleted file mode 100644 index 5418a7e24b..0000000000 --- a/dimos/manipulation/control/servo_control/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Servo Control Modules - -Real-time servo-level controllers for robotic manipulation. -Includes Cartesian motion control with PID-based tracking. -""" - -from dimos.manipulation.control.servo_control.cartesian_motion_controller import ( - CartesianMotionController, - CartesianMotionControllerConfig, - cartesian_motion_controller, -) - -__all__ = [ - "CartesianMotionController", - "CartesianMotionControllerConfig", - "cartesian_motion_controller", -] diff --git a/dimos/manipulation/control/servo_control/cartesian_motion_controller.py b/dimos/manipulation/control/servo_control/cartesian_motion_controller.py deleted file mode 100644 index f5a0810803..0000000000 --- a/dimos/manipulation/control/servo_control/cartesian_motion_controller.py +++ /dev/null @@ -1,720 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Cartesian Motion Controller - -Hardware-agnostic Cartesian space motion controller for robotic manipulators. -Converts Cartesian pose goals to joint commands using IK/FK from the arm driver. - -Architecture: -- Subscribes to joint_state and robot_state from hardware driver -- Subscribes to target_pose (PoseStamped) from high-level planners -- Publishes joint_position_command to hardware driver -- Uses PID control for smooth Cartesian tracking -- Supports velocity-based and position-based control modes -""" - -from dataclasses import dataclass -import math -import threading -import time -from typing import Any - -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Twist, Vector3 -from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState -from dimos.utils.logging_config import setup_logger -from dimos.utils.simple_controller import PIDController - -logger = setup_logger() - - -@dataclass -class CartesianMotionControllerConfig(ModuleConfig): - """Configuration for Cartesian motion controller.""" - - # Control loop parameters - control_frequency: float = 20.0 # Hz - Cartesian control loop rate - command_timeout: float = 30.0 # seconds - timeout for stale targets (RPC mode needs longer) - - # PID gains for position control (m/s per meter of error) - position_kp: float = 5.0 # Proportional gain - position_ki: float = 0.1 # Integral gain - position_kd: float = 0.1 # Derivative gain - - # PID gains for orientation control (rad/s per radian of error) - orientation_kp: float = 2.0 # Proportional gain - orientation_ki: float = 0.0 # Integral gain - orientation_kd: float = 0.2 # Derivative gain - - # Safety limits - max_linear_velocity: float = 0.2 # m/s - maximum TCP linear velocity - max_angular_velocity: float = 1.0 # rad/s - maximum TCP angular velocity - max_position_error: float = 0.7 # m - max allowed position error before emergency stop - max_orientation_error: float = 6.28 # rad (~360°) - allow any orientation - - # Convergence thresholds - position_tolerance: float = 0.001 # m - position considered "reached" - orientation_tolerance: float = 0.01 # rad (~0.57°) - orientation considered "reached" - - # Control mode - velocity_control_mode: bool = True # Use velocity control (True) or position steps (False) - - # Frame configuration - control_frame: str = "world" # Frame for target poses (world, base_link, etc.) - - -class CartesianMotionController(Module): - """ - Hardware-agnostic Cartesian motion controller. - - This controller provides Cartesian space motion control for manipulators by: - 1. Receiving target poses (PoseStamped) - 2. Computing Cartesian error (position + orientation) - 3. Generating Cartesian velocity commands (Twist) - 4. Computing IK to convert to joint space - 5. Publishing joint commands to the driver - - The controller is hardware-agnostic: it works with any arm driver that - provides IK/FK RPC methods and JointState/RobotState outputs. - """ - - default_config = CartesianMotionControllerConfig - config: CartesianMotionControllerConfig # Type hint for proper attribute access - - # RPC methods to request from other modules (resolved at blueprint build time) - rpc_calls = [ - "XArmDriver.get_forward_kinematics", - "XArmDriver.get_inverse_kinematics", - ] - - # Input topics (initialized by Module base class) - joint_state: In[JointState] = None # type: ignore[assignment] - robot_state: In[RobotState] = None # type: ignore[assignment] - target_pose: In[PoseStamped] = None # type: ignore[assignment] - - # Output topics (initialized by Module base class) - joint_position_command: Out[JointCommand] = None # type: ignore[assignment] - cartesian_velocity: Out[Twist] = None # type: ignore[assignment] - current_pose: Out[PoseStamped] = None # type: ignore[assignment] - - def __init__(self, arm_driver: Any = None, *args: Any, **kwargs: Any) -> None: - """ - Initialize the Cartesian motion controller. - - Args: - arm_driver: (Optional) Hardware driver reference (legacy mode). - When using blueprints, this is resolved automatically via rpc_calls. - """ - super().__init__(*args, **kwargs) - - # Hardware driver reference - set via arm_driver param (legacy) or RPC wiring (blueprint) - self._arm_driver_legacy = arm_driver - - # State tracking - self._latest_joint_state: JointState | None = None - self._latest_robot_state: RobotState | None = None - self._target_pose_: PoseStamped | None = None - self._last_target_time: float = 0.0 - - # Current TCP pose (computed via FK) - self._current_tcp_pose: Pose | None = None - - # Thread management - self._control_thread: threading.Thread | None = None - self._stop_event = threading.Event() - - # State locks - self._state_lock = threading.Lock() - self._target_lock = threading.Lock() - - # PID controllers for Cartesian space - self._pid_x = PIDController( - kp=self.config.position_kp, - ki=self.config.position_ki, - kd=self.config.position_kd, - output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), - ) - self._pid_y = PIDController( - kp=self.config.position_kp, - ki=self.config.position_ki, - kd=self.config.position_kd, - output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), - ) - self._pid_z = PIDController( - kp=self.config.position_kp, - ki=self.config.position_ki, - kd=self.config.position_kd, - output_limits=(-self.config.max_linear_velocity, self.config.max_linear_velocity), - ) - - # Orientation PIDs (using axis-angle representation) - self._pid_roll = PIDController( - kp=self.config.orientation_kp, - ki=self.config.orientation_ki, - kd=self.config.orientation_kd, - output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), - ) - self._pid_pitch = PIDController( - kp=self.config.orientation_kp, - ki=self.config.orientation_ki, - kd=self.config.orientation_kd, - output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), - ) - self._pid_yaw = PIDController( - kp=self.config.orientation_kp, - ki=self.config.orientation_ki, - kd=self.config.orientation_kd, - output_limits=(-self.config.max_angular_velocity, self.config.max_angular_velocity), - ) - - # Control status - self._is_tracking: bool = False - self._last_convergence_check: float = 0.0 - - logger.info( - f"CartesianMotionController initialized at {self.config.control_frequency}Hz " - f"(velocity_mode={self.config.velocity_control_mode})" - ) - - def _call_fk(self, joint_positions: list[float]) -> tuple[int, list[float] | None]: - """Call FK - uses blueprint RPC wiring or legacy arm_driver reference.""" - try: - result: tuple[int, list[float] | None] = self.get_rpc_calls( - "XArmDriver.get_forward_kinematics" - )(joint_positions) - return result - except (ValueError, KeyError): - if self._arm_driver_legacy: - result_fk: tuple[int, list[float] | None] = ( - self._arm_driver_legacy.get_forward_kinematics(joint_positions) # type: ignore[attr-defined] - ) - return result_fk - raise RuntimeError("No arm driver available - use blueprint or pass arm_driver param") - - def _call_ik(self, pose: list[float]) -> tuple[int, list[float] | None]: - """Call IK - uses blueprint RPC wiring or legacy arm_driver reference.""" - try: - result: tuple[int, list[float] | None] = self.get_rpc_calls( - "XArmDriver.get_inverse_kinematics" - )(pose) - return result - except (ValueError, KeyError): - if self._arm_driver_legacy: - result_ik: tuple[int, list[float] | None] = ( - self._arm_driver_legacy.get_inverse_kinematics(pose) # type: ignore[attr-defined] - ) - return result_ik - raise RuntimeError("No arm driver available - use blueprint or pass arm_driver param") - - @rpc - def start(self) -> None: - """Start the Cartesian motion controller.""" - super().start() - - # Subscribe to input topics - # Note: Accessing .connection property triggers transport resolution from connected streams - try: - if self.joint_state.connection is not None or self.joint_state._transport is not None: - self.joint_state.subscribe(self._on_joint_state) - logger.info("Subscribed to joint_state") - except Exception as e: - logger.warning(f"Failed to subscribe to joint_state: {e}") - - try: - if self.robot_state.connection is not None or self.robot_state._transport is not None: - self.robot_state.subscribe(self._on_robot_state) - logger.info("Subscribed to robot_state") - except Exception as e: - logger.warning(f"Failed to subscribe to robot_state: {e}") - - try: - if self.target_pose.connection is not None or self.target_pose._transport is not None: - self.target_pose.subscribe(self._on_target_pose) - logger.info("Subscribed to target_pose") - except Exception: - logger.debug("target_pose not connected (expected - uses RPC)") - - # Start control loop thread - self._stop_event.clear() - self._control_thread = threading.Thread( - target=self._control_loop, daemon=True, name="cartesian_control_thread" - ) - self._control_thread.start() - - logger.info("CartesianMotionController started") - - @rpc - def stop(self) -> None: - """Stop the Cartesian motion controller.""" - logger.info("Stopping CartesianMotionController...") - - # Signal thread to stop - self._stop_event.set() - - # Wait for control thread - if self._control_thread and self._control_thread.is_alive(): - self._control_thread.join(timeout=2.0) - - super().stop() - logger.info("CartesianMotionController stopped") - - # ========================================================================= - # RPC Methods - High-level control - # ========================================================================= - - @rpc - def set_target_pose( - self, position: list[float], orientation: list[float], frame_id: str = "world" - ) -> None: - """ - Set a target Cartesian pose for the controller to track. - - Args: - position: [x, y, z] in meters - orientation: [qx, qy, qz, qw] quaternion OR [roll, pitch, yaw] euler angles - frame_id: Reference frame for the pose - """ - # Detect if orientation is euler (3 elements) or quaternion (4 elements) - if len(orientation) == 3: - # Convert euler to quaternion using Pose's built-in conversion - euler_angles = Vector3(orientation[0], orientation[1], orientation[2]) - quat = Quaternion.from_euler(euler_angles) - orientation = [quat.x, quat.y, quat.z, quat.w] - - target = PoseStamped( - ts=time.time(), frame_id=frame_id, position=position, orientation=orientation - ) - - with self._target_lock: - self._target_pose_ = target - self._last_target_time = time.time() - self._is_tracking = True - - logger.info( - f"New target set: pos=[{position[0]:.6f}, {position[1]:.6f}, {position[2]:.6f}] m, " - f"frame={frame_id}" - ) - - @rpc - def clear_target(self) -> None: - """Clear the current target (stop tracking).""" - with self._target_lock: - self._target_pose_ = None - self._is_tracking = False - logger.info("Target cleared, tracking stopped") - - @rpc - def get_current_pose(self) -> Pose | None: - """ - Get the current TCP pose (computed via FK). - - Returns: - Current Pose or None if not available - """ - return self._current_tcp_pose - - @rpc - def is_converged(self) -> bool: - """ - Check if the controller has converged to the target. - - Returns: - True if within tolerance, False otherwise - """ - with self._target_lock: - target_pose = self._target_pose_ - - current_pose = self._current_tcp_pose - - if not target_pose or not current_pose: - return False - - pos_error, ori_error = self._compute_pose_error(current_pose, target_pose) - return ( - pos_error < self.config.position_tolerance - and ori_error < self.config.orientation_tolerance - ) - - # ========================================================================= - # Private Methods - Callbacks - # ========================================================================= - - def _on_joint_state(self, msg: JointState) -> None: - """Callback when new joint state is received.""" - logger.debug(f"Received joint_state: {len(msg.position)} joints") - with self._state_lock: - self._latest_joint_state = msg - - def _on_robot_state(self, msg: RobotState) -> None: - """Callback when new robot state is received.""" - with self._state_lock: - self._latest_robot_state = msg - - def _on_target_pose(self, msg: PoseStamped) -> None: - """Callback when new target pose is received.""" - with self._target_lock: - self._target_pose_ = msg - self._last_target_time = time.time() - self._is_tracking = True - logger.debug(f"New target received: {msg}") - - # ========================================================================= - # Private Methods - Control Loop - # ========================================================================= - - def _control_loop(self) -> None: - """ - Main control loop running at control_frequency Hz. - - Algorithm: - 1. Read current joint state - 2. Compute FK to get current TCP pose - 3. Compute Cartesian error to target - 4. Generate Cartesian velocity command (PID) - 5. Integrate velocity to get next desired pose - 6. Compute IK to get target joint angles - 7. Publish joint command - """ - period = 1.0 / self.config.control_frequency - next_time = time.time() - - logger.info(f"Cartesian control loop started at {self.config.control_frequency}Hz") - - while not self._stop_event.is_set(): - # Sleep at start of loop to maintain frequency even when using continue - sleep_time = next_time - time.time() - if sleep_time > 0: - if self._stop_event.wait(timeout=sleep_time): - break - else: - # Loop overrun - reset timing - next_time = time.time() - - next_time += period - - try: - current_time = time.time() - dt = period # Use fixed timestep for consistent control - - # Read shared state - with self._state_lock: - joint_state = self._latest_joint_state - - with self._target_lock: - target_pose = self._target_pose_ - last_target_time = self._last_target_time - is_tracking = self._is_tracking - - # Check if we have valid state - if joint_state is None or len(joint_state.position) == 0: - continue - - # Compute current TCP pose via FK - code, current_pose_list = self._call_fk(list(joint_state.position)) - - if code != 0 or current_pose_list is None: - logger.warning(f"FK failed with code: {code}") - continue - - # Convert FK result to Pose (xArm returns [x, y, z, roll, pitch, yaw] in mm) - if len(current_pose_list) == 6: - # Convert position from mm to m for internal use - position_m = [ - current_pose_list[0] / 1000.0, - current_pose_list[1] / 1000.0, - current_pose_list[2] / 1000.0, - ] - euler_angles = Vector3( - current_pose_list[3], current_pose_list[4], current_pose_list[5] - ) - quat = Quaternion.from_euler(euler_angles) - self._current_tcp_pose = Pose( - position=position_m, - orientation=[quat.x, quat.y, quat.z, quat.w], - ) - - # Publish current pose for target setters to use - current_pose_stamped = PoseStamped( - ts=current_time, - frame_id="world", - position=position_m, - orientation=[quat.x, quat.y, quat.z, quat.w], - ) - self.current_pose.publish(current_pose_stamped) - else: - logger.warning(f"Unexpected FK result format: {current_pose_list}") - continue - - # Check for target timeout - if is_tracking and (current_time - last_target_time) > self.config.command_timeout: - logger.warning("Target pose timeout - clearing target") - with self._target_lock: - self._target_pose_ = None - self._is_tracking = False - continue - - # If not tracking, skip control - if not is_tracking or target_pose is None: - logger.debug( - f"Not tracking: is_tracking={is_tracking}, target_pose={target_pose is not None}" - ) - continue - - # Check if we have current pose - if self._current_tcp_pose is None: - logger.warning("No current TCP pose available, skipping control") - continue - - # Compute Cartesian error - pos_error_mag, ori_error_mag = self._compute_pose_error( - self._current_tcp_pose, target_pose - ) - - # Log error periodically (every 1 second) - if not hasattr(self, "_last_error_log_time"): - self._last_error_log_time = 0.0 - if current_time - self._last_error_log_time > 1.0: - logger.info( - f"Curr=[{self._current_tcp_pose.x:.3f},{self._current_tcp_pose.y:.3f},{self._current_tcp_pose.z:.3f}]m Tgt=[{target_pose.x:.3f},{target_pose.y:.3f},{target_pose.z:.3f}]m Err={pos_error_mag * 1000:.1f}mm" - ) - self._last_error_log_time = current_time - - # Safety check: excessive error - if pos_error_mag > self.config.max_position_error: - logger.error( - f"Position error too large: {pos_error_mag:.3f}m > " - f"{self.config.max_position_error}m - STOPPING" - ) - with self._target_lock: - self._target_pose_ = None - self._is_tracking = False - continue - - if ori_error_mag > self.config.max_orientation_error: - logger.error( - f"Orientation error too large: {ori_error_mag:.3f}rad > " - f"{self.config.max_orientation_error}rad - STOPPING" - ) - with self._target_lock: - self._target_pose_ = None - self._is_tracking = False - continue - - # Check convergence periodically - if current_time - self._last_convergence_check > 1.0: - if ( - pos_error_mag < self.config.position_tolerance - and ori_error_mag < self.config.orientation_tolerance - ): - logger.info( - f"Converged! pos_err={pos_error_mag * 1000:.2f}mm, " - f"ori_err={math.degrees(ori_error_mag):.2f}°" - ) - self._last_convergence_check = current_time - - # Generate Cartesian velocity command - cartesian_twist = self._compute_cartesian_velocity( - self._current_tcp_pose, target_pose, dt - ) - - # Publish debug twist - if self.cartesian_velocity._transport or hasattr( - self.cartesian_velocity, "connection" - ): - try: - self.cartesian_velocity.publish(cartesian_twist) - except Exception: - pass - - # Integrate velocity to get next desired pose - next_pose = self._integrate_velocity(self._current_tcp_pose, cartesian_twist, dt) - - # Compute IK to get target joint angles - # Convert Pose to xArm format: [x, y, z, roll, pitch, yaw] - # Note: xArm IK expects position in mm, so convert from m to mm - next_pose_list = [ - next_pose.x * 1000.0, # m to mm - next_pose.y * 1000.0, # m to mm - next_pose.z * 1000.0, # m to mm - next_pose.roll, - next_pose.pitch, - next_pose.yaw, - ] - - logger.debug( - f"Calling IK for pose (mm): [{next_pose_list[0]:.1f}, {next_pose_list[1]:.1f}, {next_pose_list[2]:.1f}]" - ) - code, target_joints = self._call_ik(next_pose_list) - - if code != 0 or target_joints is None: - logger.warning(f"IK failed with code: {code}, target_joints={target_joints}") - continue - - logger.debug(f"IK successful: {len(target_joints)} joints") - - # Dynamically get joint count from actual joint_state (works for xarm5/6/7) - # IK may return extra values (e.g., gripper), so truncate to match actual DOF - num_arm_joints = len(joint_state.position) - if len(target_joints) > num_arm_joints: - if not hasattr(self, "_ik_truncation_logged"): - logger.info( - f"IK returns {len(target_joints)} joints, using first {num_arm_joints} to match arm DOF" - ) - self._ik_truncation_logged = True - target_joints = target_joints[:num_arm_joints] - elif len(target_joints) < num_arm_joints: - logger.warning( - f"IK returns {len(target_joints)} joints but arm has {num_arm_joints} - joint count mismatch!" - ) - - # Publish joint command - joint_cmd = JointCommand( - timestamp=current_time, - positions=list(target_joints), - ) - - # Always try to publish - the Out stream will handle transport availability - try: - self.joint_position_command.publish(joint_cmd) - logger.debug( - f"✓ Pub cmd: [{target_joints[0]:.6f}, {target_joints[1]:.6f}, {target_joints[2]:.6f}, ...]" - ) - except Exception as e: - logger.error(f"✗ Failed to publish joint command: {e}") - - except Exception as e: - logger.error(f"Error in control loop: {e}") - import traceback - - traceback.print_exc() - - logger.info("Cartesian control loop stopped") - - def _compute_pose_error(self, current_pose: Pose, target_pose: Pose) -> tuple[float, float]: - """ - Compute position and orientation error between current and target pose. - - Args: - current_pose: Current TCP pose - target_pose: Desired TCP pose - - Returns: - Tuple of (position_error_magnitude, orientation_error_magnitude) - """ - # Position error (Euclidean distance) - pos_error = Vector3( - target_pose.x - current_pose.x, - target_pose.y - current_pose.y, - target_pose.z - current_pose.z, - ) - pos_error_mag = math.sqrt(pos_error.x**2 + pos_error.y**2 + pos_error.z**2) - - # Orientation error (angle between quaternions) - # q_error = q_current^-1 * q_target - q_current_inv = current_pose.orientation.conjugate() - q_error = q_current_inv * target_pose.orientation - - # Extract angle from axis-angle representation - # For quaternion [x, y, z, w], angle = 2 * acos(w) - ori_error_mag = 2 * math.acos(min(1.0, abs(q_error.w))) - - return pos_error_mag, ori_error_mag - - def _compute_cartesian_velocity( - self, current_pose: Pose, target_pose: Pose, dt: float - ) -> Twist: - """ - Compute Cartesian velocity command using PID control. - - Args: - current_pose: Current TCP pose - target_pose: Desired TCP pose - dt: Time step - - Returns: - Twist message with linear and angular velocities - """ - # Position error - error_x = target_pose.x - current_pose.x - error_y = target_pose.y - current_pose.y - error_z = target_pose.z - current_pose.z - - # Compute linear velocities via PID - vel_x = self._pid_x.update(error_x, dt) # type: ignore[no-untyped-call] - vel_y = self._pid_y.update(error_y, dt) # type: ignore[no-untyped-call] - vel_z = self._pid_z.update(error_z, dt) # type: ignore[no-untyped-call] - - # Orientation error (convert to euler for simpler PID) - # This is an approximation; axis-angle would be more accurate - error_roll = self._normalize_angle(target_pose.roll - current_pose.roll) - error_pitch = self._normalize_angle(target_pose.pitch - current_pose.pitch) - error_yaw = self._normalize_angle(target_pose.yaw - current_pose.yaw) - - # Compute angular velocities via PID - omega_x = self._pid_roll.update(error_roll, dt) # type: ignore[no-untyped-call] - omega_y = self._pid_pitch.update(error_pitch, dt) # type: ignore[no-untyped-call] - omega_z = self._pid_yaw.update(error_yaw, dt) # type: ignore[no-untyped-call] - - return Twist( - linear=Vector3(vel_x, vel_y, vel_z), angular=Vector3(omega_x, omega_y, omega_z) - ) - - def _integrate_velocity(self, current_pose: Pose, velocity: Twist, dt: float) -> Pose: - """ - Integrate Cartesian velocity to compute next desired pose. - - Args: - current_pose: Current TCP pose - velocity: Desired Cartesian velocity (Twist) - dt: Time step - - Returns: - Next desired pose - """ - # Integrate position (simple Euler integration) - next_position = Vector3( - current_pose.x + velocity.linear.x * dt, - current_pose.y + velocity.linear.y * dt, - current_pose.z + velocity.linear.z * dt, - ) - - # Integrate orientation (simple euler integration - good for small dt) - next_roll = current_pose.roll + velocity.angular.x * dt - next_pitch = current_pose.pitch + velocity.angular.y * dt - next_yaw = current_pose.yaw + velocity.angular.z * dt - - euler_angles = Vector3(next_roll, next_pitch, next_yaw) - next_orientation = Quaternion.from_euler(euler_angles) - - return Pose( - position=next_position, - orientation=[ - next_orientation.x, - next_orientation.y, - next_orientation.z, - next_orientation.w, - ], - ) - - @staticmethod - def _normalize_angle(angle: float) -> float: - """Normalize angle to [-pi, pi].""" - return math.atan2(math.sin(angle), math.cos(angle)) - - -# Expose blueprint for declarative composition -cartesian_motion_controller = CartesianMotionController.blueprint diff --git a/dimos/manipulation/control/target_setter.py b/dimos/manipulation/control/target_setter.py deleted file mode 100644 index 1a937d12bb..0000000000 --- a/dimos/manipulation/control/target_setter.py +++ /dev/null @@ -1,222 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Interactive Target Pose Publisher for Cartesian Motion Control. - -Interactive terminal UI for publishing absolute target poses to /target_pose topic. -Pure publisher - OUT channel only, no subscriptions or driver connections. -""" - -import math -import sys -import time - -from dimos import core -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 - - -class TargetSetter: - """ - Publishes target poses to /target_pose topic. - - Subscribes to /xarm/current_pose to get current TCP pose for: - - Preserving orientation when left blank - - Supporting relative mode movements - """ - - def __init__(self) -> None: - """Initialize the target setter.""" - # Create LCM transport for publishing targets - self.target_pub: core.LCMTransport[PoseStamped] = core.LCMTransport( - "/target_pose", PoseStamped - ) - - # Subscribe to current pose from controller - self.current_pose_sub: core.LCMTransport[PoseStamped] = core.LCMTransport( - "/xarm/current_pose", PoseStamped - ) - self.latest_current_pose: PoseStamped | None = None - - print("TargetSetter initialized") - print(" Publishing to: /target_pose") - print(" Subscribing to: /xarm/current_pose") - - def start(self) -> bool: - """Start subscribing to current pose.""" - self.current_pose_sub.subscribe(self._on_current_pose) - print(" Waiting for current pose...") - # Wait for initial pose - for _ in range(50): # 5 second timeout - if self.latest_current_pose is not None: - print(" ✓ Current pose received") - return True - time.sleep(0.1) - print(" ⚠ Warning: No current pose received (timeout)") - return False - - def _on_current_pose(self, msg: PoseStamped) -> None: - """Callback for current pose updates.""" - self.latest_current_pose = msg - - def publish_pose( - self, x: float, y: float, z: float, roll: float = 0.0, pitch: float = 0.0, yaw: float = 0.0 - ) -> None: - """ - Publish target pose (absolute world frame coordinates). - - Args: - x, y, z: Position in meters - roll, pitch, yaw: Orientation in radians (0, 0, 0 = preserve current) - """ - # Check if orientation is identity (0, 0, 0) - preserve current orientation - is_identity = abs(roll) < 1e-6 and abs(pitch) < 1e-6 and abs(yaw) < 1e-6 - - if is_identity and self.latest_current_pose is not None: - # Use current orientation - q = self.latest_current_pose.orientation - orientation = [q.x, q.y, q.z, q.w] - print("\n✓ Published target (preserving current orientation):") - else: - # Convert Euler to Quaternion - euler = Vector3(roll, pitch, yaw) - quat = Quaternion.from_euler(euler) - orientation = [quat.x, quat.y, quat.z, quat.w] - print("\n✓ Published target:") - - pose = PoseStamped( - ts=time.time(), - frame_id="world", - position=[x, y, z], - orientation=orientation, - ) - - self.target_pub.broadcast(None, pose) - - print(f" Position: x={x:.4f}m, y={y:.4f}m, z={z:.4f}m") - print( - f" Orientation: roll={math.degrees(roll):.1f}°, " - f"pitch={math.degrees(pitch):.1f}°, yaw={math.degrees(yaw):.1f}°" - ) - - -def interactive_mode(setter: TargetSetter) -> None: - """ - Interactive mode: repeatedly prompt for target poses. - - Args: - setter: TargetSetter instance - """ - print("\n" + "=" * 80) - print("Interactive Target Setter") - print("=" * 80) - print("Mode: WORLD FRAME (absolute coordinates)") - print("\nFormat: x y z [roll pitch yaw]") - print(" - 3 values: position only (keep current orientation)") - print(" - 6 values: position + orientation (degrees)") - print("Example: 0.4 0.0 0.2 (position only)") - print("Example: 0.4 0.0 0.2 0 180 0 (with orientation)") - print("Ctrl+C to quit") - print("=" * 80) - - try: - while True: - try: - # Print current pose before asking for input - if setter.latest_current_pose is not None: - p = setter.latest_current_pose - # Convert quaternion to euler for display - quat = Quaternion(p.orientation) - euler = quat.to_euler() - print( - f"Current: {p.x:.3f} {p.y:.3f} {p.z:.3f} {math.degrees(euler.x):.1f} {math.degrees(euler.y):.1f} {math.degrees(euler.z):.1f}" - ) - - line = input("> ").strip() - - if not line: - continue - - parts = line.split() - - if len(parts) == 3: - # Position only - keep current orientation - x, y, z = [float(p) for p in parts] - setter.publish_pose(x, y, z) - - elif len(parts) == 6: - # Full pose (orientation in degrees) - x, y, z = [float(p) for p in parts[:3]] - roll = math.radians(float(parts[3])) - pitch = math.radians(float(parts[4])) - yaw = math.radians(float(parts[5])) - setter.publish_pose(x, y, z, roll, pitch, yaw) - - else: - print("⚠ Expected 3 (x y z) or 6 (x y z roll pitch yaw) values") - continue - - except ValueError as e: - print(f"⚠ Invalid input: {e}") - continue - - except KeyboardInterrupt: - print("\n\nExiting interactive mode...") - - -def print_banner() -> None: - """Print welcome banner.""" - print("\n" + "=" * 80) - print("xArm Target Pose Publisher") - print("=" * 80) - print("\nPublishes absolute target poses to /target_pose topic.") - print("Subscribes to /xarm/current_pose for orientation preservation.") - print("=" * 80) - - -def main() -> int: - """Main entry point.""" - print_banner() - - # Create setter and start subscribing to current pose - setter = TargetSetter() - if not setter.start(): - print("\n⚠ Warning: Could not get current pose - controller may not be running") - print("Make sure example_cartesian_control.py is running in another terminal!") - response = input("Continue anyway? [y/N]: ").strip().lower() - if response != "y": - return 0 - - try: - # Run interactive mode - interactive_mode(setter) - except KeyboardInterrupt: - print("\n\nInterrupted by user") - - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n\nInterrupted by user") - sys.exit(0) - except Exception as e: - print(f"\nError: {e}") - import traceback - - traceback.print_exc() - sys.exit(1) diff --git a/dimos/manipulation/control/trajectory_controller/__init__.py b/dimos/manipulation/control/trajectory_controller/__init__.py deleted file mode 100644 index fb4360d4cc..0000000000 --- a/dimos/manipulation/control/trajectory_controller/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Trajectory Controller Module - -Joint-space trajectory execution for robotic manipulators. -""" - -from dimos.manipulation.control.trajectory_controller.joint_trajectory_controller import ( - JointTrajectoryController, - JointTrajectoryControllerConfig, - joint_trajectory_controller, -) - -__all__ = [ - "JointTrajectoryController", - "JointTrajectoryControllerConfig", - "joint_trajectory_controller", -] diff --git a/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py b/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py deleted file mode 100644 index 6ecdff1714..0000000000 --- a/dimos/manipulation/control/trajectory_controller/joint_trajectory_controller.py +++ /dev/null @@ -1,368 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Joint Trajectory Controller - -A simple joint-space trajectory executor. Does NOT: -- Use Cartesian space -- Compute error -- Apply PID -- Call IK - -Just samples a trajectory at time t and sends joint positions to the driver. - -Behavior: -- execute_trajectory(): Preempts any active trajectory, starts new one immediately -- cancel(): Stops at current position -- reset(): Required to recover from FAULT state -""" - -from dataclasses import dataclass -import threading -import time -from typing import Any - -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryState, TrajectoryStatus -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class JointTrajectoryControllerConfig(ModuleConfig): - """Configuration for joint trajectory controller.""" - - control_frequency: float = 100.0 # Hz - trajectory execution rate - - -class JointTrajectoryController(Module): - """ - Joint-space trajectory executor. - - Executes joint trajectories at 100Hz by sampling and forwarding - joint positions to the arm driver. Uses ROS action-server-like - state machine for execution control. - - State Machine: - IDLE ──execute()──► EXECUTING ──done──► COMPLETED - ▲ │ │ - │ cancel() reset() - │ ▼ │ - └─────reset()───── ABORTED ◄──────────────┘ - │ - error - ▼ - FAULT ──reset()──► IDLE - """ - - default_config = JointTrajectoryControllerConfig - config: JointTrajectoryControllerConfig # Type hint for proper attribute access - - # Input topics - joint_state: In[JointState] = None # type: ignore[assignment] # Feedback from arm driver - robot_state: In[RobotState] = None # type: ignore[assignment] # Robot status from arm driver - trajectory: In[JointTrajectory] = None # type: ignore[assignment] # Trajectory to execute (topic-based) - - # Output topics - joint_position_command: Out[JointCommand] = None # type: ignore[assignment] # To arm driver - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - # State machine - self._state = TrajectoryState.IDLE - self._lock = threading.Lock() - - # Active trajectory - self._trajectory: JointTrajectory | None = None - self._start_time: float = 0.0 - - # Latest feedback - self._latest_joint_state: JointState | None = None - self._latest_robot_state: RobotState | None = None - - # Error tracking - self._error_message: str = "" - - # Execution thread - self._exec_thread: threading.Thread | None = None - self._stop_event = threading.Event() - - logger.info(f"JointTrajectoryController initialized at {self.config.control_frequency}Hz") - - @rpc - def start(self) -> None: - """Start the trajectory controller.""" - super().start() - - # Subscribe to feedback topics - try: - if self.joint_state.connection is not None or self.joint_state._transport is not None: - self.joint_state.subscribe(self._on_joint_state) - logger.info("Subscribed to joint_state") - except Exception as e: - logger.warning(f"Failed to subscribe to joint_state: {e}") - - try: - if self.robot_state.connection is not None or self.robot_state._transport is not None: - self.robot_state.subscribe(self._on_robot_state) - logger.info("Subscribed to robot_state") - except Exception as e: - logger.warning(f"Failed to subscribe to robot_state: {e}") - - # Subscribe to trajectory topic - try: - if self.trajectory.connection is not None or self.trajectory._transport is not None: - self.trajectory.subscribe(self._on_trajectory) - logger.info("Subscribed to trajectory topic") - except Exception: - logger.debug("trajectory topic not connected (expected - can use RPC instead)") - - # Start execution thread - self._stop_event.clear() - self._exec_thread = threading.Thread( - target=self._execution_loop, daemon=True, name="trajectory_exec_thread" - ) - self._exec_thread.start() - - logger.info("JointTrajectoryController started") - - @rpc - def stop(self) -> None: - """Stop the trajectory controller.""" - logger.info("Stopping JointTrajectoryController...") - - self._stop_event.set() - - if self._exec_thread and self._exec_thread.is_alive(): - self._exec_thread.join(timeout=2.0) - - super().stop() - logger.info("JointTrajectoryController stopped") - - # ========================================================================= - # RPC Methods - Action-server-like interface - # ========================================================================= - - @rpc - def execute_trajectory(self, trajectory: JointTrajectory) -> bool: - """ - Set and start executing a new trajectory immediately. - If currently executing, preempts and starts new trajectory. - - Args: - trajectory: JointTrajectory to execute - - Returns: - True if accepted, False if in FAULT state or trajectory invalid - """ - with self._lock: - # Cannot execute if in FAULT state - if self._state == TrajectoryState.FAULT: - logger.warning( - "Cannot execute trajectory: controller in FAULT state (call reset())" - ) - return False - - # Validate trajectory - if trajectory is None or trajectory.duration <= 0: - logger.warning("Invalid trajectory: None or zero duration") - return False - - if not trajectory.points: - logger.warning("Invalid trajectory: no points") - return False - - # Preempt any active trajectory - if self._state == TrajectoryState.EXECUTING: - logger.info("Preempting active trajectory") - - # Start new trajectory - self._trajectory = trajectory - self._start_time = time.time() - self._state = TrajectoryState.EXECUTING - self._error_message = "" - - logger.info( - f"Executing trajectory: {len(trajectory.points)} points, " - f"duration={trajectory.duration:.3f}s" - ) - return True - - @rpc - def cancel(self) -> bool: - """ - Cancel the currently executing trajectory. - Robot stops at current position. - - Returns: - True if cancelled, False if no active trajectory - """ - with self._lock: - if self._state != TrajectoryState.EXECUTING: - logger.debug("No active trajectory to cancel") - return False - - self._state = TrajectoryState.ABORTED - logger.info("Trajectory cancelled") - return True - - @rpc - def reset(self) -> bool: - """ - Reset from FAULT, COMPLETED, or ABORTED state back to IDLE. - Required before executing new trajectories after a fault. - - Returns: - True if reset successful, False if currently EXECUTING - """ - with self._lock: - if self._state == TrajectoryState.EXECUTING: - logger.warning("Cannot reset while executing (call cancel() first)") - return False - - self._state = TrajectoryState.IDLE - self._trajectory = None - self._error_message = "" - logger.info("Controller reset to IDLE") - return True - - @rpc - def get_status(self) -> TrajectoryStatus: - """ - Get the current status of the trajectory execution. - - Returns: - TrajectoryStatus with state, progress, and error info - """ - with self._lock: - time_elapsed = 0.0 - time_remaining = 0.0 - progress = 0.0 - - if self._trajectory is not None and self._state == TrajectoryState.EXECUTING: - time_elapsed = time.time() - self._start_time - time_remaining = max(0.0, self._trajectory.duration - time_elapsed) - progress = ( - min(1.0, time_elapsed / self._trajectory.duration) - if self._trajectory.duration > 0 - else 1.0 - ) - - return TrajectoryStatus( - state=self._state, - progress=progress, - time_elapsed=time_elapsed, - time_remaining=time_remaining, - error=self._error_message, - ) - - # ========================================================================= - # Callbacks - # ========================================================================= - - def _on_joint_state(self, msg: JointState) -> None: - """Callback for joint state feedback.""" - self._latest_joint_state = msg - - def _on_robot_state(self, msg: RobotState) -> None: - """Callback for robot state feedback.""" - self._latest_robot_state = msg - - def _on_trajectory(self, msg: JointTrajectory) -> None: - """Callback when trajectory is received via topic.""" - logger.info( - f"Received trajectory via topic: {len(msg.points)} points, duration={msg.duration:.3f}s" - ) - self.execute_trajectory(msg) - - # ========================================================================= - # Execution Loop - # ========================================================================= - - def _execution_loop(self) -> None: - """ - Main execution loop running at control_frequency Hz. - - When EXECUTING: - 1. Compute elapsed time - 2. Sample trajectory at t - 3. Publish joint command - 4. Check if done - """ - period = 1.0 / self.config.control_frequency - logger.info(f"Execution loop started at {self.config.control_frequency}Hz") - - while not self._stop_event.is_set(): - try: - with self._lock: - # Only process if executing - if self._state != TrajectoryState.EXECUTING: - # Release lock and sleep - pass - else: - # Compute elapsed time - t = time.time() - self._start_time - - # Check if trajectory complete - if self._trajectory is None: - self._state = TrajectoryState.FAULT - logger.error("Trajectory is None during execution") - elif t >= self._trajectory.duration: - self._state = TrajectoryState.COMPLETED - logger.info( - f"Trajectory completed: duration={self._trajectory.duration:.3f}s" - ) - else: - # Sample trajectory - q_ref, _qd_ref = self._trajectory.sample(t) - - # Create and publish command (outside lock would be better but simpler here) - cmd = JointCommand(positions=q_ref, timestamp=time.time()) - - # Publish - must release lock first for thread safety - trajectory_active = True - - if trajectory_active if "trajectory_active" in dir() else False: - try: - self.joint_position_command.publish(cmd) - except Exception as e: - logger.error(f"Failed to publish joint command: {e}") - with self._lock: - self._state = TrajectoryState.FAULT - self._error_message = f"Publish failed: {e}" - - # Reset flag - trajectory_active = False - - # Maintain loop frequency - time.sleep(period) - - except Exception as e: - logger.error(f"Error in execution loop: {e}") - with self._lock: - if self._state == TrajectoryState.EXECUTING: - self._state = TrajectoryState.FAULT - self._error_message = str(e) - time.sleep(period) - - logger.info("Execution loop stopped") - - -# Expose blueprint for declarative composition -joint_trajectory_controller = JointTrajectoryController.blueprint diff --git a/dimos/manipulation/control/trajectory_controller/spec.py b/dimos/manipulation/control/trajectory_controller/spec.py deleted file mode 100644 index 3da272a5b9..0000000000 --- a/dimos/manipulation/control/trajectory_controller/spec.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Joint Trajectory Controller Specification - -A simple joint-space trajectory executor. Does NOT: -- Use Cartesian space -- Compute error -- Apply PID -- Call IK - -Just samples a trajectory at time t and sends joint positions to the driver. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Protocol - -if TYPE_CHECKING: - from dimos.core import In, Out - from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState - from dimos.msgs.trajectory_msgs import JointTrajectory as JointTrajectoryMsg, TrajectoryState - -# Input topics -joint_state: In[JointState] | None = None # Feedback from arm driver -robot_state: In[RobotState] | None = None # Robot status from arm driver -trajectory: In[JointTrajectoryMsg] | None = None # Desired trajectory - -# Output topics -joint_position_command: Out[JointCommand] | None = None # To arm driver - - -def execute_trajectory() -> bool: - """ - Set and start executing a new trajectory immediately. - Returns True if accepted, False if controller busy or traj invalid. - """ - raise NotImplementedError("Protocol method") - - -def cancel() -> bool: - """ - Cancel the currently executing trajectory. - Returns True if cancelled, False if no active trajectory. - """ - raise NotImplementedError("Protocol method") - - -def get_status() -> TrajectoryStatusProtocol: - """ - Get the current status of the trajectory execution. - Returns a TrajectoryStatus message with details. - "state": "IDLE" | "EXECUTING" | "COMPLETED" | "ABORTED" | "FAULT", - "progress": float in [0,1], - "active_traj_id": Optional[str], - "error": Optional[str], - """ - raise NotImplementedError("Protocol method") - ... - - -class JointTrajectoryProtocol(Protocol): - """Protocol for a joint trajectory object.""" - - duration: float # Total duration in seconds - - def sample(self, t: float) -> tuple[list[float], list[float]]: - """ - Sample the trajectory at time t. - - Args: - t: Time in seconds (0 <= t <= duration) - - Returns: - Tuple of (q_ref, qd_ref): - - q_ref: Joint positions (radians) - - qd_ref: Joint velocities (rad/s) - """ - ... - - -class TrajectoryStatusProtocol(Protocol): - """Status of trajectory execution.""" - - state: TrajectoryState # Current state - progress: float # Progress 0.0 to 1.0 - time_elapsed: float # Seconds since trajectory start - time_remaining: float # Estimated seconds remaining - error: str | None # Error message if FAULT state diff --git a/dimos/manipulation/control/trajectory_setter.py b/dimos/manipulation/control/trajectory_setter.py deleted file mode 100644 index bad3854521..0000000000 --- a/dimos/manipulation/control/trajectory_setter.py +++ /dev/null @@ -1,467 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Interactive Trajectory Publisher for Joint Trajectory Control. - -Interactive terminal UI for creating joint trajectories using the -JointTrajectoryGenerator with trapezoidal velocity profiles. - -Workflow: -1. Add waypoints (joint positions only, no timing) -2. Generator applies trapezoidal velocity profile -3. Preview the generated trajectory -4. Publish to /trajectory topic - -Use with example_trajectory_control.py running in another terminal. -""" - -import math -import sys -import time - -from dimos import core -from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( - JointTrajectoryGenerator, -) -from dimos.msgs.sensor_msgs import JointState -from dimos.msgs.trajectory_msgs import JointTrajectory - - -class TrajectorySetter: - """ - Creates and publishes JointTrajectory using trapezoidal velocity profiles. - - Uses JointTrajectoryGenerator to compute proper timing and velocities - from a list of waypoints. Subscribes to arm-specific joint_states to get - current joint positions. - - Supports multiple arm types: - - xarm (xarm5/6/7) - - piper - - Any future arm that publishes joint_states - """ - - def __init__(self, arm_type: str = "xarm"): - """ - Initialize the trajectory setter. - - Args: - arm_type: Type of arm ("xarm", "piper", etc.) - """ - self.arm_type = arm_type.lower() - - # Publisher for trajectories - self.trajectory_pub: core.LCMTransport[JointTrajectory] = core.LCMTransport( - "/trajectory", JointTrajectory - ) - - # Subscribe to arm-specific joint state topic - joint_state_topic = f"/{self.arm_type}/joint_states" - self.joint_state_sub: core.LCMTransport[JointState] = core.LCMTransport( - joint_state_topic, JointState - ) - self.latest_joint_state: JointState | None = None - - # Will be set dynamically from joint_state - self.num_joints: int | None = None - self.generator: JointTrajectoryGenerator | None = None - - print(f"TrajectorySetter initialized for {self.arm_type.upper()}") - print(" Publishing to: /trajectory") - print(f" Subscribing to: {joint_state_topic}") - - def start(self) -> bool: - """Start subscribing to joint state.""" - self.joint_state_sub.subscribe(self._on_joint_state) - print(" Waiting for joint state...") - - for _ in range(50): # 5 second timeout - if self.latest_joint_state is not None: - # Dynamically determine joint count from actual joint_state - self.num_joints = len(self.latest_joint_state.position) - print(f" ✓ Joint state received ({self.num_joints} joints)") - - # Now create generator with correct joint count - self.generator = JointTrajectoryGenerator( - num_joints=self.num_joints, - max_velocity=1.0, # rad/s - max_acceleration=2.0, # rad/s^2 - points_per_segment=50, - ) - print(f" Max velocity: {self.generator.max_velocity[0]:.2f} rad/s") - print(f" Max acceleration: {self.generator.max_acceleration[0]:.2f} rad/s^2") - return True - time.sleep(0.1) - - print(" ⚠ Warning: No joint state received (timeout)") - return False - - def _on_joint_state(self, msg: JointState) -> None: - """Callback for joint state updates.""" - self.latest_joint_state = msg - - def get_current_joints(self) -> list[float] | None: - """Get current joint positions in radians (first num_joints only).""" - if self.latest_joint_state is None: - return None - # Only take first num_joints (exclude gripper if present) - return list(self.latest_joint_state.position[: self.num_joints]) - - def generate_trajectory(self, waypoints: list[list[float]]) -> JointTrajectory: - """ - Generate a trajectory from waypoints using trapezoidal velocity profile. - - Args: - waypoints: List of joint positions [j1, j2, ..., j6] in radians - - Returns: - JointTrajectory with proper timing and velocities - """ - if self.generator is None: - raise RuntimeError("Generator not initialized - joint state not received yet") - return self.generator.generate(waypoints) - - def publish_trajectory(self, trajectory: JointTrajectory) -> None: - """ - Publish a JointTrajectory to the /trajectory topic. - - Args: - trajectory: Generated trajectory to publish - """ - self.trajectory_pub.broadcast(None, trajectory) - print( - f"\nPublished trajectory: {len(trajectory.points)} points, " - f"duration={trajectory.duration:.2f}s" - ) - - -def parse_joint_input(line: str, num_joints: int) -> list[float] | None: - """ - Parse joint positions from user input. - - Accepts degrees by default, or radians with 'r' suffix. - """ - parts = line.strip().split() - if len(parts) != num_joints: - return None - - positions = [] - for part in parts: - try: - if part.endswith("r"): - positions.append(float(part[:-1])) - else: - positions.append(math.radians(float(part))) - except ValueError: - return None - - return positions - - -def preview_waypoints(waypoints: list[list[float]], num_joints: int) -> None: - """Show waypoints list.""" - if not waypoints: - print("No waypoints") - return - - # Dynamically generate header based on joint count - joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(num_joints)]) - line_width = 6 + 3 + num_joints * 8 + 10 - - print(f"\nWaypoints ({len(waypoints)}):") - print("-" * line_width) - print(f" # | {joint_headers} (degrees)") - print("-" * line_width) - for i, joints in enumerate(waypoints): - deg = [f"{math.degrees(j):7.1f}" for j in joints] - print(f" {i + 1:2} | {' '.join(deg)}") - print("-" * line_width) - - -def preview_trajectory(trajectory: JointTrajectory, num_joints: int) -> None: - """Show generated trajectory preview.""" - # Dynamically generate header based on joint count - joint_headers = " ".join([f"{'J' + str(i + 1):>7}" for i in range(num_joints)]) - line_width = 9 + 3 + num_joints * 8 + 10 - - print("\n" + "=" * line_width) - print("GENERATED TRAJECTORY") - print("=" * line_width) - print(f"Duration: {trajectory.duration:.3f}s") - print(f"Points: {len(trajectory.points)}") - print("-" * line_width) - print(f"{'Time':>6} | {joint_headers} (degrees)") - print("-" * line_width) - - # Sample at regular intervals - num_samples = min(15, max(len(trajectory.points) // 10, 5)) - for i in range(num_samples + 1): - t = (i / num_samples) * trajectory.duration - q_ref, _ = trajectory.sample(t) - q_deg = [f"{math.degrees(q):7.1f}" for q in q_ref] - print(f"{t:6.2f} | {' '.join(q_deg)}") - - print("-" * line_width) - - # Show velocity profile info - if trajectory.points: - max_vels = [0.0] * len(trajectory.points[0].velocities) - for pt in trajectory.points: - for j, v in enumerate(pt.velocities): - max_vels[j] = max(max_vels[j], abs(v)) - vel_deg = [f"{math.degrees(v):5.1f}" for v in max_vels] - print(f"Peak velocities (deg/s): [{', '.join(vel_deg)}]") - print("=" * line_width) - - -def interactive_mode(setter: TrajectorySetter) -> None: - """Interactive mode for creating trajectories.""" - if setter.num_joints is None: - print("Error: No joint state received. Cannot start interactive mode.") - return - - # Generate dynamic joint list for help text - joint_args = " ".join([f"" for i in range(setter.num_joints)]) - - print("\n" + "=" * 80) - print("Interactive Trajectory Setter") - print("=" * 80) - print(f"\nArm: {setter.num_joints} joints") - print("\nCommands:") - print(f" add {joint_args} - Add waypoint (degrees)") - print(" here - Add current position as waypoint") - print(" current - Show current joints") - print(" list - List waypoints") - print(" delete - Delete waypoint n") - print(" preview - Generate and preview trajectory") - print(" run - Generate and publish trajectory") - print(" clear - Clear waypoints") - print(" vel - Set max velocity (rad/s)") - print(" accel - Set max acceleration (rad/s^2)") - print(" limits - Show current limits") - print(" quit - Exit") - print("=" * 80) - - waypoints: list[list[float]] = [] - generated_trajectory: JointTrajectory | None = None - - try: - while True: - prompt = f"[{len(waypoints)} wp] > " - line = input(prompt).strip() - - if not line: - continue - - parts = line.split() - cmd = parts[0].lower() - - # ADD waypoint - if cmd == "add" and len(parts) >= setter.num_joints + 1: - joints = parse_joint_input( - " ".join(parts[1 : setter.num_joints + 1]), setter.num_joints - ) - if joints: - waypoints.append(joints) - generated_trajectory = None # Invalidate cached trajectory - deg = [f"{math.degrees(j):.1f}" for j in joints] - print(f"Added waypoint {len(waypoints)}: [{', '.join(deg)}] deg") - else: - print(f"Invalid joint values (need {setter.num_joints} values in degrees)") - - # HERE - add current position - elif cmd == "here": - joints = setter.get_current_joints() - if joints: - waypoints.append(joints) - generated_trajectory = None - deg = [f"{math.degrees(j):.1f}" for j in joints] - print(f"Added waypoint {len(waypoints)}: [{', '.join(deg)}] deg") - else: - print("No joint state available") - - # CURRENT - elif cmd == "current": - joints = setter.get_current_joints() - if joints: - deg = [f"{math.degrees(j):.1f}" for j in joints] - print(f"Current: [{', '.join(deg)}] deg") - else: - print("No joint state available") - - # LIST - elif cmd == "list": - preview_waypoints(waypoints, setter.num_joints) - - # DELETE - elif cmd == "delete" and len(parts) >= 2: - try: - idx = int(parts[1]) - 1 - if 0 <= idx < len(waypoints): - waypoints.pop(idx) - generated_trajectory = None - print(f"Deleted waypoint {idx + 1}") - else: - print(f"Invalid index (1-{len(waypoints)})") - except ValueError: - print("Invalid index") - - # PREVIEW - elif cmd == "preview": - if len(waypoints) < 2: - print("Need at least 2 waypoints") - else: - print("\nGenerating trajectory...") - try: - generated_trajectory = setter.generate_trajectory(waypoints) - preview_trajectory(generated_trajectory, setter.num_joints) - except Exception as e: - print(f"Error generating trajectory: {e}") - - # RUN - elif cmd == "run": - if len(waypoints) < 2: - print("Need at least 2 waypoints") - continue - - # Generate if not already generated - if generated_trajectory is None: - print("\nGenerating trajectory...") - try: - generated_trajectory = setter.generate_trajectory(waypoints) - except Exception as e: - print(f"Error generating trajectory: {e}") - continue - - preview_trajectory(generated_trajectory, setter.num_joints) - confirm = input("\nPublish to robot? [y/N]: ").strip().lower() - if confirm == "y": - setter.publish_trajectory(generated_trajectory) - - # CLEAR - elif cmd == "clear": - waypoints.clear() - generated_trajectory = None - print("Cleared") - - # VEL - set max velocity - elif cmd == "vel" and len(parts) >= 2: - if setter.generator is None: - print("Generator not initialized") - continue - try: - vel = float(parts[1]) - if vel <= 0: - print("Velocity must be positive") - else: - setter.generator.set_limits(vel, setter.generator.max_acceleration) - generated_trajectory = None - print( - f"Max velocity set to {vel:.2f} rad/s ({math.degrees(vel):.1f} deg/s)" - ) - except ValueError: - print("Invalid velocity") - - # ACCEL - set max acceleration - elif cmd == "accel" and len(parts) >= 2: - if setter.generator is None: - print("Generator not initialized") - continue - try: - accel = float(parts[1]) - if accel <= 0: - print("Acceleration must be positive") - else: - setter.generator.set_limits(setter.generator.max_velocity, accel) - generated_trajectory = None - print(f"Max acceleration set to {accel:.2f} rad/s^2") - except ValueError: - print("Invalid acceleration") - - # LIMITS - show current limits - elif cmd == "limits": - if setter.generator is None: - print("Generator not initialized") - continue - v = setter.generator.max_velocity[0] - a = setter.generator.max_acceleration[0] - print(f"Max velocity: {v:.2f} rad/s ({math.degrees(v):.1f} deg/s)") - print(f"Max acceleration: {a:.2f} rad/s^2 ({math.degrees(a):.1f} deg/s^2)") - - # QUIT - elif cmd in ("quit", "exit", "q"): - break - - else: - print(f"Unknown command: {cmd}") - - except KeyboardInterrupt: - print("\n\nExiting...") - - -def main() -> int: - """Main entry point.""" - import argparse - - parser = argparse.ArgumentParser(description="Interactive Trajectory Setter for robot arms") - parser.add_argument( - "--arm", - type=str, - default="xarm", - choices=["xarm", "piper"], - help="Type of arm to control (default: xarm)", - ) - parser.add_argument( - "--custom-arm", - type=str, - help="Custom arm type (will subscribe to //joint_states)", - ) - args = parser.parse_args() - - arm_type = args.custom_arm if args.custom_arm else args.arm - - print("\n" + "=" * 80) - print("Trajectory Setter") - print("=" * 80) - print(f"\nArm Type: {arm_type.upper()}") - print("Generates joint trajectories using trapezoidal velocity profiles.") - print("Run example_trajectory_control.py in another terminal first!") - print("=" * 80) - - setter = TrajectorySetter(arm_type=arm_type) - if not setter.start(): - print(f"\nWarning: Could not get joint state from /{arm_type}/joint_states") - print("Controller may not be running or arm type may be incorrect.") - response = input("Continue anyway? [y/N]: ").strip().lower() - if response != "y": - return 0 - - interactive_mode(setter) - return 0 - - -if __name__ == "__main__": - try: - sys.exit(main()) - except KeyboardInterrupt: - print("\n\nInterrupted by user") - sys.exit(0) - except Exception as e: - print(f"\nError: {e}") - import traceback - - traceback.print_exc() - sys.exit(1) diff --git a/dimos/manipulation/grasping/__init__.py b/dimos/manipulation/grasping/__init__.py deleted file mode 100644 index 41779f55e7..0000000000 --- a/dimos/manipulation/grasping/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dimos.manipulation.grasping.graspgen_module import ( - GraspGenConfig, - GraspGenModule, - graspgen, -) -from dimos.manipulation.grasping.grasping import ( - GraspingModule, - grasping_module, -) - -__all__ = [ - "GraspGenConfig", - "GraspGenModule", - "GraspingModule", - "graspgen", - "grasping_module", -] diff --git a/dimos/manipulation/grasping/demo_grasping.py b/dimos/manipulation/grasping/demo_grasping.py deleted file mode 100644 index 7c6e94d2af..0000000000 --- a/dimos/manipulation/grasping/demo_grasping.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from pathlib import Path - -from dimos.agents.agent import agent -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.camera.realsense import realsense_camera -from dimos.manipulation.grasping import graspgen -from dimos.manipulation.grasping.grasping import grasping_module -from dimos.perception.detection.detectors.yoloe import YoloePromptMode -from dimos.perception.object_scene_registration import object_scene_registration_module -from dimos.robot.foxglove_bridge import foxglove_bridge - -camera_module = realsense_camera(enable_pointcloud=False) - -demo_grasping = autoconnect( - camera_module, - object_scene_registration_module( - target_frame="camera_color_optical_frame", prompt_mode=YoloePromptMode.PROMPT - ), - grasping_module(), - graspgen( - docker_file_path=Path(__file__).parent / "docker_context" / "Dockerfile", - docker_build_context=Path(__file__).parent.parent.parent.parent, # repo root - gripper_type="robotiq_2f_140", # out of the bosx ships "robotiq_2f_140", "franka_panda", "single_suction_cup_30mm - num_grasps=400, - topk_num_grasps=100, - filter_collisions=False, - save_visualization_data=False, # to just see the visualization simply run ``grasping/visualize_grasps.py`` as a standalone script - docker_volumes=[ - ("/tmp", "/tmp", "rw") - ], # Grasp visualization debug standalone: python -m dimos.manipulation.grasping.visualize_grasps - ), - foxglove_bridge(), - agent(), -).global_config(viewer_backend="foxglove") diff --git a/dimos/manipulation/grasping/docker_context/Dockerfile b/dimos/manipulation/grasping/docker_context/Dockerfile deleted file mode 100644 index d10b3cac76..0000000000 --- a/dimos/manipulation/grasping/docker_context/Dockerfile +++ /dev/null @@ -1,72 +0,0 @@ -# GraspGen - Grasp Pose Generation Model -# https://github.com/NVlabs/GraspGen -# -# This Dockerfile packages the GraspGen model for grasp pose generation. -# Requires CUDA 12.8+ for PyTorch and CUDA extensions. - -FROM nvidia/cuda:12.8.1-cudnn-devel-ubuntu22.04 - -# System dependencies for GraspGen -RUN apt-get update && apt-get install -y \ - git wget curl build-essential \ - libgl1-mesa-glx libglib2.0-0 libsm6 libxext6 libxrender-dev \ - libglu1-mesa libglu1-mesa-dev libegl1-mesa-dev \ - iproute2 \ - && rm -rf /var/lib/apt/lists/* - -# Python environment (Miniconda) -ENV CONDA_DIR=/opt/conda -RUN wget -q https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O /tmp/miniconda.sh && \ - bash /tmp/miniconda.sh -b -p $CONDA_DIR && rm /tmp/miniconda.sh -ENV PATH=$CONDA_DIR/bin:$PATH - -RUN conda init bash && \ - conda config --set channel_priority flexible && \ - echo 'yes' | conda tos accept --override-channels --channel defaults 2>/dev/null || true && \ - conda create -n app python=3.10 -y && conda clean -afy - -# Clone GraspGen repository -WORKDIR /app -RUN git clone https://github.com/NVlabs/GraspGen.git && cd GraspGen && git checkout main - -# Install PyTorch with CUDA 12.8 -RUN conda run -n app pip install --no-cache-dir \ - torch==2.7.0 torchvision==0.22.0 --index-url https://download.pytorch.org/whl/cu128 - -# Install GraspGen package -WORKDIR /app/GraspGen -RUN conda run -n app pip install --no-cache-dir -e . - -# Build CUDA extensions (pointnet2_ops) -RUN conda run -n app bash -c "\ - export TORCH_CUDA_ARCH_LIST='8.0 8.6 8.9 9.0 12.0' && \ - export FORCE_CUDA=1 && \ - export CUDA_HOME=/usr/local/cuda && \ - cd pointnet2_ops && pip install --no-build-isolation ." - -# Install torch-scatter and torch-cluster (require CUDA compilation) -RUN conda run -n app pip install --no-cache-dir --no-build-isolation torch-scatter torch-cluster - -# Additional dependencies -RUN conda run -n app pip install --no-cache-dir \ - numpy trimesh pillow pyrender imageio scipy - -# Model checkpoints from LFS archive -COPY data/.lfs/models_graspgen.tar.gz /tmp/ -RUN tar -xzf /tmp/models_graspgen.tar.gz -C /app/GraspGen/ && \ - rm /tmp/models_graspgen.tar.gz - -# Verify checkpoints exist -RUN test -f /app/GraspGen/checkpoints/graspgen_robotiq_2f_140_gen.pth || \ - (echo "ERROR: Model checkpoints not found" && exit 1) - -# Environment variables for GraspGen -ENV GRASPGEN_PATH=/app/GraspGen -ENV DEFAULT_GRIPPER=robotiq_2f_140 -ENV PYOPENGL_PLATFORM=egl -ENV CUDA_LAUNCH_BLOCKING=0 -ENV TORCH_CUDA_ARCH_LIST="8.0 8.6 8.9 9.0 12.0" -ENV PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True - -# Default command -CMD ["conda", "run", "-n", "app", "python", "-c", "print('GraspGen ready')"] diff --git a/dimos/manipulation/grasping/graspgen_module.py b/dimos/manipulation/grasping/graspgen_module.py deleted file mode 100644 index 47520ea0e5..0000000000 --- a/dimos/manipulation/grasping/graspgen_module.py +++ /dev/null @@ -1,279 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -from dataclasses import dataclass -import os -from pathlib import Path -import sys -import time -from typing import TYPE_CHECKING, Any - -import numpy as np - -from dimos.core.core import rpc -from dimos.core.docker_runner import DockerModuleConfig -from dimos.core.module import Module -from dimos.msgs.geometry_msgs import PoseArray -from dimos.msgs.std_msgs import Header -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import matrix_to_pose - -if TYPE_CHECKING: - from dimos.core.stream import Out - from dimos.msgs.sensor_msgs import PointCloud2 - -logger = setup_logger() - -# Inference constants -MIN_POINTS_FOR_INFERENCE = 50 -OUTLIER_REMOVAL_THRESHOLD = 100 -COLLISION_FILTER_THRESHOLD = 0.02 - - -@dataclass -class GraspGenConfig(DockerModuleConfig): - """Configuration for GraspGen module.""" - - # Docker defaults - docker_image: str = "dimos-graspgen:latest" - docker_gpus: str = "all" - docker_shm_size: str = "4g" - - # GraspGen settings - gripper_type: str = ( - "robotiq_2f_140" # use any from robotiq_2f_140", "franka_panda", "single_suction_cup_30mm" - ) - num_grasps: int = 400 - topk_num_grasps: int = 100 - grasp_threshold: float = -1.0 - filter_collisions: bool = False - save_visualization_data: bool = False - visualization_output_path: str = "/tmp/grasp_visualization.json" - - -class GraspGenModule(Module[GraspGenConfig]): - """Grasp generation module running in Docker.""" - - default_config = GraspGenConfig - grasps: Out[PoseArray] - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._sampler = self._gripper_info = None - self._initialized = False - - @rpc - def start(self) -> None: - super().start() - if not self._initialize_graspgen(): - raise RuntimeError("Failed to initialize GraspGen") - logger.info(f"GraspGenModule started (gripper={self.config.gripper_type})") - - @rpc - def stop(self) -> None: - self._sampler = self._gripper_info = None - self._initialized = False - super().stop() - - @rpc - def generate_grasps( - self, - pointcloud: PointCloud2, - scene_pointcloud: PointCloud2 | None = None, - ) -> PoseArray | None: - """Generate grasp poses for the given pointcloud.""" - try: - points = self._extract_points(pointcloud) - if len(points) < 10: - return None - - # Run inference (with optional collision filtering) - scene_points = None - if scene_pointcloud is not None and self.config.filter_collisions: - scene_points = self._extract_points(scene_pointcloud) - grasps, scores = self._run_inference(points, scene_points) - if len(grasps) == 0: - return None - - # Convert and publish results - pose_array = self._grasps_to_pose_array(grasps, scores, pointcloud.frame_id) - self.grasps.publish(pose_array) - - if self.config.save_visualization_data: - self._save_visualization_data(points, grasps, scores, pointcloud.frame_id) - return pose_array - except Exception as e: - logger.error(f"Grasp generation failed: {e}") - return None - - def _initialize_graspgen(self) -> bool: - """Load GraspGen model and gripper info. Returns True on success.""" - if self._initialized: - return True - - try: - # Setup GraspGen path and environment (must be set by Dockerfile) - graspgen_path = os.environ.get("GRASPGEN_PATH") - if graspgen_path is None: - raise RuntimeError( - "GRASPGEN_PATH environment variable not set. Ensure Dockerfile sets ENV GRASPGEN_PATH." - ) - if graspgen_path not in sys.path: - sys.path.insert(0, graspgen_path) - os.environ["PYOPENGL_PLATFORM"] = "egl" - - # Load model and gripper (Docker-only imports) - from grasp_gen.grasp_server import ( # type: ignore[import-not-found] - GraspGenSampler, - load_grasp_cfg, - ) - from grasp_gen.robot import get_gripper_info # type: ignore[import-not-found] - - grasp_cfg = load_grasp_cfg(self._get_gripper_config_path()) - self._sampler = GraspGenSampler(grasp_cfg) - self._gripper_info = get_gripper_info(self.config.gripper_type) - self._initialized = True - logger.info("GraspGen initialized") - return True - except Exception as e: - logger.error(f"Failed to initialize GraspGen: {e}") - self._sampler = self._gripper_info = None - return False - - def _get_gripper_config_path(self) -> str: - graspgen_path = os.environ.get("GRASPGEN_PATH") - if graspgen_path is None: - raise RuntimeError("GRASPGEN_PATH environment variable not set") - config_name = f"graspgen_{self.config.gripper_type}.yml" - - for subdir in ("GraspGenModels/checkpoints", "checkpoints"): - path = os.path.join(graspgen_path, subdir, config_name) - if os.path.exists(path): - return path - - return os.path.join(graspgen_path, "checkpoints", config_name) - - def _run_inference( - self, object_pc: np.ndarray[Any, Any], scene_pc: np.ndarray[Any, Any] | None = None - ) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any]]: - if self._sampler is None: - return np.array([]), np.array([]) - - from grasp_gen.grasp_server import GraspGenSampler # type: ignore[import-not-found] - from grasp_gen.utils.point_cloud_utils import ( # type: ignore[import-not-found] - filter_colliding_grasps, - point_cloud_outlier_removal, - ) - import torch # type: ignore[import-not-found] - import trimesh.transformations as tra # type: ignore[import-not-found] - - pc_torch = torch.from_numpy(object_pc) - - if len(object_pc) > OUTLIER_REMOVAL_THRESHOLD: - pc_filtered, _ = point_cloud_outlier_removal(pc_torch) - object_pc_filtered = pc_filtered.numpy() - if len(object_pc_filtered) < MIN_POINTS_FOR_INFERENCE: - object_pc_filtered = object_pc - else: - object_pc_filtered = object_pc - - if len(object_pc_filtered) < MIN_POINTS_FOR_INFERENCE: - return np.array([]), np.array([]) - - grasps, scores = GraspGenSampler.run_inference( - object_pc_filtered, - self._sampler, - grasp_threshold=self.config.grasp_threshold, - num_grasps=self.config.num_grasps, - topk_num_grasps=self.config.topk_num_grasps, - remove_outliers=False, - ) - - if len(grasps) == 0: - return np.array([]), np.array([]) - - grasps_np = grasps.cpu().numpy() - scores_np = scores.cpu().numpy() - - if self.config.filter_collisions and scene_pc is not None: - if self._gripper_info is None: - return grasps_np, scores_np - - pc_mean = object_pc_filtered.mean(axis=0) - T_center = tra.translation_matrix(-pc_mean) - grasps_centered = np.array([T_center @ g for g in grasps_np]) - scene_pc_centered = tra.transform_points(scene_pc, T_center) - - collision_free_mask = filter_colliding_grasps( - scene_pc=scene_pc_centered, - grasp_poses=grasps_centered, - gripper_collision_mesh=self._gripper_info.collision_mesh, - collision_threshold=COLLISION_FILTER_THRESHOLD, - ) - grasps_np = grasps_np[collision_free_mask] - scores_np = scores_np[collision_free_mask] - - return grasps_np, scores_np - - def _extract_points(self, msg: PointCloud2) -> np.ndarray[Any, Any]: - points = msg.points().numpy() # type: ignore[no-untyped-call] - if not np.isfinite(points).all(): - raise ValueError("Point cloud contains NaN/Inf") - return points # type: ignore[no-any-return] - - def _grasps_to_pose_array( - self, grasps: np.ndarray[Any, Any], scores: np.ndarray[Any, Any], frame_id: str - ) -> PoseArray: - sorted_indices = np.argsort(scores)[::-1] - poses = [matrix_to_pose(grasps[idx]) for idx in sorted_indices] - return PoseArray(header=Header(frame_id), poses=poses) - - def _save_visualization_data( - self, - points: np.ndarray[Any, Any], - grasps: np.ndarray[Any, Any], - scores: np.ndarray[Any, Any], - frame_id: str, - ) -> None: - import json - - try: - data = { - "point_cloud": points.tolist(), - "grasps": [g.tolist() for g in grasps], - "scores": scores.tolist(), - "frame_id": frame_id, - "timestamp": time.time(), - } - output_path = Path(self.config.visualization_output_path) - output_path.parent.mkdir(parents=True, exist_ok=True) - with open(output_path, "w") as f: - json.dump(data, f) - except Exception as e: - logger.warning(f"Failed to save visualization: {e}") - - -def graspgen( - docker_file_path: Path | str, docker_build_context: Path | str | None = None, **kwargs: Any -) -> Any: - """Create a GraspGen module blueprint. All kwargs passed through to config.""" - dockerfile = Path(docker_file_path) - build_context = Path(docker_build_context) if docker_build_context else dockerfile.parent - return GraspGenModule.blueprint( - docker_file=dockerfile, docker_build_context=build_context, **kwargs - ) - - -__all__ = ["GraspGenConfig", "GraspGenModule", "graspgen"] diff --git a/dimos/manipulation/grasping/grasping.py b/dimos/manipulation/grasping/grasping.py deleted file mode 100644 index 783f899a83..0000000000 --- a/dimos/manipulation/grasping/grasping.py +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Grasping skill module - -Provides @skill interface for agents and orchestrates the grasp generation pipeline: -perception (get pointcloud) to graspgen (generate grasps in Docker) to output grasps -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import quaternion_to_euler - -if TYPE_CHECKING: - from dimos.core.stream import Out - from dimos.msgs.geometry_msgs import PoseArray - from dimos.msgs.sensor_msgs import PointCloud2 - -logger = setup_logger() - - -class GraspingModule(Module): - """Grasping skill and orchestrator module""" - - grasps: Out[PoseArray] - - rpc_calls: list[str] = [ - "ObjectSceneRegistrationModule.get_object_pointcloud_by_name", - "ObjectSceneRegistrationModule.get_object_pointcloud_by_object_id", - "ObjectSceneRegistrationModule.get_full_scene_pointcloud", - "GraspGenModule.generate_grasps", - ] - - @rpc - def start(self) -> None: - super().start() - logger.info("GraspingModule started") - - @rpc - def stop(self) -> None: - super().stop() - logger.info("GraspingModule stopped") - - @skill - def generate_grasps( - self, - object_name: str = "object", - object_id: str | None = None, - filter_collisions: bool = True, - ) -> str: - """Generate grasp poses for the specified object. - - Args: - object_name: Name of the object to grasp (e.g. "coke can", "cup", "bottle"). - object_id: Optional unique object ID from perception. If provided, uses this - instead of object_name for lookup. - filter_collisions: Whether to filter grasps that collide with scene geometry. - - """ - # Get object pointcloud from perception - pc = self._get_object_pointcloud(object_name, object_id) - if pc is None: - msg = f"No pointcloud found for '{object_id or object_name}'" - logger.warning(msg) - return msg - - # Get scene pointcloud for collision filtering - scene_pc = None - if filter_collisions: - scene_pc = self._get_scene_pointcloud(exclude_object_id=object_id) - - # Call GraspGenModule RPC (running in Docker) - try: - generate = self.get_rpc_calls("GraspGenModule.generate_grasps") - result = generate(pc, scene_pc) - except Exception as e: - msg = f"Grasp generation failed: {e}" - logger.error(msg) - return msg - - if result is None or len(result.poses) == 0: - msg = f"No grasps generated for '{object_name}'" - logger.info(msg) - return msg - - self.grasps.publish(result) - logger.info(f"Generated {len(result.poses)} grasps for '{object_name}'") - - # Format result for agent/human - return self._format_grasp_result(result, object_name) - - def _get_object_pointcloud( - self, object_name: str, object_id: str | None = None - ) -> PointCloud2 | None: - """Fetch object pointcloud from perception.""" - try: - if object_id is not None: - get_pc = self.get_rpc_calls( - "ObjectSceneRegistrationModule.get_object_pointcloud_by_object_id" - ) - return get_pc(object_id) # type: ignore[no-any-return] - - get_pc = self.get_rpc_calls( - "ObjectSceneRegistrationModule.get_object_pointcloud_by_name" - ) - return get_pc(object_name) # type: ignore[no-any-return] - except Exception as e: - logger.error(f"Failed to get object pointcloud: {e}") - return None - - def _get_scene_pointcloud(self, exclude_object_id: str | None = None) -> PointCloud2 | None: - """Fetch scene pointcloud from perception for collision filtering.""" - try: - get_scene = self.get_rpc_calls( - "ObjectSceneRegistrationModule.get_full_scene_pointcloud" - ) - return get_scene(exclude_object_id=exclude_object_id) # type: ignore[no-any-return] - except Exception as e: - logger.debug(f"Could not get scene pointcloud: {e}") - return None - - def _format_grasp_result(self, grasps: PoseArray, object_name: str) -> str: - """Format grasp result for agent/human consumption.""" - best = grasps.poses[0] - pos = best.position - rpy = quaternion_to_euler(best.orientation, degrees=True) - return ( - f"Generated {len(grasps.poses)}" - f"Best grasp: pos=({pos.x:.4f}, {pos.y:.4f}, {pos.z:.4f}), " - f"rpy=({rpy.x:.1f}, {rpy.y:.1f}, {rpy.z:.1f}) degrees" - ) - - -grasping_module = GraspingModule.blueprint -__all__ = ["GraspingModule", "grasping_module"] diff --git a/dimos/manipulation/grasping/visualize_grasps.py b/dimos/manipulation/grasping/visualize_grasps.py deleted file mode 100644 index 53edc1bb16..0000000000 --- a/dimos/manipulation/grasping/visualize_grasps.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Grasp visualization debug tool: python -m dimos.manipulation.grasping.visualize_grasps""" - -from __future__ import annotations - -import json -from pathlib import Path -from typing import Any - -import numpy as np -import open3d as o3d # type: ignore[import-untyped] - -GRIPPER_WIDTH = 0.086 -FINGER_LENGTH = 0.052 -PALM_DEPTH = 0.04 -MAX_GRASPS = 100 -VISUALIZATION_FILE = "/tmp/grasp_visualization.json" - - -def create_gripper_geometry(transform: np.ndarray[Any, Any], color: list[float]) -> list[Any]: - w = GRIPPER_WIDTH / 2.0 - fl = FINGER_LENGTH - pd = PALM_DEPTH - wrist = np.array([0.0, 0.0, -(pd + fl)]) - palm = np.array([0.0, 0.0, -fl]) - l_base = np.array([-w, 0.0, -fl]) - r_base = np.array([w, 0.0, -fl]) - l_tip = np.array([-w, 0.0, 0.25 * fl]) - r_tip = np.array([w, 0.0, 0.25 * fl]) - points = np.vstack([wrist, palm, l_base, r_base, l_tip, r_tip]) - lines = [[0, 1], [1, 2], [1, 3], [2, 4], [3, 5]] - points_h = np.hstack([points, np.ones((len(points), 1))]) - points_world = (transform @ points_h.T).T[:, :3] - line_set = o3d.geometry.LineSet() - line_set.points = o3d.utility.Vector3dVector(points_world) - line_set.lines = o3d.utility.Vector2iVector(lines) - line_set.colors = o3d.utility.Vector3dVector([color] * len(lines)) - - return [line_set] - - -def visualize_grasps(point_cloud: np.ndarray[Any, Any], grasps: list[np.ndarray[Any, Any]]) -> None: - geometries = [] - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(point_cloud) - pcd.paint_uniform_color([0.0, 0.8, 0.8]) - geometries.append(pcd) - coord_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(size=0.1) - geometries.append(coord_frame) - - num_to_show = min(len(grasps), MAX_GRASPS) - for i in range(num_to_show): - t = i / max(num_to_show - 1, 1) if i > 0 else 0.0 - color = [min(1.0, 2 * t), max(0.0, 1.0 - t), 0.0] - geometries.extend(create_gripper_geometry(grasps[i], color)) - - o3d.visualization.draw_geometries(geometries, window_name="GraspGen", width=1280, height=720) - - -def main() -> int: - filepath = Path(VISUALIZATION_FILE) - if not filepath.exists(): - print(f"File not found: {filepath}") - return 1 - - with open(filepath) as f: - data = json.load(f) - - point_cloud = np.array(data["point_cloud"]) - grasps = [np.array(g).reshape(4, 4) for g in data["grasps"]] - - visualize_grasps(point_cloud, grasps) - return 0 - - -if __name__ == "__main__": - exit(main()) diff --git a/dimos/manipulation/manipulation_blueprints.py b/dimos/manipulation/manipulation_blueprints.py deleted file mode 100644 index e95e415373..0000000000 --- a/dimos/manipulation/manipulation_blueprints.py +++ /dev/null @@ -1,410 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Blueprints for manipulation module integration with ControlCoordinator. - -Usage: - # Non-agentic (manual RPC): - dimos run coordinator-mock - dimos run xarm-perception - - # Agentic (LLM agent with skills): - dimos run coordinator-mock - dimos run xarm-perception-agent -""" - -import math -from pathlib import Path - -from dimos.agents.agent import Agent -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera.realsense import realsense_camera -from dimos.manipulation.manipulation_module import manipulation_module -from dimos.manipulation.planning.spec import RobotModelConfig -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import JointState -from dimos.perception.object_scene_registration import object_scene_registration_module -from dimos.robot.foxglove_bridge import foxglove_bridge # TODO: migrate to rerun -from dimos.utils.data import get_data - -# ============================================================================= -# Pose Helpers -# ============================================================================= - - -def _make_base_pose( - x: float = 0.0, - y: float = 0.0, - z: float = 0.0, - roll: float = 0.0, - pitch: float = 0.0, - yaw: float = 0.0, -) -> PoseStamped: - """Create a base pose with optional xyz offset and rpy orientation. - - Args: - x, y, z: Position offset in meters - roll, pitch, yaw: Orientation in radians (Euler angles) - """ - return PoseStamped( - position=Vector3(x=x, y=y, z=z), - orientation=Quaternion.from_euler(Vector3(x=roll, y=pitch, z=yaw)), - ) - - -# ============================================================================= -# URDF Helpers -# ============================================================================= - - -def _get_xarm_urdf_path() -> Path: - """Get path to xarm URDF.""" - return get_data("xarm_description") / "urdf/xarm_device.urdf.xacro" - - -def _get_xarm_package_paths() -> dict[str, Path]: - """Get package paths for xarm xacro resolution.""" - return {"xarm_description": get_data("xarm_description")} - - -def _get_piper_urdf_path() -> Path: - """Get path to piper URDF.""" - return get_data("piper_description") / "urdf/piper_description.xacro" - - -def _get_piper_package_paths() -> dict[str, Path]: - """Get package paths for piper xacro resolution.""" - return {"piper_description": get_data("piper_description")} - - -# Piper gripper collision exclusions (parallel jaw gripper) -# The gripper fingers (link7, link8) can touch each other and gripper_base -PIPER_GRIPPER_COLLISION_EXCLUSIONS: list[tuple[str, str]] = [ - ("gripper_base", "link7"), - ("gripper_base", "link8"), - ("link7", "link8"), - ("link6", "gripper_base"), -] - - -# XArm gripper collision exclusions (parallel linkage mechanism) -# The gripper uses mimic joints where non-adjacent links can overlap legitimately -XARM_GRIPPER_COLLISION_EXCLUSIONS: list[tuple[str, str]] = [ - # Inner knuckle <-> outer knuckle (parallel linkage) - ("right_inner_knuckle", "right_outer_knuckle"), - ("left_inner_knuckle", "left_outer_knuckle"), - # Inner knuckle <-> finger (parallel linkage) - ("right_inner_knuckle", "right_finger"), - ("left_inner_knuckle", "left_finger"), - # Cross-finger pairs (mimic joint symmetry) - ("left_finger", "right_finger"), - ("left_outer_knuckle", "right_outer_knuckle"), - ("left_inner_knuckle", "right_inner_knuckle"), - # Outer knuckle <-> opposite finger - ("left_outer_knuckle", "right_finger"), - ("right_outer_knuckle", "left_finger"), - # Gripper base <-> all moving parts (can touch at limits) - ("xarm_gripper_base_link", "left_inner_knuckle"), - ("xarm_gripper_base_link", "right_inner_knuckle"), - ("xarm_gripper_base_link", "left_finger"), - ("xarm_gripper_base_link", "right_finger"), - # Arm link6 <-> gripper (attached via fixed joint, can touch) - ("link6", "xarm_gripper_base_link"), - ("link6", "left_outer_knuckle"), - ("link6", "right_outer_knuckle"), -] - - -# ============================================================================= -# Robot Configs -# ============================================================================= - - -def _make_xarm6_config( - name: str = "arm", - y_offset: float = 0.0, - joint_prefix: str = "", - coordinator_task: str | None = None, - add_gripper: bool = True, -) -> RobotModelConfig: - """Create XArm6 robot config. - - Args: - name: Robot name in Drake world - y_offset: Y-axis offset for base pose (for multi-arm setups) - joint_prefix: Prefix for joint name mapping (e.g., "left_" or "right_") - coordinator_task: Task name for coordinator RPC execution - add_gripper: Whether to add the xarm gripper - """ - joint_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"] - joint_mapping = {f"{joint_prefix}{j}": j for j in joint_names} if joint_prefix else {} - - xacro_args: dict[str, str] = { - "dof": "6", - "limited": "true", - "attach_xyz": f"0 {y_offset} 0", - } - if add_gripper: - xacro_args["add_gripper"] = "true" - - return RobotModelConfig( - name=name, - urdf_path=_get_xarm_urdf_path(), - base_pose=_make_base_pose(y=y_offset), - joint_names=joint_names, - end_effector_link="link_tcp" if add_gripper else "link6", - base_link="link_base", - package_paths=_get_xarm_package_paths(), - xacro_args=xacro_args, - collision_exclusion_pairs=XARM_GRIPPER_COLLISION_EXCLUSIONS if add_gripper else [], - auto_convert_meshes=True, - max_velocity=1.0, - max_acceleration=2.0, - joint_name_mapping=joint_mapping, - coordinator_task_name=coordinator_task, - home_joints=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - - -def _make_xarm7_config( - name: str = "arm", - y_offset: float = 0.0, - z_offset: float = 0.0, - pitch: float = 0.0, - joint_prefix: str = "", - coordinator_task: str | None = None, - add_gripper: bool = False, - gripper_hardware_id: str | None = None, - tf_extra_links: list[str] | None = None, -) -> RobotModelConfig: - """Create XArm7 robot config. - - Args: - name: Robot name in Drake world - y_offset: Y-axis offset for base pose (for multi-arm setups) - z_offset: Z-axis offset for base pose (e.g., table height) - pitch: Base pitch angle in radians (e.g., tilted mount) - joint_prefix: Prefix for joint name mapping (e.g., "left_" or "right_") - coordinator_task: Task name for coordinator RPC execution - add_gripper: Whether to add the xarm gripper - gripper_hardware_id: Coordinator hardware ID for gripper control - tf_extra_links: Additional links to publish TF for (e.g., ["link7"] for camera mount) - """ - joint_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6", "joint7"] - joint_mapping = {f"{joint_prefix}{j}": j for j in joint_names} if joint_prefix else {} - - xacro_args: dict[str, str] = { - "dof": "7", - "limited": "true", - "attach_xyz": f"0 {y_offset} {z_offset}", - "attach_rpy": f"0 {pitch} 0", - } - if add_gripper: - xacro_args["add_gripper"] = "true" - - return RobotModelConfig( - name=name, - urdf_path=_get_xarm_urdf_path(), - base_pose=_make_base_pose(y=y_offset, z=z_offset, pitch=pitch), - joint_names=joint_names, - end_effector_link="link_tcp" if add_gripper else "link7", - base_link="link_base", - package_paths=_get_xarm_package_paths(), - xacro_args=xacro_args, - collision_exclusion_pairs=XARM_GRIPPER_COLLISION_EXCLUSIONS if add_gripper else [], - auto_convert_meshes=True, - max_velocity=1.0, - max_acceleration=2.0, - joint_name_mapping=joint_mapping, - coordinator_task_name=coordinator_task, - gripper_hardware_id=gripper_hardware_id, - tf_extra_links=tf_extra_links or [], - # Home configuration: arm extended forward, elbow up (safe observe pose) - home_joints=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - - -def _make_piper_config( - name: str = "piper", - y_offset: float = 0.0, - joint_prefix: str = "", - coordinator_task: str | None = None, -) -> RobotModelConfig: - """Create Piper robot config. - - Args: - name: Robot name in Drake world - y_offset: Y-axis offset for base pose (for multi-arm setups) - joint_prefix: Prefix for joint name mapping (e.g., "piper_") - coordinator_task: Task name for coordinator RPC execution - - Note: - Piper has 6 revolute joints (joint1-joint6) for the arm and 2 prismatic - joints (joint7, joint8) for the parallel jaw gripper. - """ - # Piper arm joints (6-DOF) - joint_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"] - joint_mapping = {f"{joint_prefix}{j}": j for j in joint_names} if joint_prefix else {} - - return RobotModelConfig( - name=name, - urdf_path=_get_piper_urdf_path(), - base_pose=_make_base_pose(y=y_offset), - joint_names=joint_names, - end_effector_link="gripper_base", # End of arm, before gripper fingers - base_link="arm_base", - package_paths=_get_piper_package_paths(), - xacro_args={}, # Piper xacro doesn't need special args - collision_exclusion_pairs=PIPER_GRIPPER_COLLISION_EXCLUSIONS, - auto_convert_meshes=True, - max_velocity=1.0, - max_acceleration=2.0, - joint_name_mapping=joint_mapping, - coordinator_task_name=coordinator_task, - home_joints=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - - -# ============================================================================= -# Blueprints -# ============================================================================= - - -# Single XArm6 planner (standalone, no coordinator) -xarm6_planner_only = manipulation_module( - robots=[_make_xarm6_config()], - planning_timeout=10.0, - enable_viz=True, -).transports( - { - ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), - } -) - - -# Dual XArm6 planner with coordinator integration -# Usage: Start with coordinator_dual_mock, then plan/execute via RPC -dual_xarm6_planner = manipulation_module( - robots=[ - _make_xarm6_config( - "left_arm", y_offset=0.5, joint_prefix="left_", coordinator_task="traj_left" - ), - _make_xarm6_config( - "right_arm", y_offset=-0.5, joint_prefix="right_", coordinator_task="traj_right" - ), - ], - planning_timeout=10.0, - enable_viz=True, -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -# Single XArm7 planner for coordinator-mock -# Usage: dimos run coordinator-mock, then dimos run xarm7-planner-coordinator -xarm7_planner_coordinator = manipulation_module( - robots=[_make_xarm7_config("arm", joint_prefix="arm_", coordinator_task="traj_arm")], - planning_timeout=10.0, - enable_viz=True, -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -# XArm7 with eye-in-hand RealSense camera for perception-based manipulation -# TF chain: world → link7 (ManipulationModule) → camera_link (RealSense) -# Usage: dimos run coordinator-mock, then dimos run xarm-perception -_XARM_PERCEPTION_CAMERA_TRANSFORM = Transform( - translation=Vector3(x=0.06693724, y=-0.0309563, z=0.00691482), - rotation=Quaternion(0.70513398, 0.00535696, 0.70897578, -0.01052180), # xyzw -) - -xarm_perception = ( - autoconnect( - manipulation_module( - robots=[ - _make_xarm7_config( - "arm", - pitch=math.radians(45), - joint_prefix="arm_", - coordinator_task="traj_arm", - add_gripper=True, - gripper_hardware_id="arm", - tf_extra_links=["link7"], - ), - ], - planning_timeout=10.0, - enable_viz=True, - ), - realsense_camera( - base_frame_id="link7", - base_transform=_XARM_PERCEPTION_CAMERA_TRANSFORM, - ), - object_scene_registration_module(target_frame="world"), - foxglove_bridge(), # TODO: migrate to rerun - ) - .transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } - ) - .global_config(viewer_backend="foxglove") -) - - -# XArm7 perception + LLM agent for agentic manipulation -# Skills (pick, place, move_to_pose, etc.) auto-register with the agent's SkillCoordinator. -# Usage: dimos run coordinator-mock, then dimos run xarm-perception-agent -_MANIPULATION_AGENT_SYSTEM_PROMPT = """\ -You are a robotic manipulation assistant controlling an xArm7 robot arm. - -Use ONLY these ManipulationModule skills for manipulation tasks: -- scan_objects: Scan scene and list detected objects with 3D positions. Always call this first. -- pick: Pick up an object by name. Requires scan_objects first. -- place: Place a held object at x, y, z position. -- place_back: Place a held object back at its original pick position. -- pick_and_place: Pick an object and place it at a target location. -- move_to_pose: Move end-effector to x, y, z with optional roll, pitch, yaw. -- move_to_joints: Move to a joint configuration (comma-separated radians). -- open_gripper / close_gripper / set_gripper: Control the gripper. -- go_home: Move to the home/observe position. -- go_init: Return to the startup position. -- get_scene_info: Get full robot state, detected objects, and scene info. - -Do NOT use the 'detect' or 'select' skills — use scan_objects instead. -For robot_name parameters, always omit or pass None (single-arm setup). -After pick or place, return to init with go_init unless another action follows immediately. -""" - -xarm_perception_agent = autoconnect( - xarm_perception, - Agent.blueprint(system_prompt=_MANIPULATION_AGENT_SYSTEM_PROMPT), -) - - -__all__ = [ - "PIPER_GRIPPER_COLLISION_EXCLUSIONS", - "XARM_GRIPPER_COLLISION_EXCLUSIONS", - "dual_xarm6_planner", - "xarm6_planner_only", - "xarm7_planner_coordinator", - "xarm_perception", - "xarm_perception_agent", -] diff --git a/dimos/manipulation/manipulation_interface.py b/dimos/manipulation/manipulation_interface.py deleted file mode 100644 index 524562520d..0000000000 --- a/dimos/manipulation/manipulation_interface.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -ManipulationInterface provides a unified interface for accessing manipulation data. - -This module defines the ManipulationInterface class, which serves as an access point -for agent-generated constraints, manipulation tasks, and perception streams. -""" - -from typing import TYPE_CHECKING, Any - -from dimos.types.manipulation import ( - AbstractConstraint, - ManipulationTask, - ObjectData, -) -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from reactivex.disposable import Disposable - -logger = setup_logger() - - -class ManipulationInterface: - """ - Interface for accessing and managing robot manipulation data. - - This class provides a unified interface for managing manipulation tasks and constraints. - It maintains a list of constraints generated by the Agent and provides methods to - add and manage manipulation tasks. - """ - - def __init__( - self, - perception_stream: Any = None, - ) -> None: - """ - Initialize a new ManipulationInterface instance. - - Args: - perception_stream: ObjectDetectionStream instance for real-time object data - """ - # List of manipulation tasks - self._tasks: list[ManipulationTask] = [] - - # List of constraints generated by the Agent via constraint generation skills - self.agent_constraints: list[AbstractConstraint] = [] - - # Initialize object detection stream and related properties - self.perception_stream = perception_stream - self.latest_objects: list[ObjectData] = [] - self.stream_subscription: Disposable | None = None - - # Set up subscription to perception stream if available - self._setup_perception_subscription() - - logger.info("ManipulationInterface initialized") - - def add_constraint(self, constraint: AbstractConstraint) -> None: - """ - Add a constraint generated by the Agent via a constraint generation skill. - - Args: - constraint: The constraint to add to agent_constraints - """ - self.agent_constraints.append(constraint) - logger.info(f"Added agent constraint: {constraint}") - - def get_constraints(self) -> list[AbstractConstraint]: - """ - Get all constraints generated by the Agent via constraint generation skills. - - Returns: - List of all constraints created by the Agent - """ - return self.agent_constraints - - def get_constraint(self, constraint_id: str) -> AbstractConstraint | None: - """ - Get a specific constraint by its ID. - - Args: - constraint_id: ID of the constraint to retrieve - - Returns: - The matching constraint or None if not found - """ - # Find constraint with matching ID - for constraint in self.agent_constraints: - if constraint.id == constraint_id: - return constraint - - logger.warning(f"Constraint with ID {constraint_id} not found") - return None - - def add_manipulation_task(self, task: ManipulationTask) -> None: - """ - Add a manipulation task. - - Args: - task: The ManipulationTask to add - """ - self._tasks.append(task) - logger.info(f"Added manipulation task: {task.task_id or 'unknown'}") - - def get_manipulation_task(self, task_id: str) -> ManipulationTask | None: - """ - Get a manipulation task by its ID. - - Args: - task_id: ID of the task to retrieve - - Returns: - The task object or None if not found - """ - for task in self._tasks: - if task.task_id == task_id: - return task - return None - - def get_all_manipulation_tasks(self) -> list[ManipulationTask]: - """ - Get all manipulation tasks. - - Returns: - List of all manipulation tasks - """ - return list(self._tasks) - - def update_task_result(self, task_id: str, result: dict[str, Any]) -> ManipulationTask | None: - """ - Update the result of a manipulation task. - - Args: - task_id: ID of the task to update - result: Result data from task execution - - Returns: - The updated task or None if task not found - """ - task = self.get_manipulation_task(task_id) - if task is not None: - task.result = result - return task - return None - - # === Perception stream methods === - - def _setup_perception_subscription(self) -> None: - """ - Set up subscription to perception stream if available. - """ - if self.perception_stream: - # Subscribe to the stream and update latest_objects - self.stream_subscription = self.perception_stream.get_stream().subscribe( # type: ignore[no-untyped-call] - on_next=self._update_latest_objects, - on_error=lambda e: logger.error(f"Error in perception stream: {e}"), - ) - logger.info("Subscribed to perception stream") - - def _update_latest_objects(self, data) -> None: # type: ignore[no-untyped-def] - """ - Update the latest detected objects. - - Args: - data: Data from the object detection stream - """ - if "objects" in data: - self.latest_objects = data["objects"] - - def get_latest_objects(self) -> list[ObjectData]: - """ - Get the latest detected objects from the stream. - - Returns: - List of the most recently detected objects - """ - return self.latest_objects - - def get_object_by_id(self, object_id: int) -> ObjectData | None: - """ - Get a specific object by its tracking ID. - - Args: - object_id: Tracking ID of the object - - Returns: - The object data or None if not found - """ - for obj in self.latest_objects: - if obj["object_id"] == object_id: - return obj - return None - - def get_objects_by_label(self, label: str) -> list[ObjectData]: - """ - Get all objects with a specific label. - - Args: - label: Class label to filter objects by - - Returns: - List of objects matching the label - """ - return [obj for obj in self.latest_objects if obj["label"] == label] - - def set_perception_stream(self, perception_stream) -> None: # type: ignore[no-untyped-def] - """ - Set or update the perception stream. - - Args: - perception_stream: The PerceptionStream instance - """ - # Clean up existing subscription if any - self.cleanup_perception_subscription() - - # Set new stream and subscribe - self.perception_stream = perception_stream - self._setup_perception_subscription() - - def cleanup_perception_subscription(self) -> None: - """ - Clean up the stream subscription. - """ - if self.stream_subscription: - self.stream_subscription.dispose() - self.stream_subscription = None - - # === Utility methods === - - def clear(self) -> None: - """ - Clear all manipulation tasks and agent constraints. - """ - self._tasks.clear() - self.agent_constraints.clear() - logger.info("Cleared manipulation tasks and agent constraints") - - def __str__(self) -> str: - """ - String representation of the manipulation interface. - - Returns: - String representation with key stats - """ - has_stream = self.perception_stream is not None - return f"ManipulationInterface(tasks={len(self._tasks)}, constraints={len(self.agent_constraints)}, perception_stream={has_stream}, detected_objects={len(self.latest_objects)})" - - def __del__(self) -> None: - """ - Clean up resources on deletion. - """ - self.cleanup_perception_subscription() diff --git a/dimos/manipulation/manipulation_module.py b/dimos/manipulation/manipulation_module.py deleted file mode 100644 index 310b77d766..0000000000 --- a/dimos/manipulation/manipulation_module.py +++ /dev/null @@ -1,1615 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Manipulation Module - Motion planning with ControlCoordinator execution. - -Interface layers: -- @rpc: Low-level building blocks (plan_to_pose, plan_to_joints, preview_path, execute) -- @skill (short-horizon): Single-step actions (move_to_pose, open_gripper, scan_objects, go_init) -- @skill (long-horizon): Multi-step composed behaviors (pick, place, place_back, pick_and_place) -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from enum import Enum -import math -from pathlib import Path -import threading -import time -from typing import TYPE_CHECKING, Any, TypeAlias - -from dimos.agents.annotation import skill -from dimos.constants import DIMOS_PROJECT_ROOT -from dimos.core import In, Module, rpc -from dimos.core.docker_runner import DockerModule as DockerRunner -from dimos.core.module import ModuleConfig -from dimos.manipulation.grasping.graspgen_module import GraspGenModule -from dimos.manipulation.planning import ( - JointPath, - JointTrajectoryGenerator, - KinematicsSpec, - Obstacle, - ObstacleType, - PlannerSpec, - RobotModelConfig, - RobotName, - WorldRobotID, - create_kinematics, - create_planner, -) -from dimos.manipulation.planning.monitor import WorldMonitor -from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 - -# These must be imported at runtime (not TYPE_CHECKING) for In/Out port creation -from dimos.msgs.sensor_msgs import JointState -from dimos.msgs.trajectory_msgs import JointTrajectory -from dimos.perception.detection.type.detection3d.object import Object as DetObject -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.core.rpc_client import RPCClient - from dimos.msgs.geometry_msgs import PoseArray - from dimos.msgs.sensor_msgs import PointCloud2 - from dimos.perception.detection.type.detection3d.object import Object as DetObject - -logger = setup_logger() - -# Composite type aliases for readability (using semantic IDs from planning.spec) -RobotEntry: TypeAlias = tuple[WorldRobotID, RobotModelConfig, JointTrajectoryGenerator] -"""(world_robot_id, config, trajectory_generator)""" - -RobotRegistry: TypeAlias = dict[RobotName, RobotEntry] -"""Maps robot_name -> RobotEntry""" - -PlannedPaths: TypeAlias = dict[RobotName, JointPath] -"""Maps robot_name -> planned joint path""" - -PlannedTrajectories: TypeAlias = dict[RobotName, JointTrajectory] -"""Maps robot_name -> planned trajectory""" - -# The host-side path (graspgen_visualization_output_path) is volume-mounted here. -_GRASPGEN_VIZ_CONTAINER_DIR = "/output/graspgen" -_GRASPGEN_VIZ_CONTAINER_PATH = f"{_GRASPGEN_VIZ_CONTAINER_DIR}/visualization.json" - - -class ManipulationState(Enum): - """State machine for manipulation module.""" - - IDLE = 0 - PLANNING = 1 - EXECUTING = 2 - COMPLETED = 3 - FAULT = 4 - - -@dataclass -class ManipulationModuleConfig(ModuleConfig): - """Configuration for ManipulationModule.""" - - robots: list[RobotModelConfig] = field(default_factory=list) - planning_timeout: float = 10.0 - enable_viz: bool = False - planner_name: str = "rrt_connect" # "rrt_connect" - kinematics_name: str = "jacobian" # "jacobian" or "drake_optimization" - - # GraspGen Docker settings (optional) - graspgen_docker_image: str = "dimos-graspgen:latest" - graspgen_gripper_type: str = "robotiq_2f_140" - graspgen_num_grasps: int = 400 - graspgen_topk_num_grasps: int = 100 - graspgen_grasp_threshold: float = -1.0 - graspgen_filter_collisions: bool = False - graspgen_save_visualization_data: bool = False - graspgen_visualization_output_path: Path = field( - default_factory=lambda: Path.home() / ".dimos" / "graspgen" / "visualization.json" - ) - - -class ManipulationModule(Module): - """Motion planning module with ControlCoordinator execution. - - - @rpc: Low-level building blocks (plan, execute, obstacles) - - @skill (short-horizon): Single-step actions (move_to_pose, open_gripper, scan_objects) - - @skill (long-horizon): Multi-step behaviors (pick, place, pick_and_place) - """ - - default_config = ManipulationModuleConfig - - # Type annotation for the config attribute (mypy uses this) - config: ManipulationModuleConfig - - # Input: Joint state from coordinator (for world sync) - joint_state: In[JointState] - - # Input: Objects from perception (for obstacle integration) - objects: In[list[DetObject]] - - def __init__(self, *args: object, **kwargs: object) -> None: - super().__init__(*args, **kwargs) - - # State machine - self._state = ManipulationState.IDLE - self._lock = threading.Lock() - self._error_message = "" - - # Planning components (initialized in start()) - self._world_monitor: WorldMonitor | None = None - self._planner: PlannerSpec | None = None - self._kinematics: KinematicsSpec | None = None - - # Robot registry: maps robot_name -> (world_robot_id, config, trajectory_gen) - self._robots: RobotRegistry = {} - - # Stored path for plan/preview/execute workflow (per robot) - self._planned_paths: PlannedPaths = {} - self._planned_trajectories: PlannedTrajectories = {} - - # Coordinator integration (lazy initialized) - self._coordinator_client: RPCClient | None = None - - # GraspGen Docker runner (lazy initialized on first generate_grasps call) - self._graspgen: DockerRunner | None = None - # Init joints: captured from first joint state received, used by go_init - self._init_joints: JointState | None = None - - # Last pick position: stored during pick so place_back() can return the object - self._last_pick_position: Vector3 | None = None - - # Snapshotted detections from the last scan_objects/refresh call. - # The live detection cache is volatile (labels change every frame), - # so pick/place use this stable snapshot instead. - self._detection_snapshot: list[DetObject] = [] - - # TF publishing thread - self._tf_stop_event = threading.Event() - self._tf_thread: threading.Thread | None = None - - logger.info("ManipulationModule initialized") - - @rpc - def start(self) -> None: - """Start the manipulation module.""" - super().start() - - # Initialize planning stack - self._initialize_planning() - - # Subscribe to joint state via port - if self.joint_state is not None: - self.joint_state.subscribe(self._on_joint_state) - logger.info("Subscribed to joint_state port") - - # Subscribe to objects port for perception obstacle integration - if self.objects is not None: - self.objects.observable().subscribe(self._on_objects) # type: ignore[no-untyped-call] - logger.info("Subscribed to objects port (async)") - - logger.info("ManipulationModule started") - - def _initialize_planning(self) -> None: - """Initialize world, planner, and trajectory generator.""" - if not self.config.robots: - logger.warning("No robots configured, planning disabled") - return - - self._world_monitor = WorldMonitor(enable_viz=self.config.enable_viz) - - for robot_config in self.config.robots: - robot_id = self._world_monitor.add_robot(robot_config) - traj_gen = JointTrajectoryGenerator( - num_joints=len(robot_config.joint_names), - max_velocity=robot_config.max_velocity, - max_acceleration=robot_config.max_acceleration, - ) - self._robots[robot_config.name] = (robot_id, robot_config, traj_gen) - - self._world_monitor.finalize() - - for _, (robot_id, _, _) in self._robots.items(): - self._world_monitor.start_state_monitor(robot_id) - - # Start obstacle monitor for perception integration - self._world_monitor.start_obstacle_monitor() - - if self.config.enable_viz: - self._world_monitor.start_visualization_thread(rate_hz=10.0) - if url := self._world_monitor.get_visualization_url(): - logger.info(f"Visualization: {url}") - - self._planner = create_planner(name=self.config.planner_name) - self._kinematics = create_kinematics(name=self.config.kinematics_name) - - # Start TF publishing thread if any robot has tf_extra_links - if any(c.tf_extra_links for _, c, _ in self._robots.values()): - _ = self.tf # Eager init — lazy init blocks in Dask workers - self._tf_stop_event.clear() - self._tf_thread = threading.Thread( - target=self._tf_publish_loop, name="ManipTFThread", daemon=True - ) - self._tf_thread.start() - logger.info("TF publishing thread started") - - def _get_default_robot_name(self) -> RobotName | None: - """Get default robot name (first robot if only one, else None).""" - if len(self._robots) == 1: - return next(iter(self._robots.keys())) - return None - - def _get_robot( - self, robot_name: RobotName | None = None - ) -> tuple[RobotName, WorldRobotID, RobotModelConfig, JointTrajectoryGenerator] | None: - """Get robot by name or default. - - Args: - robot_name: Robot name or None for default (if single robot) - - Returns: - (robot_name, robot_id, config, traj_gen) or None if not found - """ - if not robot_name: # None or empty string (LLMs often pass "") - robot_name = self._get_default_robot_name() - if robot_name is None: - logger.error("Multiple robots configured, must specify robot_name") - return None - - if robot_name not in self._robots: - logger.error(f"Unknown robot: {robot_name}") - return None - - robot_id, config, traj_gen = self._robots[robot_name] - return (robot_name, robot_id, config, traj_gen) - - def _on_joint_state(self, msg: JointState) -> None: - """Callback when joint state received from driver.""" - try: - # Forward to world monitor for state synchronization. - # Pass robot_id=None to broadcast to all monitors - each monitor - # extracts only its robot's joints based on joint_name_mapping. - if self._world_monitor is not None: - self._world_monitor.on_joint_state(msg, robot_id=None) - - # Capture initial joint positions on first callback - if self._init_joints is None and msg.position: - self._init_joints = JointState(name=list(msg.name), position=list(msg.position)) - logger.info( - f"Init joints captured: [{', '.join(f'{j:.3f}' for j in msg.position)}]" - ) - - except Exception as e: - logger.error(f"Exception in _on_joint_state: {e}") - import traceback - - logger.error(traceback.format_exc()) - - def _on_objects(self, objects: list[DetObject]) -> None: - """Callback when objects received from perception (runs on RxPY thread pool).""" - try: - if self._world_monitor is not None: - self._world_monitor.on_objects(objects) - except Exception as e: - logger.error(f"Exception in _on_objects: {e}") - - def _tf_publish_loop(self) -> None: - """Publish TF transforms at 10Hz for EE and extra links.""" - from dimos.msgs.geometry_msgs import Transform - - period = 0.1 # 10Hz - while not self._tf_stop_event.is_set(): - try: - if self._world_monitor is None: - break - transforms: list[Transform] = [] - for robot_id, config, _ in self._robots.values(): - # Publish world → EE - ee_pose = self._world_monitor.get_ee_pose(robot_id) - if ee_pose is not None: - ee_tf = Transform.from_pose(config.end_effector_link, ee_pose) - ee_tf.frame_id = "world" - transforms.append(ee_tf) - - # Publish world → each extra link - for link_name in config.tf_extra_links: - link_pose = self._world_monitor.get_link_pose(robot_id, link_name) - if link_pose is not None: - link_tf = Transform.from_pose(link_name, link_pose) - link_tf.frame_id = "world" - transforms.append(link_tf) - - if transforms: - self.tf.publish(*transforms) - except Exception as e: - logger.debug(f"TF publish error: {e}") - - self._tf_stop_event.wait(period) - - # ========================================================================= - # RPC Methods - # ========================================================================= - - @rpc - def get_state(self) -> str: - """Get current manipulation state name.""" - return self._state.name - - @rpc - def get_error(self) -> str: - """Get last error message. - - Returns: - Error message or empty string - """ - return self._error_message - - @rpc - def cancel(self) -> bool: - """Cancel current motion.""" - if self._state != ManipulationState.EXECUTING: - return False - self._state = ManipulationState.IDLE - logger.info("Motion cancelled") - return True - - @rpc - def reset(self) -> bool: - """Reset to IDLE state (fails if EXECUTING).""" - if self._state == ManipulationState.EXECUTING: - return False - self._state = ManipulationState.IDLE - self._error_message = "" - return True - - @rpc - def get_current_joints(self, robot_name: RobotName | None = None) -> list[float] | None: - """Get current joint positions. - - Args: - robot_name: Robot to query (required if multiple robots configured) - """ - if (robot := self._get_robot(robot_name)) and self._world_monitor: - state = self._world_monitor.get_current_joint_state(robot[1]) - if state is not None: - return list(state.position) - return None - - @rpc - def get_ee_pose(self, robot_name: RobotName | None = None) -> Pose | None: - """Get current end-effector pose. - - Args: - robot_name: Robot to query (required if multiple robots configured) - """ - if (robot := self._get_robot(robot_name)) and self._world_monitor: - return self._world_monitor.get_ee_pose(robot[1], joint_state=None) - return None - - @rpc - def is_collision_free(self, joints: list[float], robot_name: RobotName | None = None) -> bool: - """Check if joint configuration is collision-free. - - Args: - joints: Joint configuration to check - robot_name: Robot to check (required if multiple robots configured) - """ - if (robot := self._get_robot(robot_name)) and self._world_monitor: - _, robot_id, config, _ = robot - joint_state = JointState(name=config.joint_names, position=joints) - return self._world_monitor.is_state_valid(robot_id, joint_state) - return False - - # ========================================================================= - # Plan/Preview/Execute Workflow RPC Methods - # ========================================================================= - - def _begin_planning( - self, robot_name: RobotName | None = None - ) -> tuple[RobotName, WorldRobotID] | None: - """Check state and begin planning. Returns (robot_name, robot_id) or None. - - Args: - robot_name: Robot to plan for (required if multiple robots configured) - """ - if self._world_monitor is None: - logger.error("Planning not initialized") - return None - if (robot := self._get_robot(robot_name)) is None: - return None - with self._lock: - if self._state not in (ManipulationState.IDLE, ManipulationState.COMPLETED): - logger.warning(f"Cannot plan: state is {self._state.name}") - return None - self._state = ManipulationState.PLANNING - return robot[0], robot[1] - - def _fail(self, msg: str) -> bool: - """Set FAULT state with error message.""" - logger.warning(msg) - self._state = ManipulationState.FAULT - self._error_message = msg - return False - - def _dismiss_preview(self, robot_id: WorldRobotID) -> None: - """Hide the preview ghost if the world supports it.""" - if self._world_monitor is None: - return - world = self._world_monitor.world - if hasattr(world, "hide_preview"): - world.hide_preview(robot_id) # type: ignore[attr-defined] - world.publish_visualization() - - @rpc - def plan_to_pose(self, pose: Pose, robot_name: RobotName | None = None) -> bool: - """Plan motion to pose. Use preview_path() then execute(). - - Args: - pose: Target end-effector pose - robot_name: Robot to plan for (required if multiple robots configured) - """ - if self._kinematics is None or (r := self._begin_planning(robot_name)) is None: - return False - robot_name, robot_id = r - assert self._world_monitor # guaranteed by _begin_planning - - current = self._world_monitor.get_current_joint_state(robot_id) - if current is None: - return self._fail("No joint state") - - # Convert Pose to PoseStamped for the IK solver - from dimos.msgs.geometry_msgs import PoseStamped - - target_pose = PoseStamped( - frame_id="world", - position=pose.position, - orientation=pose.orientation, - ) - - ik = self._kinematics.solve( - world=self._world_monitor.world, - robot_id=robot_id, - target_pose=target_pose, - seed=current, - check_collision=True, - ) - if not ik.is_success() or ik.joint_state is None: - return self._fail(f"IK failed: {ik.status.name}") - - logger.info(f"IK solved, error: {ik.position_error:.4f}m") - return self._plan_path_only(robot_name, robot_id, ik.joint_state) - - @rpc - def plan_to_joints(self, joints: JointState, robot_name: RobotName | None = None) -> bool: - """Plan motion to joint config. Use preview_path() then execute(). - - Args: - joints: Target joint state (names + positions) - robot_name: Robot to plan for (required if multiple robots configured) - """ - if (r := self._begin_planning(robot_name)) is None: - return False - robot_name, robot_id = r - logger.info(f"Planning to joints for {robot_name}: {[f'{j:.3f}' for j in joints.position]}") - return self._plan_path_only(robot_name, robot_id, joints) - - def _plan_path_only( - self, robot_name: RobotName, robot_id: WorldRobotID, goal: JointState - ) -> bool: - """Plan path from current position to goal, store result.""" - assert self._world_monitor and self._planner # guaranteed by _begin_planning - self._dismiss_preview(robot_id) - start = self._world_monitor.get_current_joint_state(robot_id) - if start is None: - return self._fail("No joint state") - - result = self._planner.plan_joint_path( - world=self._world_monitor.world, - robot_id=robot_id, - start=start, - goal=goal, - timeout=self.config.planning_timeout, - ) - if not result.is_success(): - return self._fail(f"Planning failed: {result.status.name}") - - logger.info(f"Path: {len(result.path)} waypoints") - self._planned_paths[robot_name] = result.path - - _, _, traj_gen = self._robots[robot_name] - # Convert JointState path to list of position lists for trajectory generator - traj = traj_gen.generate([list(state.position) for state in result.path]) - self._planned_trajectories[robot_name] = traj - logger.info(f"Trajectory: {traj.duration:.3f}s") - - self._state = ManipulationState.COMPLETED - return True - - @rpc - def preview_path(self, duration: float = 3.0, robot_name: RobotName | None = None) -> bool: - """Preview the planned path in the visualizer. - - Args: - duration: Total animation duration in seconds - robot_name: Robot to preview (required if multiple robots configured) - """ - from dimos.manipulation.planning.utils.path_utils import interpolate_path - - if self._world_monitor is None: - return False - - robot = self._get_robot(robot_name) - if robot is None: - return False - robot_name, robot_id, _, _ = robot - - planned_path = self._planned_paths.get(robot_name) - if planned_path is None or len(planned_path) == 0: - logger.warning(f"No planned path to preview for {robot_name}") - return False - - # Interpolate and animate - interpolated = interpolate_path(planned_path, resolution=0.1) - self._world_monitor.world.animate_path(robot_id, interpolated, duration) - return True - - @rpc - def has_planned_path(self) -> bool: - """Check if there's a planned path ready. - - Returns: - True if a path is planned and ready - """ - robot = self._get_robot() - if robot is None: - return False - robot_name, _, _, _ = robot - - path = self._planned_paths.get(robot_name) - return path is not None and len(path) > 0 - - @rpc - def get_visualization_url(self) -> str | None: - """Get the visualization URL. - - Returns: - URL string or None if visualization not enabled - """ - if self._world_monitor is None: - return None - return self._world_monitor.get_visualization_url() - - @rpc - def clear_planned_path(self) -> bool: - """Clear the stored planned path. - - Returns: - True if cleared - """ - robot = self._get_robot() - if robot is None: - return False - robot_name, _, _, _ = robot - - self._planned_paths.pop(robot_name, None) - self._planned_trajectories.pop(robot_name, None) - return True - - @rpc - def list_robots(self) -> list[str]: - """List all configured robot names. - - Returns: - List of robot names - """ - return list(self._robots.keys()) - - @rpc - def get_robot_info(self, robot_name: RobotName | None = None) -> dict[str, Any] | None: - """Get information about a robot. - - Args: - robot_name: Robot name (uses default if None) - - Returns: - Dict with robot info or None if not found - """ - robot = self._get_robot(robot_name) - if robot is None: - return None - - robot_name, robot_id, config, _ = robot - - return { - "name": config.name, - "world_robot_id": robot_id, - "joint_names": config.joint_names, - "end_effector_link": config.end_effector_link, - "base_link": config.base_link, - "max_velocity": config.max_velocity, - "max_acceleration": config.max_acceleration, - "has_joint_name_mapping": bool(config.joint_name_mapping), - "coordinator_task_name": config.coordinator_task_name, - "home_joints": config.home_joints, - "pre_grasp_offset": config.pre_grasp_offset, - "init_joints": list(self._init_joints.position) if self._init_joints else None, - } - - @rpc - def get_init_joints(self) -> JointState | None: - """Get the init joint state (captured at startup or set manually).""" - return self._init_joints - - @rpc - def set_init_joints(self, joint_state: JointState) -> bool: - """Set the init joint state. - - Args: - joint_state: New init joint state (names + positions) - """ - self._init_joints = joint_state - logger.info(f"Init joints set: [{', '.join(f'{j:.3f}' for j in joint_state.position)}]") - return True - - @rpc - def set_init_joints_to_current(self, robot_name: RobotName | None = None) -> bool: - """Set init joints to the current joint positions. - - Args: - robot_name: Robot to capture from (required if multiple robots configured) - """ - robot = self._get_robot(robot_name) - if robot is None: - return False - _, robot_id, _, _ = robot - if self._world_monitor is None: - return False - current = self._world_monitor.get_current_joint_state(robot_id) - if current is None: - logger.error("Cannot capture init joints — no current joint state") - return False - self._init_joints = current - logger.info( - f"Init joints set to current: [{', '.join(f'{j:.3f}' for j in current.position)}]" - ) - return True - - # ========================================================================= - # Coordinator Integration RPC Methods - # ========================================================================= - - def _get_coordinator_client(self) -> RPCClient | None: - """Get or create coordinator RPC client (lazy init).""" - if not any( - c.coordinator_task_name or c.gripper_hardware_id for _, c, _ in self._robots.values() - ): - return None - if self._coordinator_client is None: - from dimos.control.coordinator import ControlCoordinator - from dimos.core.rpc_client import RPCClient - - self._coordinator_client = RPCClient(None, ControlCoordinator) - return self._coordinator_client - - def _translate_trajectory_to_coordinator( - self, - trajectory: JointTrajectory, - robot_config: RobotModelConfig, - ) -> JointTrajectory: - """Translate trajectory joint names from URDF to coordinator namespace. - - Args: - trajectory: Trajectory with URDF joint names - robot_config: Robot config with joint name mapping - - Returns: - Trajectory with coordinator joint names - """ - if not robot_config.joint_name_mapping: - return trajectory # No translation needed - - # Translate joint names - coordinator_names = [ - robot_config.get_coordinator_joint_name(j) for j in trajectory.joint_names - ] - - # Create new trajectory with translated names - # Note: duration is computed automatically from points in JointTrajectory.__init__ - return JointTrajectory( - joint_names=coordinator_names, - points=trajectory.points, - timestamp=trajectory.timestamp, - ) - - @rpc - def execute(self, robot_name: RobotName | None = None) -> bool: - """Execute planned trajectory via ControlCoordinator.""" - if (robot := self._get_robot(robot_name)) is None: - return False - robot_name, _, config, _ = robot - - if (traj := self._planned_trajectories.get(robot_name)) is None: - logger.warning("No planned trajectory") - return False - if not config.coordinator_task_name: - logger.error(f"No coordinator_task_name for '{robot_name}'") - return False - if (client := self._get_coordinator_client()) is None: - logger.error("No coordinator client") - return False - - translated = self._translate_trajectory_to_coordinator(traj, config) - logger.info( - f"Executing: task='{config.coordinator_task_name}', {len(translated.points)} pts, {translated.duration:.2f}s" - ) - - self._state = ManipulationState.EXECUTING - result = client.task_invoke( - config.coordinator_task_name, "execute", {"trajectory": translated} - ) - if result: - logger.info("Trajectory accepted") - self._state = ManipulationState.COMPLETED - return True - else: - return self._fail("Coordinator rejected trajectory") - - @rpc - def get_trajectory_status(self, robot_name: RobotName | None = None) -> dict[str, Any] | None: - """Get trajectory execution status via coordinator task_invoke.""" - if (robot := self._get_robot(robot_name)) is None: - return None - _, _, config, _ = robot - if not config.coordinator_task_name or (client := self._get_coordinator_client()) is None: - return None - try: - state = client.task_invoke(config.coordinator_task_name, "get_state", {}) - if state is not None: - return {"state": int(state), "task": config.coordinator_task_name} - return None - except Exception: - return None - - def _get_graspgen(self) -> DockerRunner: - """Get or create GraspGen Docker module (lazy init, thread-safe).""" - # Fast path: already initialized (no lock needed for read) - if self._graspgen is not None: - return self._graspgen - - # Slow path: need to initialize (acquire lock to prevent race condition) - with self._lock: - # Double-check: another thread may have initialized while we waited for lock - if self._graspgen is not None: - return self._graspgen - - # Ensure GraspGen model checkpoints are pulled from LFS - get_data("models_graspgen") - - docker_file = ( - DIMOS_PROJECT_ROOT - / "dimos" - / "manipulation" - / "grasping" - / "docker_context" - / "Dockerfile" - ) - - # Auto-mount host directory for visualization output when enabled. - docker_volumes: list[tuple[str, str, str]] = [] - if self.config.graspgen_save_visualization_data: - host_dir = self.config.graspgen_visualization_output_path.parent - host_dir.mkdir(parents=True, exist_ok=True) - docker_volumes.append((str(host_dir), _GRASPGEN_VIZ_CONTAINER_DIR, "rw")) - - graspgen = DockerRunner( - GraspGenModule, # type: ignore[arg-type] - docker_file=docker_file, - docker_build_context=DIMOS_PROJECT_ROOT, - docker_image=self.config.graspgen_docker_image, - docker_env={"CI": "1"}, # skip interactive system config prompt in container - docker_volumes=docker_volumes, - gripper_type=self.config.graspgen_gripper_type, - num_grasps=self.config.graspgen_num_grasps, - topk_num_grasps=self.config.graspgen_topk_num_grasps, - grasp_threshold=self.config.graspgen_grasp_threshold, - filter_collisions=self.config.graspgen_filter_collisions, - save_visualization_data=self.config.graspgen_save_visualization_data, - visualization_output_path=_GRASPGEN_VIZ_CONTAINER_PATH, - ) - graspgen.start() - self._graspgen = graspgen # cache only after successful start - return self._graspgen - - @rpc - def generate_grasps( - self, - pointcloud: PointCloud2, - scene_pointcloud: PointCloud2 | None = None, - ) -> PoseArray | None: - """Generate grasp poses for the given point cloud via GraspGen Docker module.""" - try: - graspgen = self._get_graspgen() - return graspgen.generate_grasps(pointcloud, scene_pointcloud) # type: ignore[no-any-return] - except Exception as e: - logger.error(f"Grasp generation failed: {e}") - return None - - @property - def world_monitor(self) -> WorldMonitor | None: - """Access the world monitor for advanced obstacle/world operations.""" - return self._world_monitor - - @rpc - def add_obstacle( - self, - name: str, - pose: Pose, - shape: str, - dimensions: list[float] | None = None, - mesh_path: str | None = None, - ) -> str: - """Add obstacle: shape='box'|'sphere'|'cylinder'|'mesh'. Returns obstacle_id.""" - if not self._world_monitor: - return "" - - # Map shape string to ObstacleType - shape_map = { - "box": ObstacleType.BOX, - "sphere": ObstacleType.SPHERE, - "cylinder": ObstacleType.CYLINDER, - "mesh": ObstacleType.MESH, - } - obstacle_type = shape_map.get(shape) - if obstacle_type is None: - logger.warning(f"Unknown obstacle shape: {shape}") - return "" - - # Validate mesh_path for mesh type - if obstacle_type == ObstacleType.MESH and not mesh_path: - logger.warning("mesh_path required for mesh obstacles") - return "" - - # Import PoseStamped here to avoid circular imports - from dimos.msgs.geometry_msgs import PoseStamped - - obstacle = Obstacle( - name=name, - obstacle_type=obstacle_type, - pose=PoseStamped(position=pose.position, orientation=pose.orientation), - dimensions=tuple(dimensions) if dimensions else (), - mesh_path=mesh_path, - ) - return self._world_monitor.add_obstacle(obstacle) - - @rpc - def remove_obstacle(self, obstacle_id: str) -> bool: - """Remove an obstacle from the planning world.""" - if self._world_monitor is None: - return False - return self._world_monitor.remove_obstacle(obstacle_id) - - # ========================================================================= - # Perception RPC Methods - # ========================================================================= - - @rpc - def refresh_obstacles(self, min_duration: float = 0.0) -> list[dict[str, Any]]: - """Refresh perception obstacles. Returns the list of obstacles added. - - Also snapshots the current detections so pick/place can use stable labels. - """ - if self._world_monitor is None: - return [] - result = self._world_monitor.refresh_obstacles(min_duration) - # Snapshot detections at refresh time — the live cache is volatile - self._detection_snapshot = self._world_monitor.get_cached_objects() - logger.info(f"Detection snapshot: {[d.name for d in self._detection_snapshot]}") - return result - - @rpc - def clear_perception_obstacles(self) -> int: - """Remove all perception obstacles. Returns count removed.""" - if self._world_monitor is None: - return 0 - return self._world_monitor.clear_perception_obstacles() - - @rpc - def get_perception_status(self) -> dict[str, int]: - """Get perception obstacle status (cached/added counts).""" - if self._world_monitor is None: - return {"cached": 0, "added": 0} - return self._world_monitor.get_perception_status() - - @rpc - def list_cached_detections(self) -> list[dict[str, Any]]: - """List cached detections from perception.""" - if self._world_monitor is None: - return [] - return self._world_monitor.list_cached_detections() - - @rpc - def list_added_obstacles(self) -> list[dict[str, Any]]: - """List perception obstacles currently in the planning world.""" - if self._world_monitor is None: - return [] - return self._world_monitor.list_added_obstacles() - - # ========================================================================= - # Gripper Methods - # ========================================================================= - - def _get_gripper_hardware_id(self, robot_name: RobotName | None = None) -> str | None: - """Get gripper hardware ID for a robot.""" - robot = self._get_robot(robot_name) - if robot is None: - return None - _, _, config, _ = robot - if not config.gripper_hardware_id: - logger.warning(f"No gripper_hardware_id configured for '{config.name}'") - return None - return str(config.gripper_hardware_id) - - def _set_gripper_position(self, position: float, robot_name: RobotName | None = None) -> bool: - """Internal: set gripper position in meters.""" - hw_id = self._get_gripper_hardware_id(robot_name) - if hw_id is None: - return False - client = self._get_coordinator_client() - if client is None: - logger.error("No coordinator client for gripper control") - return False - return bool(client.set_gripper_position(hw_id, position)) - - @rpc - def get_gripper(self, robot_name: RobotName | None = None) -> float | None: - """Get gripper position in meters. - - Args: - robot_name: Robot to query (required if multiple robots configured) - """ - hw_id = self._get_gripper_hardware_id(robot_name) - if hw_id is None: - return None - client = self._get_coordinator_client() - if client is None: - return None - result = client.get_gripper_position(hw_id) - return float(result) if result is not None else None - - @skill - def set_gripper(self, position: float, robot_name: str | None = None) -> str: - """Set gripper to a specific opening in meters. - - Args: - position: Gripper opening in meters (0.0 = closed, 0.85 = fully open). - robot_name: Robot to control (only needed for multi-arm setups). - """ - if self._set_gripper_position(position, robot_name): - return f"Gripper set to {position:.3f}m" - return "Error: Failed to set gripper position" - - @skill - def open_gripper(self, robot_name: str | None = None) -> str: - """Open the robot gripper fully. - - Args: - robot_name: Robot to control (only needed for multi-arm setups). - """ - if self._set_gripper_position(0.85, robot_name): - return "Gripper opened" - return "Error: Failed to open gripper" - - @skill - def close_gripper(self, robot_name: str | None = None) -> str: - """Close the robot gripper fully. - - Args: - robot_name: Robot to control (only needed for multi-arm setups). - """ - if self._set_gripper_position(0.0, robot_name): - return "Gripper closed" - return "Error: Failed to close gripper" - - # ========================================================================= - # Skill Helpers (internal) - # ========================================================================= - - def _wait_for_trajectory_completion( - self, robot_name: RobotName | None = None, timeout: float = 60.0, poll_interval: float = 0.2 - ) -> bool: - """Wait for trajectory execution to complete. - - Polls the coordinator task state via task_invoke. Falls back to waiting - for the trajectory duration if the coordinator is unavailable. - - Args: - robot_name: Robot to monitor - timeout: Maximum wait time in seconds - poll_interval: Time between status checks - - Returns: - True if trajectory completed successfully - """ - robot = self._get_robot(robot_name) - if robot is None: - return True - rname, _, config, _ = robot - client = self._get_coordinator_client() - - if client is None or not config.coordinator_task_name: - # No coordinator — wait for trajectory duration as fallback - traj = self._planned_trajectories.get(rname) - if traj is not None: - logger.info(f"No coordinator status — waiting {traj.duration:.1f}s for trajectory") - time.sleep(traj.duration + 0.5) - return True - - # Poll task state via task_invoke - start = time.time() - while (time.time() - start) < timeout: - try: - state = client.task_invoke(config.coordinator_task_name, "get_state", {}) - # TrajectoryState is an IntEnum: IDLE=0, EXECUTING=1, COMPLETED=2, ABORTED=3, FAULT=4 - if state is not None: - state_val = int(state) - if state_val in (0, 2): # IDLE or COMPLETED - return True - if state_val in (3, 4): # ABORTED or FAULT - logger.warning(f"Trajectory failed: state={state}") - return False - # state_val == 1 means EXECUTING, keep polling - else: - # task_invoke returned None — task not found, assume done - return True - except Exception: - # Fallback: wait for trajectory duration - traj = self._planned_trajectories.get(rname) - if traj is not None: - remaining = traj.duration - (time.time() - start) - if remaining > 0: - logger.info(f"Status poll failed — waiting {remaining:.1f}s for trajectory") - time.sleep(remaining + 0.5) - return True - time.sleep(poll_interval) - - logger.warning(f"Trajectory execution timed out after {timeout}s") - return False - - def _preview_execute_wait( - self, robot_name: RobotName | None = None, preview_duration: float = 0.5 - ) -> str | None: - """Preview planned path, execute, and wait for completion. - - Returns None on success, or an error string on failure. - - Args: - robot_name: Robot to operate on - preview_duration: Duration to animate the preview in Meshcat (seconds) - """ - logger.info("Previewing trajectory...") - self.preview_path(preview_duration, robot_name) - - logger.info("Executing trajectory...") - if not self.execute(robot_name): - return "Error: Trajectory execution failed" - - if not self._wait_for_trajectory_completion(robot_name): - return "Error: Trajectory execution timed out" - - return None - - def _compute_pre_grasp_pose(self, grasp_pose: Pose, offset: float = 0.10) -> Pose: - """Compute a pre-grasp pose offset along the approach direction (local -Z). - - Args: - grasp_pose: The final grasp pose - offset: Distance to retract along the approach direction (meters) - - Returns: - Pre-grasp pose offset from the grasp pose - """ - from dimos.utils.transform_utils import offset_distance - - return offset_distance(grasp_pose, offset) - - def _find_object_in_detections( - self, object_name: str, object_id: str | None = None - ) -> DetObject | None: - """Find an object in the detection snapshot by name or ID. - - Uses the snapshot taken during the last scan_objects/refresh call, - not the volatile live cache (which changes labels every frame). - - Args: - object_name: Name/label to search for - object_id: Optional specific object ID - - Returns: - Matching DetObject, or None - """ - if not self._detection_snapshot: - logger.warning("No detection snapshot — call scan_objects() first") - return None - - for det in self._detection_snapshot: - if object_id and det.object_id == object_id: - return det - if object_name.lower() in det.name.lower() or det.name.lower() in object_name.lower(): - return det - - available = [det.name for det in self._detection_snapshot] - logger.warning(f"Object '{object_name}' not found in snapshot. Available: {available}") - return None - - def _generate_grasps_for_pick( - self, object_name: str, object_id: str | None = None - ) -> list[Pose] | None: - """Generate grasp poses for an object. - - Computes a top-down approach grasp from the object's detected position. - - Args: - object_name: Name of the object - object_id: Optional object ID - - Returns: - List of grasp poses (best first), or None if object not found - """ - det = self._find_object_in_detections(object_name, object_id) - if det is None: - logger.warning(f"Object '{object_name}' not found in detections") - return None - - c = det.center - grasp_pose = Pose(Vector3(c.x, c.y, c.z), Quaternion.from_euler(Vector3(0.0, math.pi, 0.0))) - logger.info(f"Heuristic grasp for '{object_name}' at ({c.x:.3f}, {c.y:.3f}, {c.z:.3f})") - return [grasp_pose] - - # ========================================================================= - # Short-Horizon Skills — Single-step actions - # ========================================================================= - - @skill - def move_to_pose( - self, - x: float, - y: float, - z: float, - roll: float = 0.0, - pitch: float = 0.0, - yaw: float = 0.0, - robot_name: str | None = None, - ) -> str: - """Move the robot end-effector to a target pose. - - Plans a collision-free trajectory and executes it. - - Args: - x: Target X position in meters. - y: Target Y position in meters. - z: Target Z position in meters. - roll: Target roll in radians (default 0). - pitch: Target pitch in radians (default 0). - yaw: Target yaw in radians (default 0). - robot_name: Robot to move (only needed for multi-arm setups). - """ - logger.info(f"Planning motion to ({x:.3f}, {y:.3f}, {z:.3f})...") - pose = Pose(Vector3(x, y, z), Quaternion.from_euler(Vector3(roll, pitch, yaw))) - - if not self.plan_to_pose(pose, robot_name): - return f"Error: Planning failed — pose ({x:.3f}, {y:.3f}, {z:.3f}) may be unreachable or in collision" - - err = self._preview_execute_wait(robot_name) - if err: - return err - - return f"Reached target pose ({x:.3f}, {y:.3f}, {z:.3f})" - - @skill - def move_to_joints( - self, - joints: str, - robot_name: str | None = None, - ) -> str: - """Move the robot to a target joint configuration. - - Plans a collision-free trajectory and executes it. - - Args: - joints: Comma-separated joint positions in radians, e.g. "0.1, -0.5, 1.2, 0.0, 0.3, -0.1". - robot_name: Robot to move (only needed for multi-arm setups). - """ - try: - joint_values = [float(j.strip()) for j in joints.split(",")] - except ValueError: - return f"Error: Invalid joints format '{joints}'. Expected comma-separated floats." - - robot = self._get_robot(robot_name) - if robot is None: - return "Error: Robot not found" - rname, _, config, _ = robot - goal = JointState(name=config.joint_names, position=joint_values) - - logger.info(f"Planning motion to joints [{', '.join(f'{j:.3f}' for j in joint_values)}]...") - if not self.plan_to_joints(goal, rname): - return "Error: Planning failed — joint configuration may be unreachable or in collision" - - err = self._preview_execute_wait(robot_name) - if err: - return err - - return "Reached target joint configuration" - - @skill - def get_scene_info(self, robot_name: str | None = None) -> str: - """Get current robot state, detected objects, and scene information. - - Returns a summary of the robot's joint positions, end-effector pose, - gripper state, detected objects, and obstacle count. - - Args: - robot_name: Robot to query (only needed for multi-arm setups). - """ - lines: list[str] = [] - - # Robot state - joints = self.get_current_joints(robot_name) - if joints is not None: - lines.append(f"Joints: [{', '.join(f'{j:.3f}' for j in joints)}]") - else: - lines.append("Joints: unavailable (no state received)") - - ee_pose = self.get_ee_pose(robot_name) - if ee_pose is not None: - p = ee_pose.position - lines.append(f"EE pose: ({p.x:.4f}, {p.y:.4f}, {p.z:.4f})") - else: - lines.append("EE pose: unavailable") - - # Gripper - gripper_pos = self.get_gripper(robot_name) - if gripper_pos is not None: - lines.append(f"Gripper: {gripper_pos:.3f}m") - else: - lines.append("Gripper: not configured") - - # Perception - perception = self.get_perception_status() - lines.append( - f"Perception: {perception.get('cached', 0)} cached, {perception.get('added', 0)} obstacles added" - ) - - detections = self._detection_snapshot - if detections: - lines.append(f"Detected objects ({len(detections)}):") - for det in detections: - c = det.center - lines.append(f" - {det.name}: ({c.x:.3f}, {c.y:.3f}, {c.z:.3f})") - else: - lines.append("Detected objects: none") - - # Visualization - url = self.get_visualization_url() - if url: - lines.append(f"Visualization: {url}") - - # State - lines.append(f"State: {self.get_state()}") - - return "\n".join(lines) - - @skill - def scan_objects(self, min_duration: float = 1.0, robot_name: str | None = None) -> str: - """Scan the scene and list detected objects with their 3D positions. - - Refreshes perception obstacles from the latest sensor data and returns - a formatted list of all detected objects. - - Args: - min_duration: Minimum time in seconds to wait for stable detections. - robot_name: Robot context (only needed for multi-arm setups). - """ - obstacles = self.refresh_obstacles(min_duration) - - detections = self._detection_snapshot - if not detections: - return "No objects detected in scene" - - lines = [f"Detected {len(detections)} object(s):"] - for det in detections: - c = det.center - lines.append(f" - {det.name}: ({c.x:.3f}, {c.y:.3f}, {c.z:.3f})") - - if obstacles: - lines.append(f"\n{len(obstacles)} obstacle(s) added to planning world") - - return "\n".join(lines) - - # ========================================================================= - # Long-Horizon Skills — Multi-step composed behaviors - # ========================================================================= - - @skill - def go_home(self, robot_name: str | None = None) -> str: - """Move the robot to its home/observe joint configuration. - - Opens the gripper and moves to the predefined home position. - - Args: - robot_name: Robot to move (only needed for multi-arm setups). - """ - robot = self._get_robot(robot_name) - if robot is None: - return "Error: Robot not found" - rname, _, config, _ = robot - - if config.home_joints is None: - return "Error: No home_joints configured for this robot" - - logger.info("Opening gripper...") - self._set_gripper_position(0.85, rname) - time.sleep(0.5) - - goal = JointState(name=config.joint_names, position=config.home_joints) - logger.info("Planning motion to home position...") - if not self.plan_to_joints(goal, rname): - return "Error: Failed to plan path to home position" - - err = self._preview_execute_wait(robot_name) - if err: - return err - - return "Reached home position" - - @skill - def go_init(self, robot_name: str | None = None) -> str: - """Move the robot to its init position (captured at startup or set manually). - - The init position is the joint configuration the robot was in when the - module first received joint state. It can be changed with set_init_joints(). - - Args: - robot_name: Robot to move (only needed for multi-arm setups). - """ - if self._init_joints is None: - return "Error: No init joints captured — robot may not have reported joint state yet" - - logger.info( - f"Planning motion to init position [{', '.join(f'{j:.3f}' for j in self._init_joints.position)}]..." - ) - if not self.plan_to_joints(self._init_joints, robot_name): - return "Error: Failed to plan path to init position" - - err = self._preview_execute_wait(robot_name) - if err: - return err - - return "Reached init position" - - @skill - def pick( - self, - object_name: str, - object_id: str | None = None, - robot_name: str | None = None, - ) -> str: - """Pick up an object by name using grasp planning and motion execution. - - Generates grasp poses, plans collision-free approach/grasp/retract motions, - and executes them. - - Args: - object_name: Name of the object to pick (e.g. "cup", "bottle", "can"). - object_id: Optional unique object ID from perception for precise identification. - robot_name: Robot to use (only needed for multi-arm setups). - """ - robot = self._get_robot(robot_name) - if robot is None: - return "Error: Robot not found" - rname, _, config, _ = robot - pre_grasp_offset = config.pre_grasp_offset - - # 1. Generate grasps (uses already-cached detections — call scan_objects first) - logger.info(f"Generating grasp poses for '{object_name}'...") - grasp_poses = self._generate_grasps_for_pick(object_name, object_id) - if not grasp_poses: - return f"Error: No grasp poses found for '{object_name}'. Object may not be detected." - - # 2. Try each grasp candidate - max_attempts = min(len(grasp_poses), 5) - for i, grasp_pose in enumerate(grasp_poses[:max_attempts]): - pre_grasp_pose = self._compute_pre_grasp_pose(grasp_pose, pre_grasp_offset) - - logger.info(f"Planning approach to pre-grasp (attempt {i + 1}/{max_attempts})...") - if not self.plan_to_pose(pre_grasp_pose, rname): - logger.info(f"Grasp candidate {i + 1} approach planning failed, trying next") - continue # Try next candidate - - # Open gripper before approach - logger.info("Opening gripper...") - self._set_gripper_position(0.85, rname) - time.sleep(0.5) - - # 3. Preview + execute approach - err = self._preview_execute_wait(rname) - if err: - return err - - # 4. Move to grasp pose - logger.info("Moving to grasp position...") - if not self.plan_to_pose(grasp_pose, rname): - return "Error: Grasp pose planning failed" - err = self._preview_execute_wait(rname) - if err: - return err - - # 5. Close gripper - logger.info("Closing gripper...") - self._set_gripper_position(0.0, rname) - time.sleep(1.5) # Wait for gripper to close - - # 6. Retract to pre-grasp - logger.info("Retracting with object...") - if not self.plan_to_pose(pre_grasp_pose, rname): - return "Error: Retract planning failed" - err = self._preview_execute_wait(rname) - if err: - return err - - # Store pick position so place_back() can return the object - self._last_pick_position = grasp_pose.position - - return f"Pick complete — grasped '{object_name}' successfully" - - return f"Error: All {max_attempts} grasp attempts failed for '{object_name}'" - - @skill - def place( - self, - x: float, - y: float, - z: float, - robot_name: str | None = None, - ) -> str: - """Place a held object at the specified position. - - Plans and executes an approach, lowers to the target, releases the gripper, - and retracts. - - Args: - x: Target X position in meters. - y: Target Y position in meters. - z: Target Z position in meters. - robot_name: Robot to use (only needed for multi-arm setups). - """ - robot = self._get_robot(robot_name) - if robot is None: - return "Error: Robot not found" - rname, _, config, _ = robot - pre_place_offset = config.pre_grasp_offset - - # Compute place pose (top-down approach) - place_pose = Pose(Vector3(x, y, z), Quaternion.from_euler(Vector3(0.0, math.pi, 0.0))) - pre_place_pose = self._compute_pre_grasp_pose(place_pose, pre_place_offset) - - # 1. Move to pre-place - logger.info(f"Planning approach to place position ({x:.3f}, {y:.3f}, {z:.3f})...") - if not self.plan_to_pose(pre_place_pose, rname): - return "Error: Pre-place approach planning failed" - - err = self._preview_execute_wait(rname) - if err: - return err - - # 2. Lower to place position - logger.info("Lowering to place position...") - if not self.plan_to_pose(place_pose, rname): - return "Error: Place pose planning failed" - err = self._preview_execute_wait(rname) - if err: - return err - - # 3. Release - logger.info("Releasing object...") - self._set_gripper_position(0.85, rname) - time.sleep(1.0) - - # 4. Retract - logger.info("Retracting...") - if not self.plan_to_pose(pre_place_pose, rname): - return "Error: Retract planning failed" - err = self._preview_execute_wait(rname) - if err: - return err - - return f"Place complete — object released at ({x:.3f}, {y:.3f}, {z:.3f})" - - @skill - def place_back(self, robot_name: str | None = None) -> str: - """Place the held object back at its original pick position. - - Uses the position stored from the last successful pick operation. - - Args: - robot_name: Robot to use (only needed for multi-arm setups). - """ - if self._last_pick_position is None: - return "Error: No previous pick position stored — run pick() first" - - p = self._last_pick_position - logger.info(f"Placing back at original position ({p.x:.3f}, {p.y:.3f}, {p.z:.3f})...") - return self.place(p.x, p.y, p.z, robot_name) - - @skill - def pick_and_place( - self, - object_name: str, - place_x: float, - place_y: float, - place_z: float, - object_id: str | None = None, - robot_name: str | None = None, - ) -> str: - """Pick up an object and place it at a target location. - - Combines the pick and place skills into a single end-to-end operation. - - Args: - object_name: Name of the object to pick (e.g. "cup", "bottle"). - place_x: Target X position to place the object (meters). - place_y: Target Y position to place the object (meters). - place_z: Target Z position to place the object (meters). - object_id: Optional unique object ID from perception. - robot_name: Robot to use (only needed for multi-arm setups). - """ - logger.info( - f"Starting pick and place: pick '{object_name}' → place at ({place_x:.3f}, {place_y:.3f}, {place_z:.3f})" - ) - - # Pick phase - result = self.pick(object_name, object_id, robot_name) - if result.startswith("Error:"): - return result - - # Place phase - return self.place(place_x, place_y, place_z, robot_name) - - # ========================================================================= - # Lifecycle - # ========================================================================= - - @rpc - def stop(self) -> None: - """Stop the manipulation module.""" - logger.info("Stopping ManipulationModule") - - # Stop GraspGen Docker container (thread-safe access to shared state) - with self._lock: - if self._graspgen is not None: - self._graspgen.stop() - self._graspgen = None - - # Stop TF thread - if self._tf_thread is not None: - self._tf_stop_event.set() - self._tf_thread.join(timeout=1.0) - self._tf_thread = None - - # Stop world monitor (includes visualization thread) - if self._world_monitor is not None: - self._world_monitor.stop_all_monitors() - - super().stop() - - -# Expose blueprint for declarative composition -manipulation_module = ManipulationModule.blueprint diff --git a/dimos/manipulation/planning/README.md b/dimos/manipulation/planning/README.md deleted file mode 100644 index 803d8b166e..0000000000 --- a/dimos/manipulation/planning/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Manipulation Planning Stack - -Motion planning for robotic manipulators. Backend-agnostic design with Drake implementation. - -## Quick Start - -```bash -# Terminal 1: Mock coordinator -dimos run coordinator-mock - -# Terminal 2: Manipulation planner -dimos run xarm7-planner-coordinator - -# Terminal 3: IPython client -python -m dimos.manipulation.planning.examples.manipulation_client -``` - -In IPython: -```python -joints() # Get current joints -plan([0.1] * 7) # Plan to target -preview() # Preview in Meshcat (url() for link) -execute() # Execute via coordinator -``` - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ ManipulationModule │ -│ (RPC interface, state machine, multi-robot) │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ Backend-Agnostic Components │ -│ ┌──────────────────┐ ┌─────────────────────────────┐ │ -│ │ RRTConnectPlanner│ │ JacobianIK │ │ -│ │ (rrt_planner.py) │ │ (iterative & differential) │ │ -│ └──────────────────┘ └─────────────────────────────┘ │ -│ Uses only WorldSpec interface │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ WorldSpec Protocol │ -│ Context management, collision checking, FK, Jacobian │ -└─────────────────────────────────────────────────────────────┘ - │ -┌─────────────────────────────────────────────────────────────┐ -│ Backend-Specific Implementations │ -│ ┌──────────────────┐ ┌─────────────────────────────┐ │ -│ │ DrakeWorld │ │ DrakeOptimizationIK │ │ -│ │ (physics/viz) │ │ (nonlinear IK) │ │ -│ └──────────────────┘ └─────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Using ManipulationModule - -```python -from pathlib import Path -from dimos.manipulation import ManipulationModule -from dimos.manipulation.planning.spec import RobotModelConfig - -config = RobotModelConfig( - name="xarm7", - urdf_path=Path("/path/to/xarm7.urdf"), - base_pose=PoseStamped(position=Vector3(), orientation=Quaternion()), - joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6", "joint7"], - end_effector_link="link7", - base_link="link_base", - joint_name_mapping={"arm_joint1": "joint1", ...}, # coordinator <-> URDF - coordinator_task_name="traj_arm", -) - -module = ManipulationModule( - robots=[config], - planning_timeout=10.0, - enable_viz=True, - planner_name="rrt_connect", # Only option - kinematics_name="drake_optimization", # Or "jacobian" -) -module.start() -module.plan_to_joints([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7]) -module.execute() # Sends to coordinator -``` - -## RobotModelConfig Fields - -| Field | Description | -|-------|-------------| -| `name` | Robot identifier | -| `urdf_path` | Path to URDF/XACRO file | -| `base_pose` | PoseStamped for robot base in world frame | -| `joint_names` | Joint names in URDF | -| `end_effector_link` | EE link name | -| `base_link` | Base link name | -| `max_velocity` | Max joint velocity (rad/s) | -| `max_acceleration` | Max acceleration (rad/s²) | -| `joint_name_mapping` | Coordinator → URDF name mapping | -| `coordinator_task_name` | Task name for execution RPC | -| `package_paths` | ROS package paths for meshes | -| `xacro_args` | Xacro arguments (e.g., `{"dof": "7"}`) | - -## Components - -### Planners (Backend-Agnostic) - -| Planner | Description | -|---------|-------------| -| `RRTConnectPlanner` | Bi-directional RRT-Connect (fast, reliable) | - -### IK Solvers - -| Solver | Type | Description | -|--------|------|-------------| -| `JacobianIK` | Backend-agnostic | Iterative damped least-squares | -| `DrakeOptimizationIK` | Drake-specific | Full nonlinear optimization | - -### World Backends - -| Backend | Description | -|---------|-------------| -| `DrakeWorld` | Drake physics with Meshcat visualization | - -## Blueprints - -| Blueprint | Description | -|-----------|-------------| -| `xarm6_planner_only` | XArm 6-DOF standalone (no coordinator) | -| `xarm7-planner-coordinator` | XArm 7-DOF with coordinator | -| `dual-xarm6-planner` | Dual XArm 6-DOF | - -## Directory Structure - -``` -planning/ -├── spec.py # Protocols (WorldSpec, KinematicsSpec, PlannerSpec) -├── factory.py # create_world, create_kinematics, create_planner -├── world/ -│ └── drake_world.py # DrakeWorld implementation -├── kinematics/ -│ ├── jacobian_ik.py # Backend-agnostic Jacobian IK -│ └── drake_optimization_ik.py # Drake nonlinear IK -├── planners/ -│ └── rrt_planner.py # RRTConnectPlanner -├── monitor/ # WorldMonitor (live state sync) -├── trajectory_generator/ # Time-parameterized trajectories -└── examples/ - ├── planning_tester.py # Standalone CLI tester - └── manipulation_client.py # IPython RPC client -``` - -## Obstacle Types - -| Type | Dimensions | -|------|------------| -| `BOX` | (width, height, depth) | -| `SPHERE` | (radius,) | -| `CYLINDER` | (radius, height) | -| `MESH` | mesh_path | - -## Supported Robots - -| Robot | DOF | -|-------|-----| -| `piper` | 6 | -| `xarm6` | 6 | -| `xarm7` | 7 | - -## Testing - -```bash -# Unit tests (fast, no Drake) -pytest dimos/manipulation/test_manipulation_unit.py -v - -# Integration tests (requires Drake) -pytest dimos/e2e_tests/test_manipulation_module.py -v -``` diff --git a/dimos/manipulation/planning/__init__.py b/dimos/manipulation/planning/__init__.py deleted file mode 100644 index 8aaf0caa25..0000000000 --- a/dimos/manipulation/planning/__init__.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Manipulation Planning Module - -Motion planning stack for robotic manipulators using Protocol-based architecture. - -## Architecture - -- WorldSpec: Core backend owning physics/collision (DrakeWorld, future: MuJoCoWorld) -- KinematicsSpec: IK solvers - - JacobianIK: Backend-agnostic iterative/differential IK - - DrakeOptimizationIK: Drake-specific nonlinear optimization IK -- PlannerSpec: Backend-agnostic joint-space path planning - - RRTConnectPlanner: Bi-directional RRT-Connect - - RRTStarPlanner: RRT* (asymptotically optimal) - -## Factory Functions - -Use factory functions to create components: - -```python -from dimos.manipulation.planning.factory import ( - create_world, - create_kinematics, - create_planner, -) - -world = create_world(backend="drake", enable_viz=True) -kinematics = create_kinematics(name="jacobian") # or "drake_optimization" -planner = create_planner(name="rrt_connect") # backend-agnostic -``` - -## Monitors - -Use WorldMonitor for reactive state synchronization: - -```python -from dimos.manipulation.planning.monitor import WorldMonitor - -monitor = WorldMonitor(enable_viz=True) -robot_id = monitor.add_robot(config) -monitor.finalize() -monitor.start_state_monitor(robot_id) -``` -""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "factory": ["create_kinematics", "create_planner", "create_planning_stack", "create_world"], - "spec": [ - "CollisionObjectMessage", - "IKResult", - "IKStatus", - "JointPath", - "KinematicsSpec", - "Obstacle", - "ObstacleType", - "PlannerSpec", - "PlanningResult", - "PlanningStatus", - "RobotModelConfig", - "RobotName", - "WorldRobotID", - "WorldSpec", - ], - "trajectory_generator.joint_trajectory_generator": ["JointTrajectoryGenerator"], - }, -) diff --git a/dimos/manipulation/planning/examples/__init__.py b/dimos/manipulation/planning/examples/__init__.py deleted file mode 100644 index 7971835dab..0000000000 --- a/dimos/manipulation/planning/examples/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Manipulation planning examples. -""" diff --git a/dimos/manipulation/planning/factory.py b/dimos/manipulation/planning/factory.py deleted file mode 100644 index d392bac563..0000000000 --- a/dimos/manipulation/planning/factory.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Factory functions for manipulation planning components.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from dimos.manipulation.planning.spec import ( - KinematicsSpec, - PlannerSpec, - WorldSpec, - ) - - -def create_world( - backend: str = "drake", - enable_viz: bool = False, - **kwargs: Any, -) -> WorldSpec: - """Create a world instance. backend='drake', enable_viz for Meshcat.""" - if backend == "drake": - from dimos.manipulation.planning.world.drake_world import DrakeWorld - - return DrakeWorld(enable_viz=enable_viz, **kwargs) - else: - raise ValueError(f"Unknown backend: {backend}. Available: ['drake']") - - -def create_kinematics( - name: str = "jacobian", - **kwargs: Any, -) -> KinematicsSpec: - """Create IK solver. name='jacobian'|'drake_optimization'.""" - if name == "jacobian": - from dimos.manipulation.planning.kinematics.jacobian_ik import JacobianIK - - return JacobianIK(**kwargs) - elif name == "drake_optimization": - from dimos.manipulation.planning.kinematics.drake_optimization_ik import ( - DrakeOptimizationIK, - ) - - return DrakeOptimizationIK(**kwargs) - else: - raise ValueError( - f"Unknown kinematics solver: {name}. Available: ['jacobian', 'drake_optimization']" - ) - - -def create_planner( - name: str = "rrt_connect", - **kwargs: Any, -) -> PlannerSpec: - """Create motion planner. name='rrt_connect'.""" - if name == "rrt_connect": - from dimos.manipulation.planning.planners.rrt_planner import RRTConnectPlanner - - return RRTConnectPlanner(**kwargs) - else: - raise ValueError(f"Unknown planner: {name}. Available: ['rrt_connect']") - - -def create_planning_stack( - robot_config: Any, - enable_viz: bool = False, - planner_name: str = "rrt_connect", - kinematics_name: str = "jacobian", -) -> tuple[WorldSpec, KinematicsSpec, PlannerSpec, str]: - """Create complete planning stack. Returns (world, kinematics, planner, robot_id).""" - world = create_world(backend="drake", enable_viz=enable_viz) - kinematics = create_kinematics(name=kinematics_name) - planner = create_planner(name=planner_name) - - robot_id = world.add_robot(robot_config) - world.finalize() - - return world, kinematics, planner, robot_id diff --git a/dimos/manipulation/planning/kinematics/__init__.py b/dimos/manipulation/planning/kinematics/__init__.py deleted file mode 100644 index dacd2007cb..0000000000 --- a/dimos/manipulation/planning/kinematics/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Kinematics Module - -Contains IK solver implementations that use WorldSpec. - -## Implementations - -- JacobianIK: Backend-agnostic iterative/differential IK (works with any WorldSpec) -- DrakeOptimizationIK: Drake-specific nonlinear optimization IK (requires DrakeWorld) - -## Usage - -Use factory functions to create IK solvers: - -```python -from dimos.manipulation.planning.factory import create_kinematics - -# Backend-agnostic (works with any WorldSpec) -kinematics = create_kinematics(name="jacobian") - -# Drake-specific (requires DrakeWorld, more accurate) -kinematics = create_kinematics(name="drake_optimization") - -result = kinematics.solve(world, robot_id, target_pose) -``` -""" - -from dimos.manipulation.planning.kinematics.drake_optimization_ik import ( - DrakeOptimizationIK, -) -from dimos.manipulation.planning.kinematics.jacobian_ik import JacobianIK -from dimos.manipulation.planning.kinematics.pinocchio_ik import ( - PinocchioIK, - PinocchioIKConfig, -) - -__all__ = ["DrakeOptimizationIK", "JacobianIK", "PinocchioIK", "PinocchioIKConfig"] diff --git a/dimos/manipulation/planning/kinematics/drake_optimization_ik.py b/dimos/manipulation/planning/kinematics/drake_optimization_ik.py deleted file mode 100644 index 1e6b1962a5..0000000000 --- a/dimos/manipulation/planning/kinematics/drake_optimization_ik.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Drake optimization-based IK using SNOPT/IPOPT. Requires DrakeWorld.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -from dimos.manipulation.planning.spec import IKResult, IKStatus, WorldRobotID, WorldSpec -from dimos.manipulation.planning.utils.kinematics_utils import compute_pose_error -from dimos.msgs.geometry_msgs import PoseStamped, Transform -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import pose_to_matrix - -if TYPE_CHECKING: - from numpy.typing import NDArray - -try: - from pydrake.math import RigidTransform, RotationMatrix # type: ignore[import-not-found] - from pydrake.multibody.inverse_kinematics import ( # type: ignore[import-not-found] - InverseKinematics, - ) - from pydrake.solvers import Solve # type: ignore[import-not-found] - - DRAKE_AVAILABLE = True -except ImportError: - DRAKE_AVAILABLE = False - -logger = setup_logger() - - -class DrakeOptimizationIK: - """Drake optimization-based IK solver using constrained nonlinear optimization. - - Requires DrakeWorld. For backend-agnostic IK, use JacobianIK. - """ - - def __init__(self) -> None: - if not DRAKE_AVAILABLE: - raise ImportError("Drake is not installed. Install with: pip install drake") - - def _validate_world(self, world: WorldSpec) -> IKResult | None: - from dimos.manipulation.planning.world.drake_world import DrakeWorld - - if not isinstance(world, DrakeWorld): - return _create_failure_result( - IKStatus.NO_SOLUTION, "DrakeOptimizationIK requires DrakeWorld" - ) - if not world.is_finalized: - return _create_failure_result(IKStatus.NO_SOLUTION, "World must be finalized before IK") - return None - - def solve( - self, - world: WorldSpec, - robot_id: WorldRobotID, - target_pose: PoseStamped, - seed: JointState | None = None, - position_tolerance: float = 0.001, - orientation_tolerance: float = 0.01, - check_collision: bool = True, - max_attempts: int = 10, - ) -> IKResult: - """Solve IK with multiple random restarts, returning the best collision-free solution.""" - error = self._validate_world(world) - if error is not None: - return error - - # Convert PoseStamped to 4x4 matrix via Transform - target_matrix = Transform( - translation=target_pose.position, - rotation=target_pose.orientation, - ).to_matrix() - - # Get joint limits - lower_limits, upper_limits = world.get_joint_limits(robot_id) - - # Get seed from current state if not provided - if seed is None: - with world.scratch_context() as ctx: - seed = world.get_joint_state(ctx, robot_id) - - # Extract joint names and seed positions - joint_names = seed.name - seed_positions = np.array(seed.position, dtype=np.float64) - - # Target transform - target_transform = RigidTransform(target_matrix) - - best_result: IKResult | None = None - best_error = float("inf") - - for attempt in range(max_attempts): - # Generate seed positions - if attempt == 0: - current_seed = seed_positions - else: - # Random seed within joint limits - current_seed = np.random.uniform(lower_limits, upper_limits) - - # Solve IK - result = self._solve_single( - world=world, - robot_id=robot_id, - target_transform=target_transform, - seed=current_seed, - joint_names=joint_names, - position_tolerance=position_tolerance, - orientation_tolerance=orientation_tolerance, - lower_limits=lower_limits, - upper_limits=upper_limits, - ) - - if result.is_success() and result.joint_state is not None: - # Check collision if requested - if check_collision: - if not world.check_config_collision_free(robot_id, result.joint_state): - continue # Try another seed - - # Check error - total_error = result.position_error + result.orientation_error - if total_error < best_error: - best_error = total_error - best_result = result - - # If error is within tolerance, we're done - if ( - result.position_error <= position_tolerance - and result.orientation_error <= orientation_tolerance - ): - return result - - if best_result is not None: - return best_result - - return _create_failure_result( - IKStatus.NO_SOLUTION, - f"IK failed after {max_attempts} attempts", - ) - - def _solve_single( - self, - world: WorldSpec, - robot_id: WorldRobotID, - target_transform: RigidTransform, - seed: NDArray[np.float64], - joint_names: list[str], - position_tolerance: float, - orientation_tolerance: float, - lower_limits: NDArray[np.float64], - upper_limits: NDArray[np.float64], - ) -> IKResult: - # Get robot data from world internals (Drake-specific access) - robot_data = world._robots[robot_id] # type: ignore[attr-defined] - plant = world.plant # type: ignore[attr-defined] - - # Create IK problem - ik = InverseKinematics(plant) - - # Get end-effector frame - ee_frame = robot_data.ee_frame - - # Add position constraint - ik.AddPositionConstraint( - frameB=ee_frame, - p_BQ=np.array([0.0, 0.0, 0.0]), # type: ignore[arg-type] - frameA=plant.world_frame(), - p_AQ_lower=target_transform.translation() - np.array([position_tolerance] * 3), - p_AQ_upper=target_transform.translation() + np.array([position_tolerance] * 3), - ) - - # Add orientation constraint - ik.AddOrientationConstraint( - frameAbar=plant.world_frame(), - R_AbarA=target_transform.rotation(), - frameBbar=ee_frame, - R_BbarB=RotationMatrix(), - theta_bound=orientation_tolerance, - ) - - # Get program and set initial guess - prog = ik.get_mutable_prog() - q = ik.q() - - # Set initial guess (full positions vector) - full_seed = np.zeros(plant.num_positions()) - for i, joint_idx in enumerate(robot_data.joint_indices): - full_seed[joint_idx] = seed[i] - prog.SetInitialGuess(q, full_seed) - - # Solve - result = Solve(prog) - - if not result.is_success(): - return _create_failure_result( - IKStatus.NO_SOLUTION, - f"Optimization failed: {result.get_solution_result()}", - ) - - # Extract solution for this robot's joints - full_solution = result.GetSolution(q) - joint_solution = np.array([full_solution[idx] for idx in robot_data.joint_indices]) - - # Clip to limits - joint_solution = np.clip(joint_solution, lower_limits, upper_limits) - - # Compute actual error using FK - solution_state = JointState(name=joint_names, position=joint_solution.tolist()) - with world.scratch_context() as ctx: - world.set_joint_state(ctx, robot_id, solution_state) - actual_pose = world.get_ee_pose(ctx, robot_id) - - position_error, orientation_error = compute_pose_error( - pose_to_matrix(actual_pose), - target_transform.GetAsMatrix4(), # type: ignore[arg-type] - ) - - return _create_success_result( - joint_names=joint_names, - joint_positions=joint_solution, - position_error=position_error, - orientation_error=orientation_error, - iterations=1, - ) - - -def _create_success_result( - joint_names: list[str], - joint_positions: NDArray[np.float64], - position_error: float, - orientation_error: float, - iterations: int, -) -> IKResult: - return IKResult( - status=IKStatus.SUCCESS, - joint_state=JointState(name=joint_names, position=joint_positions.tolist()), - position_error=position_error, - orientation_error=orientation_error, - iterations=iterations, - message="IK solution found", - ) - - -def _create_failure_result( - status: IKStatus, - message: str, - iterations: int = 0, -) -> IKResult: - return IKResult( - status=status, - joint_state=None, - iterations=iterations, - message=message, - ) diff --git a/dimos/manipulation/planning/kinematics/jacobian_ik.py b/dimos/manipulation/planning/kinematics/jacobian_ik.py deleted file mode 100644 index 5f80642058..0000000000 --- a/dimos/manipulation/planning/kinematics/jacobian_ik.py +++ /dev/null @@ -1,430 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Backend-agnostic Jacobian-based inverse kinematics. - -JacobianIK provides iterative and differential IK methods that work with any -WorldSpec implementation. It only uses the standard WorldSpec interface methods -(get_jacobian, get_ee_pose, get_joint_limits) and doesn't depend on any specific -physics backend. - -For full nonlinear optimization IK with Drake, use DrakeOptimizationIK. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -from dimos.manipulation.planning.spec import IKResult, IKStatus, WorldRobotID, WorldSpec -from dimos.manipulation.planning.utils.kinematics_utils import ( - check_singularity, - compute_error_twist, - compute_pose_error, - damped_pseudoinverse, -) -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import pose_to_matrix - -if TYPE_CHECKING: - from numpy.typing import NDArray - -from dimos.msgs.geometry_msgs import PoseStamped, Transform, Twist, Vector3 -from dimos.msgs.sensor_msgs import JointState - -logger = setup_logger() - - -class JacobianIK: - """Backend-agnostic Jacobian-based IK solver. - - This class provides iterative and differential IK methods using only - the standard WorldSpec interface. It works with any physics backend - (Drake, MuJoCo, PyBullet, etc.). - - Methods: - - solve_iterative(): Iterative Jacobian-based IK until convergence - - solve_differential(): Single Jacobian step for velocity control - - solve_differential_position_only(): Position-only differential IK - - solve(): Wrapper for solve_iterative with multiple random restarts - - Example: - ik = JacobianIK(damping=0.01) - result = ik.solve_iterative( - world, robot_id, - target_pose=target, - seed=current_joints, - ) - if result.is_success(): - print(f"Solution: {result.joint_positions}") - """ - - def __init__( - self, - damping: float = 0.05, - max_iterations: int = 200, - singularity_threshold: float = 1e-6, - ): - """Create Jacobian IK solver. - - Args: - damping: Damping factor for pseudoinverse (higher = more stable near singularities) - max_iterations: Default maximum iterations for iterative IK - singularity_threshold: Manipulability threshold for singularity detection - """ - self._damping = damping - self._max_iterations = max_iterations - self._singularity_threshold = singularity_threshold - - def solve( - self, - world: WorldSpec, - robot_id: WorldRobotID, - target_pose: PoseStamped, - seed: JointState | None = None, - position_tolerance: float = 0.001, - orientation_tolerance: float = 0.01, - check_collision: bool = True, - max_attempts: int = 10, - ) -> IKResult: - """Solve IK with multiple random restarts. - - Tries iterative IK from multiple starting configurations to find - a collision-free solution. - - Args: - world: World for FK/collision checking - robot_id: Robot to solve IK for - target_pose: Target end-effector pose - seed: Initial guess (uses current state if None) - position_tolerance: Required position accuracy (meters) - orientation_tolerance: Required orientation accuracy (radians) - check_collision: Whether to check collision of solution - max_attempts: Maximum random restart attempts - - Returns: - IKResult with solution or failure status - """ - if not world.is_finalized: - return _create_failure_result(IKStatus.NO_SOLUTION, "World must be finalized before IK") - - lower_limits, upper_limits = world.get_joint_limits(robot_id) - - # Get seed from current state if not provided - if seed is None: - with world.scratch_context() as ctx: - seed = world.get_joint_state(ctx, robot_id) - - # Extract joint names for creating random seeds - joint_names = seed.name - - best_result: IKResult | None = None - best_error = float("inf") - - for attempt in range(max_attempts): - # Generate seed JointState - if attempt == 0: - current_seed = seed - else: - # Random seed within joint limits - random_positions = np.random.uniform(lower_limits, upper_limits) - current_seed = JointState(name=joint_names, position=random_positions.tolist()) - - # Solve iterative IK - result = self.solve_iterative( - world=world, - robot_id=robot_id, - target_pose=target_pose, - seed=current_seed, - max_iterations=self._max_iterations, - position_tolerance=position_tolerance, - orientation_tolerance=orientation_tolerance, - ) - - if result.is_success() and result.joint_state is not None: - # Check collision if requested - if check_collision: - if not world.check_config_collision_free(robot_id, result.joint_state): - continue # Try another seed - - # Check error - total_error = result.position_error + result.orientation_error - if total_error < best_error: - best_error = total_error - best_result = result - - # If error is within tolerance, we're done - if ( - result.position_error <= position_tolerance - and result.orientation_error <= orientation_tolerance - ): - return result - - if best_result is not None: - return best_result - - return _create_failure_result( - IKStatus.NO_SOLUTION, - f"IK failed after {max_attempts} attempts", - ) - - def solve_iterative( - self, - world: WorldSpec, - robot_id: WorldRobotID, - target_pose: PoseStamped, - seed: JointState, - max_iterations: int = 100, - position_tolerance: float = 0.001, - orientation_tolerance: float = 0.01, - ) -> IKResult: - """Iterative Jacobian-based IK until convergence. - - Uses the damped pseudoinverse method with adaptive step size. - Converges when both position and orientation errors are within tolerance. - - Args: - world: World for FK/Jacobian computation - robot_id: Robot to solve IK for - target_pose: Target end-effector pose - seed: Initial joint configuration - max_iterations: Maximum iterations before giving up - position_tolerance: Required position accuracy (meters) - orientation_tolerance: Required orientation accuracy (radians) - - Returns: - IKResult with solution or failure status - """ - # Convert to internal representation - target_matrix = Transform( - translation=target_pose.position, - rotation=target_pose.orientation, - ).to_matrix() - current_joints = np.array(seed.position, dtype=np.float64) - joint_names = seed.name - - max_iterations = max_iterations or self._max_iterations - lower_limits, upper_limits = world.get_joint_limits(robot_id) - - for iteration in range(max_iterations): - with world.scratch_context() as ctx: - # Set current position (convert to JointState for API) - current_state = JointState(name=joint_names, position=current_joints.tolist()) - world.set_joint_state(ctx, robot_id, current_state) - - # Get current pose (as matrix for error computation) - current_pose = pose_to_matrix(world.get_ee_pose(ctx, robot_id)) - - # Compute error - pos_error, ori_error = compute_pose_error(current_pose, target_matrix) - - # Check convergence - if pos_error <= position_tolerance and ori_error <= orientation_tolerance: - return _create_success_result( - joint_names=joint_names, - joint_positions=current_joints, - position_error=pos_error, - orientation_error=ori_error, - iterations=iteration + 1, - ) - - # Compute twist to reduce error - twist = compute_error_twist(current_pose, target_matrix, gain=0.5) - - # Get Jacobian - J = world.get_jacobian(ctx, robot_id) - - # Adaptive damping near singularities - if check_singularity(J, threshold=self._singularity_threshold): - # Increase damping near singularity instead of failing - effective_damping = self._damping * 10.0 - else: - effective_damping = self._damping - - # Compute joint velocities - J_pinv = damped_pseudoinverse(J, effective_damping) - q_dot = J_pinv @ twist - - # Clamp maximum joint change per iteration (like reference implementations) - max_delta = 0.1 # radians per iteration - max_change = np.max(np.abs(q_dot)) - if max_change > max_delta: - q_dot = q_dot * (max_delta / max_change) - - current_joints = current_joints + q_dot - - # Clip to limits - current_joints = np.clip(current_joints, lower_limits, upper_limits) - - # Compute final error - with world.scratch_context() as ctx: - final_state = JointState(name=joint_names, position=current_joints.tolist()) - world.set_joint_state(ctx, robot_id, final_state) - final_pose = pose_to_matrix(world.get_ee_pose(ctx, robot_id)) - pos_error, ori_error = compute_pose_error(final_pose, target_matrix) - - return _create_failure_result( - IKStatus.NO_SOLUTION, - f"Did not converge after {max_iterations} iterations (pos_err={pos_error:.4f}, ori_err={ori_error:.4f})", - iterations=max_iterations, - ) - - def solve_differential( - self, - world: WorldSpec, - robot_id: WorldRobotID, - current_joints: JointState, - twist: Twist, - dt: float, - ) -> JointState | None: - """Single Jacobian step for velocity control. - - Computes joint velocities from desired end-effector twist using - the damped pseudoinverse method. Returns None if near singularity. - - Args: - world: World for Jacobian computation - robot_id: Robot to compute for - current_joints: Current joint configuration - twist: Desired end-effector twist (linear + angular velocity) - dt: Time step (not used, but kept for interface compatibility) - - Returns: - JointState with velocities, or None if near singularity - """ - # Convert Twist to 6D array [vx, vy, vz, wx, wy, wz] - twist_array = np.array( - [ - twist.linear.x, - twist.linear.y, - twist.linear.z, - twist.angular.x, - twist.angular.y, - twist.angular.z, - ], - dtype=np.float64, - ) - - joint_names = current_joints.name - with world.scratch_context() as ctx: - world.set_joint_state(ctx, robot_id, current_joints) - J = world.get_jacobian(ctx, robot_id) - - # Check for singularity - if check_singularity(J, threshold=self._singularity_threshold): - logger.warning("Near singularity in differential IK") - return None - - # Compute damped pseudoinverse - J_pinv = damped_pseudoinverse(J, self._damping) - - # Compute joint velocities - q_dot = J_pinv @ twist_array - - # Apply velocity limits if available - config = world.get_robot_config(robot_id) - if config.velocity_limits is not None: - velocity_limits = np.array(config.velocity_limits) - # Only consider joints with non-zero velocity limits - nonzero_mask = velocity_limits > 0 - if np.any(nonzero_mask): - max_ratio = np.max(np.abs(q_dot[nonzero_mask]) / velocity_limits[nonzero_mask]) - if max_ratio > 1.0: - q_dot = q_dot / max_ratio - - return JointState(name=joint_names, velocity=q_dot.tolist()) - - def solve_differential_position_only( - self, - world: WorldSpec, - robot_id: WorldRobotID, - current_joints: JointState, - linear_velocity: Vector3, - ) -> JointState | None: - """Position-only differential IK using linear Jacobian. - - Computes joint velocities from desired linear velocity, ignoring - orientation. Returns None if near singularity. - - Args: - world: World for Jacobian computation - robot_id: Robot to compute for - current_joints: Current joint configuration - linear_velocity: Desired linear velocity - - Returns: - JointState with velocities, or None if singular - """ - # Convert Vector3 to array - vel_array = np.array( - [linear_velocity.x, linear_velocity.y, linear_velocity.z], dtype=np.float64 - ) - - joint_names = current_joints.name - with world.scratch_context() as ctx: - world.set_joint_state(ctx, robot_id, current_joints) - J = world.get_jacobian(ctx, robot_id) - - # Extract linear part (first 3 rows) - J_linear = J[:3, :] - - # Check for singularity - JJT = J_linear @ J_linear.T - manipulability = np.sqrt(max(0, np.linalg.det(JJT))) - if manipulability < self._singularity_threshold: - return None - - # Compute damped pseudoinverse - I = np.eye(3) - J_pinv = J_linear.T @ np.linalg.inv(JJT + self._damping**2 * I) - - # Compute joint velocities - q_dot = J_pinv @ vel_array - return JointState(name=joint_names, velocity=q_dot.tolist()) - - -# ============= Result Helpers ============= - - -def _create_success_result( - joint_names: list[str], - joint_positions: NDArray[np.float64], - position_error: float, - orientation_error: float, - iterations: int, -) -> IKResult: - """Create a successful IK result.""" - return IKResult( - status=IKStatus.SUCCESS, - joint_state=JointState(name=joint_names, position=joint_positions.tolist()), - position_error=position_error, - orientation_error=orientation_error, - iterations=iterations, - message="IK solution found", - ) - - -def _create_failure_result( - status: IKStatus, - message: str, - iterations: int = 0, -) -> IKResult: - """Create a failed IK result.""" - return IKResult( - status=status, - joint_state=None, - iterations=iterations, - message=message, - ) diff --git a/dimos/manipulation/planning/kinematics/pinocchio_ik.py b/dimos/manipulation/planning/kinematics/pinocchio_ik.py deleted file mode 100644 index 4224dda556..0000000000 --- a/dimos/manipulation/planning/kinematics/pinocchio_ik.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pinocchio-based inverse kinematics solver. - -Standalone IK solver using Pinocchio for forward kinematics and Jacobian -computation. Uses damped least-squares (Levenberg-Marquardt) for robust -convergence near singularities. - -Unlike JacobianIK (which uses the WorldSpec interface), this solver operates -directly on a Pinocchio model. This makes it suitable for lightweight, -real-time IK in control tasks where a full WorldSpec is not needed. - -Usage: - >>> from dimos.manipulation.planning.kinematics.pinocchio_ik import PinocchioIK - >>> ik = PinocchioIK.from_model_path("robot.urdf", ee_joint_id=6) - >>> q_solution, converged, error = ik.solve(target_se3, q_init) - >>> ee_pose = ik.forward_kinematics(q_solution) -""" - -from __future__ import annotations - -from dataclasses import dataclass -from pathlib import Path -from typing import TYPE_CHECKING, Any - -import numpy as np -from numpy.linalg import norm, solve -import pinocchio # type: ignore[import-untyped] - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from numpy.typing import NDArray - - from dimos.msgs.geometry_msgs import Pose, PoseStamped - -logger = setup_logger() - - -# ============================================================================= -# Configuration -# ============================================================================= - - -@dataclass -class PinocchioIKConfig: - """Configuration for the Pinocchio IK solver. - - Attributes: - max_iter: Maximum IK solver iterations - eps: Convergence threshold (SE3 log-error norm) - damp: Damping factor for singularity handling (higher = more stable) - dt: Integration step size - max_velocity: Max joint velocity per iteration (rad/s), clamps near singularities - """ - - max_iter: int = 100 - eps: float = 1e-4 - damp: float = 1e-2 - dt: float = 1.0 - max_velocity: float = 10.0 - - -# ============================================================================= -# PinocchioIK Solver -# ============================================================================= - - -class PinocchioIK: - """Pinocchio-based damped least-squares IK solver. - - Loads a URDF or MJCF model and provides: - - solve(): Damped least-squares IK from SE3 target - - forward_kinematics(): FK from joint angles to EE pose - - Thread safety: NOT thread-safe. Each caller should use its own instance - or protect calls with an external lock. Control tasks typically hold a - lock around compute() which covers IK calls. - - Example: - >>> ik = PinocchioIK.from_model_path("robot.urdf", ee_joint_id=6) - >>> target = pose_to_se3(pose_stamped) - >>> q, converged, err = ik.solve(target, q_current) - >>> if converged: - ... ee = ik.forward_kinematics(q) - """ - - def __init__( - self, - model: pinocchio.Model, - data: pinocchio.Data, - ee_joint_id: int, - config: PinocchioIKConfig | None = None, - ) -> None: - """Initialize solver with an existing Pinocchio model. - - Args: - model: Pinocchio model - data: Pinocchio data (created from model) - ee_joint_id: End-effector joint ID in the kinematic chain - config: Solver configuration (uses defaults if None) - """ - self._model = model - self._data = data - self._ee_joint_id = ee_joint_id - self._config = config or PinocchioIKConfig() - - @classmethod - def from_model_path( - cls, - model_path: str | Path, - ee_joint_id: int, - ) -> PinocchioIK: - """Create solver by loading a URDF or MJCF file. - - Args: - model_path: Path to URDF (.urdf) or MJCF (.xml) file - ee_joint_id: End-effector joint ID in the kinematic chain - - Returns: - Configured PinocchioIK instance - - Raises: - FileNotFoundError: If model file doesn't exist - """ - path = Path(str(model_path)) - if not path.exists(): - raise FileNotFoundError(f"Model file not found: {path}") - - if path.suffix == ".xml": - model = pinocchio.buildModelFromMJCF(str(path)) - else: - model = pinocchio.buildModelFromUrdf(str(path)) - - data = model.createData() - return cls(model, data, ee_joint_id) - - @property - def model(self) -> pinocchio.Model: - """The Pinocchio model.""" - return self._model - - @property - def nq(self) -> int: - """Number of configuration variables (DOF).""" - return int(self._model.nq) - - @property - def ee_joint_id(self) -> int: - """End-effector joint ID.""" - return self._ee_joint_id - - # ========================================================================= - # Core IK - # ========================================================================= - - def solve( - self, - target_pose: pinocchio.SE3, - q_init: NDArray[np.floating[Any]], - config: PinocchioIKConfig | None = None, - ) -> tuple[NDArray[np.floating[Any]], bool, float]: - """Solve IK using damped least-squares (Levenberg-Marquardt). - - Args: - target_pose: Target end-effector pose as SE3 - q_init: Initial joint configuration (warm-start) - config: Override solver config for this call (uses instance config if None) - - Returns: - Tuple of (joint_angles, converged, final_error) - """ - cfg = config or self._config - q = q_init.copy() - final_err = float("inf") - - for _ in range(cfg.max_iter): - pinocchio.forwardKinematics(self._model, self._data, q) - iMd = self._data.oMi[self._ee_joint_id].actInv(target_pose) - - err = pinocchio.log(iMd).vector - final_err = float(norm(err)) - if final_err < cfg.eps: - return q, True, final_err - - J = pinocchio.computeJointJacobian(self._model, self._data, q, self._ee_joint_id) - J = -np.dot(pinocchio.Jlog6(iMd.inverse()), J) - v = -J.T.dot(solve(J.dot(J.T) + cfg.damp * np.eye(6), err)) - - # Clamp velocity to prevent explosion near singularities - v_norm = norm(v) - if v_norm > cfg.max_velocity: - v = v * (cfg.max_velocity / v_norm) - - q = pinocchio.integrate(self._model, q, v * cfg.dt) - - return q, False, final_err - - # ========================================================================= - # Forward Kinematics - # ========================================================================= - - def forward_kinematics(self, joint_positions: NDArray[np.floating[Any]]) -> pinocchio.SE3: - """Compute end-effector pose from joint positions. - - Args: - joint_positions: Joint angles in radians - - Returns: - End-effector pose as SE3 - """ - pinocchio.forwardKinematics(self._model, self._data, joint_positions) - return self._data.oMi[self._ee_joint_id].copy() - - -# ============================================================================= -# Pose Conversion Helpers -# ============================================================================= - - -def pose_to_se3(pose: Pose | PoseStamped) -> pinocchio.SE3: - """Convert Pose or PoseStamped to pinocchio SE3""" - - position = np.array([pose.x, pose.y, pose.z]) - quat = pose.orientation - rotation = pinocchio.Quaternion(quat.w, quat.x, quat.y, quat.z).toRotationMatrix() - return pinocchio.SE3(rotation, position) - - -# ============================================================================= -# Safety Utilities -# ============================================================================= - - -def check_joint_delta( - q_new: NDArray[np.floating[Any]], - q_current: NDArray[np.floating[Any]], - max_delta_deg: float, -) -> bool: - """Check if joint position change is within safety limits. - - Args: - q_new: Proposed joint positions (radians) - q_current: Current joint positions (radians) - max_delta_deg: Maximum allowed change per joint (degrees) - - Returns: - True if all joint deltas are within limits - """ - max_delta_rad = np.radians(max_delta_deg) - joint_deltas = np.abs(q_new - q_current) - return bool(np.all(joint_deltas <= max_delta_rad)) - - -def get_worst_joint_delta( - q_new: NDArray[np.floating[Any]], - q_current: NDArray[np.floating[Any]], -) -> tuple[int, float]: - """Find the joint with the largest position change. - - Args: - q_new: Proposed joint positions (radians) - q_current: Current joint positions (radians) - - Returns: - Tuple of (joint_index, delta_in_degrees) - """ - joint_deltas = np.abs(q_new - q_current) - worst_idx = int(np.argmax(joint_deltas)) - return worst_idx, float(np.degrees(joint_deltas[worst_idx])) - - -__all__ = [ - "PinocchioIK", - "PinocchioIKConfig", - "check_joint_delta", - "get_worst_joint_delta", - "pose_to_se3", -] diff --git a/dimos/manipulation/planning/monitor/__init__.py b/dimos/manipulation/planning/monitor/__init__.py deleted file mode 100644 index c280bd4d56..0000000000 --- a/dimos/manipulation/planning/monitor/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -World Monitor Module - -Provides reactive monitoring for keeping WorldSpec synchronized with the real world. - -## Components - -- WorldMonitor: Top-level monitor using WorldSpec Protocol -- WorldStateMonitor: Syncs joint state to WorldSpec -- WorldObstacleMonitor: Syncs obstacles to WorldSpec - -All monitors use the factory pattern and Protocol types. - -## Example - -```python -from dimos.manipulation.planning.monitor import WorldMonitor - -monitor = WorldMonitor(enable_viz=True) -robot_id = monitor.add_robot(config) -monitor.finalize() - -# Start monitoring -monitor.start_state_monitor(robot_id) -monitor.start_obstacle_monitor() - -# Handle joint state messages -monitor.on_joint_state(msg, robot_id) - -# Thread-safe collision checking -is_valid = monitor.is_state_valid(robot_id, q_test) -``` -""" - -from dimos.manipulation.planning.monitor.world_monitor import WorldMonitor -from dimos.manipulation.planning.monitor.world_obstacle_monitor import ( - WorldObstacleMonitor, -) -from dimos.manipulation.planning.monitor.world_state_monitor import WorldStateMonitor - -# Re-export message types from spec for convenience -from dimos.manipulation.planning.spec import CollisionObjectMessage - -__all__ = [ - "CollisionObjectMessage", - "WorldMonitor", - "WorldObstacleMonitor", - "WorldStateMonitor", -] diff --git a/dimos/manipulation/planning/monitor/world_monitor.py b/dimos/manipulation/planning/monitor/world_monitor.py deleted file mode 100644 index 33017957dc..0000000000 --- a/dimos/manipulation/planning/monitor/world_monitor.py +++ /dev/null @@ -1,483 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""World Monitor - keeps WorldSpec synchronized with real robot state and obstacles.""" - -from __future__ import annotations - -from contextlib import contextmanager -import threading -from typing import TYPE_CHECKING, Any - -from dimos.manipulation.planning.factory import create_world -from dimos.manipulation.planning.monitor.world_obstacle_monitor import WorldObstacleMonitor -from dimos.manipulation.planning.monitor.world_state_monitor import WorldStateMonitor -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Generator - - import numpy as np - from numpy.typing import NDArray - - from dimos.manipulation.planning.spec import ( - CollisionObjectMessage, - JointPath, - Obstacle, - RobotModelConfig, - WorldRobotID, - WorldSpec, - ) - from dimos.msgs.vision_msgs import Detection3D - from dimos.perception.detection.type.detection3d.object import Object - -logger = setup_logger() - - -class WorldMonitor: - """Manages WorldSpec with state/obstacle monitors. Thread-safe via RLock.""" - - def __init__( - self, - backend: str = "drake", - enable_viz: bool = False, - **kwargs: Any, - ): - self._backend = backend - self._world: WorldSpec = create_world(backend=backend, enable_viz=enable_viz, **kwargs) - self._lock = threading.RLock() - self._robot_joints: dict[WorldRobotID, list[str]] = {} - self._state_monitors: dict[WorldRobotID, WorldStateMonitor] = {} - self._obstacle_monitor: WorldObstacleMonitor | None = None - self._viz_thread: threading.Thread | None = None - self._viz_stop_event = threading.Event() - self._viz_rate_hz: float = 10.0 - - # ============= Robot Management ============= - - def add_robot(self, config: RobotModelConfig) -> WorldRobotID: - """Add a robot. Returns robot_id.""" - with self._lock: - robot_id = self._world.add_robot(config) - self._robot_joints[robot_id] = config.joint_names - logger.info(f"Added robot '{config.name}' as '{robot_id}'") - return robot_id - - def get_robot_ids(self) -> list[WorldRobotID]: - """Get all robot IDs.""" - with self._lock: - return self._world.get_robot_ids() - - def get_robot_config(self, robot_id: WorldRobotID) -> RobotModelConfig: - """Get robot configuration.""" - with self._lock: - return self._world.get_robot_config(robot_id) - - def get_joint_limits( - self, robot_id: WorldRobotID - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Get joint limits for a robot.""" - with self._lock: - return self._world.get_joint_limits(robot_id) - - # ============= Obstacle Management ============= - - def add_obstacle(self, obstacle: Obstacle) -> str: - """Add an obstacle. Returns obstacle_id.""" - with self._lock: - return self._world.add_obstacle(obstacle) - - def remove_obstacle(self, obstacle_id: str) -> bool: - """Remove an obstacle.""" - with self._lock: - return self._world.remove_obstacle(obstacle_id) - - def clear_obstacles(self) -> None: - """Remove all obstacles.""" - with self._lock: - self._world.clear_obstacles() - - # ============= Monitor Control ============= - - def start_state_monitor( - self, - robot_id: WorldRobotID, - joint_names: list[str] | None = None, - joint_name_mapping: dict[str, str] | None = None, - ) -> None: - """Start monitoring joint states. Uses config defaults if args are None.""" - with self._lock: - if robot_id in self._state_monitors: - logger.warning(f"State monitor for '{robot_id}' already started") - return - - # Get config for defaults - config = self._world.get_robot_config(robot_id) - - # Get joint names from config if not provided - if joint_names is None: - if robot_id in self._robot_joints: - joint_names = self._robot_joints[robot_id] - else: - joint_names = config.joint_names - - # Get joint name mapping from config if not provided - if joint_name_mapping is None and config.joint_name_mapping: - joint_name_mapping = config.joint_name_mapping - - monitor = WorldStateMonitor( - world=self._world, - lock=self._lock, - robot_id=robot_id, - joint_names=joint_names, - joint_name_mapping=joint_name_mapping, - ) - monitor.start() - self._state_monitors[robot_id] = monitor - logger.info(f"State monitor started for '{robot_id}'") - - def start_obstacle_monitor(self) -> None: - """Start monitoring obstacle updates.""" - with self._lock: - if self._obstacle_monitor is not None: - logger.warning("Obstacle monitor already started") - return - - self._obstacle_monitor = WorldObstacleMonitor( - world=self._world, - lock=self._lock, - ) - self._obstacle_monitor.start() - logger.info("Obstacle monitor started") - - def stop_all_monitors(self) -> None: - """Stop all monitors and visualization thread.""" - # Stop visualization thread first (outside lock to avoid deadlock) - self.stop_visualization_thread() - - with self._lock: - for _robot_id, monitor in self._state_monitors.items(): - monitor.stop() - self._state_monitors.clear() - - if self._obstacle_monitor is not None: - self._obstacle_monitor.stop() - self._obstacle_monitor = None - - logger.info("All monitors stopped") - - self._world.close() - - # ============= Message Handlers ============= - - def on_joint_state(self, msg: JointState, robot_id: WorldRobotID | None = None) -> None: - """Handle joint state message. Broadcasts to all monitors if robot_id is None.""" - try: - if robot_id is not None: - if robot_id in self._state_monitors: - self._state_monitors[robot_id].on_joint_state(msg) - else: - logger.warning(f"No state monitor for robot_id: {robot_id}") - else: - # Broadcast to all monitors - for monitor in self._state_monitors.values(): - monitor.on_joint_state(msg) - except Exception as e: - logger.error(f"[WorldMonitor] Exception in on_joint_state: {e}") - import traceback - - logger.error(traceback.format_exc()) - - def on_collision_object(self, msg: CollisionObjectMessage) -> None: - """Handle collision object message.""" - if self._obstacle_monitor is not None: - self._obstacle_monitor.on_collision_object(msg) - - def on_detections(self, detections: list[Detection3D]) -> None: - """Handle perception detections (Detection3D from dimos.msgs.vision_msgs).""" - if self._obstacle_monitor is not None: - self._obstacle_monitor.on_detections(detections) - - def on_objects(self, objects: object) -> None: - """Handle Object detections from ObjectDB (preserves object_id).""" - if self._obstacle_monitor is not None and isinstance(objects, list): - self._obstacle_monitor.on_objects(objects) - - def refresh_obstacles(self, min_duration: float = 0.0) -> list[dict[str, Any]]: - """Refresh perception obstacles from cache. Returns list of added obstacles.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.refresh_obstacles(min_duration) - return [] - - def clear_perception_obstacles(self) -> int: - """Remove all perception obstacles. Returns count removed.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.clear_perception_obstacles() - return 0 - - def get_perception_status(self) -> dict[str, int]: - """Get perception obstacle status.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.get_perception_status() - return {"cached": 0, "added": 0} - - def get_cached_objects(self) -> list[Object]: - """Get cached Object instances from perception.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.get_cached_objects() - return [] - - def list_cached_detections(self) -> list[dict[str, Any]]: - """List cached detections from perception.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.list_cached_detections() - return [] - - def list_added_obstacles(self) -> list[dict[str, Any]]: - """List perception obstacles currently in the planning world.""" - if self._obstacle_monitor is not None: - return self._obstacle_monitor.list_added_obstacles() - return [] - - # ============= State Access ============= - - def get_current_joint_state(self, robot_id: WorldRobotID) -> JointState | None: - """Get current joint state. Returns None if not yet received.""" - # Try state monitor first for positions - if robot_id in self._state_monitors: - positions = self._state_monitors[robot_id].get_current_positions() - velocities = self._state_monitors[robot_id].get_current_velocities() - if positions is not None: - joint_names = self._robot_joints.get(robot_id, []) - return JointState( - name=joint_names, - position=positions.tolist(), - velocity=velocities.tolist() if velocities is not None else [], - ) - - # Fall back to world's live context - with self._lock: - ctx = self._world.get_live_context() - return self._world.get_joint_state(ctx, robot_id) - - def get_current_velocities(self, robot_id: WorldRobotID) -> JointState | None: - """Get current joint velocities as JointState. Returns None if not available.""" - if robot_id in self._state_monitors: - velocities = self._state_monitors[robot_id].get_current_velocities() - if velocities is not None: - joint_names = self._robot_joints.get(robot_id, []) - return JointState(name=joint_names, velocity=velocities.tolist()) - return None - - def wait_for_state(self, robot_id: WorldRobotID, timeout: float = 1.0) -> bool: - """Wait until state is received. Returns False on timeout.""" - if robot_id in self._state_monitors: - return self._state_monitors[robot_id].wait_for_state(timeout) - return False - - def is_state_stale(self, robot_id: WorldRobotID, max_age: float = 1.0) -> bool: - """Check if state is stale.""" - if robot_id in self._state_monitors: - return self._state_monitors[robot_id].is_state_stale(max_age) - return True - - # ============= Context Management ============= - - @contextmanager - def scratch_context(self) -> Generator[Any, None, None]: - """Thread-safe scratch context for planning.""" - with self._world.scratch_context() as ctx: - yield ctx - - def get_live_context(self) -> Any: - """Get live context. Prefer scratch_context() for planning.""" - return self._world.get_live_context() - - # ============= Collision Checking ============= - - def is_state_valid(self, robot_id: WorldRobotID, joint_state: JointState) -> bool: - """Check if configuration is collision-free.""" - return self._world.check_config_collision_free(robot_id, joint_state) - - def is_path_valid( - self, robot_id: WorldRobotID, path: JointPath, step_size: float = 0.05 - ) -> bool: - """Check if path is collision-free with interpolation. - - Args: - robot_id: Robot to check - path: List of JointState waypoints - step_size: Max step size for interpolation (radians) - - Returns: - True if entire path is collision-free - """ - if len(path) < 2: - return len(path) == 0 or self._world.check_config_collision_free(robot_id, path[0]) - - # Check each edge - for i in range(len(path) - 1): - if not self._world.check_edge_collision_free(robot_id, path[i], path[i + 1], step_size): - return False - - return True - - def get_min_distance(self, robot_id: WorldRobotID) -> float: - """Get minimum distance to obstacles for current state.""" - with self._world.scratch_context() as ctx: - return self._world.get_min_distance(ctx, robot_id) - - # ============= Kinematics ============= - - def get_ee_pose( - self, robot_id: WorldRobotID, joint_state: JointState | None = None - ) -> PoseStamped: - """Get end-effector pose. Uses current state if joint_state is None.""" - with self._world.scratch_context() as ctx: - # If no state provided, fetch current from state monitor - if joint_state is None: - joint_state = self.get_current_joint_state(robot_id) - - if joint_state is not None: - self._world.set_joint_state(ctx, robot_id, joint_state) - - return self._world.get_ee_pose(ctx, robot_id) - - def get_link_pose( - self, robot_id: WorldRobotID, link_name: str, joint_state: JointState | None = None - ) -> PoseStamped | None: - """Get arbitrary link pose as PoseStamped. - - Args: - robot_id: Robot to query - link_name: Name of the link in the URDF - joint_state: Joint state to use (uses current if None) - """ - from dimos.msgs.geometry_msgs import Quaternion - - with self._world.scratch_context() as ctx: - if joint_state is None: - joint_state = self.get_current_joint_state(robot_id) - if joint_state is not None: - self._world.set_joint_state(ctx, robot_id, joint_state) - try: - mat = self._world.get_link_pose(ctx, robot_id, link_name) - except KeyError: - logger.warning(f"Link '{link_name}' not found in robot '{robot_id}'") - return None - - pos = mat[:3, 3] - rot = mat[:3, :3] - quat = Quaternion.from_rotation_matrix(rot) - return PoseStamped( - frame_id="world", - position=[float(pos[0]), float(pos[1]), float(pos[2])], - orientation=[float(quat.x), float(quat.y), float(quat.z), float(quat.w)], - ) - - def get_jacobian(self, robot_id: WorldRobotID, joint_state: JointState) -> NDArray[np.float64]: - """Get 6xN Jacobian matrix.""" - with self._world.scratch_context() as ctx: - self._world.set_joint_state(ctx, robot_id, joint_state) - return self._world.get_jacobian(ctx, robot_id) - - # ============= Lifecycle ============= - - def finalize(self) -> None: - """Finalize world. Must be called before collision checking.""" - with self._lock: - self._world.finalize() - logger.info("World finalized") - - @property - def is_finalized(self) -> bool: - """Check if world is finalized.""" - return self._world.is_finalized - - # ============= Visualization ============= - - def get_visualization_url(self) -> str | None: - """Get visualization URL or None if not enabled.""" - if hasattr(self._world, "get_visualization_url"): - url = self._world.get_visualization_url() - return str(url) if url else None - return None - - def publish_visualization(self) -> None: - """Force publish current state to visualization.""" - if hasattr(self._world, "publish_visualization"): - self._world.publish_visualization() - - def start_visualization_thread(self, rate_hz: float = 10.0) -> None: - """Start background thread for visualization updates at given rate.""" - if self._viz_thread is not None and self._viz_thread.is_alive(): - logger.warning("Visualization thread already running") - return - - if not hasattr(self._world, "publish_visualization"): - logger.warning("World does not support visualization") - return - - self._viz_rate_hz = rate_hz - self._viz_stop_event.clear() - self._viz_thread = threading.Thread( - target=self._visualization_loop, - name="MeshcatVizThread", - daemon=True, - ) - self._viz_thread.start() - logger.info(f"Visualization thread started at {rate_hz}Hz") - - def stop_visualization_thread(self) -> None: - """Stop the visualization thread.""" - if self._viz_thread is None: - return - - self._viz_stop_event.set() - self._viz_thread.join(timeout=1.0) - if self._viz_thread.is_alive(): - logger.warning("Visualization thread did not stop cleanly") - self._viz_thread = None - logger.info("Visualization thread stopped") - - def _visualization_loop(self) -> None: - """Internal: Visualization update loop.""" - import time - - period = 1.0 / self._viz_rate_hz - while not self._viz_stop_event.is_set(): - try: - if hasattr(self._world, "publish_visualization"): - self._world.publish_visualization() - except Exception as e: - logger.debug(f"Visualization publish failed: {e}") - time.sleep(period) - - # ============= Direct World Access ============= - - @property - def world(self) -> WorldSpec: - """Get underlying WorldSpec. Not thread-safe for modifications.""" - return self._world - - def get_state_monitor(self, robot_id: str) -> WorldStateMonitor | None: - """Get state monitor for a robot (may be None).""" - return self._state_monitors.get(robot_id) - - @property - def obstacle_monitor(self) -> WorldObstacleMonitor | None: - """Get obstacle monitor (may be None if not started).""" - return self._obstacle_monitor diff --git a/dimos/manipulation/planning/monitor/world_obstacle_monitor.py b/dimos/manipulation/planning/monitor/world_obstacle_monitor.py deleted file mode 100644 index 6082ab93a9..0000000000 --- a/dimos/manipulation/planning/monitor/world_obstacle_monitor.py +++ /dev/null @@ -1,607 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -World Obstacle Monitor - -Monitors obstacle updates and applies them to a WorldSpec instance. -This is the WorldSpec-based replacement for WorldGeometryMonitor. - -Example: - monitor = WorldObstacleMonitor(world, lock) - monitor.start() - monitor.on_collision_object(collision_msg) # Called by subscriber -""" - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING, Any - -from dimos.manipulation.planning.spec import ( - CollisionObjectMessage, - Obstacle, - ObstacleType, -) -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - import threading - - from dimos.manipulation.planning.spec import WorldSpec - from dimos.msgs.vision_msgs import Detection3D - from dimos.perception.detection.type.detection3d.object import Object - -logger = setup_logger() - - -class WorldObstacleMonitor: - """Monitors world obstacles and updates WorldSpec. - - This class handles updates from: - - Explicit collision objects (CollisionObjectMessage) - - Perception detections (Detection3D from dimos.msgs.vision_msgs) - - ## Thread Safety - - All obstacle operations are protected by the provided lock. - Callbacks can be called from any thread. - - ## Comparison with WorldGeometryMonitor - - - WorldGeometryMonitor: Works with PlanningScene ABC - - WorldObstacleMonitor: Works with WorldSpec Protocol - """ - - def __init__( - self, - world: WorldSpec, - lock: threading.RLock, - detection_timeout: float = 2.0, - use_mesh_obstacles: bool = True, - ): - """Create a world obstacle monitor. - - Args: - world: WorldSpec instance to update - lock: Shared lock for thread-safe access - detection_timeout: Time before removing stale detections (seconds) - use_mesh_obstacles: Use convex hull meshes from pointclouds instead of bounding boxes - """ - self._world = world - self._lock = lock - self._detection_timeout = detection_timeout - self._use_mesh_obstacles = use_mesh_obstacles - - # Track obstacles from different sources - self._collision_objects: dict[str, str] = {} # msg_id -> obstacle_id - self._perception_objects: dict[str, str] = {} # detection_id -> obstacle_id - self._perception_timestamps: dict[str, float] = {} # detection_id -> timestamp - - # Object-based cache (from ObjectDB, keyed by object_id) - # object_id -> (Object, first_seen, last_seen) - self._object_cache: dict[str, tuple[Object, float, float]] = {} - # object_id -> obstacle_id (objects currently added to Drake world) - self._object_obstacles: dict[str, str] = {} - - # Running state - self._running = False - - # Callbacks: (operation, obstacle_id, obstacle) where operation is "add"/"update"/"remove" - self._obstacle_callbacks: list[Callable[[str, str, Obstacle | None], None]] = [] - - def start(self) -> None: - """Start the obstacle monitor.""" - self._running = True - logger.info("World obstacle monitor started") - - def stop(self) -> None: - """Stop the obstacle monitor.""" - self._running = False - logger.info("World obstacle monitor stopped") - - def is_running(self) -> bool: - """Check if monitor is running.""" - return self._running - - def on_collision_object(self, msg: CollisionObjectMessage) -> None: - """Handle explicit collision object message. - - Args: - msg: Collision object message - """ - if not self._running: - return - - with self._lock: - if msg.operation == "add": - self._add_collision_object(msg) - elif msg.operation == "remove": - self._remove_collision_object(msg.id) - elif msg.operation == "update": - self._update_collision_object(msg) - else: - logger.warning(f"Unknown collision object operation: {msg.operation}") - - def _add_collision_object(self, msg: CollisionObjectMessage) -> None: - """Add a collision object from message.""" - if msg.id in self._collision_objects: - logger.debug(f"Collision object '{msg.id}' already exists, updating") - self._update_collision_object(msg) - return - - obstacle = self._msg_to_obstacle(msg) - if obstacle is None: - logger.warning(f"Failed to create obstacle from message: {msg.id}") - return - - obstacle_id = self._world.add_obstacle(obstacle) - self._collision_objects[msg.id] = obstacle_id - - logger.debug(f"Added collision object '{msg.id}' as '{obstacle_id}'") - - # Notify callbacks - for callback in self._obstacle_callbacks: - try: - callback("add", obstacle_id, obstacle) - except Exception as e: - logger.error(f"Obstacle callback error: {e}") - - def _remove_collision_object(self, msg_id: str) -> None: - """Remove a collision object.""" - if msg_id not in self._collision_objects: - logger.debug(f"Collision object '{msg_id}' not found") - return - - obstacle_id = self._collision_objects[msg_id] - self._world.remove_obstacle(obstacle_id) - del self._collision_objects[msg_id] - - logger.debug(f"Removed collision object '{msg_id}'") - - # Notify callbacks - for callback in self._obstacle_callbacks: - try: - callback("remove", obstacle_id, None) - except Exception as e: - logger.error(f"Obstacle callback error: {e}") - - def _update_collision_object(self, msg: CollisionObjectMessage) -> None: - """Update a collision object pose.""" - if msg.id not in self._collision_objects: - # Treat as add if doesn't exist - self._add_collision_object(msg) - return - - obstacle_id = self._collision_objects[msg.id] - - if msg.pose is not None: - self._world.update_obstacle_pose(obstacle_id, msg.pose) - logger.debug(f"Updated collision object '{msg.id}' pose") - - # Notify callbacks - for callback in self._obstacle_callbacks: - try: - callback("update", obstacle_id, None) - except Exception as e: - logger.error(f"Obstacle callback error: {e}") - - def _msg_to_obstacle(self, msg: CollisionObjectMessage) -> Obstacle | None: - """Convert collision object message to Obstacle.""" - if msg.primitive_type is None or msg.pose is None or msg.dimensions is None: - return None - - type_map = { - "box": ObstacleType.BOX, - "sphere": ObstacleType.SPHERE, - "cylinder": ObstacleType.CYLINDER, - } - - obstacle_type = type_map.get(msg.primitive_type.lower()) - if obstacle_type is None: - logger.warning(f"Unknown primitive type: {msg.primitive_type}") - return None - - return Obstacle( - name=msg.id, - obstacle_type=obstacle_type, - pose=msg.pose, - dimensions=msg.dimensions, - color=msg.color, - ) - - def on_detections(self, detections: list[Detection3D]) -> None: - """Handle perception detection results. - - Updates obstacles based on detections: - - Adds new obstacles for new detections - - Updates existing obstacles - - Removes obstacles for detections that are no longer present - - Args: - detections: List of Detection3D messages from dimos.msgs.vision_msgs - """ - if not self._running: - return - - with self._lock: - current_time = time.time() - seen_ids = set() - - for detection in detections: - det_id = detection.id - seen_ids.add(det_id) - - pose = self._detection3d_to_pose(detection) - - if det_id in self._perception_objects: - # Update existing obstacle - obstacle_id = self._perception_objects[det_id] - self._world.update_obstacle_pose(obstacle_id, pose) - self._perception_timestamps[det_id] = current_time - else: - # Add new obstacle - obstacle = self._detection_to_obstacle(detection) - obstacle_id = self._world.add_obstacle(obstacle) - self._perception_objects[det_id] = obstacle_id - self._perception_timestamps[det_id] = current_time - - logger.debug(f"Added perception object '{det_id}' as '{obstacle_id}'") - - # Notify callbacks - for callback in self._obstacle_callbacks: - try: - callback("add", obstacle_id, obstacle) - except Exception as e: - logger.error(f"Obstacle callback error: {e}") - - # Remove stale detections - self._cleanup_stale_detections(current_time, seen_ids) - - def _detection3d_to_pose(self, detection: Detection3D) -> PoseStamped: - """Convert Detection3D bbox.center to PoseStamped.""" - center = detection.bbox.center - return PoseStamped( - position=center.position, - orientation=center.orientation, - ) - - def _detection_to_obstacle(self, detection: Detection3D) -> Obstacle: - """Convert Detection3D to Obstacle.""" - pose = self._detection3d_to_pose(detection) - size = detection.bbox.size - return Obstacle( - name=f"detection_{detection.id}", - obstacle_type=ObstacleType.BOX, - pose=pose, - dimensions=(size.x, size.y, size.z), - color=(0.2, 0.8, 0.2, 0.6), # Green for perception objects - ) - - def _cleanup_stale_detections( - self, - current_time: float, - seen_ids: set[str], - ) -> None: - """Remove detections that haven't been seen recently.""" - stale_ids = [] - - for det_id, timestamp in self._perception_timestamps.items(): - age = current_time - timestamp - if det_id not in seen_ids and age > self._detection_timeout: - stale_ids.append(det_id) - - for det_id in stale_ids: - obstacle_id = self._perception_objects[det_id] - removed = self._world.remove_obstacle(obstacle_id) - if not removed: - logger.warning(f"Obstacle '{obstacle_id}' not found in world during cleanup") - del self._perception_objects[det_id] - del self._perception_timestamps[det_id] - - logger.debug(f"Removed stale perception object '{det_id}'") - - # Notify callbacks - for callback in self._obstacle_callbacks: - try: - callback("remove", obstacle_id, None) - except Exception as e: - logger.error(f"Obstacle callback error: {e}") - - def add_static_obstacle( - self, - name: str, - obstacle_type: str, - pose: PoseStamped, - dimensions: tuple[float, ...], - color: tuple[float, float, float, float] = (0.8, 0.2, 0.2, 0.8), - ) -> str: - """Manually add a static obstacle. - - Args: - name: Unique name for the obstacle - obstacle_type: Type ("box", "sphere", "cylinder") - pose: Pose of the obstacle in world frame - dimensions: Type-specific dimensions - color: RGBA color - - Returns: - Obstacle ID - """ - msg = CollisionObjectMessage( - id=name, - operation="add", - primitive_type=obstacle_type, - pose=pose, - dimensions=dimensions, - color=color, - ) - self.on_collision_object(msg) - return self._collision_objects.get(name, "") - - def remove_static_obstacle(self, name: str) -> bool: - """Remove a static obstacle by name. - - Args: - name: Name of the obstacle - - Returns: - True if removed - """ - if name not in self._collision_objects: - return False - - msg = CollisionObjectMessage(id=name, operation="remove") - self.on_collision_object(msg) - return True - - def clear_all_obstacles(self) -> None: - """Remove all tracked obstacles.""" - with self._lock: - # Clear collision objects - for msg_id in list(self._collision_objects.keys()): - self._remove_collision_object(msg_id) - - # Clear perception objects - for det_id, obstacle_id in list(self._perception_objects.items()): - self._world.remove_obstacle(obstacle_id) - del self._perception_objects[det_id] - del self._perception_timestamps[det_id] - - def get_obstacle_count(self) -> int: - """Get total number of tracked obstacles.""" - with self._lock: - return len(self._collision_objects) + len(self._perception_objects) - - def add_obstacle_callback( - self, - callback: Callable[[str, str, Obstacle | None], None], - ) -> None: - """Add callback for obstacle changes. - - Args: - callback: Function called with (operation, obstacle_id, obstacle) - where operation is "add", "update", or "remove" - """ - self._obstacle_callbacks.append(callback) - - def remove_obstacle_callback( - self, - callback: Callable[[str, str, Obstacle | None], None], - ) -> None: - """Remove an obstacle callback.""" - if callback in self._obstacle_callbacks: - self._obstacle_callbacks.remove(callback) - - # ============= Object-Based Perception (from ObjectDB) ============= - - def on_objects(self, objects: list[object]) -> None: - """Cache objects from ObjectDB (preserves stable object_id). - - Unlike on_detections(), this receives Object instances with stable IDs - from ObjectDB deduplication, making the cache trivially keyed by object_id. - - Args: - objects: List of Object instances from ObjectDB - """ - if not self._running: - return - - from dimos.perception.detection.type.detection3d.object import Object - - now = time.time() - seen: set[str] = set() - - with self._lock: - for obj in objects: - if not isinstance(obj, Object): - continue - oid = obj.object_id - seen.add(oid) - if oid in self._object_cache: - _, first, _ = self._object_cache[oid] - self._object_cache[oid] = (obj, first, now) - else: - self._object_cache[oid] = (obj, now, now) - - # Remove objects no longer reported by ObjectDB - stale = [oid for oid in self._object_cache if oid not in seen] - for oid in stale: - del self._object_cache[oid] - - def refresh_obstacles(self, min_duration: float = 0.0) -> list[dict[str, Any]]: - """Full sync: remove all object obstacles, re-add from cache. - - Args: - min_duration: Minimum seconds an object must have been seen to be included - - Returns: - List of added obstacles with object_id, obstacle_id, name, center, size - """ - from dimos.perception.detection.type.detection3d.object import Object - - # Step 1: snapshot eligible objects under lock (fast) - eligible: list[tuple[str, Object]] = [] - with self._lock: - for oid, (obj, first_seen, last_seen) in self._object_cache.items(): - if not isinstance(obj, Object): - continue - if last_seen - first_seen < min_duration: - continue - eligible.append((oid, obj)) - - # Step 2: compute obstacles OUTSIDE lock (convex hull can be slow) - prepared: list[tuple[str, Object, Obstacle]] = [] - for oid, obj in eligible: - obstacle = self._object_to_obstacle(obj) - prepared.append((oid, obj, obstacle)) - - # Step 3: apply to Drake world under lock (fast) - with self._lock: - for obs_id in self._object_obstacles.values(): - self._world.remove_obstacle(obs_id) - self._object_obstacles.clear() - - result: list[dict[str, Any]] = [] - for oid, obj, obstacle in prepared: - assert isinstance(obj, Object) - obs_id = self._world.add_obstacle(obstacle) - self._object_obstacles[oid] = obs_id - result.append( - { - "object_id": oid, - "obstacle_id": obs_id, - "name": obj.name, - "center": [float(obj.center.x), float(obj.center.y), float(obj.center.z)], - "size": [float(obj.size.x), float(obj.size.y), float(obj.size.z)], - } - ) - logger.debug(f"Added object obstacle '{oid}' ({obj.name}) as '{obs_id}'") - - return result - - def clear_perception_obstacles(self) -> int: - """Remove all object obstacles from the planning world. - - Returns: - Number of obstacles removed - """ - with self._lock: - count = len(self._object_obstacles) - for obs_id in self._object_obstacles.values(): - self._world.remove_obstacle(obs_id) - self._object_obstacles.clear() - return count - - def get_perception_status(self) -> dict[str, int]: - """Get perception obstacle status.""" - with self._lock: - return { - "cached": len(self._object_cache), - "added": len(self._object_obstacles), - } - - def get_cached_objects(self) -> list[Object]: - """Get cached Object instances from perception. - - Returns raw Object instances for typed access to .name, .center, .size etc. - """ - from dimos.perception.detection.type.detection3d.object import Object as _Object - - with self._lock: - return [obj for obj, _, _ in self._object_cache.values() if isinstance(obj, _Object)] - - def list_cached_detections(self) -> list[dict[str, Any]]: - """List cached detections from perception.""" - from dimos.perception.detection.type.detection3d.object import Object - - with self._lock: - result: list[dict[str, Any]] = [] - for oid, (obj, first_seen, last_seen) in self._object_cache.items(): - if not isinstance(obj, Object): - continue - result.append( - { - "object_id": oid, - "name": obj.name, - "center": [float(obj.center.x), float(obj.center.y), float(obj.center.z)], - "size": [float(obj.size.x), float(obj.size.y), float(obj.size.z)], - "duration": round(last_seen - first_seen, 1), - "in_world": oid in self._object_obstacles, - } - ) - return result - - def list_added_obstacles(self) -> list[dict[str, Any]]: - """List perception obstacles currently in the planning world.""" - from dimos.perception.detection.type.detection3d.object import Object - - with self._lock: - result: list[dict[str, Any]] = [] - for oid, obs_id in self._object_obstacles.items(): - entry = self._object_cache.get(oid) - if entry is None: - continue - obj, first_seen, last_seen = entry - if not isinstance(obj, Object): - continue - result.append( - { - "object_id": oid, - "obstacle_id": obs_id, - "name": obj.name, - "center": [float(obj.center.x), float(obj.center.y), float(obj.center.z)], - "size": [float(obj.size.x), float(obj.size.y), float(obj.size.z)], - } - ) - return result - - def _object_to_obstacle(self, obj: object) -> Obstacle: - """Convert Object to obstacle. Uses bounding box by default, convex hull if use_mesh_obstacles=True.""" - from dimos.perception.detection.type.detection3d.object import Object - - assert isinstance(obj, Object) - name = f"object_{obj.object_id}" - - # Try convex hull from pointcloud (opt-in) - if self._use_mesh_obstacles and obj.pointcloud is not None: - try: - from dimos.manipulation.planning.utils.mesh_utils import ( - pointcloud_to_convex_hull_obj, - ) - - points, _ = obj.pointcloud.as_numpy() - if points is not None and points.shape[0] >= 4: - mesh_path = pointcloud_to_convex_hull_obj(points) - if mesh_path is not None: - return Obstacle( - name=name, - obstacle_type=ObstacleType.MESH, - pose=obj.pose, - color=(0.2, 0.8, 0.2, 0.6), - mesh_path=mesh_path, - ) - except Exception as e: - logger.debug(f"Convex hull failed for {name}, falling back to box: {e}") - - # Default: bounding box - return Obstacle( - name=name, - obstacle_type=ObstacleType.BOX, - pose=obj.pose or PoseStamped(position=obj.center), - dimensions=(float(obj.size.x), float(obj.size.y), float(obj.size.z)), - color=(0.2, 0.8, 0.2, 0.6), - ) diff --git a/dimos/manipulation/planning/monitor/world_state_monitor.py b/dimos/manipulation/planning/monitor/world_state_monitor.py deleted file mode 100644 index 87d61bb66f..0000000000 --- a/dimos/manipulation/planning/monitor/world_state_monitor.py +++ /dev/null @@ -1,331 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -World State Monitor - -Monitors joint state updates and syncs them to a WorldSpec instance. -This is the WorldSpec-based replacement for StateMonitor. - -Example: - monitor = WorldStateMonitor(world, lock, robot_id, joint_names) - monitor.start() - monitor.on_joint_state(joint_state_msg) # Called by subscriber -""" - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -import numpy as np - -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - import threading - - from numpy.typing import NDArray - - from dimos.manipulation.planning.spec import WorldSpec - -logger = setup_logger() - - -class WorldStateMonitor: - """Monitors joint state updates and syncs them to WorldSpec. - - This class subscribes to joint state messages and calls - world.sync_from_joint_state() to keep the world's live context - synchronized with the real robot state. - - ## Thread Safety - - All state updates are protected by the provided lock. The on_joint_state - callback can be called from any thread. - - ## Comparison with StateMonitor - - - StateMonitor: Works with PlanningScene ABC - - WorldStateMonitor: Works with WorldSpec Protocol - """ - - def __init__( - self, - world: WorldSpec, - lock: threading.RLock, - robot_id: str, - joint_names: list[str], - joint_name_mapping: dict[str, str] | None = None, - timeout: float = 1.0, - ): - """Create a world state monitor. - - Args: - world: WorldSpec instance to sync state to - lock: Shared lock for thread-safe access - robot_id: ID of the robot to monitor - joint_names: Ordered list of joint names for this robot (URDF names) - joint_name_mapping: Maps coordinator joint names to URDF joint names. - Example: {"left_joint1": "joint1"} means messages with "left_joint1" - will be mapped to URDF "joint1". If None, names must match exactly. - timeout: Timeout for waiting for initial state (seconds) - """ - self._world = world - self._lock = lock - self._robot_id = robot_id - self._joint_names = joint_names - self._timeout = timeout - - # Joint name mapping: coordinator name -> URDF name - self._joint_name_mapping = joint_name_mapping or {} - # Build reverse mapping: URDF name -> coordinator name - self._reverse_mapping = {v: k for k, v in self._joint_name_mapping.items()} - - # Latest state - self._latest_positions: NDArray[np.float64] | None = None - self._latest_velocities: NDArray[np.float64] | None = None - self._last_update_time: float | None = None - - # Running state - self._running = False - - # Callbacks: (robot_id, joint_state) called on each state update - self._state_callbacks: list[Callable[[str, JointState], None]] = [] - - def start(self) -> None: - """Start the state monitor.""" - self._running = True - logger.info(f"World state monitor started for robot '{self._robot_id}'") - - def stop(self) -> None: - """Stop the state monitor.""" - self._running = False - logger.info(f"World state monitor stopped for robot '{self._robot_id}'") - - def is_running(self) -> bool: - """Check if monitor is running.""" - return self._running - - @property - def robot_id(self) -> str: - """Get the robot ID being monitored.""" - return self._robot_id - - def on_joint_state(self, msg: JointState) -> None: - """Handle incoming joint state message. - - This is called by the subscriber when a new JointState message arrives. - It extracts joint positions and syncs them to the world. - - Args: - msg: JointState message with joint names and positions - """ - try: - if not self._running: - return - - # Extract positions for our robot's joints - positions = self._extract_positions(msg) - if positions is None: - logger.debug( - "[WorldStateMonitor] Failed to extract positions - joint names mismatch" - ) - logger.debug(f" Expected joints: {self._joint_names}") - logger.debug(f" Received joints: {msg.name}") - return # Not all joints present in message - - velocities = self._extract_velocities(msg) - - # Track message count for debugging - self._msg_count = getattr(self, "_msg_count", 0) + 1 - - with self._lock: - current_time = time.time() - - # Store latest state FIRST - this ensures planning always has - # current positions even if sync_from_joint_state fails - # (e.g., after dynamically adding obstacles) - self._latest_positions = positions - self._latest_velocities = velocities - self._last_update_time = current_time - - # Sync to world's live context (for visualization) - try: - # Create JointState for world sync (API uses JointState) - joint_state = JointState( - name=self._joint_names, - position=positions.tolist(), - ) - self._world.sync_from_joint_state(self._robot_id, joint_state) - except Exception as e: - logger.error(f"Failed to sync joint state to live context: {e}") - - # Call registered callbacks - for callback in self._state_callbacks: - try: - callback(self._robot_id, joint_state) - except Exception as e: - logger.error(f"State callback error: {e}") - - except Exception as e: - logger.error(f"[WorldStateMonitor] Unexpected exception in on_joint_state: {e}") - import traceback - - logger.error(traceback.format_exc()) - - def _extract_positions(self, msg: JointState) -> NDArray[np.float64] | None: - """Extract positions for our joints from JointState message. - - Handles joint name translation from coordinator namespace to URDF namespace. - If joint_name_mapping is set, message names are looked up via the reverse mapping. - - Args: - msg: JointState message (may use coordinator joint names) - - Returns: - Array of joint positions or None if any joint is missing - """ - # Build name->index map from message (coordinator names) - name_to_idx = {name: i for i, name in enumerate(msg.name)} - - positions = [] - for urdf_joint_name in self._joint_names: - # Try direct match first (when no mapping or names already match) - if urdf_joint_name in name_to_idx: - idx = name_to_idx[urdf_joint_name] - else: - # Try reverse mapping: URDF name -> coordinator name -> msg index - orch_name = self._reverse_mapping.get(urdf_joint_name) - if orch_name is None or orch_name not in name_to_idx: - return None # Missing joint - idx = name_to_idx[orch_name] - - if idx >= len(msg.position): - return None # Position not available - positions.append(msg.position[idx]) - - return np.array(positions, dtype=np.float64) - - def _extract_velocities(self, msg: JointState) -> NDArray[np.float64] | None: - """Extract velocities for our joints. - - Uses same name translation as _extract_positions. - """ - if not msg.velocity or len(msg.velocity) == 0: - return None - - name_to_idx = {name: i for i, name in enumerate(msg.name)} - - velocities = [] - for urdf_joint_name in self._joint_names: - # Try direct match first - if urdf_joint_name in name_to_idx: - idx = name_to_idx[urdf_joint_name] - else: - # Try reverse mapping - orch_name = self._reverse_mapping.get(urdf_joint_name) - if orch_name is None or orch_name not in name_to_idx: - return None - idx = name_to_idx[orch_name] - - if idx >= len(msg.velocity): - return None - velocities.append(msg.velocity[idx]) - - return np.array(velocities, dtype=np.float64) - - def get_current_positions(self) -> NDArray[np.float64] | None: - """Get current joint positions (thread-safe). - - Returns: - Current positions or None if not yet received - """ - with self._lock: - return self._latest_positions.copy() if self._latest_positions is not None else None - - def get_current_velocities(self) -> NDArray[np.float64] | None: - """Get current joint velocities (thread-safe). - - Returns: - Current velocities or None if not available - """ - with self._lock: - return self._latest_velocities.copy() if self._latest_velocities is not None else None - - def wait_for_state(self, timeout: float | None = None) -> bool: - """Wait until a state is received. - - Args: - timeout: Maximum time to wait (uses default if None) - - Returns: - True if state was received, False if timeout - """ - timeout = timeout if timeout is not None else self._timeout - start_time = time.time() - - while time.time() - start_time < timeout: - with self._lock: - if self._latest_positions is not None: - return True - time.sleep(0.01) - - return False - - def get_state_age(self) -> float | None: - """Get age of the latest state in seconds. - - Returns: - Age in seconds or None if no state received - """ - with self._lock: - if self._last_update_time is None: - return None - return time.time() - self._last_update_time - - def is_state_stale(self, max_age: float = 1.0) -> bool: - """Check if state is stale (older than max_age). - - Args: - max_age: Maximum acceptable age in seconds - - Returns: - True if state is stale or not received - """ - age = self.get_state_age() - if age is None: - return True - return age > max_age - - def add_state_callback( - self, - callback: Callable[[str, JointState], None], - ) -> None: - """Add callback for state updates. - - Args: - callback: Function called with (robot_id, joint_state) on each update - """ - self._state_callbacks.append(callback) - - def remove_state_callback( - self, - callback: Callable[[str, JointState], None], - ) -> None: - """Remove a state callback.""" - if callback in self._state_callbacks: - self._state_callbacks.remove(callback) diff --git a/dimos/manipulation/planning/planners/__init__.py b/dimos/manipulation/planning/planners/__init__.py deleted file mode 100644 index 8fb8ae042b..0000000000 --- a/dimos/manipulation/planning/planners/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Motion Planners Module - -Contains motion planning implementations that use WorldSpec. - -All planners are backend-agnostic - they only use WorldSpec methods and -work with any physics backend (Drake, MuJoCo, PyBullet, etc.). - -## Implementations - -- RRTConnectPlanner: Bi-directional RRT-Connect planner (fast, reliable) - -## Usage - -Use factory functions to create planners: - -```python -from dimos.manipulation.planning.factory import create_planner - -planner = create_planner(name="rrt_connect") # Returns PlannerSpec -result = planner.plan_joint_path(world, robot_id, q_start, q_goal) -``` -""" - -from dimos.manipulation.planning.planners.rrt_planner import RRTConnectPlanner - -__all__ = ["RRTConnectPlanner"] diff --git a/dimos/manipulation/planning/planners/rrt_planner.py b/dimos/manipulation/planning/planners/rrt_planner.py deleted file mode 100644 index f2be8736d5..0000000000 --- a/dimos/manipulation/planning/planners/rrt_planner.py +++ /dev/null @@ -1,350 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""RRT-Connect and RRT* motion planners implementing PlannerSpec. - -These planners are backend-agnostic - they only use WorldSpec methods and can work -with any physics backend (Drake, MuJoCo, PyBullet, etc.). -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -import time -from typing import TYPE_CHECKING - -import numpy as np - -from dimos.manipulation.planning.spec import ( - JointPath, - PlanningResult, - PlanningStatus, - WorldRobotID, - WorldSpec, -) -from dimos.manipulation.planning.utils.path_utils import compute_path_length -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from numpy.typing import NDArray - -logger = setup_logger() - - -@dataclass(eq=False) -class TreeNode: - """Node in RRT tree with optional cost tracking (for RRT*).""" - - config: NDArray[np.float64] - parent: TreeNode | None = None - children: list[TreeNode] = field(default_factory=list) - cost: float = 0.0 - - def path_to_root(self) -> list[NDArray[np.float64]]: - """Get path from this node to root.""" - path = [] - node: TreeNode | None = self - while node is not None: - path.append(node.config) - node = node.parent - return list(reversed(path)) - - -class RRTConnectPlanner: - """Bi-directional RRT-Connect planner. - - This planner is backend-agnostic - it only uses WorldSpec methods for - collision checking and can work with any physics backend. - """ - - def __init__( - self, - step_size: float = 0.1, - connect_step_size: float = 0.05, - goal_tolerance: float = 0.1, - collision_step_size: float = 0.02, - ): - self._step_size = step_size - self._connect_step_size = connect_step_size - self._goal_tolerance = goal_tolerance - self._collision_step_size = collision_step_size - - def plan_joint_path( - self, - world: WorldSpec, - robot_id: WorldRobotID, - start: JointState, - goal: JointState, - timeout: float = 10.0, - max_iterations: int = 5000, - ) -> PlanningResult: - """Plan collision-free path using bi-directional RRT.""" - start_time = time.time() - - # Extract positions as numpy arrays for internal computation - q_start = np.array(start.position, dtype=np.float64) - q_goal = np.array(goal.position, dtype=np.float64) - joint_names = start.name # Store for converting back to JointState - - error = self._validate_inputs(world, robot_id, start, goal) - if error is not None: - return error - - lower, upper = world.get_joint_limits(robot_id) - start_tree = [TreeNode(config=q_start.copy())] - goal_tree = [TreeNode(config=q_goal.copy())] - trees_swapped = False - - for iteration in range(max_iterations): - if time.time() - start_time > timeout: - return _create_failure_result( - PlanningStatus.TIMEOUT, - f"Timeout after {iteration} iterations", - time.time() - start_time, - iteration, - ) - - sample = np.random.uniform(lower, upper) - extended = self._extend_tree( - world, robot_id, start_tree, sample, self._step_size, joint_names - ) - - if extended is not None: - connected = self._connect_tree( - world, - robot_id, - goal_tree, - extended.config, - self._connect_step_size, - joint_names, - ) - if connected is not None: - path = self._extract_path(extended, connected, joint_names) - if trees_swapped: - path = list(reversed(path)) - path = self._simplify_path(world, robot_id, path) - return _create_success_result(path, time.time() - start_time, iteration + 1) - - start_tree, goal_tree = goal_tree, start_tree - trees_swapped = not trees_swapped - - return _create_failure_result( - PlanningStatus.NO_SOLUTION, - f"No path found after {max_iterations} iterations", - time.time() - start_time, - max_iterations, - ) - - def get_name(self) -> str: - """Get planner name.""" - return "RRTConnect" - - def _validate_inputs( - self, - world: WorldSpec, - robot_id: WorldRobotID, - start: JointState, - goal: JointState, - ) -> PlanningResult | None: - """Validate planning inputs, returns error result or None if valid.""" - # Check world is finalized - if not world.is_finalized: - return _create_failure_result( - PlanningStatus.NO_SOLUTION, - "World must be finalized before planning", - ) - - # Check robot exists - if robot_id not in world.get_robot_ids(): - return _create_failure_result( - PlanningStatus.NO_SOLUTION, - f"Robot '{robot_id}' not found", - ) - - # Check start validity using context-free method - if not world.check_config_collision_free(robot_id, start): - return _create_failure_result( - PlanningStatus.COLLISION_AT_START, - "Start configuration is in collision", - ) - - # Check goal validity using context-free method - if not world.check_config_collision_free(robot_id, goal): - return _create_failure_result( - PlanningStatus.COLLISION_AT_GOAL, - "Goal configuration is in collision", - ) - - # Check limits with small tolerance for driver floating-point drift - lower, upper = world.get_joint_limits(robot_id) - q_start = np.array(start.position, dtype=np.float64) - q_goal = np.array(goal.position, dtype=np.float64) - limit_eps = 1e-3 # ~0.06 degrees - - if np.any(q_start < lower - limit_eps) or np.any(q_start > upper + limit_eps): - return _create_failure_result( - PlanningStatus.INVALID_START, - "Start configuration is outside joint limits", - ) - - if np.any(q_goal < lower - limit_eps) or np.any(q_goal > upper + limit_eps): - return _create_failure_result( - PlanningStatus.INVALID_GOAL, - "Goal configuration is outside joint limits", - ) - - return None - - def _extend_tree( - self, - world: WorldSpec, - robot_id: WorldRobotID, - tree: list[TreeNode], - target: NDArray[np.float64], - step_size: float, - joint_names: list[str], - ) -> TreeNode | None: - """Extend tree toward target, returns new node if successful.""" - # Find nearest node - nearest = min(tree, key=lambda n: float(np.linalg.norm(n.config - target))) - - # Compute new config - diff = target - nearest.config - dist = float(np.linalg.norm(diff)) - - if dist <= step_size: - new_config = target.copy() - else: - new_config = nearest.config + step_size * (diff / dist) - - # Check validity of edge using context-free method - start_state = JointState(name=joint_names, position=nearest.config.tolist()) - end_state = JointState(name=joint_names, position=new_config.tolist()) - if world.check_edge_collision_free( - robot_id, start_state, end_state, self._collision_step_size - ): - new_node = TreeNode(config=new_config, parent=nearest) - nearest.children.append(new_node) - tree.append(new_node) - return new_node - - return None - - def _connect_tree( - self, - world: WorldSpec, - robot_id: WorldRobotID, - tree: list[TreeNode], - target: NDArray[np.float64], - step_size: float, - joint_names: list[str], - ) -> TreeNode | None: - """Try to connect tree to target, returns connected node if successful.""" - # Keep extending toward target - while True: - result = self._extend_tree(world, robot_id, tree, target, step_size, joint_names) - - if result is None: - return None # Extension failed - - # Check if reached target - if float(np.linalg.norm(result.config - target)) < self._goal_tolerance: - return result - - def _extract_path( - self, - start_node: TreeNode, - goal_node: TreeNode, - joint_names: list[str], - ) -> JointPath: - """Extract path from two connected nodes.""" - # Path from start node to its root (reversed to be root->node) - start_path = start_node.path_to_root() - - # Path from goal node to its root - goal_path = goal_node.path_to_root() - - # Combine: start_root -> start_node -> goal_node -> goal_root - # But we need start -> goal, so reverse the goal path - full_path_arrays = start_path + list(reversed(goal_path)) - - # Convert to list of JointState - return [JointState(name=joint_names, position=q.tolist()) for q in full_path_arrays] - - def _simplify_path( - self, - world: WorldSpec, - robot_id: WorldRobotID, - path: JointPath, - max_iterations: int = 100, - ) -> JointPath: - """Simplify path by random shortcutting.""" - if len(path) <= 2: - return path - - simplified = list(path) - - for _ in range(max_iterations): - if len(simplified) <= 2: - break - - # Pick two random indices (at least 2 apart) - i = np.random.randint(0, len(simplified) - 2) - j = np.random.randint(i + 2, len(simplified)) - - # Check if direct connection is valid using context-free method - # path elements are already JointState - if world.check_edge_collision_free( - robot_id, simplified[i], simplified[j], self._collision_step_size - ): - # Remove intermediate waypoints - simplified = simplified[: i + 1] + simplified[j:] - - return simplified - - -# ============= Result Helpers ============= - - -def _create_success_result( - path: JointPath, - planning_time: float, - iterations: int, -) -> PlanningResult: - """Create a successful planning result.""" - return PlanningResult( - status=PlanningStatus.SUCCESS, - path=path, - planning_time=planning_time, - path_length=compute_path_length(path), - iterations=iterations, - message="Path found", - ) - - -def _create_failure_result( - status: PlanningStatus, - message: str, - planning_time: float = 0.0, - iterations: int = 0, -) -> PlanningResult: - """Create a failed planning result.""" - return PlanningResult( - status=status, - path=[], - planning_time=planning_time, - iterations=iterations, - message=message, - ) diff --git a/dimos/manipulation/planning/spec/__init__.py b/dimos/manipulation/planning/spec/__init__.py deleted file mode 100644 index a78fb6e5fd..0000000000 --- a/dimos/manipulation/planning/spec/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Manipulation Planning Specifications.""" - -from dimos.manipulation.planning.spec.config import RobotModelConfig -from dimos.manipulation.planning.spec.enums import IKStatus, ObstacleType, PlanningStatus -from dimos.manipulation.planning.spec.protocols import ( - KinematicsSpec, - PlannerSpec, - WorldSpec, -) -from dimos.manipulation.planning.spec.types import ( - CollisionObjectMessage, - IKResult, - Jacobian, - JointPath, - Obstacle, - PlanningResult, - RobotName, - WorldRobotID, -) - -__all__ = [ - "CollisionObjectMessage", - "IKResult", - "IKStatus", - "Jacobian", - "JointPath", - "KinematicsSpec", - "Obstacle", - "ObstacleType", - "PlannerSpec", - "PlanningResult", - "PlanningStatus", - "RobotModelConfig", - "RobotName", - "WorldRobotID", - "WorldSpec", -] diff --git a/dimos/manipulation/planning/spec/config.py b/dimos/manipulation/planning/spec/config.py deleted file mode 100644 index dc302689ea..0000000000 --- a/dimos/manipulation/planning/spec/config.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Robot configuration for manipulation planning.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pathlib import Path - - from dimos.msgs.geometry_msgs import PoseStamped - - -@dataclass -class RobotModelConfig: - """Configuration for adding a robot to the world. - - Attributes: - name: Human-readable robot name - urdf_path: Path to URDF file (can be .urdf or .xacro) - base_pose: Pose of robot base in world frame (position + orientation) - joint_names: Ordered list of controlled joint names (in URDF namespace) - end_effector_link: Name of the end-effector link for FK/IK - base_link: Name of the base link (default: "base_link") - package_paths: Dict mapping package names to filesystem Paths - joint_limits_lower: Lower joint limits (radians) - joint_limits_upper: Upper joint limits (radians) - velocity_limits: Joint velocity limits (rad/s) - auto_convert_meshes: Auto-convert DAE/STL meshes to OBJ for Drake - xacro_args: Arguments to pass to xacro processor (for .xacro files) - collision_exclusion_pairs: List of (link1, link2) pairs to exclude from collision. - Useful for parallel linkage mechanisms like grippers where non-adjacent - links may legitimately overlap (e.g., mimic joints). - max_velocity: Maximum joint velocity for trajectory generation (rad/s) - max_acceleration: Maximum joint acceleration for trajectory generation (rad/s^2) - joint_name_mapping: Maps coordinator joint names to URDF joint names. - Example: {"left_joint1": "joint1"} means coordinator's "left_joint1" - corresponds to URDF's "joint1". If empty, names are assumed to match. - coordinator_task_name: Task name for executing trajectories via coordinator RPC. - If set, trajectories can be executed via execute_trajectory() RPC. - """ - - name: str - urdf_path: Path - base_pose: PoseStamped - joint_names: list[str] - end_effector_link: str - base_link: str = "base_link" - package_paths: dict[str, Path] = field(default_factory=dict) - joint_limits_lower: list[float] | None = None - joint_limits_upper: list[float] | None = None - velocity_limits: list[float] | None = None - auto_convert_meshes: bool = False - xacro_args: dict[str, str] = field(default_factory=dict) - collision_exclusion_pairs: list[tuple[str, str]] = field(default_factory=list) - # Motion constraints for trajectory generation - max_velocity: float = 1.0 - max_acceleration: float = 2.0 - # Coordinator integration - joint_name_mapping: dict[str, str] = field(default_factory=dict) - coordinator_task_name: str | None = None - gripper_hardware_id: str | None = None - # TF publishing for extra links (e.g., camera mount) - tf_extra_links: list[str] = field(default_factory=list) - # Home/observe joint configuration for go_home skill - home_joints: list[float] | None = None - # Pre-grasp offset distance in meters (along approach direction) - pre_grasp_offset: float = 0.10 - - def get_urdf_joint_name(self, coordinator_name: str) -> str: - """Translate coordinator joint name to URDF joint name.""" - return self.joint_name_mapping.get(coordinator_name, coordinator_name) - - def get_coordinator_joint_name(self, urdf_name: str) -> str: - """Translate URDF joint name to coordinator joint name.""" - for coord_name, u_name in self.joint_name_mapping.items(): - if u_name == urdf_name: - return coord_name - return urdf_name - - def get_coordinator_joint_names(self) -> list[str]: - """Get joint names in coordinator namespace.""" - if not self.joint_name_mapping: - return self.joint_names - return [self.get_coordinator_joint_name(j) for j in self.joint_names] diff --git a/dimos/manipulation/planning/spec/enums.py b/dimos/manipulation/planning/spec/enums.py deleted file mode 100644 index 66a17ee199..0000000000 --- a/dimos/manipulation/planning/spec/enums.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Enumerations for manipulation planning.""" - -from enum import Enum, auto - - -class ObstacleType(Enum): - """Type of obstacle geometry.""" - - BOX = auto() - SPHERE = auto() - CYLINDER = auto() - MESH = auto() - - -class IKStatus(Enum): - """Status of IK solution.""" - - SUCCESS = auto() - NO_SOLUTION = auto() - SINGULARITY = auto() - JOINT_LIMITS = auto() - COLLISION = auto() - TIMEOUT = auto() - - -class PlanningStatus(Enum): - """Status of motion planning.""" - - SUCCESS = auto() - NO_SOLUTION = auto() - TIMEOUT = auto() - INVALID_START = auto() - INVALID_GOAL = auto() - COLLISION_AT_START = auto() - COLLISION_AT_GOAL = auto() diff --git a/dimos/manipulation/planning/spec/protocols.py b/dimos/manipulation/planning/spec/protocols.py deleted file mode 100644 index dea4718abb..0000000000 --- a/dimos/manipulation/planning/spec/protocols.py +++ /dev/null @@ -1,231 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Protocol definitions for manipulation planning. - -All code should use these Protocol types (not concrete classes). -Use factory functions from dimos.manipulation.planning.factory to create instances. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable - -if TYPE_CHECKING: - from contextlib import AbstractContextManager - - import numpy as np - from numpy.typing import NDArray - - from dimos.manipulation.planning.spec.config import RobotModelConfig - from dimos.manipulation.planning.spec.types import ( - IKResult, - JointPath, - Obstacle, - PlanningResult, - WorldRobotID, - ) - from dimos.msgs.geometry_msgs import PoseStamped - from dimos.msgs.sensor_msgs import JointState - - -@runtime_checkable -class WorldSpec(Protocol): - """Protocol for the world/scene backend. - - The world owns the physics/collision backend and provides: - - Robot/obstacle management - - Collision checking - - Forward kinematics - - Context management for thread safety - - Context Management: - - Live context: Mirrors current robot state (synced from driver) - - Scratch contexts: Thread-safe clones for planning/IK operations - - Implementations: - - DrakeWorld: Uses Drake's MultibodyPlant and SceneGraph - """ - - # Robot Management - def add_robot(self, config: RobotModelConfig) -> WorldRobotID: - """Add a robot to the world. Returns unique robot ID.""" - ... - - def get_robot_ids(self) -> list[WorldRobotID]: - """Get all robot IDs.""" - ... - - def get_robot_config(self, robot_id: WorldRobotID) -> RobotModelConfig: - """Get robot configuration.""" - ... - - def get_joint_limits( - self, robot_id: WorldRobotID - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: # lower limits, upper limits - """Get joint limits (lower, upper) for a robot.""" - ... - - # Obstacle Management - def add_obstacle(self, obstacle: Obstacle) -> str: - """Add an obstacle to the world. Returns unique obstacle ID.""" - ... - - def remove_obstacle(self, obstacle_id: str) -> bool: - """Remove an obstacle. Returns True if removed.""" - ... - - def update_obstacle_pose(self, obstacle_id: str, pose: PoseStamped) -> bool: - """Update obstacle pose. Returns True if updated.""" - ... - - def clear_obstacles(self) -> None: - """Remove all obstacles.""" - ... - - # Lifecycle - def finalize(self) -> None: - """Finalize the world. Must be called after adding robots.""" - ... - - @property - def is_finalized(self) -> bool: - """Check if world is finalized.""" - ... - - # Context Management - def get_live_context(self) -> Any: - """Get the live context (mirrors real robot state).""" - ... - - def scratch_context(self) -> AbstractContextManager[Any]: - """Get a scratch context for planning (thread-safe clone).""" - ... - - def sync_from_joint_state(self, robot_id: WorldRobotID, joint_state: JointState) -> None: - """Sync live context from joint state message.""" - ... - - # State Operations (require context) - def set_joint_state(self, ctx: Any, robot_id: WorldRobotID, joint_state: JointState) -> None: - """Set robot joint state in a context.""" - ... - - def get_joint_state(self, ctx: Any, robot_id: WorldRobotID) -> JointState: - """Get robot joint state from a context.""" - ... - - # Collision Checking (require context) - def is_collision_free(self, ctx: Any, robot_id: WorldRobotID) -> bool: - """Check if robot configuration is collision-free.""" - ... - - def get_min_distance(self, ctx: Any, robot_id: WorldRobotID) -> float: - """Get minimum distance to obstacles (negative if collision).""" - ... - - # Collision Checking (context-free, for planning) - def check_config_collision_free(self, robot_id: WorldRobotID, joint_state: JointState) -> bool: - """Check if a joint state is collision-free (manages context internally).""" - ... - - def check_edge_collision_free( - self, - robot_id: WorldRobotID, - start: JointState, - end: JointState, - step_size: float = 0.05, - ) -> bool: - """Check if the entire edge between two joint states is collision-free.""" - ... - - # Forward Kinematics (require context) - def get_ee_pose(self, ctx: Any, robot_id: WorldRobotID) -> PoseStamped: - """Get end-effector pose.""" - ... - - def get_link_pose( - self, ctx: Any, robot_id: WorldRobotID, link_name: str - ) -> NDArray[np.float64]: - """Get link pose as 4x4 homogeneous transform.""" - ... - - def get_jacobian(self, ctx: Any, robot_id: WorldRobotID) -> NDArray[np.float64]: - """Get end-effector Jacobian (6 x n_joints).""" - ... - - # Visualization (optional) - def get_visualization_url(self) -> str | None: - """Get visualization URL if enabled.""" - ... - - def publish_visualization(self, ctx: Any | None = None) -> None: - """Publish current state to visualization.""" - ... - - def animate_path(self, robot_id: WorldRobotID, path: JointPath, duration: float = 3.0) -> None: - """Animate a path in visualization.""" - ... - - def close(self) -> None: - """Release visualization resources.""" - ... - - -@runtime_checkable -class KinematicsSpec(Protocol): - """Protocol for inverse kinematics solvers. Stateless, uses WorldSpec for FK/collision.""" - - def solve( - self, - world: WorldSpec, - robot_id: WorldRobotID, - target_pose: PoseStamped, - seed: JointState | None = None, - position_tolerance: float = 0.001, - orientation_tolerance: float = 0.01, - check_collision: bool = True, - max_attempts: int = 10, - ) -> IKResult: - """Solve IK with optional collision checking.""" - ... - - -@runtime_checkable -class PlannerSpec(Protocol): - """Protocol for motion planner. - - Planners find collision-free paths from start to goal configurations. - They use WorldSpec for collision checking and are stateless. - All planners are backend-agnostic - they only use WorldSpec methods. - - Implementations: - - RRTConnectPlanner: Bi-directional RRT-Connect planner - - RRTStarPlanner: RRT* planner (asymptotically optimal) - """ - - def plan_joint_path( - self, - world: WorldSpec, - robot_id: WorldRobotID, - start: JointState, - goal: JointState, - timeout: float = 10.0, - ) -> PlanningResult: - """Plan a collision-free joint-space path.""" - ... - - def get_name(self) -> str: - """Get planner name.""" - ... diff --git a/dimos/manipulation/planning/spec/types.py b/dimos/manipulation/planning/spec/types.py deleted file mode 100644 index a38cc0da26..0000000000 --- a/dimos/manipulation/planning/spec/types.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Data types for manipulation planning.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, TypeAlias - -from dimos.manipulation.planning.spec.enums import ( - IKStatus, - ObstacleType, - PlanningStatus, -) - -if TYPE_CHECKING: - import numpy as np - from numpy.typing import NDArray - - from dimos.msgs.geometry_msgs import PoseStamped - from dimos.msgs.sensor_msgs import JointState - -# ============================================================================= -# Semantic ID Types (documentation only, not enforced at runtime) -# ============================================================================= - -RobotName: TypeAlias = str -"""User-facing robot name (e.g., 'left_arm', 'right_arm')""" - -WorldRobotID: TypeAlias = str -"""Internal Drake world robot ID""" - -JointPath: TypeAlias = "list[JointState]" -"""List of joint states forming a path (each waypoint has names + positions)""" - -# ============================================================================= -# Numeric Array Types -# ============================================================================= - -Jacobian: TypeAlias = "NDArray[np.float64]" -"""6 x n Jacobian matrix (rows: [vx, vy, vz, wx, wy, wz])""" - - -# ============================================================================= -# Data Classes -# ============================================================================= - - -@dataclass -class Obstacle: - """Obstacle specification for collision avoidance. - - Attributes: - name: Unique name for the obstacle - obstacle_type: Type of geometry (BOX, SPHERE, CYLINDER, MESH) - pose: Pose of the obstacle in world frame - dimensions: Type-specific dimensions: - - BOX: (width, height, depth) - - SPHERE: (radius,) - - CYLINDER: (radius, height) - - MESH: Not used - color: RGBA color tuple (0-1 range) - mesh_path: Path to mesh file (for MESH type) - """ - - name: str - obstacle_type: ObstacleType - pose: PoseStamped - dimensions: tuple[float, ...] = () - color: tuple[float, float, float, float] = (0.8, 0.2, 0.2, 0.8) - mesh_path: str | None = None - - -@dataclass -class IKResult: - """Result of an IK solve. - - Attributes: - status: Solution status - joint_state: Solution joint state with names and positions (None if failed) - position_error: Cartesian position error (meters) - orientation_error: Orientation error (radians) - iterations: Number of iterations taken - message: Human-readable status message - """ - - status: IKStatus - joint_state: JointState | None = None - position_error: float = 0.0 - orientation_error: float = 0.0 - iterations: int = 0 - message: str = "" - - def is_success(self) -> bool: - """Check if IK was successful.""" - return self.status == IKStatus.SUCCESS - - -@dataclass -class PlanningResult: - """Result of motion planning. - - Attributes: - status: Planning status - path: List of joint states forming the path (empty if failed). - Each JointState contains names, positions, and optionally velocities. - planning_time: Time taken to plan (seconds) - path_length: Total path length in joint space (radians) - iterations: Number of iterations/nodes expanded - message: Human-readable status message - timestamps: Optional timestamps for each waypoint (seconds from start). - If provided by the planner, trajectory generator can use these directly. - """ - - status: PlanningStatus - path: list[JointState] = field(default_factory=list) - planning_time: float = 0.0 - path_length: float = 0.0 - iterations: int = 0 - message: str = "" - # Optional timing (set by optimization-based planners) - timestamps: list[float] | None = None - - def is_success(self) -> bool: - """Check if planning was successful.""" - return self.status == PlanningStatus.SUCCESS - - -@dataclass -class CollisionObjectMessage: - """Message for adding/updating/removing obstacles. - - Used by monitors to handle obstacle updates from external sources. - - Attributes: - id: Unique identifier for the object - operation: "add", "update", or "remove" - primitive_type: "box", "sphere", or "cylinder" (for add/update) - pose: Pose of the obstacle (for add/update) - dimensions: Type-specific dimensions (for add/update) - color: RGBA color tuple - """ - - id: str - operation: str # "add", "update", "remove" - primitive_type: str | None = None - pose: PoseStamped | None = None - dimensions: tuple[float, ...] | None = None - color: tuple[float, float, float, float] = (0.8, 0.2, 0.2, 0.8) diff --git a/dimos/manipulation/planning/trajectory_generator/__init__.py b/dimos/manipulation/planning/trajectory_generator/__init__.py deleted file mode 100644 index a7449cf45f..0000000000 --- a/dimos/manipulation/planning/trajectory_generator/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Trajectory Generator Module - -Generates time-parameterized trajectories from waypoints. -""" - -from dimos.manipulation.planning.trajectory_generator.joint_trajectory_generator import ( - JointTrajectoryGenerator, -) - -__all__ = ["JointTrajectoryGenerator"] diff --git a/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py b/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py deleted file mode 100644 index 6b732d133c..0000000000 --- a/dimos/manipulation/planning/trajectory_generator/joint_trajectory_generator.py +++ /dev/null @@ -1,453 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Joint Trajectory Generator - -Generates time-parameterized joint trajectories from waypoints using -trapezoidal velocity profiles. - -Trapezoidal Profile: - velocity - ^ - | ____________________ - | / \ - | / \ - | / \ - |/ \ - +------------------------------> time - accel cruise decel -""" - -import math - -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryPoint - - -class JointTrajectoryGenerator: - """ - Generates joint trajectories with trapezoidal velocity profiles. - - For each segment between waypoints: - 1. Determines the limiting joint (one that takes longest) - 2. Applies trapezoidal velocity profile based on limits - 3. Scales other joints to complete in the same time - 4. Generates trajectory points with proper timing - - Usage: - generator = JointTrajectoryGenerator(num_joints=6) - generator.set_limits(max_velocity=1.0, max_acceleration=2.0) - trajectory = generator.generate(waypoints) - """ - - def __init__( - self, - num_joints: int = 6, - max_velocity: list[float] | float = 1.0, - max_acceleration: list[float] | float = 2.0, - points_per_segment: int = 50, - ) -> None: - """ - Initialize trajectory generator. - - Args: - num_joints: Number of joints - max_velocity: rad/s (single value applies to all joints, or per-joint list) - max_acceleration: rad/s^2 (single value or per-joint list) - points_per_segment: Number of intermediate points per waypoint segment - """ - self.num_joints = num_joints - self.points_per_segment = points_per_segment - - # Initialize limits - self.max_velocity: list[float] = [] - self.max_acceleration: list[float] = [] - self.set_limits(max_velocity, max_acceleration) - - def set_limits( - self, - max_velocity: list[float] | float, - max_acceleration: list[float] | float, - ) -> None: - """ - Set velocity and acceleration limits. - - Args: - max_velocity: rad/s (single value applies to all joints, or per-joint) - max_acceleration: rad/s^2 (single value or per-joint) - """ - if isinstance(max_velocity, (int, float)): - self.max_velocity = [float(max_velocity)] * self.num_joints - else: - self.max_velocity = list(max_velocity) - - if isinstance(max_acceleration, (int, float)): - self.max_acceleration = [float(max_acceleration)] * self.num_joints - else: - self.max_acceleration = list(max_acceleration) - - def generate(self, waypoints: list[list[float]]) -> JointTrajectory: - """ - Generate a trajectory through waypoints with trapezoidal velocity profile. - - Args: - waypoints: List of joint positions [q1, q2, ..., qn] in radians - First waypoint is start, last is goal - - Returns: - JointTrajectory with time-parameterized points - """ - if not waypoints or len(waypoints) < 2: - raise ValueError("Need at least 2 waypoints") - - all_points: list[TrajectoryPoint] = [] - current_time = 0.0 - - # Add first waypoint - all_points.append( - TrajectoryPoint( - time_from_start=0.0, - positions=list(waypoints[0]), - velocities=[0.0] * self.num_joints, - ) - ) - - # Process each segment - for i in range(len(waypoints) - 1): - start = waypoints[i] - end = waypoints[i + 1] - - # Generate segment with trapezoidal profile - segment_points, segment_duration = self._generate_segment(start, end, current_time) - - # Add points (skip first as it duplicates previous endpoint) - all_points.extend(segment_points[1:]) - current_time += segment_duration - - return JointTrajectory(points=all_points) - - def _generate_segment( - self, - start: list[float], - end: list[float], - start_time: float, - ) -> tuple[list[TrajectoryPoint], float]: - """ - Generate trajectory points for a single segment using trapezoidal profile. - - Args: - start: Starting joint positions - end: Ending joint positions - start_time: Time offset for this segment - - Returns: - Tuple of (list of TrajectoryPoints, segment duration) - """ - # Calculate displacement for each joint - displacements = [end[j] - start[j] for j in range(self.num_joints)] - - # Find the limiting joint (one that takes longest) - segment_duration = 0.0 - for j in range(self.num_joints): - t = self._compute_trapezoidal_time( - abs(displacements[j]), - self.max_velocity[j], - self.max_acceleration[j], - ) - segment_duration = max(segment_duration, t) - - # Ensure minimum duration - segment_duration = max(segment_duration, 0.01) - - # Generate points along the segment - points: list[TrajectoryPoint] = [] - - for i in range(self.points_per_segment + 1): - # Normalized time [0, 1] - s = i / self.points_per_segment - t = start_time + s * segment_duration - - # Compute position and velocity for each joint - positions = [] - velocities = [] - - for j in range(self.num_joints): - # Compute scaled limits for this joint to fit in segment_duration - v_scaled, a_scaled = self._compute_scaled_limits( - abs(displacements[j]), - segment_duration, - self.max_velocity[j], - self.max_acceleration[j], - ) - - pos, vel = self._trapezoidal_interpolate( - s, - start[j], - end[j], - segment_duration, - v_scaled, - a_scaled, - ) - positions.append(pos) - velocities.append(vel) - - points.append( - TrajectoryPoint( - time_from_start=t, - positions=positions, - velocities=velocities, - ) - ) - - return points, segment_duration - - def _compute_trapezoidal_time( - self, - distance: float, - v_max: float, - a_max: float, - ) -> float: - """ - Compute time to travel a distance with trapezoidal velocity profile. - - Two cases: - 1. Triangle profile: Can't reach v_max (short distance) - 2. Trapezoidal profile: Reaches v_max with cruise phase - - Args: - distance: Absolute distance to travel - v_max: Maximum velocity - a_max: Maximum acceleration - - Returns: - Time to complete the motion - """ - if distance < 1e-9: - return 0.0 - - # Time to accelerate to v_max - t_accel = v_max / a_max - - # Distance covered during accel + decel (both at a_max) - d_accel = 0.5 * a_max * t_accel**2 - d_total_ramp = 2 * d_accel # accel + decel - - if distance <= d_total_ramp: - # Triangle profile - can't reach v_max - # d = 2 * (0.5 * a * t^2) = a * t^2 - # t = sqrt(d / a) - t_ramp = math.sqrt(distance / a_max) - return 2 * t_ramp - else: - # Trapezoidal profile - has cruise phase - d_cruise = distance - d_total_ramp - t_cruise = d_cruise / v_max - return 2 * t_accel + t_cruise - - def _compute_scaled_limits( - self, - distance: float, - duration: float, - v_max: float, - a_max: float, - ) -> tuple[float, float]: - """ - Compute scaled velocity and acceleration to travel distance in given duration. - - This scales down the profile so the joint travels its distance in the - same time as the limiting joint. - - Args: - distance: Absolute distance to travel - duration: Required duration (from limiting joint) - v_max: Maximum velocity limit - a_max: Maximum acceleration limit - - Returns: - Tuple of (scaled_velocity, scaled_acceleration) - """ - if distance < 1e-9 or duration < 1e-9: - return v_max, a_max - - # Compute optimal time for this joint - t_opt = self._compute_trapezoidal_time(distance, v_max, a_max) - - if t_opt >= duration - 1e-9: - # This is the limiting joint or close to it - return v_max, a_max - - # Need to scale down to fit in longer duration - # Use simple scaling: scale both v and a by the same factor - # This preserves the profile shape - scale = t_opt / duration - - # For a symmetric trapezoidal/triangular profile: - # If we scale time by k, we need to scale velocity by 1/k - # But we also need to ensure we travel the same distance - - # Simpler approach: compute the average velocity needed - distance / duration - - # For trapezoidal profile, v_avg = v_peak * (1 - t_accel/duration) - # For simplicity, use a heuristic: scale velocity so trajectory fits - - # Check if we can use a triangle profile - # Triangle: d = 0.5 * v_peak * T, so v_peak = 2 * d / T - v_peak_triangle = 2 * distance / duration - a_for_triangle = 4 * distance / (duration * duration) - - if v_peak_triangle <= v_max and a_for_triangle <= a_max: - # Use triangle profile with these params - return v_peak_triangle, a_for_triangle - - # Use trapezoidal with reduced velocity - # Solve: distance = v * t_cruise + v^2/a - # where t_cruise = duration - 2*v/a - # This is complex, so use iterative scaling - v_scaled = v_max * scale - a_scaled = a_max * scale * scale # acceleration scales with square of time scale - - # Verify and adjust - t_check = self._compute_trapezoidal_time(distance, v_scaled, a_scaled) - if abs(t_check - duration) > 0.01 * duration: - # Fallback: use triangle profile scaled to fit - v_scaled = 2 * distance / duration - a_scaled = 4 * distance / (duration * duration) - - return min(v_scaled, v_max), min(a_scaled, a_max) - - def _trapezoidal_interpolate( - self, - s: float, - start: float, - end: float, - duration: float, - v_max: float, - a_max: float, - ) -> tuple[float, float]: - """ - Interpolate position and velocity using trapezoidal profile. - - Args: - s: Normalized time [0, 1] - start: Start position - end: End position - duration: Total segment duration - v_max: Max velocity for this joint (scaled) - a_max: Max acceleration for this joint (scaled) - - Returns: - Tuple of (position, velocity) - """ - distance = abs(end - start) - direction = 1.0 if end >= start else -1.0 - - if distance < 1e-9 or duration < 1e-9: - return end, 0.0 - - # Handle endpoint exactly - if s >= 1.0 - 1e-9: - return end, 0.0 - if s <= 1e-9: - return start, 0.0 - - # Current time - t = s * duration - - # Compute profile parameters for this joint - t_accel = v_max / a_max if a_max > 1e-9 else duration / 2 - d_accel = 0.5 * a_max * t_accel**2 - d_total_ramp = 2 * d_accel - - if distance <= d_total_ramp + 1e-9: - # Triangle profile - t_peak = duration / 2 - v_peak = 2 * distance / duration - a_eff = v_peak / t_peak if t_peak > 1e-9 else a_max - - if t <= t_peak: - # Accelerating - pos_offset = 0.5 * a_eff * t * t - vel = direction * a_eff * t - else: - # Decelerating - dt = t - t_peak - pos_offset = distance / 2 + v_peak * dt - 0.5 * a_eff * dt * dt - vel = direction * max(0.0, v_peak - a_eff * dt) - else: - # Trapezoidal profile - d_cruise = distance - d_total_ramp - t_cruise = d_cruise / v_max if v_max > 1e-9 else 0 - - if t <= t_accel: - # Accelerating phase - pos_offset = 0.5 * a_max * t * t - vel = direction * a_max * t - elif t <= t_accel + t_cruise: - # Cruise phase - dt = t - t_accel - pos_offset = d_accel + v_max * dt - vel = direction * v_max - else: - # Decelerating phase - dt = t - t_accel - t_cruise - pos_offset = d_accel + d_cruise + v_max * dt - 0.5 * a_max * dt * dt - vel = direction * max(0.0, v_max - a_max * dt) - - position = start + direction * pos_offset - - # Clamp to ensure we don't overshoot - if direction > 0: - position = min(position, end) - else: - position = max(position, end) - - return position, vel - - def preview(self, trajectory: JointTrajectory) -> str: - """ - Generate a text preview of the trajectory. - - Args: - trajectory: Generated trajectory to preview - - Returns: - Formatted string showing trajectory details - """ - lines = [ - "Trajectory Preview", - "=" * 60, - f"Duration: {trajectory.duration:.3f}s", - f"Points: {len(trajectory.points)}", - "", - "Waypoints (time -> positions):", - "-" * 60, - ] - - # Show key points (first, last, and evenly spaced) - indices = [0] - step = max(1, len(trajectory.points) // 5) - indices.extend(range(step, len(trajectory.points) - 1, step)) - indices.append(len(trajectory.points) - 1) - indices = sorted(set(indices)) - - for i in indices: - pt = trajectory.points[i] - pos_str = ", ".join(f"{p:+.3f}" for p in pt.positions) - vel_str = ", ".join(f"{v:+.3f}" for v in pt.velocities) - lines.append(f" t={pt.time_from_start:6.3f}s: pos=[{pos_str}]") - lines.append(f" vel=[{vel_str}]") - - lines.append("-" * 60) - return "\n".join(lines) diff --git a/dimos/manipulation/planning/trajectory_generator/spec.py b/dimos/manipulation/planning/trajectory_generator/spec.py deleted file mode 100644 index 5357679f28..0000000000 --- a/dimos/manipulation/planning/trajectory_generator/spec.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Joint Trajectory Generator Specification - -Generates time-parameterized joint trajectories from waypoints using -trapezoidal velocity profiles. Does NOT execute - just generates. - -Input: List of joint positions (waypoints) without timing -Output: JointTrajectory with proper time parameterization - -Trapezoidal Profile: - velocity - ^ - | ____________________ - | / \ - | / \ - | / \ - |/ \ - +------------------------------> time - accel cruise decel -""" - -from typing import Protocol - -from dimos.msgs.trajectory_msgs import JointTrajectory - - -class JointTrajectoryGeneratorSpec(Protocol): - """Protocol for joint trajectory generator. - - Generates time-parameterized trajectories from waypoints. - """ - - # Configuration - max_velocity: list[float] # rad/s per joint - max_acceleration: list[float] # rad/s^2 per joint - - def generate(self, waypoints: list[list[float]]) -> JointTrajectory: - """ - Generate a trajectory through waypoints with trapezoidal velocity profile. - - Args: - waypoints: List of joint positions [q1, q2, ..., qn] in radians - First waypoint is start, last is goal - - Returns: - JointTrajectory with time-parameterized points - """ - ... - - def set_limits( - self, - max_velocity: list[float] | float, - max_acceleration: list[float] | float, - ) -> None: - """ - Set velocity and acceleration limits. - - Args: - max_velocity: rad/s (single value applies to all joints, or per-joint) - max_acceleration: rad/s^2 (single value or per-joint) - """ - ... diff --git a/dimos/manipulation/planning/utils/__init__.py b/dimos/manipulation/planning/utils/__init__.py deleted file mode 100644 index 04ec1806b5..0000000000 --- a/dimos/manipulation/planning/utils/__init__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Manipulation Planning Utilities - -Standalone utility functions for kinematics and path operations. -These are extracted from the old ABC base classes to enable composition over inheritance. - -## Modules - -- kinematics_utils: Jacobian operations, singularity detection, pose error computation -- path_utils: Path interpolation, simplification, length computation -""" - -from dimos.manipulation.planning.utils.kinematics_utils import ( - check_singularity, - compute_error_twist, - compute_pose_error, - damped_pseudoinverse, - get_manipulability, -) -from dimos.manipulation.planning.utils.path_utils import ( - compute_path_length, - interpolate_path, - interpolate_segment, -) - -__all__ = [ - # Kinematics utilities - "check_singularity", - "compute_error_twist", - # Path utilities - "compute_path_length", - "compute_pose_error", - "damped_pseudoinverse", - "get_manipulability", - "interpolate_path", - "interpolate_segment", -] diff --git a/dimos/manipulation/planning/utils/kinematics_utils.py b/dimos/manipulation/planning/utils/kinematics_utils.py deleted file mode 100644 index c9f3f95a3d..0000000000 --- a/dimos/manipulation/planning/utils/kinematics_utils.py +++ /dev/null @@ -1,296 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Kinematics Utilities - -Standalone utility functions for inverse kinematics operations. -These functions are stateless and can be used by any IK solver implementation. - -## Functions - -- damped_pseudoinverse(): Compute damped pseudoinverse of Jacobian -- check_singularity(): Check if Jacobian is near singularity -- get_manipulability(): Compute manipulability measure -- compute_pose_error(): Compute position/orientation error between poses -- compute_error_twist(): Compute error twist for differential IK -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -if TYPE_CHECKING: - from numpy.typing import NDArray - - from dimos.manipulation.planning.spec import Jacobian - - -def damped_pseudoinverse( - J: Jacobian, - damping: float = 0.01, -) -> NDArray[np.float64]: - """Compute damped pseudoinverse of Jacobian. - - Uses the damped least-squares formula: - J_pinv = J^T @ (J @ J^T + λ²I)^(-1) - - This avoids numerical issues near singularities where J @ J^T becomes - ill-conditioned. The damping factor λ controls the trade-off between - accuracy and stability. - - Args: - J: 6 x n Jacobian matrix (rows: [vx, vy, vz, wx, wy, wz]) - damping: Damping factor λ (higher = more regularization, more stable) - - Returns: - n x 6 pseudoinverse matrix - - Example: - J = world.get_jacobian(ctx, robot_id) - J_pinv = damped_pseudoinverse(J, damping=0.01) - q_dot = J_pinv @ twist - """ - JJT = J @ J.T - I = np.eye(JJT.shape[0]) - result: NDArray[np.float64] = J.T @ np.linalg.inv(JJT + damping**2 * I) - return result - - -def check_singularity( - J: Jacobian, - threshold: float = 0.01, -) -> bool: - """Check if Jacobian is near singularity. - - Computes the manipulability measure (sqrt(det(J @ J^T))) and checks - if it's below the threshold. Near singularities, the manipulability - approaches zero. - - Args: - J: 6 x n Jacobian matrix - threshold: Manipulability threshold (default 0.01) - - Returns: - True if near singularity (manipulability < threshold) - - Example: - J = world.get_jacobian(ctx, robot_id) - if check_singularity(J, threshold=0.001): - logger.warning("Near singularity, using damped IK") - """ - return get_manipulability(J) < threshold - - -def get_manipulability(J: Jacobian) -> float: - """Compute manipulability measure. - - The manipulability measure w = sqrt(det(J @ J^T)) represents the - volume of the velocity ellipsoid - how well the robot can move - in all directions. - - Values: - - Higher = better manipulability - - Zero = singularity - - Lower = near singularity - - Args: - J: 6 x n Jacobian matrix - - Returns: - Manipulability measure (non-negative) - - Example: - J = world.get_jacobian(ctx, robot_id) - w = get_manipulability(J) - print(f"Manipulability: {w:.4f}") - """ - JJT = J @ J.T - det = np.linalg.det(JJT) - return float(np.sqrt(max(0, det))) - - -def compute_pose_error( - current_pose: NDArray[np.float64], - target_pose: NDArray[np.float64], -) -> tuple[float, float]: - """Compute position and orientation error between two poses. - - Position error is the Euclidean distance between origins. - Orientation error is the angle of the rotation matrix relating the two frames. - - Args: - current_pose: Current 4x4 homogeneous transform - target_pose: Target 4x4 homogeneous transform - - Returns: - Tuple of (position_error, orientation_error) in meters and radians - - Example: - current = world.get_ee_pose(ctx, robot_id) - pos_err, ori_err = compute_pose_error(current, target) - converged = pos_err < 0.001 and ori_err < 0.01 - """ - # Position error (Euclidean distance) - position_error = float(np.linalg.norm(target_pose[:3, 3] - current_pose[:3, 3])) - - # Orientation error using rotation matrices - R_current = current_pose[:3, :3] - R_target = target_pose[:3, :3] - R_error = R_target @ R_current.T - - # Convert to axis-angle for scalar error - trace = np.trace(R_error) - # Clamp to valid range for arccos (numerical stability) - cos_angle = (trace - 1) / 2 - cos_angle = np.clip(cos_angle, -1, 1) - orientation_error = float(np.arccos(cos_angle)) - - return position_error, orientation_error - - -def compute_error_twist( - current_pose: NDArray[np.float64], - target_pose: NDArray[np.float64], - gain: float = 1.0, -) -> NDArray[np.float64]: - """Compute error twist for differential IK. - - Computes the 6D twist (linear + angular velocity) that would move - from the current pose toward the target pose. Used in iterative IK. - - The twist is expressed in the world frame: - twist = [vx, vy, vz, wx, wy, wz] - - Args: - current_pose: Current 4x4 homogeneous transform - target_pose: Target 4x4 homogeneous transform - gain: Proportional gain (higher = faster convergence, less stable) - - Returns: - 6D twist vector [vx, vy, vz, wx, wy, wz] - - Example: - twist = compute_error_twist(current_pose, target_pose, gain=0.5) - q_dot = damped_pseudoinverse(J) @ twist - q_new = q + q_dot * dt - """ - # Position error (linear velocity direction) - pos_error = target_pose[:3, 3] - current_pose[:3, 3] - - # Orientation error -> angular velocity - R_current = current_pose[:3, :3] - R_target = target_pose[:3, :3] - R_error = R_target @ R_current.T - - # Extract axis-angle from rotation matrix - # Using Rodrigues' formula inverse - trace = np.trace(R_error) - cos_angle = (trace - 1) / 2 - cos_angle = np.clip(cos_angle, -1, 1) - angle = np.arccos(cos_angle) - - if angle < 1e-6: - # No rotation needed - angular_error = np.zeros(3) - elif angle > np.pi - 1e-6: - # 180 degree rotation - extract axis from diagonal - diag = np.diag(R_error) - idx = np.argmax(diag) - axis = np.zeros(3) - axis[idx] = 1.0 - # Refine axis - axis = axis * np.sqrt((diag[idx] + 1) / 2) - angular_error = axis * angle - else: - # General case: axis from skew-symmetric part - sin_angle = np.sin(angle) - axis = np.array( - [ - R_error[2, 1] - R_error[1, 2], - R_error[0, 2] - R_error[2, 0], - R_error[1, 0] - R_error[0, 1], - ] - ) / (2 * sin_angle) - angular_error = axis * angle - - # Combine into twist with gain - twist: NDArray[np.float64] = np.concatenate([pos_error * gain, angular_error * gain]) - - return twist - - -def skew_symmetric(v: NDArray[np.float64]) -> NDArray[np.float64]: - """Create skew-symmetric matrix from 3D vector. - - The skew-symmetric matrix [v]_x satisfies: [v]_x @ w = v cross w - - Args: - v: 3D vector - - Returns: - 3x3 skew-symmetric matrix - """ - return np.array( - [ - [0, -v[2], v[1]], - [v[2], 0, -v[0]], - [-v[1], v[0], 0], - ] - ) - - -def rotation_matrix_to_axis_angle(R: NDArray[np.float64]) -> tuple[NDArray[np.float64], float]: - """Convert rotation matrix to axis-angle representation. - - Args: - R: 3x3 rotation matrix - - Returns: - Tuple of (axis, angle) where axis is unit vector and angle is radians - """ - trace = np.trace(R) - cos_angle = (trace - 1) / 2 - cos_angle = np.clip(cos_angle, -1, 1) - angle = float(np.arccos(cos_angle)) - - if angle < 1e-6: - # Identity rotation - return np.array([1.0, 0.0, 0.0]), 0.0 - - if angle > np.pi - 1e-6: - # 180 degree rotation - diag = np.diag(R) - idx = int(np.argmax(diag)) - axis = np.zeros(3) - axis[idx] = np.sqrt((diag[idx] + 1) / 2) - if axis[idx] > 1e-12: - for j in range(3): - if j != idx: - axis[j] = R[idx, j] / (2 * axis[idx]) - return axis, angle - - # General case - sin_angle = np.sin(angle) - axis = np.array( - [ - R[2, 1] - R[1, 2], - R[0, 2] - R[2, 0], - R[1, 0] - R[0, 1], - ] - ) / (2 * sin_angle) - - return axis, angle diff --git a/dimos/manipulation/planning/utils/mesh_utils.py b/dimos/manipulation/planning/utils/mesh_utils.py deleted file mode 100644 index 92fcfc6eca..0000000000 --- a/dimos/manipulation/planning/utils/mesh_utils.py +++ /dev/null @@ -1,354 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Mesh Utilities for Drake - -Provides utilities for preparing URDF files for use with Drake: -- Xacro processing -- Mesh format conversion (DAE/STL to OBJ) -- Package path resolution - -Example: - urdf_path = prepare_urdf_for_drake( - urdf_path="/path/to/robot.xacro", - package_paths={"robot_description": "/path/to/robot_description"}, - xacro_args={"use_sim": "true"}, - convert_meshes=True, - ) -""" - -from __future__ import annotations - -import hashlib -from pathlib import Path -import re -import shutil -import tempfile -from typing import TYPE_CHECKING - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - import numpy as np - from numpy.typing import NDArray - -logger = setup_logger() - -# Cache directory for processed URDFs -_CACHE_DIR = Path(tempfile.gettempdir()) / "dimos_urdf_cache" - - -def prepare_urdf_for_drake( - urdf_path: Path | str, - package_paths: dict[str, Path] | None = None, - xacro_args: dict[str, str] | None = None, - convert_meshes: bool = False, -) -> str: - """Prepare a URDF/xacro file for use with Drake. - - This function: - 1. Processes xacro files if needed - 2. Resolves package:// URIs in mesh paths - 3. Optionally converts DAE/STL meshes to OBJ format - - Args: - urdf_path: Path to URDF or xacro file - package_paths: Dict mapping package names to filesystem paths - xacro_args: Arguments to pass to xacro processor - convert_meshes: Convert DAE/STL meshes to OBJ for Drake compatibility - - Returns: - Path to the prepared URDF file (may be cached) - """ - urdf_path = Path(urdf_path) - package_paths = package_paths or {} - xacro_args = xacro_args or {} - - # Generate cache key - cache_key = _generate_cache_key(urdf_path, package_paths, xacro_args, convert_meshes) - cache_path = _CACHE_DIR / cache_key / urdf_path.stem - cache_path.mkdir(parents=True, exist_ok=True) - cached_urdf = cache_path / f"{urdf_path.stem}.urdf" - - # Check cache - if cached_urdf.exists(): - logger.debug(f"Using cached URDF: {cached_urdf}") - return str(cached_urdf) - - # Process xacro if needed - if urdf_path.suffix in (".xacro", ".urdf.xacro"): - urdf_content = _process_xacro(urdf_path, package_paths, xacro_args) - else: - urdf_content = urdf_path.read_text() - - # Strip transmission blocks (Drake doesn't need them, and they can cause issues) - urdf_content = _strip_transmission_blocks(urdf_content) - - # Resolve package:// URIs - urdf_content = _resolve_package_uris(urdf_content, package_paths, cache_path) - - # Convert meshes if requested - if convert_meshes: - urdf_content = _convert_meshes(urdf_content, cache_path) - - # Write processed URDF - cached_urdf.write_text(urdf_content) - logger.info(f"Prepared URDF cached at: {cached_urdf}") - - return str(cached_urdf) - - -def _generate_cache_key( - urdf_path: Path, - package_paths: dict[str, Path], - xacro_args: dict[str, str], - convert_meshes: bool, -) -> str: - """Generate a cache key for the URDF configuration. - - Includes a version number to invalidate cache when processing logic changes. - """ - # Include file modification time - mtime = urdf_path.stat().st_mtime if urdf_path.exists() else 0 - - # Version number to invalidate cache when processing logic changes - # Increment this when adding new processing steps (e.g., stripping transmission blocks) - processing_version = "v2" - - key_data = f"{processing_version}:{urdf_path}:{mtime}:{sorted(package_paths.items())}:{sorted(xacro_args.items())}:{convert_meshes}" - return hashlib.md5(key_data.encode()).hexdigest()[:16] - - -def _process_xacro( - xacro_path: Path, - package_paths: dict[str, Path], - xacro_args: dict[str, str], -) -> str: - """Process xacro file to URDF.""" - try: - import xacro # type: ignore[import-not-found,import-untyped] - except ImportError: - raise ImportError( - "xacro is required for processing .xacro files. Install with: pip install xacro" - ) - - # Create a custom substitution_args_context that resolves $(find pkg) to our paths - # This avoids requiring ROS package discovery - from xacro import substitution_args - - # Store original function - original_find = substitution_args._find - - def custom_find(resolved: str, a: str, args: list[str], context: dict[str, str]) -> str: - """Custom $(find pkg) handler that uses our package_paths.""" - pkg_name = args[0] if args else "" - if pkg_name in package_paths: - pkg_path = str(Path(package_paths[pkg_name]).resolve()) - return resolved.replace(f"$({a})", pkg_path) - # Fall back to original behavior - return str(original_find(resolved, a, args, context)) - - # Monkey-patch the find function temporarily - substitution_args._find = custom_find - - try: - # Process xacro with our mappings - doc = xacro.process_file( - str(xacro_path), - mappings=xacro_args, - ) - return str(doc.toprettyxml(indent=" ")) - finally: - # Restore original function - substitution_args._find = original_find - - -def _strip_transmission_blocks(urdf_content: str) -> str: - """Remove transmission blocks from URDF content. - - Drake doesn't need transmission blocks (they're for Gazebo/ROS control), - and they can cause parsing errors if they contain malformed actuator names. - - Args: - urdf_content: URDF XML content as string - - Returns: - URDF content with transmission blocks removed - """ - # Pattern to match ... blocks and self-closing - # Uses non-greedy matching and handles nested tags - pattern = r"]*(?:/>|>.*?)" - - # Remove transmission blocks (with flags for multiline and dotall) - result = re.sub(pattern, "", urdf_content, flags=re.DOTALL | re.MULTILINE) - - # Also remove any standalone blocks that might reference transmissions - # (some URDFs have gazebo plugins that reference transmissions) - gazebo_pattern = r".*?]*gazebo_ros_control[^>]*>.*?.*?" - result = re.sub(gazebo_pattern, "", result, flags=re.DOTALL | re.MULTILINE) - - return result - - -def _resolve_package_uris( - urdf_content: str, - package_paths: dict[str, Path], - output_dir: Path, -) -> str: - """Resolve package:// URIs to filesystem paths.""" - # Pattern for package:// URIs (handles both single and double quotes) - # Note: Use triple quotes so \s is correctly interpreted as whitespace, not literal 's' - pattern = r"""package://([^/]+)/(.+?)(["'<>\s])""" - - def replace_uri(match: re.Match[str]) -> str: - pkg_name = match.group(1) - rel_path = match.group(2) - suffix = match.group(3) - - if pkg_name in package_paths: - # Ensure absolute path for proper resolution - pkg_path = Path(package_paths[pkg_name]).resolve() - full_path = pkg_path / rel_path - if full_path.exists(): - return f"{full_path}{suffix}" - else: - logger.warning(f"File not found: {full_path}") - - # Return original if not found - return match.group(0) - - return re.sub(pattern, replace_uri, urdf_content) - - -def _convert_meshes(urdf_content: str, output_dir: Path) -> str: - """Convert DAE/STL meshes to OBJ format for Drake compatibility.""" - try: - import trimesh - except ImportError: - logger.warning("trimesh not installed, skipping mesh conversion") - return urdf_content - - mesh_dir = output_dir / "meshes" - mesh_dir.mkdir(exist_ok=True) - - # Find mesh file references - pattern = r'filename="([^"]+\.(dae|stl|DAE|STL))"' - - converted: dict[str, str] = {} - - def convert_mesh(match: re.Match[str]) -> str: - original_path = match.group(1) - - if original_path in converted: - return f'filename="{converted[original_path]}"' - - try: - # Load mesh - mesh = trimesh.load(original_path, force="mesh") - - # Generate output path - mesh_name = Path(original_path).stem - obj_path = mesh_dir / f"{mesh_name}.obj" - - # Export as OBJ (trimesh.export returns None, ignore) - mesh.export(str(obj_path), file_type="obj") # type: ignore[no-untyped-call] - logger.debug(f"Converted mesh: {original_path} -> {obj_path}") - - converted[original_path] = str(obj_path) - return f'filename="{obj_path}"' - - except Exception as e: - logger.warning(f"Failed to convert mesh {original_path}: {e}") - return match.group(0) - - return re.sub(pattern, convert_mesh, urdf_content) - - -def pointcloud_to_convex_hull_obj( - points: NDArray[np.float64], - output_path: Path | str | None = None, - *, - voxel_size: float = 0.005, - min_points: int = 4, -) -> str | None: - """Compute convex hull from point cloud and save as OBJ file. - - Points are centered at origin so the mesh is in local frame. - The caller sets the obstacle pose to place it in the world. - - Args: - points: Nx3 numpy array of 3D points (world frame) - output_path: Where to save OBJ. If None, uses a temp file. - voxel_size: Downsample voxel size in meters (0 to skip) - min_points: Minimum points required for convex hull - - Returns: - Path to OBJ file, or None if hull computation fails - """ - import numpy as np - - if points.shape[0] < min_points: - logger.warning(f"Too few points ({points.shape[0]}) for convex hull") - return None - - try: - import open3d as o3d # type: ignore[import-untyped] - except ImportError: - logger.warning("open3d not installed, cannot compute convex hull") - return None - - # Center at origin so mesh is in local frame - centered = points - points.mean(axis=0) - - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(centered.astype(np.float64)) - - if voxel_size > 0 and len(pcd.points) > 100: - pcd = pcd.voxel_down_sample(voxel_size) - - if len(pcd.points) < min_points: - logger.warning(f"Too few points after downsample ({len(pcd.points)})") - return None - - try: - hull, _ = pcd.compute_convex_hull() - except Exception as e: - logger.warning(f"Convex hull computation failed: {e}") - return None - - if output_path is None: - hull_dir = _CACHE_DIR / "convex_hulls" - hull_dir.mkdir(parents=True, exist_ok=True) - output_path = hull_dir / f"hull_{id(points):x}.obj" - - output_path = Path(output_path) - output_path.parent.mkdir(parents=True, exist_ok=True) - - try: - o3d.io.write_triangle_mesh(str(output_path), hull) - logger.debug( - f"Convex hull: {len(hull.vertices)} verts, {len(hull.triangles)} faces -> {output_path}" - ) - return str(output_path) - except Exception as e: - logger.warning(f"Failed to write convex hull OBJ: {e}") - return None - - -def clear_cache() -> None: - """Clear the URDF cache directory.""" - if _CACHE_DIR.exists(): - shutil.rmtree(_CACHE_DIR) - logger.info(f"Cleared URDF cache: {_CACHE_DIR}") diff --git a/dimos/manipulation/planning/utils/path_utils.py b/dimos/manipulation/planning/utils/path_utils.py deleted file mode 100644 index fbf8af4032..0000000000 --- a/dimos/manipulation/planning/utils/path_utils.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Path Utilities - -Standalone utility functions for path manipulation and post-processing. -These functions are stateless and can be used by any planner implementation. - -## Functions - -- interpolate_path(): Interpolate path to uniform resolution -- interpolate_segment(): Interpolate between two configurations -- simplify_path(): Remove unnecessary waypoints (requires WorldSpec) -- compute_path_length(): Compute total path length in joint space -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np - -from dimos.msgs.sensor_msgs import JointState - -if TYPE_CHECKING: - from numpy.typing import NDArray - - from dimos.manipulation.planning.spec import JointPath, WorldRobotID, WorldSpec - - -def interpolate_path( - path: JointPath, - resolution: float = 0.05, -) -> JointPath: - """Interpolate path to have uniform resolution. - - Adds intermediate waypoints so that the maximum joint-space distance - between consecutive waypoints is at most `resolution`. - - Args: - path: Original path (list of JointState waypoints) - resolution: Maximum distance between waypoints (radians) - - Returns: - Interpolated path with more waypoints - - Example: - # After planning, interpolate for smoother execution - raw_path = planner.plan_joint_path(world, robot_id, start, goal).path - smooth_path = interpolate_path(raw_path, resolution=0.02) - """ - if len(path) <= 1: - return list(path) - - interpolated: list[JointState] = [path[0]] - joint_names = path[0].name - - for i in range(len(path) - 1): - q_start = np.array(path[i].position, dtype=np.float64) - q_end = np.array(path[i + 1].position, dtype=np.float64) - - diff = q_end - q_start - max_diff = float(np.max(np.abs(diff))) - - if max_diff <= resolution: - interpolated.append(path[i + 1]) - else: - num_steps = int(np.ceil(max_diff / resolution)) - for step in range(1, num_steps + 1): - alpha = step / num_steps - q_interp = q_start + alpha * diff - interpolated.append(JointState(name=joint_names, position=q_interp.tolist())) - - return interpolated - - -def interpolate_segment( - start: JointState, - end: JointState, - step_size: float, -) -> JointPath: - """Interpolate between two configurations. - - Returns a list of configurations from start to end (inclusive) - with at most `step_size` distance between consecutive points. - - Args: - start: Start joint configuration - end: End joint configuration - step_size: Maximum step size (radians) - - Returns: - List of interpolated JointState waypoints [start, ..., end] - - Example: - # Check collision along a segment - segment = interpolate_segment(start_state, end_state, step_size=0.02) - for state in segment: - if not world.check_config_collision_free(robot_id, state): - return False - """ - q_start = np.array(start.position, dtype=np.float64) - q_end = np.array(end.position, dtype=np.float64) - joint_names = start.name - - diff = q_end - q_start - distance = float(np.linalg.norm(diff)) - - if distance <= step_size: - return [start, end] - - num_steps = int(np.ceil(distance / step_size)) - segment: JointPath = [] - - for i in range(num_steps + 1): - alpha = i / num_steps - q_interp = q_start + alpha * diff - segment.append(JointState(name=joint_names, position=q_interp.tolist())) - - return segment - - -def simplify_path( - world: WorldSpec, - robot_id: WorldRobotID, - path: JointPath, - max_iterations: int = 100, - collision_step_size: float = 0.02, -) -> JointPath: - """Simplify path by removing unnecessary waypoints. - - Uses random shortcutting: randomly select two points and check if - the direct connection is collision-free. If so, remove intermediate - waypoints. - - Args: - world: World for collision checking - robot_id: Which robot - path: Original path (list of JointState waypoints) - max_iterations: Maximum shortcutting attempts - collision_step_size: Step size for collision checking along shortcuts - - Returns: - Simplified path with fewer waypoints - - Example: - raw_path = planner.plan_joint_path(world, robot_id, start, goal).path - simplified = simplify_path(world, robot_id, raw_path) - """ - if len(path) <= 2: - return list(path) - - simplified = list(path) - - for _ in range(max_iterations): - if len(simplified) <= 2: - break - - # Pick two random indices (at least 2 apart) - i = np.random.randint(0, len(simplified) - 2) - j = np.random.randint(i + 2, len(simplified)) - - # Check if direct connection is valid using context-free API - if world.check_edge_collision_free( - robot_id, simplified[i], simplified[j], collision_step_size - ): - # Remove intermediate waypoints - simplified = simplified[: i + 1] + simplified[j:] - - return simplified - - -def compute_path_length(path: JointPath) -> float: - """Compute total path length in joint space. - - Sums the Euclidean distances between consecutive waypoints. - - Args: - path: Path to measure (list of JointState waypoints) - - Returns: - Total length in radians - - Example: - length = compute_path_length(path) - print(f"Path length: {length:.2f} rad") - """ - if len(path) <= 1: - return 0.0 - - length = 0.0 - for i in range(len(path) - 1): - q_curr = np.array(path[i].position, dtype=np.float64) - q_next = np.array(path[i + 1].position, dtype=np.float64) - length += float(np.linalg.norm(q_next - q_curr)) - - return length - - -def is_path_within_limits( - path: JointPath, - lower_limits: NDArray[np.float64], - upper_limits: NDArray[np.float64], -) -> bool: - """Check if all waypoints in path are within joint limits. - - Args: - path: Path to check (list of JointState waypoints) - lower_limits: Lower joint limits (radians) - upper_limits: Upper joint limits (radians) - - Returns: - True if all waypoints are within limits - """ - for state in path: - q = np.array(state.position, dtype=np.float64) - if np.any(q < lower_limits) or np.any(q > upper_limits): - return False - return True - - -def clip_path_to_limits( - path: JointPath, - lower_limits: NDArray[np.float64], - upper_limits: NDArray[np.float64], -) -> JointPath: - """Clip all waypoints in path to joint limits. - - Args: - path: Path to clip (list of JointState waypoints) - lower_limits: Lower joint limits (radians) - upper_limits: Upper joint limits (radians) - - Returns: - Path with all waypoints clipped to limits - """ - clipped: list[JointState] = [] - for state in path: - q = np.array(state.position, dtype=np.float64) - q_clipped = np.clip(q, lower_limits, upper_limits) - clipped.append(JointState(name=state.name, position=q_clipped.tolist())) - return clipped - - -def reverse_path(path: JointPath) -> JointPath: - """Reverse a path (for returning to start, etc.). - - Args: - path: Path to reverse - - Returns: - Reversed path - """ - return list(reversed(path)) - - -def concatenate_paths( - *paths: JointPath, - remove_duplicates: bool = True, -) -> JointPath: - """Concatenate multiple paths into one. - - Args: - *paths: Paths to concatenate (each is a list of JointState waypoints) - remove_duplicates: If True, remove duplicate waypoints at junctions - - Returns: - Single concatenated path - """ - result: list[JointState] = [] - - for path in paths: - if not path: - continue - - if remove_duplicates and result: - # Check if last point matches first point (tight tolerance for joint space) - q_last = np.array(result[-1].position, dtype=np.float64) - q_first = np.array(path[0].position, dtype=np.float64) - if np.allclose(q_last, q_first, atol=1e-6, rtol=0): - result.extend(path[1:]) - else: - result.extend(path) - else: - result.extend(path) - - return result diff --git a/dimos/manipulation/planning/world/__init__.py b/dimos/manipulation/planning/world/__init__.py deleted file mode 100644 index 8ddef7fdff..0000000000 --- a/dimos/manipulation/planning/world/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -World Module - -Contains world implementations that own the physics/collision backend. - -## Implementations - -- DrakeWorld: Uses Drake MultibodyPlant + SceneGraph -""" - -from dimos.manipulation.planning.world.drake_world import DrakeWorld - -__all__ = ["DrakeWorld"] diff --git a/dimos/manipulation/planning/world/drake_world.py b/dimos/manipulation/planning/world/drake_world.py deleted file mode 100644 index 2ab996f410..0000000000 --- a/dimos/manipulation/planning/world/drake_world.py +++ /dev/null @@ -1,1047 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Drake World Implementation - WorldSpec using Drake's MultibodyPlant and SceneGraph.""" - -from __future__ import annotations - -from concurrent.futures import ThreadPoolExecutor -from contextlib import contextmanager -from dataclasses import dataclass, field -from pathlib import Path -from threading import RLock, current_thread -from typing import TYPE_CHECKING, Any - -import numpy as np - -from dimos.manipulation.planning.spec import ( - JointPath, - Obstacle, - ObstacleType, - RobotModelConfig, - WorldRobotID, - WorldSpec, -) -from dimos.manipulation.planning.utils.mesh_utils import prepare_urdf_for_drake -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Generator - - from numpy.typing import NDArray - -from dimos.msgs.geometry_msgs import PoseStamped, Transform -from dimos.msgs.sensor_msgs import JointState - -try: - from pydrake.geometry import ( # type: ignore[import-not-found] - AddContactMaterial, - Box, - CollisionFilterDeclaration, - Convex, - Cylinder, - GeometryInstance, - GeometrySet, - IllustrationProperties, - MakePhongIllustrationProperties, - Meshcat, - MeshcatVisualizer, - MeshcatVisualizerParams, - ProximityProperties, - Rgba, - Role, - RoleAssign, - SceneGraph, - Sphere, - ) - from pydrake.math import RigidTransform # type: ignore[import-not-found] - from pydrake.multibody.parsing import Parser # type: ignore[import-not-found] - from pydrake.multibody.plant import ( # type: ignore[import-not-found] - AddMultibodyPlantSceneGraph, - CoulombFriction, - MultibodyPlant, - ) - from pydrake.multibody.tree import JacobianWrtVariable # type: ignore[import-not-found] - from pydrake.systems.framework import Context, DiagramBuilder # type: ignore[import-not-found] - - DRAKE_AVAILABLE = True -except ImportError: - DRAKE_AVAILABLE = False - -logger = setup_logger() - - -@dataclass -class _RobotData: - """Internal data for tracking a robot in the world.""" - - robot_id: WorldRobotID - config: RobotModelConfig - model_instance: Any # ModelInstanceIndex - joint_indices: list[int] # Indices into plant's position vector - ee_frame: Any # BodyFrame for end-effector - base_frame: Any # BodyFrame for base - preview_model_instance: Any = None # ModelInstanceIndex for preview (yellow) robot - preview_joint_indices: list[int] = field(default_factory=list) - - -@dataclass -class _ObstacleData: - """Internal data for tracking an obstacle in the world.""" - - obstacle_id: str - obstacle: Obstacle - geometry_id: Any # GeometryId - source_id: Any # SourceId - - -class _ThreadSafeMeshcat: - """Wraps Drake Meshcat so all calls run on the creator thread. - - Drake throws SystemExit from non-creator threads for every Meshcat operation. - This class creates a single-thread executor, constructs Meshcat on it, - and proxies all calls through it. - """ - - def __init__(self) -> None: - self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="meshcat") - self._thread = self._executor.submit(current_thread).result() - self._inner: Meshcat = self._executor.submit(Meshcat).result() - - def _call(self, fn: Any, *args: Any, **kwargs: Any) -> Any: - if current_thread() is self._thread: - return fn(*args, **kwargs) - return self._executor.submit(fn, *args, **kwargs).result() - - # --- Meshcat proxies --- - - def SetObject(self, *args: Any, **kwargs: Any) -> Any: - return self._call(self._inner.SetObject, *args, **kwargs) - - def SetTransform(self, *args: Any, **kwargs: Any) -> Any: - return self._call(self._inner.SetTransform, *args, **kwargs) - - def SetProperty(self, *args: Any, **kwargs: Any) -> Any: - return self._call(self._inner.SetProperty, *args, **kwargs) - - def Delete(self, *args: Any, **kwargs: Any) -> Any: - return self._call(self._inner.Delete, *args, **kwargs) - - def web_url(self) -> str: - result: str = self._call(self._inner.web_url) - return result - - def forced_publish(self, visualizer: Any, viz_ctx: Any) -> None: - """Run MeshcatVisualizer.ForcedPublish on the creator thread.""" - self._call(visualizer.ForcedPublish, viz_ctx) - - def close(self) -> None: - self._executor.shutdown(wait=False) - - -class DrakeWorld(WorldSpec): - """Drake implementation of WorldSpec with MultibodyPlant, SceneGraph, optional Meshcat.""" - - def __init__(self, time_step: float = 0.0, enable_viz: bool = False): - if not DRAKE_AVAILABLE: - raise ImportError("Drake is not installed. Install with: pip install drake") - - self._time_step = time_step - self._enable_viz = enable_viz - self._lock = RLock() - - # Build Drake diagram - self._builder = DiagramBuilder() - self._plant: MultibodyPlant - self._scene_graph: SceneGraph - self._plant, self._scene_graph = AddMultibodyPlantSceneGraph( - self._builder, time_step=time_step - ) - self._parser = Parser(self._plant) - # Enable auto-renaming to avoid conflicts when adding multiple robots - # with the same URDF (e.g., 4 XArm6 arms all have model name "UF_ROBOT") - self._parser.SetAutoRenaming(True) - - # Visualization — wrapped to enforce Drake's thread affinity - self._meshcat: _ThreadSafeMeshcat | None = None - self._meshcat_visualizer: MeshcatVisualizer | None = None - if enable_viz: - self._meshcat = _ThreadSafeMeshcat() - - # Create model instance for obstacles - self._obstacles_model_instance = self._plant.AddModelInstance("obstacles") - - # Tracking data - self._robots: dict[WorldRobotID, _RobotData] = {} - self._obstacles: dict[str, _ObstacleData] = {} - self._robot_counter = 0 - self._obstacle_counter = 0 - - # Built diagram and contexts (created after finalize) - self._diagram: Any = None - self._live_context: Context | None = None - self._plant_context: Context | None = None - self._scene_graph_context: Context | None = None - self._finalized = False - - # Obstacle source for dynamic obstacles - self._obstacle_source_id: Any = None - - def add_robot(self, config: RobotModelConfig) -> WorldRobotID: - """Add a robot to the world. Returns robot_id.""" - if self._finalized: - raise RuntimeError("Cannot add robot after world is finalized") - - with self._lock: - self._robot_counter += 1 - robot_id = f"robot_{self._robot_counter}" - - model_instance = self._load_urdf(config) - self._weld_base_if_needed(config, model_instance) - self._validate_joints(config, model_instance) - - ee_frame = self._plant.GetBodyByName( - config.end_effector_link, model_instance - ).body_frame() - base_frame = self._plant.GetBodyByName(config.base_link, model_instance).body_frame() - - # Load a second copy of the URDF as the preview (yellow ghost) robot - preview_model_instance = None - if self._enable_viz: - preview_model_instance = self._load_urdf(config) - self._weld_base_if_needed(config, preview_model_instance) - - self._robots[robot_id] = _RobotData( - robot_id=robot_id, - config=config, - model_instance=model_instance, - joint_indices=[], - ee_frame=ee_frame, - base_frame=base_frame, - preview_model_instance=preview_model_instance, - ) - - logger.info(f"Added robot '{robot_id}' ({config.name})") - return robot_id - - def _load_urdf(self, config: RobotModelConfig) -> Any: - """Load URDF/xacro and return model instance.""" - original_path = config.urdf_path.resolve() - if not original_path.exists(): - raise FileNotFoundError(f"URDF/xacro not found: {original_path}") - - urdf_path = prepare_urdf_for_drake( - urdf_path=original_path, - package_paths=config.package_paths, - xacro_args=config.xacro_args, - convert_meshes=config.auto_convert_meshes, - ) - urdf_path_obj = Path(urdf_path) - logger.info(f"Using prepared URDF: {urdf_path_obj}") - - # Register package paths - if config.package_paths: - for pkg_name, pkg_path in config.package_paths.items(): - self._parser.package_map().Add(pkg_name, Path(pkg_path)) - else: - self._parser.package_map().Add(f"{config.name}_description", urdf_path_obj.parent) - - model_instances = self._parser.AddModels(urdf_path_obj) - if not model_instances: - raise ValueError(f"Failed to parse URDF: {urdf_path}") - return model_instances[0] - - def _weld_base_if_needed(self, config: RobotModelConfig, model_instance: Any) -> None: - """Weld robot base to world if not already welded in URDF.""" - base_body = self._plant.GetBodyByName(config.base_link, model_instance) - - # Check if any joint already connects world to base_link - for joint_index in self._plant.GetJointIndices(model_instance): - joint = self._plant.get_joint(joint_index) - if ( - joint.parent_body().name() == "world" - and joint.child_body().name() == config.base_link - ): - logger.info( - f"URDF already has joint '{joint.name()}' welding " - f"world→{config.base_link}, skipping weld" - ) - return - - # Weld base to world - base_transform = self._pose_to_rigid_transform(config.base_pose) - self._plant.WeldFrames( - self._plant.world_frame(), - base_body.body_frame(), - base_transform, - ) - - def _validate_joints(self, config: RobotModelConfig, model_instance: Any) -> None: - """Validate that all configured joints exist in URDF.""" - for joint_name in config.joint_names: - try: - self._plant.GetJointByName(joint_name, model_instance) - except RuntimeError: - raise ValueError(f"Joint '{joint_name}' not found in URDF") - - def get_robot_ids(self) -> list[WorldRobotID]: - """Get all robot IDs in the world.""" - return list(self._robots.keys()) - - def get_robot_config(self, robot_id: WorldRobotID) -> RobotModelConfig: - """Get robot configuration by ID.""" - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - return self._robots[robot_id].config - - def get_joint_limits( - self, robot_id: WorldRobotID - ) -> tuple[NDArray[np.float64], NDArray[np.float64]]: - """Get joint limits (lower, upper) in radians.""" - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - config = self._robots[robot_id].config - - if config.joint_limits_lower is not None and config.joint_limits_upper is not None: - return ( - np.array(config.joint_limits_lower), - np.array(config.joint_limits_upper), - ) - - # Default to ±π - n_joints = len(config.joint_names) - return ( - np.full(n_joints, -np.pi), - np.full(n_joints, np.pi), - ) - - # ============= Obstacle Management ============= - - def add_obstacle(self, obstacle: Obstacle) -> str: - """Add an obstacle to the world.""" - with self._lock: - # Use obstacle's name as ID (allows external ID management) - obstacle_id = obstacle.name - - # Check for duplicate in our tracking - if obstacle_id in self._obstacles: - logger.debug(f"Obstacle '{obstacle_id}' already exists, skipping") - return obstacle_id - - try: - if not self._finalized: - geometry_id = self._add_obstacle_to_plant(obstacle, obstacle_id) - self._obstacles[obstacle_id] = _ObstacleData( - obstacle_id=obstacle_id, - obstacle=obstacle, - geometry_id=geometry_id, - source_id=self._plant.get_source_id(), - ) - else: - geometry_id = self._add_obstacle_to_scene_graph(obstacle, obstacle_id) - self._obstacles[obstacle_id] = _ObstacleData( - obstacle_id=obstacle_id, - obstacle=obstacle, - geometry_id=geometry_id, - source_id=self._obstacle_source_id, - ) - - logger.debug(f"Added obstacle '{obstacle_id}': {obstacle.obstacle_type.value}") - except RuntimeError as e: - # Handle case where geometry name already exists in SceneGraph - # (can happen with concurrent access) - if "already been used" in str(e): - logger.debug(f"Obstacle '{obstacle_id}' already in SceneGraph, skipping") - else: - raise - - return obstacle_id - - def _add_obstacle_to_plant(self, obstacle: Obstacle, obstacle_id: str) -> Any: - """Add obstacle to plant (before finalization).""" - shape = self._create_shape(obstacle) - - body = self._plant.AddRigidBody( - obstacle_id, - self._obstacles_model_instance, # type: ignore[arg-type] - ) - - transform = self._pose_to_rigid_transform(obstacle.pose) - geometry_id = self._plant.RegisterCollisionGeometry( - body, - RigidTransform(), - shape, - obstacle_id + "_collision", - ProximityProperties(), - ) - - diffuse_color = np.array(obstacle.color) - self._plant.RegisterVisualGeometry( - body, - RigidTransform(), - shape, - obstacle_id + "_visual", - diffuse_color, # type: ignore[arg-type] - ) - - self._plant.WeldFrames( - self._plant.world_frame(), - body.body_frame(), - transform, - ) - - return geometry_id - - def _add_obstacle_to_scene_graph(self, obstacle: Obstacle, obstacle_id: str) -> Any: - """Add obstacle to scene graph (after finalization).""" - if self._obstacle_source_id is None: - raise RuntimeError("Obstacle source not initialized") - - shape = self._create_shape(obstacle) - transform = self._pose_to_rigid_transform(obstacle.pose) - # MakePhongIllustrationProperties expects numpy array, not Rgba - rgba_array = np.array(obstacle.color, dtype=np.float64) - - # Create proximity properties with contact material for collision detection - # Without these properties, the geometry is invisible to collision queries - proximity_props = ProximityProperties() - AddContactMaterial( - dissipation=0.0, - point_stiffness=1e6, - friction=CoulombFriction(static_friction=1.0, dynamic_friction=1.0), - properties=proximity_props, - ) - - geometry_instance = GeometryInstance( - X_PG=transform, - shape=shape, - name=obstacle_id, - ) - geometry_instance.set_illustration_properties( - MakePhongIllustrationProperties(rgba_array) # type: ignore[arg-type] - ) - geometry_instance.set_proximity_properties(proximity_props) - - frame_id = self._scene_graph.world_frame_id() - geometry_id = self._scene_graph.RegisterGeometry( - self._obstacle_source_id, - frame_id, - geometry_instance, - ) - - # Also add to Meshcat directly (MeshcatVisualizer doesn't show dynamic geometries) - if self._meshcat is not None: - self._add_obstacle_to_meshcat(obstacle, obstacle_id) - - return geometry_id - - def _add_obstacle_to_meshcat(self, obstacle: Obstacle, obstacle_id: str) -> None: - """Add obstacle visualization directly to Meshcat.""" - if self._meshcat is None: - return - - # Use Drake's geometry types for Meshcat - path = f"obstacles/{obstacle_id}" - transform = self._pose_to_rigid_transform(obstacle.pose) - rgba = Rgba(*obstacle.color) - - # Create Drake shape and add to Meshcat - drake_shape = self._create_shape(obstacle) - self._meshcat.SetObject(path, drake_shape, rgba) - self._meshcat.SetTransform(path, transform) - - def _pose_to_rigid_transform(self, pose: PoseStamped) -> Any: - """Convert PoseStamped to Drake RigidTransform.""" - pose_matrix = Transform( - translation=pose.position, - rotation=pose.orientation, - ).to_matrix() - return RigidTransform(pose_matrix) - - def _create_shape(self, obstacle: Obstacle) -> Any: - """Create Drake shape from obstacle specification.""" - if obstacle.obstacle_type == ObstacleType.BOX: - return Box(*obstacle.dimensions) - elif obstacle.obstacle_type == ObstacleType.SPHERE: - return Sphere(obstacle.dimensions[0]) - elif obstacle.obstacle_type == ObstacleType.CYLINDER: - return Cylinder(obstacle.dimensions[0], obstacle.dimensions[1]) - elif obstacle.obstacle_type == ObstacleType.MESH: - if not obstacle.mesh_path: - raise ValueError("MESH obstacle requires mesh_path") - return Convex(Path(obstacle.mesh_path)) - else: - raise ValueError(f"Unsupported obstacle type: {obstacle.obstacle_type}") - - def remove_obstacle(self, obstacle_id: str) -> bool: - """Remove an obstacle by ID.""" - with self._lock: - if obstacle_id not in self._obstacles: - return False - - obstacle_data = self._obstacles[obstacle_id] - - if self._finalized and self._scene_graph_context is not None: - self._scene_graph.RemoveGeometry( - obstacle_data.source_id, - obstacle_data.geometry_id, - ) - - # Also remove from Meshcat - if self._meshcat is not None: - path = f"obstacles/{obstacle_id}" - self._meshcat.Delete(path) - - del self._obstacles[obstacle_id] - logger.debug(f"Removed obstacle '{obstacle_id}'") - return True - - def update_obstacle_pose(self, obstacle_id: str, pose: PoseStamped) -> bool: - """Update obstacle pose.""" - with self._lock: - if obstacle_id not in self._obstacles: - return False - - # Store PoseStamped directly - self._obstacles[obstacle_id].obstacle.pose = pose - - # Update Meshcat visualization - if self._meshcat is not None: - path = f"obstacles/{obstacle_id}" - transform = self._pose_to_rigid_transform(pose) - self._meshcat.SetTransform(path, transform) - - # Note: SceneGraph geometry pose is fixed after registration - # Meshcat is updated for visualization, but collision checking - # uses the original pose. For dynamic obstacles, remove and re-add. - - return True - - def clear_obstacles(self) -> None: - """Remove all obstacles.""" - with self._lock: - obstacle_ids = list(self._obstacles.keys()) - for obs_id in obstacle_ids: - self.remove_obstacle(obs_id) - - # ============= Preview Robot Setup ============= - - def _set_preview_colors(self) -> None: - """Set all preview robot visual geometries to yellow/semi-transparent.""" - source_id: Any = self._plant.get_source_id() - preview_color = Rgba(1.0, 0.8, 0.0, 0.4) - - for robot_data in self._robots.values(): - if robot_data.preview_model_instance is None: - continue - for body_idx in self._plant.GetBodyIndices(robot_data.preview_model_instance): - body = self._plant.get_body(body_idx) - for geom_id in self._plant.GetVisualGeometriesForBody(body): - props = IllustrationProperties() - props.AddProperty("phong", "diffuse", preview_color) - self._scene_graph.AssignRole(source_id, geom_id, props, RoleAssign.kReplace) # type: ignore[call-overload] - - def _remove_preview_collision_roles(self) -> None: - """Remove proximity (collision) role from all preview robot geometries.""" - source_id: Any = self._plant.get_source_id() # SourceId - - for robot_data in self._robots.values(): - if robot_data.preview_model_instance is None: - continue - for body_idx in self._plant.GetBodyIndices(robot_data.preview_model_instance): - body = self._plant.get_body(body_idx) - for geom_id in self._plant.GetCollisionGeometriesForBody(body): - self._scene_graph.RemoveRole(source_id, geom_id, Role.kProximity) - - # ============= Lifecycle ============= - - def finalize(self) -> None: - """Finalize world - locks robot topology, enables collision checking.""" - if self._finalized: - logger.warning("World already finalized") - return - - with self._lock: - # Finalize plant - self._plant.Finalize() - - # Compute joint indices for each robot (live + preview) - for robot_id, robot_data in self._robots.items(): - joint_indices: list[int] = [] - for joint_name in robot_data.config.joint_names: - joint = self._plant.GetJointByName(joint_name, robot_data.model_instance) - start_idx = joint.position_start() - num_positions = joint.num_positions() - joint_indices.extend(range(start_idx, start_idx + num_positions)) - robot_data.joint_indices = joint_indices - logger.debug(f"Robot '{robot_id}' joint indices: {joint_indices}") - - # Compute preview joint indices - if robot_data.preview_model_instance is not None: - preview_indices: list[int] = [] - for joint_name in robot_data.config.joint_names: - joint = self._plant.GetJointByName( - joint_name, robot_data.preview_model_instance - ) - start_idx = joint.position_start() - num_positions = joint.num_positions() - preview_indices.extend(range(start_idx, start_idx + num_positions)) - robot_data.preview_joint_indices = preview_indices - logger.debug(f"Robot '{robot_id}' preview joint indices: {preview_indices}") - - # Setup collision filters - self._setup_collision_filters() - - # Remove collision roles from preview robots (visual-only) - self._remove_preview_collision_roles() - - # Set preview robots to yellow/semi-transparent - self._set_preview_colors() - - # Register obstacle source for dynamic obstacles - self._obstacle_source_id = self._scene_graph.RegisterSource("dynamic_obstacles") - - # Add visualization if enabled - if self._meshcat is not None: - params = MeshcatVisualizerParams() - params.role = Role.kIllustration - self._meshcat_visualizer = MeshcatVisualizer.AddToBuilder( - self._builder, - self._scene_graph, - self._meshcat._inner, - params, - ) - - # Build diagram - self._diagram = self._builder.Build() - self._live_context = self._diagram.CreateDefaultContext() - - # Get subsystem contexts - self._plant_context = self._diagram.GetMutableSubsystemContext( - self._plant, self._live_context - ) - self._scene_graph_context = self._diagram.GetMutableSubsystemContext( - self._scene_graph, self._live_context - ) - - self._finalized = True - logger.info(f"World finalized with {len(self._robots)} robots") - - # Initial visualization publish (routed to Meshcat thread) - if self._meshcat_visualizer is not None: - self.publish_visualization() - # Hide all preview robots initially - for robot_id in self._robots: - self.hide_preview(robot_id) - - @property - def is_finalized(self) -> bool: - """Check if world is finalized.""" - return self._finalized - - def _setup_collision_filters(self) -> None: - """Filter collisions between adjacent links and user-specified pairs.""" - for robot_data in self._robots.values(): - # Filter parent-child pairs (adjacent links always "collide") - for joint_idx in self._plant.GetJointIndices(robot_data.model_instance): - joint = self._plant.get_joint(joint_idx) - parent, child = joint.parent_body(), joint.child_body() - if parent.index() != self._plant.world_body().index(): - self._exclude_body_pair(parent, child) - - # Filter user-specified pairs (e.g., parallel linkage grippers) - for name1, name2 in robot_data.config.collision_exclusion_pairs: - try: - body1 = self._plant.GetBodyByName(name1, robot_data.model_instance) - body2 = self._plant.GetBodyByName(name2, robot_data.model_instance) - self._exclude_body_pair(body1, body2) - except RuntimeError: - logger.warning(f"Collision exclusion: link not found: {name1} or {name2}") - - logger.info("Collision filters applied") - - def _exclude_body_pair(self, body1: Any, body2: Any) -> None: - """Exclude collision between two bodies.""" - geoms1 = self._plant.GetCollisionGeometriesForBody(body1) - geoms2 = self._plant.GetCollisionGeometriesForBody(body2) - if geoms1 and geoms2: - self._scene_graph.collision_filter_manager().Apply( - CollisionFilterDeclaration().ExcludeBetween( - GeometrySet(geoms1), GeometrySet(geoms2) - ) - ) - - # ============= Context Management ============= - - def get_live_context(self) -> Context: - """Get the live context (mirrors current robot state). - - WARNING: Not thread-safe for reads during writes. - Use scratch_context() for planning operations. - """ - if not self._finalized or self._live_context is None: - raise RuntimeError("World must be finalized first") - return self._live_context - - @contextmanager - def scratch_context(self) -> Generator[Context, None, None]: - """Thread-safe context for planning. Copies current robot states for inter-robot collision checking.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - ctx = self._diagram.CreateDefaultContext() - - # Copy live robot states so inter-robot collision checking works - with self._lock: - if self._plant_context is not None: - plant_ctx = self._diagram.GetMutableSubsystemContext(self._plant, ctx) - for robot_data in self._robots.values(): - try: - positions = self._plant.GetPositions( - self._plant_context, robot_data.model_instance - ) - self._plant.SetPositions(plant_ctx, robot_data.model_instance, positions) - except RuntimeError: - pass # Robot not yet synced - - yield ctx - - def sync_from_joint_state(self, robot_id: WorldRobotID, joint_state: JointState) -> None: - """Sync live context from driver's joint state message. - - Called by StateMonitor when new JointState arrives. - """ - if not self._finalized or self._plant_context is None: - return # Silently ignore before finalization - - # Extract positions as numpy array for internal use - positions = np.array(joint_state.position, dtype=np.float64) - - with self._lock: - self._set_positions_internal(self._plant_context, robot_id, positions) - - # NOTE: ForcedPublish is intentionally NOT called here. - # Calling ForcedPublish from the LCM callback thread blocks message processing. - # Visualization can be updated via publish_to_meshcat() from non-callback contexts. - - # ============= State Operations (context-based) ============= - - def set_joint_state( - self, ctx: Context, robot_id: WorldRobotID, joint_state: JointState - ) -> None: - """Set robot joint state in given context.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - # Extract positions as numpy array for internal use - positions = np.array(joint_state.position, dtype=np.float64) - - # Get plant context from diagram context - plant_ctx = self._diagram.GetMutableSubsystemContext(self._plant, ctx) - self._set_positions_internal(plant_ctx, robot_id, positions) - - def _set_positions_internal( - self, plant_ctx: Context, robot_id: WorldRobotID, positions: NDArray[np.float64] - ) -> None: - """Internal: Set positions in a plant context.""" - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - robot_data = self._robots[robot_id] - full_positions = self._plant.GetPositions(plant_ctx).copy() - - for i, joint_idx in enumerate(robot_data.joint_indices): - full_positions[joint_idx] = positions[i] - - self._plant.SetPositions(plant_ctx, full_positions) - - def get_joint_state(self, ctx: Context, robot_id: WorldRobotID) -> JointState: - """Get robot joint state from given context.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - robot_data = self._robots[robot_id] - plant_ctx = self._diagram.GetSubsystemContext(self._plant, ctx) - full_positions = self._plant.GetPositions(plant_ctx) - - positions = [float(full_positions[idx]) for idx in robot_data.joint_indices] - return JointState(name=robot_data.config.joint_names, position=positions) - - # ============= Collision Checking (context-based) ============= - - def is_collision_free(self, ctx: Context, robot_id: WorldRobotID) -> bool: - """Check if current configuration in context is collision-free.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - scene_graph_ctx = self._diagram.GetSubsystemContext(self._scene_graph, ctx) - query_object = self._scene_graph.get_query_output_port().Eval(scene_graph_ctx) - - return not query_object.HasCollisions() # type: ignore[attr-defined] - - def get_min_distance(self, ctx: Context, robot_id: WorldRobotID) -> float: - """Get minimum signed distance (positive = clearance, negative = penetration).""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - scene_graph_ctx = self._diagram.GetSubsystemContext(self._scene_graph, ctx) - query_object = self._scene_graph.get_query_output_port().Eval(scene_graph_ctx) - - signed_distance_pairs = query_object.ComputeSignedDistancePairwiseClosestPoints() # type: ignore[attr-defined] - - if not signed_distance_pairs: - return float("inf") - - return float(min(pair.distance for pair in signed_distance_pairs)) - - # ============= Collision Checking (context-free, for planning) ============= - - def check_config_collision_free(self, robot_id: WorldRobotID, joint_state: JointState) -> bool: - """Check if a joint state is collision-free (manages context internally). - - This is a convenience method for planners that don't need to manage contexts. - """ - with self.scratch_context() as ctx: - self.set_joint_state(ctx, robot_id, joint_state) - return self.is_collision_free(ctx, robot_id) - - def check_edge_collision_free( - self, - robot_id: WorldRobotID, - start: JointState, - end: JointState, - step_size: float = 0.05, - ) -> bool: - """Check if the entire edge between two joint states is collision-free. - - Interpolates between start and end at the given step_size and checks - each configuration for collisions. This is more efficient than checking - each configuration separately as it uses a single scratch context. - """ - # Extract positions as numpy arrays for interpolation - q_start = np.array(start.position, dtype=np.float64) - q_end = np.array(end.position, dtype=np.float64) - - # Compute number of steps needed - dist = float(np.linalg.norm(q_end - q_start)) - if dist < 1e-8: - return self.check_config_collision_free(robot_id, start) - - n_steps = max(2, int(np.ceil(dist / step_size)) + 1) - - with self.scratch_context() as ctx: - for i in range(n_steps): - t = i / (n_steps - 1) - q = q_start + t * (q_end - q_start) - # Create interpolated JointState - interp_state = JointState(name=start.name, position=q.tolist()) - self.set_joint_state(ctx, robot_id, interp_state) - if not self.is_collision_free(ctx, robot_id): - return False - - return True - - # ============= Forward Kinematics (context-based) ============= - - def get_ee_pose(self, ctx: Context, robot_id: WorldRobotID) -> PoseStamped: - """Get end-effector pose.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - robot_data = self._robots[robot_id] - plant_ctx = self._diagram.GetSubsystemContext(self._plant, ctx) - - ee_body = robot_data.ee_frame.body() - X_WE = self._plant.EvalBodyPoseInWorld(plant_ctx, ee_body) - - # Extract position and quaternion from Drake transform - pos = X_WE.translation() - quat = X_WE.rotation().ToQuaternion() # Drake returns [w, x, y, z] - - return PoseStamped( - frame_id="world", - position=[float(pos[0]), float(pos[1]), float(pos[2])], - orientation=[float(quat.x()), float(quat.y()), float(quat.z()), float(quat.w())], - ) - - def get_link_pose( - self, ctx: Context, robot_id: WorldRobotID, link_name: str - ) -> NDArray[np.float64]: - """Get link pose as 4x4 transform.""" - if not self._finalized: - raise RuntimeError("World must be finalized first") - - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - robot_data = self._robots[robot_id] - plant_ctx = self._diagram.GetSubsystemContext(self._plant, ctx) - - try: - body = self._plant.GetBodyByName(link_name, robot_data.model_instance) - except RuntimeError: - raise KeyError(f"Link '{link_name}' not found in robot '{robot_id}'") - - X_WL = self._plant.EvalBodyPoseInWorld(plant_ctx, body) - - result = X_WL.GetAsMatrix4() - return result # type: ignore[no-any-return, return-value] - - def get_jacobian(self, ctx: Context, robot_id: WorldRobotID) -> NDArray[np.float64]: - """Get geometric Jacobian (6 x n_joints). - - Rows: [vx, vy, vz, wx, wy, wz] (linear, then angular) - """ - if not self._finalized: - raise RuntimeError("World must be finalized first") - - if robot_id not in self._robots: - raise KeyError(f"Robot '{robot_id}' not found") - - robot_data = self._robots[robot_id] - plant_ctx = self._diagram.GetSubsystemContext(self._plant, ctx) - - # Compute full Jacobian - J_full = self._plant.CalcJacobianSpatialVelocity( - plant_ctx, - JacobianWrtVariable.kQDot, - robot_data.ee_frame, - np.array([0.0, 0.0, 0.0]), # type: ignore[arg-type] # Point on end-effector - self._plant.world_frame(), - self._plant.world_frame(), - ) - - # Extract columns for this robot's joints - n_joints = len(robot_data.joint_indices) - J_robot = np.zeros((6, n_joints)) - - for i, joint_idx in enumerate(robot_data.joint_indices): - J_robot[:, i] = J_full[:, joint_idx] - - # Reorder rows: Drake uses [angular, linear], we want [linear, angular] - J_reordered = np.vstack([J_robot[3:6, :], J_robot[0:3, :]]) - - return J_reordered - - # ============= Visualization ============= - - def get_visualization_url(self) -> str | None: - """Get visualization URL if enabled.""" - if self._meshcat is not None: - return self._meshcat.web_url() - return None - - def publish_visualization(self, ctx: Context | None = None) -> None: - """Publish current state to visualization.""" - if self._meshcat_visualizer is None or self._meshcat is None: - return - if ctx is None: - ctx = self._live_context - if ctx is not None: - viz_ctx = self._diagram.GetSubsystemContext(self._meshcat_visualizer, ctx) - self._meshcat.forced_publish(self._meshcat_visualizer, viz_ctx) - - def _set_preview_positions( - self, plant_ctx: Context, robot_id: WorldRobotID, positions: NDArray[np.float64] - ) -> None: - """Set preview robot positions in a plant context.""" - robot_data = self._robots.get(robot_id) - if robot_data is None or robot_data.preview_model_instance is None: - return - - full_positions = self._plant.GetPositions(plant_ctx).copy() - for i, idx in enumerate(robot_data.preview_joint_indices): - full_positions[idx] = positions[i] - self._plant.SetPositions(plant_ctx, full_positions) - - def show_preview(self, robot_id: WorldRobotID) -> None: - """Show the preview (yellow ghost) robot in Meshcat.""" - if self._meshcat is None: - return - robot_data = self._robots.get(robot_id) - if robot_data is None or robot_data.preview_model_instance is None: - return - model_name = self._plant.GetModelInstanceName(robot_data.preview_model_instance) - self._meshcat.SetProperty(f"visualizer/{model_name}", "visible", True) - - def hide_preview(self, robot_id: WorldRobotID) -> None: - """Hide the preview (yellow ghost) robot in Meshcat.""" - if self._meshcat is None: - return - robot_data = self._robots.get(robot_id) - if robot_data is None or robot_data.preview_model_instance is None: - return - model_name = self._plant.GetModelInstanceName(robot_data.preview_model_instance) - self._meshcat.SetProperty(f"visualizer/{model_name}", "visible", False) - - def animate_path( - self, - robot_id: WorldRobotID, - path: JointPath, - duration: float = 3.0, - ) -> None: - """Animate a path using the preview (yellow ghost) robot. - - The preview stays visible after animation completes. - """ - if self._meshcat is None or len(path) < 2: - return - - robot_data = self._robots.get(robot_id) - if robot_data is None or robot_data.preview_model_instance is None: - return - - import time - - self.show_preview(robot_id) - dt = duration / (len(path) - 1) - for joint_state in path: - positions = np.array(joint_state.position, dtype=np.float64) - with self._lock: - assert self._plant_context is not None - self._set_preview_positions(self._plant_context, robot_id, positions) - self.publish_visualization() - time.sleep(dt) - - def close(self) -> None: - """Shut down the viz thread.""" - if self._meshcat is not None: - self._meshcat.close() - - # ============= Direct Access (use with caution) ============= - - @property - def plant(self) -> MultibodyPlant: - """Get underlying MultibodyPlant.""" - return self._plant - - @property - def scene_graph(self) -> SceneGraph: - """Get underlying SceneGraph.""" - return self._scene_graph - - @property - def diagram(self) -> Any: - """Get underlying Diagram.""" - return self._diagram diff --git a/dimos/manipulation/test_manipulation_module.py b/dimos/manipulation/test_manipulation_module.py deleted file mode 100644 index c30ba9b55c..0000000000 --- a/dimos/manipulation/test_manipulation_module.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Integration tests for ManipulationModule. - -These tests verify the full planning stack with Drake backend. -They require Drake to be installed and will be skipped otherwise. -""" - -from __future__ import annotations - -import importlib.util -from unittest.mock import MagicMock - -import pytest - -from dimos.manipulation.manipulation_module import ( - ManipulationModule, - ManipulationState, -) -from dimos.manipulation.planning.spec import RobotModelConfig -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Vector3 -from dimos.msgs.sensor_msgs import JointState -from dimos.utils.data import get_data - - -def _drake_available() -> bool: - return importlib.util.find_spec("pydrake") is not None - - -def _xarm_urdf_available() -> bool: - try: - desc_path = get_data("xarm_description") - urdf_path = desc_path / "urdf/xarm_device.urdf.xacro" - return urdf_path.exists() - except Exception: - return False - - -def _get_xarm7_config() -> RobotModelConfig: - """Create XArm7 robot config for testing.""" - desc_path = get_data("xarm_description") - return RobotModelConfig( - name="test_arm", - urdf_path=desc_path / "urdf/xarm_device.urdf.xacro", - base_pose=PoseStamped(position=Vector3(), orientation=Quaternion()), - joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6", "joint7"], - end_effector_link="link7", - base_link="link_base", - package_paths={"xarm_description": desc_path}, - xacro_args={"dof": "7", "limited": "true"}, - auto_convert_meshes=True, - max_velocity=1.0, - max_acceleration=2.0, - joint_name_mapping={ - "arm_joint1": "joint1", - "arm_joint2": "joint2", - "arm_joint3": "joint3", - "arm_joint4": "joint4", - "arm_joint5": "joint5", - "arm_joint6": "joint6", - "arm_joint7": "joint7", - }, - coordinator_task_name="traj_arm", - ) - - -@pytest.fixture -def xarm7_config(): - return _get_xarm7_config() - - -@pytest.fixture -def joint_state_zeros(): - """Create a JointState message with zeros for XArm7.""" - return JointState( - name=[ - "arm_joint1", - "arm_joint2", - "arm_joint3", - "arm_joint4", - "arm_joint5", - "arm_joint6", - "arm_joint7", - ], - position=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - velocity=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - effort=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ) - - -@pytest.fixture -def module(xarm7_config): - """Create a started ManipulationModule with ports disabled.""" - mod = ManipulationModule( - robots=[xarm7_config], - planning_timeout=10.0, - enable_viz=False, - ) - mod.joint_state = None - mod.objects = None - mod.start() - yield mod - mod.stop() - - -@pytest.mark.skipif(not _drake_available(), reason="Drake not installed") -@pytest.mark.skipif(not _xarm_urdf_available(), reason="XArm URDF not available") -class TestManipulationModuleIntegration: - """Integration tests for ManipulationModule with real Drake backend.""" - - def test_module_initialization(self, module): - """Test module initializes with real Drake world.""" - assert module._state == ManipulationState.IDLE - assert module._world_monitor is not None - assert module._planner is not None - assert module._kinematics is not None - assert "test_arm" in module._robots - - def test_joint_state_sync(self, module, joint_state_zeros): - """Test joint state synchronization to Drake world.""" - module._on_joint_state(joint_state_zeros) - - joints = module.get_current_joints() - assert joints is not None - assert len(joints) == 7 - assert all(abs(j) < 0.01 for j in joints) - - def test_collision_check(self, module, joint_state_zeros): - """Test collision checking at a configuration.""" - module._on_joint_state(joint_state_zeros) - - is_free = module.is_collision_free([0.0] * 7) - assert is_free is True - - def test_plan_to_joints(self, module, joint_state_zeros): - """Test planning to a joint configuration.""" - module._on_joint_state(joint_state_zeros) - - target = JointState(position=[0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]) - success = module.plan_to_joints(target) - - assert success is True - assert module._state == ManipulationState.COMPLETED - assert module.has_planned_path() is True - - assert "test_arm" in module._planned_trajectories - traj = module._planned_trajectories["test_arm"] - assert len(traj.points) > 1 - assert traj.duration > 0 - - def test_add_and_remove_obstacle(self, module, joint_state_zeros): - """Test adding and removing obstacles.""" - module._on_joint_state(joint_state_zeros) - - pose = Pose( - position=Vector3(0.5, 0.0, 0.3), - orientation=Quaternion(), # default is identity (w=1) - ) - obstacle_id = module.add_obstacle("test_box", pose, "box", [0.1, 0.1, 0.1]) - - assert obstacle_id != "" - assert obstacle_id is not None - - removed = module.remove_obstacle(obstacle_id) - assert removed is True - - def test_robot_info(self, module): - """Test getting robot information.""" - info = module.get_robot_info() - - assert info is not None - assert info["name"] == "test_arm" - assert len(info["joint_names"]) == 7 - assert info["end_effector_link"] == "link7" - assert info["coordinator_task_name"] == "traj_arm" - assert info["has_joint_name_mapping"] is True - - def test_ee_pose(self, module, joint_state_zeros): - """Test getting end-effector pose.""" - module._on_joint_state(joint_state_zeros) - - pose = module.get_ee_pose() - - assert pose is not None - assert hasattr(pose, "x") - assert hasattr(pose, "y") - assert hasattr(pose, "z") - - def test_trajectory_name_translation(self, module, joint_state_zeros): - """Test that trajectory joint names are translated for coordinator.""" - module._on_joint_state(joint_state_zeros) - - success = module.plan_to_joints(JointState(position=[0.05] * 7)) - assert success is True - - traj = module._planned_trajectories["test_arm"] - robot_config = module._robots["test_arm"][1] - - translated = module._translate_trajectory_to_coordinator(traj, robot_config) - - for name in translated.joint_names: - assert name.startswith("arm_") # Should have arm_ prefix - - -@pytest.mark.skipif(not _drake_available(), reason="Drake not installed") -@pytest.mark.skipif(not _xarm_urdf_available(), reason="XArm URDF not available") -class TestCoordinatorIntegration: - """Test coordinator integration with mocked RPC client.""" - - def test_execute_with_mock_coordinator(self, module, joint_state_zeros): - """Test execute sends trajectory to coordinator.""" - module._on_joint_state(joint_state_zeros) - - success = module.plan_to_joints(JointState(position=[0.05] * 7)) - assert success is True - - # Mock the coordinator client - mock_client = MagicMock() - mock_client.task_invoke.return_value = True - module._coordinator_client = mock_client - - result = module.execute() - - assert result is True - assert module._state == ManipulationState.COMPLETED - - # Verify coordinator was called - mock_client.task_invoke.assert_called_once() - call_args = mock_client.task_invoke.call_args - task_name, method_name, kwargs = call_args[0] - - assert task_name == "traj_arm" - assert method_name == "execute" - trajectory = kwargs["trajectory"] - assert len(trajectory.points) > 1 - # Joint names should be translated - assert all(n.startswith("arm_") for n in trajectory.joint_names) - - def test_execute_rejected_by_coordinator(self, module, joint_state_zeros): - """Test handling of coordinator rejection.""" - module._on_joint_state(joint_state_zeros) - - module.plan_to_joints(JointState(position=[0.05] * 7)) - - # Mock coordinator to reject - mock_client = MagicMock() - mock_client.task_invoke.return_value = False - module._coordinator_client = mock_client - - result = module.execute() - - assert result is False - assert module._state == ManipulationState.FAULT - assert "rejected" in module._error_message.lower() - - def test_state_transitions_during_execution(self, module, joint_state_zeros): - """Test state transitions during plan and execute.""" - assert module._state == ManipulationState.IDLE - - module._on_joint_state(joint_state_zeros) - - # Plan - should go through PLANNING -> COMPLETED - module.plan_to_joints(JointState(position=[0.05] * 7)) - assert module._state == ManipulationState.COMPLETED - - # Reset works from COMPLETED - module.reset() - assert module._state == ManipulationState.IDLE - - # Plan again - module.plan_to_joints(JointState(position=[0.05] * 7)) - - # Mock coordinator - mock_client = MagicMock() - mock_client.task_invoke.return_value = True - module._coordinator_client = mock_client - - # Execute - should go to EXECUTING then COMPLETED - module.execute() - assert module._state == ManipulationState.COMPLETED diff --git a/dimos/manipulation/test_manipulation_unit.py b/dimos/manipulation/test_manipulation_unit.py deleted file mode 100644 index de551d99cd..0000000000 --- a/dimos/manipulation/test_manipulation_unit.py +++ /dev/null @@ -1,308 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unit tests for the ManipulationModule.""" - -from __future__ import annotations - -from pathlib import Path -import threading -from unittest.mock import MagicMock, patch - -import pytest - -from dimos.manipulation.manipulation_module import ( - ManipulationModule, - ManipulationState, -) -from dimos.manipulation.planning.spec import RobotModelConfig -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.msgs.trajectory_msgs import JointTrajectory, TrajectoryPoint - -# ============================================================================= -# Fixtures -# ============================================================================= - - -@pytest.fixture -def robot_config(): - """Create a robot config for testing.""" - return RobotModelConfig( - name="test_arm", - urdf_path=Path("/path/to/robot.urdf"), - base_pose=PoseStamped(position=Vector3(), orientation=Quaternion()), - joint_names=["joint1", "joint2", "joint3"], - end_effector_link="link_tcp", - base_link="link_base", - max_velocity=1.0, - max_acceleration=2.0, - coordinator_task_name="traj_arm", - ) - - -@pytest.fixture -def robot_config_with_mapping(): - """Create a robot config with joint name mapping (dual-arm scenario).""" - return RobotModelConfig( - name="left_arm", - urdf_path=Path("/path/to/robot.urdf"), - base_pose=PoseStamped(position=Vector3(), orientation=Quaternion()), - joint_names=["joint1", "joint2", "joint3"], - end_effector_link="link_tcp", - base_link="link_base", - joint_name_mapping={ - "left_joint1": "joint1", - "left_joint2": "joint2", - "left_joint3": "joint3", - }, - coordinator_task_name="traj_left", - ) - - -@pytest.fixture -def simple_trajectory(): - """Create a simple trajectory for testing.""" - return JointTrajectory( - joint_names=["joint1", "joint2", "joint3"], - points=[ - TrajectoryPoint( - positions=[0.0, 0.0, 0.0], velocities=[0.0, 0.0, 0.0], time_from_start=0.0 - ), - TrajectoryPoint( - positions=[0.5, 0.5, 0.5], velocities=[0.0, 0.0, 0.0], time_from_start=1.0 - ), - ], - ) - - -def _make_module(): - """Create a ManipulationModule instance with mocked __init__.""" - with patch.object(ManipulationModule, "__init__", lambda self: None): - module = ManipulationModule.__new__(ManipulationModule) - module._state = ManipulationState.IDLE - module._lock = threading.Lock() - module._error_message = "" - module._robots = {} - module._planned_paths = {} - module._planned_trajectories = {} - module._world_monitor = None - module._planner = None - module._kinematics = None - module._coordinator_client = None - module._graspgen = None - return module - - -# ============================================================================= -# Test State Machine -# ============================================================================= - - -class TestStateMachine: - """Test state transitions.""" - - def test_cancel_only_during_execution(self): - """Cancel only works in EXECUTING state.""" - module = _make_module() - - module._state = ManipulationState.IDLE - assert module.cancel() is False - - module._state = ManipulationState.EXECUTING - assert module.cancel() is True - assert module._state == ManipulationState.IDLE - - def test_reset_not_during_execution(self): - """Reset works in any state except EXECUTING.""" - module = _make_module() - - module._state = ManipulationState.FAULT - module._error_message = "Error" - assert module.reset() is True - assert module._state == ManipulationState.IDLE - assert module._error_message == "" - - module._state = ManipulationState.EXECUTING - assert module.reset() is False - - def test_fail_sets_fault_state(self): - """_fail helper sets FAULT state and message.""" - module = _make_module() - module._state = ManipulationState.PLANNING - - result = module._fail("Test error") - assert result is False - assert module._state == ManipulationState.FAULT - assert module._error_message == "Test error" - - def test_begin_planning_state_checks(self, robot_config): - """_begin_planning only allowed from IDLE or COMPLETED.""" - module = _make_module() - module._world_monitor = MagicMock() - module._robots = {"test_arm": ("robot_id", robot_config, MagicMock())} - - # From IDLE - OK - module._state = ManipulationState.IDLE - assert module._begin_planning() == ("test_arm", "robot_id") - assert module._state == ManipulationState.PLANNING - - # From COMPLETED - OK - module._state = ManipulationState.COMPLETED - assert module._begin_planning() == ("test_arm", "robot_id") - - # From EXECUTING - Fail - module._state = ManipulationState.EXECUTING - assert module._begin_planning() is None - - -# ============================================================================= -# Test Robot Selection -# ============================================================================= - - -class TestRobotSelection: - """Test robot selection logic.""" - - def test_single_robot_default(self, robot_config): - """Single robot is used by default.""" - module = _make_module() - module._robots = {"arm": ("id", robot_config, MagicMock())} - - result = module._get_robot() - assert result is not None - assert result[0] == "arm" - - def test_multiple_robots_require_name(self, robot_config): - """Multiple robots require explicit name.""" - module = _make_module() - module._robots = { - "left": ("id1", robot_config, MagicMock()), - "right": ("id2", robot_config, MagicMock()), - } - - # No name - fails - assert module._get_robot() is None - - # With name - works - result = module._get_robot("left") - assert result is not None - assert result[0] == "left" - - -# ============================================================================= -# Test Joint Name Translation (for coordinator integration) -# ============================================================================= - - -class TestJointNameTranslation: - """Test trajectory joint name translation for coordinator.""" - - def test_no_mapping_returns_original(self, robot_config, simple_trajectory): - """Without mapping, trajectory is returned unchanged.""" - module = _make_module() - - result = module._translate_trajectory_to_coordinator(simple_trajectory, robot_config) - assert result is simple_trajectory # Same object - - def test_mapping_translates_names(self, robot_config_with_mapping, simple_trajectory): - """With mapping, joint names are translated.""" - module = _make_module() - - result = module._translate_trajectory_to_coordinator( - simple_trajectory, robot_config_with_mapping - ) - assert result.joint_names == ["left_joint1", "left_joint2", "left_joint3"] - assert len(result.points) == 2 # Points preserved - - -# ============================================================================= -# Test Execute Method -# ============================================================================= - - -class TestExecute: - """Test coordinator execution.""" - - def test_execute_requires_trajectory(self, robot_config): - """Execute fails without planned trajectory.""" - module = _make_module() - module._robots = {"test_arm": ("id", robot_config, MagicMock())} - module._planned_trajectories = {} - - assert module.execute() is False - - def test_execute_requires_task_name(self): - """Execute fails without coordinator_task_name.""" - module = _make_module() - config_no_task = RobotModelConfig( - name="arm", - urdf_path=Path("/path"), - base_pose=PoseStamped(position=Vector3(), orientation=Quaternion()), - joint_names=["j1"], - end_effector_link="ee", - ) - module._robots = {"arm": ("id", config_no_task, MagicMock())} - module._planned_trajectories = {"arm": MagicMock()} - - assert module.execute() is False - - def test_execute_success(self, robot_config, simple_trajectory): - """Successful execute calls coordinator via task_invoke.""" - module = _make_module() - module._robots = {"test_arm": ("id", robot_config, MagicMock())} - module._planned_trajectories = {"test_arm": simple_trajectory} - - mock_client = MagicMock() - mock_client.task_invoke.return_value = True - module._coordinator_client = mock_client - - assert module.execute() is True - assert module._state == ManipulationState.COMPLETED - mock_client.task_invoke.assert_called_once_with( - "traj_arm", "execute", {"trajectory": simple_trajectory} - ) - - def test_execute_rejected(self, robot_config, simple_trajectory): - """Rejected execution sets FAULT state.""" - module = _make_module() - module._robots = {"test_arm": ("id", robot_config, MagicMock())} - module._planned_trajectories = {"test_arm": simple_trajectory} - - mock_client = MagicMock() - mock_client.task_invoke.return_value = False - module._coordinator_client = mock_client - - assert module.execute() is False - assert module._state == ManipulationState.FAULT - - -# ============================================================================= -# Test RobotModelConfig Mapping Helpers -# ============================================================================= - - -class TestRobotModelConfigMapping: - """Test RobotModelConfig joint name mapping helpers.""" - - def test_bidirectional_mapping(self, robot_config_with_mapping): - """Test URDF <-> coordinator name translation.""" - config = robot_config_with_mapping - - # Coordinator -> URDF - assert config.get_urdf_joint_name("left_joint1") == "joint1" - assert config.get_urdf_joint_name("unknown") == "unknown" - - # URDF -> Coordinator - assert config.get_coordinator_joint_name("joint1") == "left_joint1" - assert config.get_coordinator_joint_name("unknown") == "unknown" diff --git a/dimos/mapping/__init__.py b/dimos/mapping/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/mapping/costmapper.py b/dimos/mapping/costmapper.py deleted file mode 100644 index 70cd770777..0000000000 --- a/dimos/mapping/costmapper.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import asdict, dataclass, field -import time - -from reactivex import operators as ops - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module import ModuleConfig -from dimos.mapping.pointclouds.occupancy import ( - OCCUPANCY_ALGOS, - HeightCostConfig, - OccupancyConfig, -) -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class Config(ModuleConfig): - algo: str = "height_cost" - config: OccupancyConfig = field(default_factory=HeightCostConfig) - - -class CostMapper(Module): - default_config = Config - config: Config - - global_map: In[PointCloud2] - global_costmap: Out[OccupancyGrid] - - def __init__(self, cfg: GlobalConfig = global_config, **kwargs: object) -> None: - super().__init__(**kwargs) - self._global_config = cfg - - @rpc - def start(self) -> None: - super().start() - - def _publish_costmap(grid: OccupancyGrid, calc_time_ms: float, rx_monotonic: float) -> None: - self.global_costmap.publish(grid) - - def _calculate_and_time( - msg: PointCloud2, - ) -> tuple[OccupancyGrid, float, float]: - rx_monotonic = time.monotonic() # Capture receipt time - start = time.perf_counter() - grid = self._calculate_costmap(msg) - elapsed_ms = (time.perf_counter() - start) * 1000 - return grid, elapsed_ms, rx_monotonic - - self._disposables.add( - self.global_map.observable() # type: ignore[no-untyped-call] - .pipe(ops.map(_calculate_and_time)) - .subscribe(lambda result: _publish_costmap(result[0], result[1], result[2])) - ) - - @rpc - def stop(self) -> None: - super().stop() - - # @timed() # TODO: fix thread leak in timed decorator - def _calculate_costmap(self, msg: PointCloud2) -> OccupancyGrid: - fn = OCCUPANCY_ALGOS[self.config.algo] - return fn(msg, **asdict(self.config.config)) - - -cost_mapper = CostMapper.blueprint diff --git a/dimos/mapping/google_maps/conftest.py b/dimos/mapping/google_maps/conftest.py deleted file mode 100644 index 725100bcc8..0000000000 --- a/dimos/mapping/google_maps/conftest.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -from pathlib import Path - -import pytest - -from dimos.mapping.google_maps.google_maps import GoogleMaps - -_FIXTURE_DIR = Path(__file__).parent / "fixtures" - - -@pytest.fixture -def maps_client(mocker): - ret = GoogleMaps() - ret._client = mocker.MagicMock() - return ret - - -@pytest.fixture -def maps_fixture(): - def open_file(relative: str) -> str: - with open(_FIXTURE_DIR / relative) as f: - return json.load(f) - - return open_file diff --git a/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json b/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json deleted file mode 100644 index 9196eaadee..0000000000 --- a/dimos/mapping/google_maps/fixtures/get_location_context_places_nearby.json +++ /dev/null @@ -1,965 +0,0 @@ -{ - "html_attributions": [], - "next_page_token": "AciIO2fBDpHRl2XoG9zreRkt9prSCk9LDy3sxfc-6uK7JcTxGSvbWY-XX87H38Pr547AkGKiHbzLzhvJxo99ZgbyGYP-9On6WhEFfvtiSnxWrLbz3V7Cfwpi_2GYt1TMeAqGnGlhFev1--1WgmfBnapSl95c7Myuh4Yby8UM34rMAWh9Md-T9DOVExJuqunnZMrS2ViNa1IRyboIu9ixrNTNYJXQ6hoSVlkM26Yw2sJB900sQFiChr_FrDIP6dbdIzZMZ3si7-3CFrR4gy6Y6wlyeVEiriGye9cFi8U0d0BprgdSIHC3hmp-pG8qtOHvn5tXJp6bDvU12hvRL32D4FFxgM1xKHqGdrun3N06tW2G_XuXZww3voN-bZh2y5y8ubZRJbcLjZQ-rpMUKVsfNPbdVYYPgV0oiLA8IlPQkbF5MM4M", - "results": [ - { - "geometry": { - "location": { - "lat": 37.7749295, - "lng": -122.4194155 - }, - "viewport": { - "northeast": { - "lat": 37.812, - "lng": -122.3482 - }, - "southwest": { - "lat": 37.70339999999999, - "lng": -122.527 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/geocode-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "San Francisco", - "photos": [ - { - "height": 675, - "html_attributions": [ - "Zameer Dalvi" - ], - "photo_reference": "AciIO2d9Esuu4AjK5SCX_Byk2t2jNOCJ1TkBc9V7So6HH2AjHH7SccRs-n7fGxN2bdQdm_t-jrSdyt7rmGoPil2-_phu5dXOszGmOG6HITWPRmQOajaPG4WrTQvAV6BCs5RGFq3NxJZ-uFyHCT472OFg15-d-iytsU_nKWjPuX1xCwmNDmuWxTc8YBWi05Cf0MxIFsVw7oj5gaHvGFx0ngYJlk67Jwl6vOTIBiEHfseOHkGhkMD7tX-RCPBhnaAUgGXRbuawYXkiu32c9RhxRaXReyFE_TtX09yqvmA6zr9WhaCLT0vTt4-KMOxpoACBnVt7gYVvRk-FWUXBiHISzppFi6o7FbEW4OE4WWsAXSFamzI5Z5Co9cAb8BTPZX8P3E-tZiWyoOb1WyhqjpGPKYsa7YJ_SRLFMI3kv8GWOb744A4t-3kLBIgZQi9nE5M4cfqmMmdofXLEct9srvrDVEjKns5kP3yp94xrV9205rGcqMtQ3rcQWhl62pLDxf3iEahwvxV-adcMVmaPjLFCrPiUCT1xKtBtRSQDjPcuUMBPaZ-7ylCuFvJLSEaEt8WpDiSDbn22NiuM0hPqu8tqL7hJpxsXPi6fLCreITtMwCBK_sS_-3C--VNxDhyAIAdjA3iOPnTtIw", - "width": 1080 - } - ], - "place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo", - "reference": "ChIJIQBpAG2ahYAR_6128GcTUEo", - "scope": "GOOGLE", - "types": [ - "locality", - "political" - ], - "vicinity": "San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7795744, - "lng": -122.4137147 - }, - "viewport": { - "northeast": { - "lat": 37.78132539999999, - "lng": -122.41152835 - }, - "southwest": { - "lat": 37.777981, - "lng": -122.41572655 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Civic Center / UN Plaza", - "photos": [ - { - "height": 3072, - "html_attributions": [ - "Neal" - ], - "photo_reference": "AciIO2eQ5UqsRXvWTmbnbL9VjSIH-1SXRtU1k0UuJlVEyM_giS9ELQ-M4rjAF2wkan-7aE2l4yFtF4QmTEvORdTaj_lgO-_r9nTF2z7FKAFGcFxLL4wff1BD2NRu1cfYVWvStgOkdKGbZOmqKEpSU7qoFM_GjUdLO5ztvMCAJ8_h0-3VDy33ha8hGIa8AGuLhpitRAsRK9sztugTtxtaOruuuTtagZdfpyIvUjW1pJMCR3thLaWO2C4DVElGqhv4tynPVByugRqINceswryUNVh1yf_TD664L6AyyqjIL5Vv2583bIEefWHB3uEYJA2ohOV2YW_XhH5rY8Xg5Rdy6i8EUtW9GiVH694YHIgDEZsT-Or4uw_OHHYANd3z7MuQmLZ_JzyUCr8_ex8qxfzluml2bkfciWx3cqJ7YzodaED5nvzjffEuKXwp8cIz5cWF-xm1XSbTWZK5dafqVTC83ps9wDvoCmkPY2lXOgXhmTv85VTQNe8nj75LsplDo73CPg4XFRi6fZi-oicmtCjdjzpjUTHbHe3PEGB1F11BOPh_Hx8QkZlbWwIFooJc9FF8dgAh1GQzlwYb93tcPmRLAiaunw-h9F3eKDb7YghwBPtiBh6HygyNMnA4gtqdBd_qGQ6rVt9cLGCz", - "width": 4080 - } - ], - "place_id": "ChIJYTKuRpuAhYAR8O67wA_IE9s", - "plus_code": { - "compound_code": "QHHP+RG Mid-Market, San Francisco, CA, USA", - "global_code": "849VQHHP+RG" - }, - "rating": 3.5, - "reference": "ChIJYTKuRpuAhYAR8O67wA_IE9s", - "scope": "GOOGLE", - "types": [ - "subway_station", - "transit_station", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 375, - "vicinity": "1150 Market Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7802611, - "lng": -122.4145017 - }, - "viewport": { - "northeast": { - "lat": 37.7817168802915, - "lng": -122.4131737197085 - }, - "southwest": { - "lat": 37.7790189197085, - "lng": -122.4158716802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", - "name": "U.S. General Services Administration - Pacific Rim Region", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 2448, - "html_attributions": [ - "Wai Ki Wong" - ], - "photo_reference": "AciIO2cN35fxs7byGa6qiTiJAxxJMorGoHDJp95RMFDnTMm-wDrb0QUZbujgJUBIV3uLQuBDpEdvyxxzc-fyzT3DgFlJSLKcnPcm_A-Fe3cj7rjPdEO9VMj0HHRf0aqDnRQXmtv2Ouh3QUH8OdvaoOlNMw293LOxjri9JvpjhPHCwJvwkKjxFYButiE_7XywtIRyQXRkZyDKxqKVxITircGB1P3efABFUQIye8hA71QZqTfYnBzT5wDSoV3oZRaB9aXUlTDGzNl3rJXE74BrlpgVhf-uYP_POcNqMbYmLXyWOjjVEZ4YZL58Ls53etW_ZUGGeiUAcrI3Uuq4glX5GRfGHssf_dqOWA29j0HZh6A_OFSluLSDbpy-HgXcW4Zg_qgF6XqobV78J_Ira4m8lgHiT3nDffo2YfELDcIvFxOJwpl1W3TUWawmHqvHiVTvHAQ_8-TcWE_rGCVIAAc8I0W25qRFngkVJ828ZIMHsnEiLLgsKTQlxKW94uAC8kgxh6v-iXP_7vP6-0aWGkFs4a2irwfQK5n5fKmDz7LBdVjyuAhoHwcCwE8VTn0wtwUcuiVCVBFs4-AnLWhwnVxf3fdmcMsZm91lPbm3fECbnt6SBhvXR48cM_ZZpMiyfIF1QuNE-vhfsnlK", - "width": 3264 - } - ], - "place_id": "ChIJZxSUVZeAhYAReWcieluNDvY", - "plus_code": { - "compound_code": "QHJP+45 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+45" - }, - "rating": 2, - "reference": "ChIJZxSUVZeAhYAReWcieluNDvY", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 4, - "vicinity": "50 United Nations Plaza, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7801589, - "lng": -122.4143371 - }, - "viewport": { - "northeast": { - "lat": 37.7818405302915, - "lng": -122.4131042697085 - }, - "southwest": { - "lat": 37.7791425697085, - "lng": -122.4158022302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", - "name": "Federal Office Building", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 3024, - "html_attributions": [ - "espresso" - ], - "photo_reference": "AciIO2eg880rvWAbnaX5xUzP_b6dEVp4hOnZqnxo7W_1S2BZwdC0H9io5KptUm2MGue3FOw3KWPjTZeVu8B_gnFh-5EyAhJHqhlDrllLsL8K-cumkjtTDT3mxDaDXeU7XB9BWD7S0g0f4qbjEu_sKhvWAXE81_r1W5I8minbMbvzu3eU1sYICwWOk_5g4D1-690I_4V-4aJ-fDD04kHxsqkweZcxzUHgrmcKEOlt48UKVHe-GEOLD5-BRNZ3k4tx50T1SKqPeNUI_WtTrYkSkeNzCp4t9680YqCW7LBsES9viJdW_QBTgQd59gvMeIWEXQ-YBGPEobIS0hE73Eedi_1ATESgKI-tzOeeoeytLnmFFVC8c2obgt2Bd7cLOFjIjm5Oxn9jH0auBWPx8JsQifkXiyhXz2VP2AawCmID4TMtMwt-9ozTV6I_j5f_guI34w7MxKnHiyTQvupi0S4O2ByezHx56M7Ptmxjk8yia84SG20H7sRhEk3yeQHl_ujDGYhNFCtPmHWkCsdWm1go-FuMalIzkUL4ERuREN1hhdvYhswbbigJUG8mKKOBzHuPVLNK5KFs_N7E5l4g3v-drOKe1m_GafTHwQDRvEzJfL0UnIERhRYcRLMJWxeEbjtsnKch", - "width": 4032 - } - ], - "place_id": "ChIJzdTUHpuAhYAR3ZHR1a8TJ-k", - "plus_code": { - "compound_code": "QHJP+37 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+37" - }, - "rating": 4.2, - "reference": "ChIJzdTUHpuAhYAR3ZHR1a8TJ-k", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 5, - "vicinity": "50 United Nations Plaza, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7799364, - "lng": -122.4147625 - }, - "viewport": { - "northeast": { - "lat": 37.78122733029149, - "lng": -122.4136141697085 - }, - "southwest": { - "lat": 37.7785293697085, - "lng": -122.4163121302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", - "name": "UN Plaza", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 3024, - "html_attributions": [ - "Douglas Cheung" - ], - "photo_reference": "AciIO2f7vWVfkBpMV-nKU0k06pZS--irdg7JnnJBrztgXtRf0MYFd0085Gfjm7TjBB4bCefkTdJBsNtyKiklgknHCuWhz3aqwx81XHDM51Jn-g5wI0hbG6dx8RpheFxfht_vpk9CQgjjg8mFEUp-aQaEc3hivi_bog295AUmEKdhTCRlYWLQJFPEpP-AKOpLwXdKYAjddd2nh18x9p8-gF0WphREBQFaOChd9lnWyuSKX-MOecG-ff1Brwpkcroc6VUeW6z1RQcLFNCUOomOpBCmeujvTquM_bI7a6T4WzM2o6Et_47EXmPzJhSAONorX8epNNHjZspoAd-LZ_PrBgy8H-WQEm6vlY88Dtc1Sucewnrv4Cd8xm2I1ywKPSsd2mgYBMVAipSS2XHuufe5FWzZM9vPZonW0Vb-X6HOAnVeQ52ZxNddc5pjDtU5GOZNb2oF-uLwo5-qrplZDryO5if0CPQRzE6iRbO9xLsWV0S7MGmxJ_bZk7nxWXjKAFNITIZ6dQcGJxuWH_LKDsF3Sfbg1emM4Xdujx0ZHhgFcBISAfHjX5hf0kBxGhpMlFIPxRns2Eng4HzTaebZAmMeqDoN_3KlnAof47SQyeLSQNy1K6PjWGrIPfaVOpubOTLJF_dLKt5pxQ", - "width": 4032 - } - ], - "place_id": "ChIJ60hDVZeAhYAReuCqOWYsr_k", - "plus_code": { - "compound_code": "QHHP+X3 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+X3" - }, - "rating": 4, - "reference": "ChIJ60hDVZeAhYAReuCqOWYsr_k", - "scope": "GOOGLE", - "types": [ - "city_hall", - "point_of_interest", - "local_government_office", - "establishment" - ], - "user_ratings_total": 428, - "vicinity": "355 McAllister Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.781006, - "lng": -122.4143741 - }, - "viewport": { - "northeast": { - "lat": 37.78226673029149, - "lng": -122.4129892697085 - }, - "southwest": { - "lat": 37.7795687697085, - "lng": -122.4156872302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/shopping-71.png", - "icon_background_color": "#4B96F3", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/shopping_pinlet", - "name": "McAllister Market & Deli", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 4608, - "html_attributions": [ - "Asteria Moore" - ], - "photo_reference": "AciIO2chI9JnbQNwZt2yo7E--ruAq6ax7U4NrW_3PcNpGgFzXhxMqvYTtktvSLwFO5k21vHpEH-2AMYuaD6qctoIYdyt_g5EWhF88Ptb75HmmIEQzMqk2Ktpe3Vx06TnJKF47TZnQupjVdy_YTW3XGOGkA33Phe8I3I9szr54QqmYLFs6fPJMxo-M3keen9PlFiqqjvKAV170CuJ6HQ70AkRREWq3h18IcPUHHEKiZng5TKPSB7t_3dbyB_DWETnVQHu6P33XEmcKw77rgCuUogyxXZNMBulq305-FtBlH5lnvjy1F5Hpwf-q5cSB_40p082Joz0Vyazc1o4s-hnEyUnaQ6Zra1B_ODKvHqEKHoeJUKT4nAfFU4kBE5A7nmxkozqyks4MfaoN_P72atAhggEV5rog4EEtzFyeC1bx8GtQKhYccbeANSF5R9mAEpeefOrpYZpNW1uLffUMOpceZpZtNsE-yG59_v-56V1dxqCIGW9KOtVmfoEL0WLP6l-pMhKMv3EdSRmGqhbRtCA2fZNyFBWRyMwpfToRImtYxRbMiqriGONDU1e1m8j895QvLDknS6lY_qRMNv4YY3FLooGcag4YzcaDHwtI-ipxEcFknzhIIYt-_fdlTcUk0JMctC5re--5A", - "width": 2592 - } - ], - "place_id": "ChIJz3oI4ZqAhYARYviYtbeKIFQ", - "plus_code": { - "compound_code": "QHJP+C7 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+C7" - }, - "rating": 3.6, - "reference": "ChIJz3oI4ZqAhYARYviYtbeKIFQ", - "scope": "GOOGLE", - "types": [ - "liquor_store", - "atm", - "grocery_or_supermarket", - "finance", - "point_of_interest", - "food", - "store", - "establishment" - ], - "user_ratings_total": 12, - "vicinity": "136 McAllister Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7802423, - "lng": -122.4145234 - }, - "viewport": { - "northeast": { - "lat": 37.78171363029151, - "lng": -122.4131986197085 - }, - "southwest": { - "lat": 37.77901566970851, - "lng": -122.4158965802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", - "name": "US Health & Human Services Department/Office of the Regional Director", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 1200, - "html_attributions": [ - "Patrick Berkeley" - ], - "photo_reference": "AciIO2eP4AmtKmitmIZbdY4bI2mc8aCNzT2vh8plui7wj0BJt-51HlfW7-arowozWM9Os9hSUBkXItcmlXnH08GpOYXc1u6gN-XmO7AL9ifSJfgWYt6XE0CkXfQ9iBQdHF1WFlfteWLOvL0mev0reMuAz78N7It7eWQY8HW3nm2_i14G_R51kbRK2djxoWjDqY9-xP5hTxWUs1u7JFqXtzOZAeMGlhFHHmqVe4A8nWMP7tr6Y385wmCIJvGwXivQmct7flmN6NpNqqp1U5CI1jy60x7Z2Zoq_uxzWpIB-1M-VRMJHblbb_1rPAc1Sg29n5XfhX4E1M1YqlEBdqg08VaqQSLbaJEHkvfDMFKlN36IsZmb8mZfFEinYSmkcISO6x-vuhgR7G4FJZLtt74goVGKIPsQoC9oPsPyN0mLaQJs9ZTS6D2mw5zIQXYBs2IfBdnG9sWDCQTujtdGWJv_SlWUHW499I-NK0MzNPjpLB4FW3dYOuqDQdk-8hzC1A5giSjr7J783WRLVhVKjfo8G8vCPCSY4JW6x3XB5bl9IJn5j_47sGhJOrHnHVkNaMmJMtdhGflXwT42-i033uzLJEGN1e887Jqe7OHRHqa97oPbXu3FQgVPjXvdBX33gmXc8XXeDg7gcQ", - "width": 1600 - } - ], - "place_id": "ChIJ84fbMZuAhYARravvIpQYCY8", - "plus_code": { - "compound_code": "QHJP+35 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+35" - }, - "rating": 4, - "reference": "ChIJ84fbMZuAhYARravvIpQYCY8", - "scope": "GOOGLE", - "types": [ - "local_government_office", - "health", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 1, - "vicinity": "San Francisco Federal Building, 90 7th Street #5, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7794949, - "lng": -122.414318 - }, - "viewport": { - "northeast": { - "lat": 37.78079848029149, - "lng": -122.4128637197085 - }, - "southwest": { - "lat": 37.7781005197085, - "lng": -122.4155616802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/school-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/school_pinlet", - "name": "Oasis For Girls", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 3024, - "html_attributions": [ - "Alex" - ], - "photo_reference": "AciIO2cENrSmK967GV0iLgnIakOvEMavm9r5kA_LjIOHIji_Pc0T74VL-vwiFlUgoVgetRw9B-PzYrJ54EVfnbUQT-9XRi2LGt9rUOGX6V7h7lOVqgEJ1eaWEUtTDyk93eQRs3cc3GhXY2RIjL-nVdaxkwRc_RWpRPLcc8Om_aTYwyCQ5S7ZpmxPS419DoCJHt4sQJqzRsD6gz7I8AGj0c03MHYascQn4efsvFhjzaPex21ZKI9iGz923oe9WM8zq4BhgKJ3B9_IITYDuoO1mYdyIgU57ceuRoKb6n4zoCgyhLne1_SzGnFz7DrP9jL8luHSVHeoZcSKmU34Gr-sGfVs4kfH33lzlNurHQI6gIoOOWOXq7BTP-Jf5ArqGexfQfue7IGJpYjR4p5r4cJZ-dd0tzhlGvrZ2cSEnjQdv4oTx3U3kElm6foWI3xySsa1jmqsZ8BBBzEQ75rzHHhsW26xwwR9ZIKYV-_DZ9r0hrb0qPCEF3aAC9r2m6rfwrHWAfDy_-Egmv_5T1QyBFaAUT0Faay7EezCxCyWwx_0x0o2DRIOAcA8a01veJJPv1LhYcXCUnTgIATbSr-t30d9FdosyX0Vk9w4eSXU6B4qUWpusHVHPShTHhAcLMig0OOIXlZyyWtPT2sb", - "width": 4032 - } - ], - "place_id": "ChIJyTuyEoKAhYARr0GnPKZSGCk", - "plus_code": { - "compound_code": "QHHP+Q7 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+Q7" - }, - "rating": 5, - "reference": "ChIJyTuyEoKAhYARr0GnPKZSGCk", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 4, - "vicinity": "1170 Market Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.77929669999999, - "lng": -122.4143825 - }, - "viewport": { - "northeast": { - "lat": 37.78060218029149, - "lng": -122.4129812697085 - }, - "southwest": { - "lat": 37.77790421970849, - "lng": -122.4156792302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "San Francisco Culinary Bartenders & Service Employees Trust Funds", - "place_id": "ChIJpS60CuyBt4cRzO3UB4vL3L0", - "plus_code": { - "compound_code": "QHHP+P6 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+P6" - }, - "rating": 3.3, - "reference": "ChIJpS60CuyBt4cRzO3UB4vL3L0", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 6, - "vicinity": "1182 Market Street #320, San Francisco" - }, - { - "business_status": "CLOSED_TEMPORARILY", - "geometry": { - "location": { - "lat": 37.7801722, - "lng": -122.4140068 - }, - "viewport": { - "northeast": { - "lat": 37.7817733302915, - "lng": -122.4129124197085 - }, - "southwest": { - "lat": 37.7790753697085, - "lng": -122.4156103802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/civic_building-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/civic-bldg_pinlet", - "name": "San Francisco Federal Executive Board", - "permanently_closed": true, - "photos": [ - { - "height": 943, - "html_attributions": [ - "San Francisco Federal Executive Board" - ], - "photo_reference": "AciIO2ecs5V8ZC8IEmpnMKdhn2pSWsCYSZ6C9Zf6lnQbp3owjaXeXRZuPMtnIJag_ga0uw8Jwa8SB-Wsb2YyB9PrdAzutETaYb56zja6D8NwiKdf9Z4EGnZ45JH20x7119EzrunOm1q4Ii6wuY0TudtYsadmJC0NPLnUZlua4PNnW7Zl76OQwLBcaPWu6rXBHCTT6iiBqSZeKiKJ8w4RzttHfN3oYB-IE02CXQPQX1xxFEeQ5cyuGPtv8ghXHRoSJdhvYDH_P0aSrOt9ibRtrH5kv7nAamKSVUNWvT5vuPrXao9PkaJd5f16tZiDoM_61tat9r1izspBFhU", - "width": 943 - } - ], - "place_id": "ChIJu4Q_XDqBhYARojXRyiKC12g", - "plus_code": { - "compound_code": "QHJP+39 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+39" - }, - "reference": "ChIJu4Q_XDqBhYARojXRyiKC12g", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "vicinity": "50 United Nations Plaza, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.779756, - "lng": -122.41415 - }, - "viewport": { - "northeast": { - "lat": 37.78130935000001, - "lng": -122.411308 - }, - "southwest": { - "lat": 37.77806635, - "lng": -122.4163908 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Civic Center/UN Plaza BART Station", - "photos": [ - { - "height": 4032, - "html_attributions": [ - "Arthur Glauberman" - ], - "photo_reference": "AciIO2f1VMpAIRJouUVjkeEUyHB-4jzRZ2_U3kfRr-LaavcPlVYClnn2DMGMiWo9Oun0t-qo9z5WIHp1BQBHazbPqrWnSGvQoO3FpJMra0OOGSgrpsD5T4dvinfSzWqwOOlRtMyQ4vlGvR99TpxcNVcasRyNflpZxRcYD9nBUPnrNUstxTCfKqSqLdYD3ZI0xZiX3wOJ_hlUVgRfSs04iqzREGvRR8cZRaufh1Hakq3bzaBL1KGuLF8ggV94iGQmzWYmU_FddWgH9ZhjGyMPi8LYdNmypH0fBenoYGVE_bUV9dWqh5dFIKDwCyxkbIseJ6Z49MRFnSEFTtBr02xVz7Q1vAx0iKSRAMof3o5dqEd5Y1fVhDuLk3KT5JisNQZd_yWXDflaHmEgjEqza7uTrdR6LWysHDD8EdUrGQxWWHmneyc3qdWlc0TBxhGp3Q8V0a3Ian1k75PqrfkyC_IITP0KIDmaylgMSMmAQbzvkeHDtPcibG-BiNn2FNK7T77m7GpQkubMwYOI1PkoGSmveiuooTTqj6PSDGrQdDfRllk_HSwcTnd9csLazAQP_tLKHX8lsHTtTE7Orkcf8IEUfmV35Ltx2HzLYytejCYYS7ZoSfgjDTZUOY41QQ-YS0tIDKHpgr_PJqtT", - "width": 3024 - } - ], - "place_id": "ChIJK0jeP5uAhYARcxPNUpvfc7A", - "plus_code": { - "compound_code": "QHHP+W8 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+W8" - }, - "rating": 3.5, - "reference": "ChIJK0jeP5uAhYARcxPNUpvfc7A", - "scope": "GOOGLE", - "types": [ - "transit_station", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 2, - "vicinity": "United States" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.779989, - "lng": -122.4138743 - }, - "viewport": { - "northeast": { - "lat": 37.7811369802915, - "lng": -122.4131672197085 - }, - "southwest": { - "lat": 37.7784390197085, - "lng": -122.4158651802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "UN Skate Plaza", - "place_id": "ChIJR4ivYwCBhYAR2xEDgcXd8oE", - "plus_code": { - "compound_code": "QHHP+XF Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+XF" - }, - "reference": "ChIJR4ivYwCBhYAR2xEDgcXd8oE", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "vicinity": "1484 Market Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7798254, - "lng": -122.4149907 - }, - "viewport": { - "northeast": { - "lat": 37.7811608302915, - "lng": -122.4137199197085 - }, - "southwest": { - "lat": 37.77846286970851, - "lng": -122.4164178802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Curry Without Worry", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 3024, - "html_attributions": [ - "Sterling Gerard" - ], - "photo_reference": "AciIO2cHQr4ENxn9-409JJPj5hKunwLPi9gn-eN4W0X85UOvQVHoKQBUA4AotH3pkFTPxm1X76omOi2jbTiRSL9-eRFhA9wWpiXoSj2ggXeHrUxLMQBZb7cQuH4lg9YCOasXwXz3-e3H1lrByl7en3XSTkvuZUDrbtHocGV-0XNw2YpOmVvN-mLcRxgUpWhguLsvnO7B5JzXjz4ewOAxBLF9f-ZOdRktRcHDczoA0zYsOFwri0CXVjfYdB4HxjwXBPm1vXQY1U5qRydrI0Eru1tbTI9alsrmBOL4l0BAY--_fd3luNnwiQAYHzBJoZ7pqHjGOHtHa-OH7GFawpbxKr8MqeT3KVMcDVWm8sOy-zd2Gjbez5CQ5ld0w-q_2QDTVzHV5ybrzDm1OIl4vIW9eBTQVwkBwnmUjKFSZEQ-ANezOwN6XfW_jkWleRJ28dpXLo25dhW7gmYZxRcGpPwWRpcH3jyenU59CRJ6EG8nqVhTs-JzGOawmsLs4Kyg4f16fJE2lDTySU82fcQgd8uBkJGE-XrFYNOakpMWBKo1GWNOvfPsceoyB4qiLwf7VFM5Sa8yQUmNxdKRvVvhqCRjzGwVQmcPEOgpANBuDTUdz9VscmOhPO_29jRMca1S9AuseiZBdmRO4HHv", - "width": 4032 - } - ], - "place_id": "ChIJKZtFDpuAhYAR7xKvaP5D1dI", - "plus_code": { - "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+W2" - }, - "rating": 4.7, - "reference": "ChIJKZtFDpuAhYAR7xKvaP5D1dI", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 14, - "vicinity": "50 United Nations Plaza, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7798179, - "lng": -122.4149928 - }, - "viewport": { - "northeast": { - "lat": 37.7811602302915, - "lng": -122.4137218697085 - }, - "southwest": { - "lat": 37.7784622697085, - "lng": -122.4164198302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "UN Skatepark", - "photos": [ - { - "height": 4096, - "html_attributions": [ - "Ghassan G" - ], - "photo_reference": "AciIO2cuIIcUq2yO7nQ_aENkWHN-EBW8baPzWgyrlTnoDLJnZ3xkqA3qGN06NxagIX9LHoTMQKoBBtLKns2IEl90Mb3H_2P13nbPfRUkK0LEwZYq8jrhkAr1kkiuSzQZwXaQEw8o3W4kTBjRhrSnqv69l-mQjTnOMPnIvfdsfM-7-5cCCbReiG2UuhJaxEEP4HEQhpoKPdeysLMtlmOG3AkapY9hUggeffNhVVSc55UEM7CRWozNOoy8oVS6E-kixEK5Zvnrs2JgCarGttCGaQPrxg_R3LjCfWNCqbHD5pz5UGlN_Nixxf5un7OoTvmvxHCjSblmFZttvdfpoI9H54u-rdY6XBeCXON4hcc8vTt-H7pUoPOYQAQvOEsMknrcKQ10Fr7MdsMqp495fV0xc1WK-TMf0sd8aTHjJlDh0_yvi9gzBd47UzJddXi81F0y7HLNpwAHorBvYsPKM3c3pCCKjzOJKtieqvv-xvvdygIEFh4GvIfqInYEpsZeIgvnpUWZKeRoBeAh46AWyHe_-iZzkG94o5TRWiX1McziIr0nXb-2-V0uDhY1CZzDZZxTNPuaanEBSekt9tUMoF-TF-0YSyxGSlm4w8EfGhBrde4vKu2JyunwApDogalJbiDVsX5x7ZqwvBS6sBQxmxotvhRApbUOSRE", - "width": 3072 - } - ], - "place_id": "ChIJfZvlNy-BhYARYrz8xesnfo8", - "plus_code": { - "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+W2" - }, - "rating": 5, - "reference": "ChIJfZvlNy-BhYARYrz8xesnfo8", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 1, - "vicinity": "50 United Nations Plz, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.7798164, - "lng": -122.4149956 - }, - "viewport": { - "northeast": { - "lat": 37.7811597302915, - "lng": -122.4137233697085 - }, - "southwest": { - "lat": 37.7784617697085, - "lng": -122.4164213302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Sim\u00f3n Bol\u00edvar Statue", - "opening_hours": { - "open_now": true - }, - "photos": [ - { - "height": 3452, - "html_attributions": [ - "Willians Rodriguez" - ], - "photo_reference": "AciIO2cWoTT9PaFCzH3sXfgvrgMG7uflXzfYSi4jJwNNBJRMxVPQp1TO-_3F0HFe4cWsF-z2g0MrluTzpSdWET57_kIPxx_rRh7TpX6Nv6jpWStd6hDBSAu-WGoaV8T2KESXe-N4WhG0afkZV61_rKqYtk9tc_NsE7Und84qxrQHTD2U-SYCSevUE4EkOGtinTv1o9Ll9yS2Svct_xPp5dAPJEJLBj2JBmWyn2p-sK-DzFHaGzP4r1NfAxQx0oQdoa3R0IUOXLIM6Xx8B_By8Vv9x9Z6wRlblRIM9CiX497_oDaYINg0w8lBtaEN5SSO7QxPRfV8o5NtJWBMqabnW7wepbRqq7BQh43-3HO_HXB1H6nP-cHLXetjXtN775nnAWlhXCEV_2Gb2HTRK0s7xQXHGZdKQCwDXAiTLtHFNGSaqQ3GhQ6iZdGquwh3q46lv6aRczhbo2kGRUgnkYYUa8AquE7Et0miHHw2zKc3lXX9FHQQannKHRc_yMQUpeKQGlBIxTmGvKLeatxHN6iLrtlfSIuHSc4FJWaYqkkiPAny1ZYcM61Jar67gMpf3-3RVwckUMqy4a9yDJawO-g8d-9svKI-5QlZXqlayrNnPsU6KSEgJhkJ95Fdi0nNM9qRYVFVbFVzosF0", - "width": 1868 - } - ], - "place_id": "ChIJxwBPDpuAhYAREmyxJOv11Nk", - "plus_code": { - "compound_code": "QHHP+W2 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+W2" - }, - "rating": 4.4, - "reference": "ChIJxwBPDpuAhYAREmyxJOv11Nk", - "scope": "GOOGLE", - "types": [ - "tourist_attraction", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 23, - "vicinity": "50 United Nations Plaza, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.77961519999999, - "lng": -122.4143835 - }, - "viewport": { - "northeast": { - "lat": 37.78097603029151, - "lng": -122.4127372697085 - }, - "southwest": { - "lat": 37.77827806970851, - "lng": -122.4154352302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Fitness Court at UN Plaza", - "opening_hours": { - "open_now": true - }, - "photos": [ - { - "height": 3213, - "html_attributions": [ - "Ally Lim" - ], - "photo_reference": "AciIO2c46aZ1fy0jImtc4i9AybRpqmgpwtnxt0yabDDt0HSzMy6bLyNo06EfEpKBi6cvmAnTmtGPILHAMUacEz6idLBwFO6ClbLGSLpaGmrE-ER462n6AvHQXwHXjL1REr-EU_cWAGUj7vMDJ_8oJwBlON1J6OoUi4N4eaJCgGa2nYN2KhQ_IsxlW06jBWAJ_8i5UzDCk9paPMLTlx6XGrN_ARqihZrDHp1ejLT9LsQuBny8qSHSq6N_cgDjhB6x8DLxLrNeZzFcY6RTwhLDeYqAaV1xlyQN68D8rCd-THrFbXYh0eqnCUNPO2mY0KgET5ifiuIsqEAfpOJp5JHKduPfdRphmIPJfag_kwtJ5kwmjQaDcpmLpVRLxBaFKDmjZ1oFjIm68YpF0z3Tz7chAD90lfLzKKIfQadS5xZLJR-34rJwZA6uiLx-9mEe3upotSZzDmtGQCEbkEJIbWA5TXa0Gr-dK4wQ2RHkzHhIprVlxu6oiXkBzrxx5De5dULfVOtZe25GbYgC6yOGVWppzAawylRfzfroxgD0Q4Qm3vZhrSVdousQjlhvOOd4vNjF4ab1SM0NrBHydXTzm9qO-Q9O45FAGe6DG_9ftmhsrMX57SZpBlnbsYFHZEgNOJhNkAyxcW6rvg", - "width": 5712 - } - ], - "place_id": "ChIJOxlsRwCBhYAR5FY6A3dg8Ek", - "plus_code": { - "compound_code": "QHHP+R6 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+R6" - }, - "rating": 5, - "reference": "ChIJOxlsRwCBhYAR5FY6A3dg8Ek", - "scope": "GOOGLE", - "types": [ - "gym", - "health", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 3, - "vicinity": "3537 Fulton Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.77961519999999, - "lng": -122.4143835 - }, - "viewport": { - "northeast": { - "lat": 37.7810261302915, - "lng": -122.4129955697085 - }, - "southwest": { - "lat": 37.7783281697085, - "lng": -122.4156935302915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/cafe-71.png", - "icon_background_color": "#FF9E67", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/cafe_pinlet", - "name": "United Nations Cafe", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 1836, - "html_attributions": [ - "Steven Smith" - ], - "photo_reference": "AciIO2dhjLdgjy4fMy59en74_XnQ8CoXenGsfvaQ3MM7TohCqXE2tS7BYvyYoNu5gZbhJsNRulbldWgRUT1EpRPkiFZoqa1leeUttiHt1NUuSOEOYULofcZ8ShClkfIPk2U6i6-OajtQc5Aj9rYRtS8WmF_19ducNw0h4f3CSSuDPqKIloeNRsWm-uqi2faqjsgqe8iWvsmgABAmcdUhdAuDFWW31TnrtRe3D58TkvUJGv6-cpIDzuNv8gYPyokrz6lngguIGgNfy53t6xdLFbHMQFnLzgFx2NJbFeC2ZX3-WjKMXuy85hHuVUmucmLz80z6_yHa7kxlbpnruFdjhehwajdG7c0uy-HhxG7LVhRy9I4-aE0f5i4lBoZONibJ7KaHGoJLEMLcm5ig-hXHXfGoXIX3MIl5y5IOxhe4N4bimc1IsmMTs0MKw4O0ZbMhQ8yF4Uqb67ZWfIiEKEL7sXxkWGlgE65OAIutewzFNjOuWzsbQ7oCMK77hVI72s83jl3qT7SX4BQcy0wkSblVVTrm1VWf1PajA9Bzye0ZFi4yClaARpsQH8ZnOOsA3igFlJbjNohPzM8EaOPV3eWUqr8o-tkIp8IIAx5OLBqJjOs_E10AvQB7Pc4z2c6viTZDda9E", - "width": 3264 - } - ], - "place_id": "ChIJ4ZfeFJuAhYAREGTVnroeXsg", - "plus_code": { - "compound_code": "QHHP+R6 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+R6" - }, - "rating": 4.5, - "reference": "ChIJ4ZfeFJuAhYAREGTVnroeXsg", - "scope": "GOOGLE", - "types": [ - "cafe", - "point_of_interest", - "food", - "establishment" - ], - "user_ratings_total": 33, - "vicinity": "3537 Fulton Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.78012649999999, - "lng": -122.4136321 - }, - "viewport": { - "northeast": { - "lat": 37.78198923029149, - "lng": -122.4121925197085 - }, - "southwest": { - "lat": 37.7792912697085, - "lng": -122.4148904802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "UN Skate Plaza", - "photos": [ - { - "height": 2268, - "html_attributions": [ - "Ally Lim" - ], - "photo_reference": "AciIO2fVA9xB6yslvpFQ1lHcw50PP-CHL5GT3WOtJCZ9pXvXUQ_PO0UhhmED-HG6hgIzaN5asxwB8vmzFa4xU4PPKu_LIu4XoCl3PDszzyju1ve916Kpw4jxHkXej81y_IwngvIAFFEfehH5n3lgfdkiZW176mppdHS3A1FpuvUP7yRA3jhenmFvSwmhpJJ6qdicxFvd0Gk-0R-bgzE2bowKaDhUE05PdDInRQCc83j4DsKXfu0eyTUSxzKVJ_Cwy8qdyCfKLXKkdPC8puMSa4nHnaATsWwFNY0eIBKwjACewkHIw5cfCOtcnmg8C-k-iElrgDHrZbDuuFTazC44CAaY2IR-H6cylBKKo8vY73T0iWF2OFJN7hQiL41iWu49OkDv_0cLyOveKyCo-TXh-Fw3RXpsf4fOSsO8UO0l9okQ2f62L_2XRYSZtPMoax2ZrlCTiegxYScg4dvuEuKDQ6_lAqDUawZcb92EHPRV39JI8trLJLlpn0UjWEYQZJ6dVPEJkjcJbeVbxlCkxiIIrym5ljDDTCOv226BX8uEdWlEZSk5jrxt3Js7gNcNJYHlNbjb9KV1Oa_NWFU7AKzVXDJR7ZS-K9OAiAnISbJOviAroCh3vaVP958bxNJu6Cwt_jphUuYEnw", - "width": 4032 - } - ], - "place_id": "ChIJQaVbEAuBhYARTcbgmBM8tVE", - "plus_code": { - "compound_code": "QHJP+3G Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+3G" - }, - "rating": 4.6, - "reference": "ChIJQaVbEAuBhYARTcbgmBM8tVE", - "scope": "GOOGLE", - "types": [ - "point_of_interest", - "establishment" - ], - "user_ratings_total": 21, - "vicinity": "1140 Market Street, San Francisco" - }, - { - "business_status": "OPERATIONAL", - "geometry": { - "location": { - "lat": 37.78093459999999, - "lng": -122.4144382 - }, - "viewport": { - "northeast": { - "lat": 37.7822385302915, - "lng": -122.4130778197085 - }, - "southwest": { - "lat": 37.7795405697085, - "lng": -122.4157757802915 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/cafe-71.png", - "icon_background_color": "#FF9E67", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/cafe_pinlet", - "name": "Paris Cafe", - "opening_hours": { - "open_now": false - }, - "photos": [ - { - "height": 4032, - "html_attributions": [ - "Paris Cafe" - ], - "photo_reference": "AciIO2fMlGoVgo_TLdvq2CENHw2KFOvcDW45EWxcL8DAw7QPnBbPPS0665SVCCKmKdPI9upG7wCidO6UyCCcMGc4gF32SbUAAPa-whL7CHURZfb-9STDUqcrh-HWmP3K7ZmVoPpWHgFxkfsjfls6LzpphMo3DLXw5mdUIiRbg8d8PM0N-mVp-e7MBPMRIPm1t3RCBA3MdO5cBwHrRs2J3XB05ao22l6a-FBtIiaZWKEikHT9DsQnUH4bHgfvM7lPoCSCikwucTQasUYfXPbaNXm8z-LNvR6ZsTcGsOkRKsu5S7k7eEE3jK68GJxd7nV7C3217lyN12VxZ6U", - "width": 3024 - } - ], - "place_id": "ChIJOYG2HACBhYAR51qH-8IsnFM", - "plus_code": { - "compound_code": "QHJP+96 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+96" - }, - "price_level": 2, - "rating": 4.8, - "reference": "ChIJOYG2HACBhYAR51qH-8IsnFM", - "scope": "GOOGLE", - "types": [ - "cafe", - "point_of_interest", - "store", - "food", - "establishment" - ], - "user_ratings_total": 78, - "vicinity": "142 McAllister Street, San Francisco" - }, - { - "geometry": { - "location": { - "lat": 37.7773082, - "lng": -122.4196412 - }, - "viewport": { - "northeast": { - "lat": 37.78237885897592, - "lng": -122.4125122545961 - }, - "southwest": { - "lat": 37.77303595794733, - "lng": -122.4237308429429 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/geocode-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Civic Center", - "photos": [ - { - "height": 2268, - "html_attributions": [ - "Tobias Peyerl" - ], - "photo_reference": "AciIO2cy7yjg95KUbhq9hn7tUsXX0uuUcS8pB9NHPMos5CJwF9b-za_UzQEnJeyopweobag8YKyuK5xbVUhjdgpb-QFhXNknGAD7vs6skcUi4i_2tPQ-ludpZX3_p3upeF2d0Y91HGvucbf6Opj7dKjNgp7gGyY-ZTwhfqo32bmEcu3G_CbTmvbyhuJXocIcJOIXwOM7VVxVB-_3vrcpWPHeV18Y6ilm_atTzkouUvclYwo5i_YInAZ_cNN1DPiNNsK4uHEOR-1wYHjaF8A2G-Y80ieN9G9TxZl6E04wxiiEx3lAYuUuOq4Be5RyMTSDKgv75gvjKmQPvxSD2nVKl8OKxXCWAujxI44xi0Mj_Jr7-K55rwJjTPpIPa-ng72LSvyQ4Er-tjC83O17SFUMNNxE5ixb-xDuARpu3UjB-0pzD8vJJ9BAnwHkUhvDueMMVrrQ7W7BNYw7T4-A-eiznIpS6pft_vc2Kkq3t-CE3-VlZAUC7dSoCiK-Kag77oB2WlIjJltl9dgtlNid2qoGE6nNkWBYlDnxADFBkHDEIeh6jIzqGMcUbr-rtw1H4otL8MjlWf65JpbCAmXifV1rSPqylFatmfp74jIuJSmnODs-lG_-R1eObSQ3oaDi280kJmvX6VOK5XDV", - "width": 4032 - } - ], - "place_id": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", - "reference": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", - "scope": "GOOGLE", - "types": [ - "neighborhood", - "political" - ], - "vicinity": "San Francisco" - } - ], - "status": "OK" -} diff --git a/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json b/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json deleted file mode 100644 index 216c02aca9..0000000000 --- a/dimos/mapping/google_maps/fixtures/get_location_context_reverse_geocode.json +++ /dev/null @@ -1,1140 +0,0 @@ -[ - { - "address_components": [ - { - "long_name": "50", - "short_name": "50", - "types": [ - "street_number" - ] - }, - { - "long_name": "United Nations Plaza", - "short_name": "United Nations Plaza", - "types": [ - "route" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - }, - { - "long_name": "4917", - "short_name": "4917", - "types": [ - "postal_code_suffix" - ] - } - ], - "formatted_address": "50 United Nations Plaza, San Francisco, CA 94102, USA", - "geometry": { - "location": { - "lat": 37.78021, - "lng": -122.4144194 - }, - "location_type": "ROOFTOP", - "viewport": { - "northeast": { - "lat": 37.78155898029149, - "lng": -122.4130704197085 - }, - "southwest": { - "lat": 37.77886101970849, - "lng": -122.4157683802915 - } - } - }, - "navigation_points": [ - { - "location": { - "latitude": 37.7799875, - "longitude": -122.4143728 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.7807662, - "longitude": -122.4145332 - }, - "restricted_travel_modes": [ - "WALK" - ] - } - ], - "place_id": "ChIJp9HdGZuAhYAR9HQeU37hyx0", - "types": [ - "street_address", - "subpremise" - ] - }, - { - "address_components": [ - { - "long_name": "50", - "short_name": "50", - "types": [ - "street_number" - ] - }, - { - "long_name": "Hyde Street", - "short_name": "Hyde St", - "types": [ - "route" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - } - ], - "formatted_address": "50 Hyde St, San Francisco, CA 94102, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.78081540000001, - "lng": -122.4137806 - }, - "southwest": { - "lat": 37.7800522, - "lng": -122.415187 - } - }, - "location": { - "lat": 37.7805991, - "lng": -122.4147826 - }, - "location_type": "ROOFTOP", - "viewport": { - "northeast": { - "lat": 37.78178278029151, - "lng": -122.4131348197085 - }, - "southwest": { - "lat": 37.77908481970851, - "lng": -122.4158327802915 - } - } - }, - "navigation_points": [ - { - "location": { - "latitude": 37.7799291, - "longitude": -122.4143652 - }, - "restricted_travel_modes": [ - "WALK" - ] - } - ], - "place_id": "ChIJ7Q9FGZuAhYARSovheSUzVeE", - "types": [ - "premise", - "street_address" - ] - }, - { - "address_components": [ - { - "long_name": "Civic Center/UN Plaza BART Station", - "short_name": "Civic Center/UN Plaza BART Station", - "types": [ - "establishment", - "point_of_interest", - "transit_station" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "Civic Center/UN Plaza BART Station, San Francisco, CA, USA", - "geometry": { - "location": { - "lat": 37.779756, - "lng": -122.41415 - }, - "location_type": "GEOMETRIC_CENTER", - "viewport": { - "northeast": { - "lat": 37.7811049802915, - "lng": -122.4128010197085 - }, - "southwest": { - "lat": 37.7784070197085, - "lng": -122.4154989802915 - } - } - }, - "navigation_points": [ - { - "location": { - "latitude": 37.7797284, - "longitude": -122.4142112 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.779631, - "longitude": -122.4150367 - }, - "restricted_travel_modes": [ - "WALK" - ] - }, - { - "location": { - "latitude": 37.7795262, - "longitude": -122.4138289 - }, - "restricted_travel_modes": [ - "WALK" - ] - }, - { - "location": { - "latitude": 37.7796804, - "longitude": -122.4136322 - } - }, - { - "location": { - "latitude": 37.7804986, - "longitude": -122.4129601 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.7788771, - "longitude": -122.414549 - } - } - ], - "place_id": "ChIJK0jeP5uAhYARcxPNUpvfc7A", - "plus_code": { - "compound_code": "QHHP+W8 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+W8" - }, - "types": [ - "establishment", - "point_of_interest", - "transit_station" - ] - }, - { - "address_components": [ - { - "long_name": "1-99", - "short_name": "1-99", - "types": [ - "street_number" - ] - }, - { - "long_name": "United Nations Plaza", - "short_name": "United Nations Plz", - "types": [ - "route" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - }, - { - "long_name": "7402", - "short_name": "7402", - "types": [ - "postal_code_suffix" - ] - } - ], - "formatted_address": "1-99 United Nations Plz, San Francisco, CA 94102, USA", - "geometry": { - "location": { - "lat": 37.779675, - "lng": -122.41408 - }, - "location_type": "ROOFTOP", - "viewport": { - "northeast": { - "lat": 37.78102398029149, - "lng": -122.4127310197085 - }, - "southwest": { - "lat": 37.7783260197085, - "lng": -122.4154289802915 - } - } - }, - "navigation_points": [ - { - "location": { - "latitude": 37.7796351, - "longitude": -122.4141273 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.7796283, - "longitude": -122.4138453 - }, - "restricted_travel_modes": [ - "WALK" - ] - } - ], - "place_id": "ChIJD8AMQJuAhYARgQPDkMbiVZE", - "plus_code": { - "compound_code": "QHHP+V9 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHHP+V9" - }, - "types": [ - "street_address" - ] - }, - { - "address_components": [ - { - "long_name": "QHJP+36", - "short_name": "QHJP+36", - "types": [ - "plus_code" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - } - ], - "formatted_address": "QHJP+36 Civic Center, San Francisco, CA, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.78025, - "lng": -122.414375 - }, - "southwest": { - "lat": 37.780125, - "lng": -122.4145 - } - }, - "location": { - "lat": 37.7801776, - "lng": -122.4144952 - }, - "location_type": "GEOMETRIC_CENTER", - "viewport": { - "northeast": { - "lat": 37.78153648029149, - "lng": -122.4130885197085 - }, - "southwest": { - "lat": 37.77883851970849, - "lng": -122.4157864802915 - } - } - }, - "place_id": "GhIJMIkO3NzjQkARVhbgFoeaXsA", - "plus_code": { - "compound_code": "QHJP+36 Civic Center, San Francisco, CA, USA", - "global_code": "849VQHJP+36" - }, - "types": [ - "plus_code" - ] - }, - { - "address_components": [ - { - "long_name": "39", - "short_name": "39", - "types": [ - "street_number" - ] - }, - { - "long_name": "Hyde Street", - "short_name": "Hyde St", - "types": [ - "route" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - } - ], - "formatted_address": "39 Hyde St, San Francisco, CA 94102, USA", - "geometry": { - "location": { - "lat": 37.7800157, - "lng": -122.4151997 - }, - "location_type": "RANGE_INTERPOLATED", - "viewport": { - "northeast": { - "lat": 37.7813646802915, - "lng": -122.4138507197085 - }, - "southwest": { - "lat": 37.7786667197085, - "lng": -122.4165486802915 - } - } - }, - "place_id": "EigzOSBIeWRlIFN0LCBTYW4gRnJhbmNpc2NvLCBDQSA5NDEwMiwgVVNBIhoSGAoUChIJNcWgBpuAhYARvBLCxkfib9AQJw", - "types": [ - "street_address" - ] - }, - { - "address_components": [ - { - "long_name": "47-35", - "short_name": "47-35", - "types": [ - "street_number" - ] - }, - { - "long_name": "Hyde Street", - "short_name": "Hyde St", - "types": [ - "route" - ] - }, - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - } - ], - "formatted_address": "47-35 Hyde St, San Francisco, CA 94102, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.7803333, - "lng": -122.4151588 - }, - "southwest": { - "lat": 37.7798162, - "lng": -122.4152658 - } - }, - "location": { - "lat": 37.7800748, - "lng": -122.415212 - }, - "location_type": "GEOMETRIC_CENTER", - "viewport": { - "northeast": { - "lat": 37.7814237302915, - "lng": -122.4138633197085 - }, - "southwest": { - "lat": 37.7787257697085, - "lng": -122.4165612802915 - } - } - }, - "place_id": "ChIJNcWgBpuAhYARvBLCxkfib9A", - "types": [ - "route" - ] - }, - { - "address_components": [ - { - "long_name": "Civic Center", - "short_name": "Civic Center", - "types": [ - "neighborhood", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - }, - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - } - ], - "formatted_address": "Civic Center, San Francisco, CA 94102, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.7823789, - "lng": -122.4125123 - }, - "southwest": { - "lat": 37.773036, - "lng": -122.4237308 - } - }, - "location": { - "lat": 37.7773082, - "lng": -122.4196412 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 37.7823789, - "lng": -122.4125123 - }, - "southwest": { - "lat": 37.773036, - "lng": -122.4237308 - } - } - }, - "place_id": "ChIJ3eJWtI6AhYAR2ovTWatCF8s", - "types": [ - "neighborhood", - "political" - ] - }, - { - "address_components": [ - { - "long_name": "94102", - "short_name": "94102", - "types": [ - "postal_code" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "San Francisco, CA 94102, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.789226, - "lng": -122.4034491 - }, - "southwest": { - "lat": 37.7694409, - "lng": -122.429849 - } - }, - "location": { - "lat": 37.7786871, - "lng": -122.4212424 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 37.789226, - "lng": -122.4034491 - }, - "southwest": { - "lat": 37.7694409, - "lng": -122.429849 - } - } - }, - "place_id": "ChIJs88qnZmAhYARk8u-7t1Sc2g", - "types": [ - "postal_code" - ] - }, - { - "address_components": [ - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "San Francisco County, San Francisco, CA, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.929824, - "lng": -122.28178 - }, - "southwest": { - "lat": 37.63983, - "lng": -123.1327983 - } - }, - "location": { - "lat": 37.7618219, - "lng": -122.5146439 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 37.929824, - "lng": -122.28178 - }, - "southwest": { - "lat": 37.63983, - "lng": -123.1327983 - } - } - }, - "place_id": "ChIJIQBpAG2ahYARUksNqd0_1h8", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "address_components": [ - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "San Francisco, CA, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 37.929824, - "lng": -122.28178 - }, - "southwest": { - "lat": 37.6398299, - "lng": -123.1328145 - } - }, - "location": { - "lat": 37.7749295, - "lng": -122.4194155 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 37.929824, - "lng": -122.28178 - }, - "southwest": { - "lat": 37.6398299, - "lng": -123.1328145 - } - } - }, - "place_id": "ChIJIQBpAG2ahYAR_6128GcTUEo", - "types": [ - "locality", - "political" - ] - }, - { - "address_components": [ - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "California, USA", - "geometry": { - "bounds": { - "northeast": { - "lat": 42.009503, - "lng": -114.131211 - }, - "southwest": { - "lat": 32.52950810000001, - "lng": -124.482003 - } - }, - "location": { - "lat": 36.778261, - "lng": -119.4179324 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 42.009503, - "lng": -114.131211 - }, - "southwest": { - "lat": 32.52950810000001, - "lng": -124.482003 - } - } - }, - "place_id": "ChIJPV4oX_65j4ARVW8IJ6IJUYs", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "address_components": [ - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "United States", - "geometry": { - "bounds": { - "northeast": { - "lat": 74.071038, - "lng": -66.885417 - }, - "southwest": { - "lat": 18.7763, - "lng": 166.9999999 - } - }, - "location": { - "lat": 38.7945952, - "lng": -106.5348379 - }, - "location_type": "APPROXIMATE", - "viewport": { - "northeast": { - "lat": 74.071038, - "lng": -66.885417 - }, - "southwest": { - "lat": 18.7763, - "lng": 166.9999999 - } - } - }, - "place_id": "ChIJCzYy5IS16lQRQrfeQ5K5Oxw", - "types": [ - "country", - "political" - ] - } -] diff --git a/dimos/mapping/google_maps/fixtures/get_position.json b/dimos/mapping/google_maps/fixtures/get_position.json deleted file mode 100644 index 410d2add2a..0000000000 --- a/dimos/mapping/google_maps/fixtures/get_position.json +++ /dev/null @@ -1,141 +0,0 @@ -[ - { - "address_components": [ - { - "long_name": "Golden Gate Bridge", - "short_name": "Golden Gate Bridge", - "types": [ - "establishment", - "point_of_interest", - "tourist_attraction" - ] - }, - { - "long_name": "Golden Gate Bridge", - "short_name": "Golden Gate Brg", - "types": [ - "route" - ] - }, - { - "long_name": "San Francisco", - "short_name": "SF", - "types": [ - "locality", - "political" - ] - }, - { - "long_name": "San Francisco County", - "short_name": "San Francisco County", - "types": [ - "administrative_area_level_2", - "political" - ] - }, - { - "long_name": "California", - "short_name": "CA", - "types": [ - "administrative_area_level_1", - "political" - ] - }, - { - "long_name": "United States", - "short_name": "US", - "types": [ - "country", - "political" - ] - } - ], - "formatted_address": "Golden Gate Bridge, Golden Gate Brg, San Francisco, CA, USA", - "geometry": { - "location": { - "lat": 37.8199109, - "lng": -122.4785598 - }, - "location_type": "GEOMETRIC_CENTER", - "viewport": { - "northeast": { - "lat": 37.8324583, - "lng": -122.4756692 - }, - "southwest": { - "lat": 37.8075604, - "lng": -122.4810829 - } - } - }, - "navigation_points": [ - { - "location": { - "latitude": 37.8075604, - "longitude": -122.4756957 - } - }, - { - "location": { - "latitude": 37.80756119999999, - "longitude": -122.4756922 - }, - "restricted_travel_modes": [ - "WALK" - ] - }, - { - "location": { - "latitude": 37.8324279, - "longitude": -122.4810829 - } - }, - { - "location": { - "latitude": 37.8324382, - "longitude": -122.4810669 - }, - "restricted_travel_modes": [ - "WALK" - ] - }, - { - "location": { - "latitude": 37.8083987, - "longitude": -122.4765643 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.8254712, - "longitude": -122.4791469 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - }, - { - "location": { - "latitude": 37.8321189, - "longitude": -122.4808249 - }, - "restricted_travel_modes": [ - "DRIVE" - ] - } - ], - "place_id": "ChIJw____96GhYARCVVwg5cT7c0", - "plus_code": { - "compound_code": "RG9C+XH Presidio of San Francisco, San Francisco, CA", - "global_code": "849VRG9C+XH" - }, - "types": [ - "establishment", - "point_of_interest", - "tourist_attraction" - ] - } -] diff --git a/dimos/mapping/google_maps/fixtures/get_position_with_places.json b/dimos/mapping/google_maps/fixtures/get_position_with_places.json deleted file mode 100644 index d471a8368a..0000000000 --- a/dimos/mapping/google_maps/fixtures/get_position_with_places.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "html_attributions": [], - "results": [ - { - "business_status": "OPERATIONAL", - "formatted_address": "Golden Gate Brg, San Francisco, CA, United States", - "geometry": { - "location": { - "lat": 37.8199109, - "lng": -122.4785598 - }, - "viewport": { - "northeast": { - "lat": 37.84490724999999, - "lng": -122.47296235 - }, - "southwest": { - "lat": 37.79511145000001, - "lng": -122.48378975 - } - } - }, - "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png", - "icon_background_color": "#7B9EB0", - "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet", - "name": "Golden Gate Bridge", - "photos": [ - { - "height": 12240, - "html_attributions": [ - "Jitesh Patil" - ], - "photo_reference": "AciIO2dcF-W6JeWe01lyR39crDHHon3awa5LlBNNhxAZcAExA3sTr33iFa8HjDgPPfdNrl3C-0Bzqp2qEndFz3acXtm1kmj7puXUOtO48-Qmovp9Nvi5k3XJVbIEPYYRCXOshrYQ1od2tHe-MBkvFNxsg4uNByEbJxkstLLTuEOmSbCEx53EQfuJoxbPQgRGphAPDFkTeiCODXd7KzdL9-2GvVYTrGl_IK-AIds1-UYwWJPOi1mkM-iXFVoVm0R1LOgt-ydhnAaRFQPzOlz9Oezc0kDiuxvzjTO4mgeY79Nqcxq2osBqYGyJTLINYfNphZHzncxWqpWXP_mvQt77YaW368RGbBGDrHubXHJBkj7sdru0N1-qf5Q28rsxCSI5yyNsHm8zFmNWm1PlWA_LItL5LpoxG9Xkuuhuvv3XjWtBs5hnHxNDHP4jbJinWz2DPd9IPxHH-BAfwfJGdtgW1juBAEDi8od5KP95Drt8e9XOaG6I5UIeJnvUqq4Q1McAiVx5rVn7FGwu3NsTAeeS4FCKy2Ql_YoQpcqzRO45w8tI4DqFd8F19pZHw3t7p1t7DwmzAMzIS_17_2aScA", - "width": 16320 - } - ], - "place_id": "ChIJw____96GhYARCVVwg5cT7c0", - "plus_code": { - "compound_code": "RG9C+XH Presidio of San Francisco, San Francisco, CA, USA", - "global_code": "849VRG9C+XH" - }, - "rating": 4.8, - "reference": "ChIJw____96GhYARCVVwg5cT7c0", - "types": [ - "tourist_attraction", - "point_of_interest", - "establishment" - ], - "user_ratings_total": 83799 - } - ], - "status": "OK" -} diff --git a/dimos/mapping/google_maps/google_maps.py b/dimos/mapping/google_maps/google_maps.py deleted file mode 100644 index 7f5ce32e99..0000000000 --- a/dimos/mapping/google_maps/google_maps.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import googlemaps # type: ignore[import-untyped] - -from dimos.mapping.google_maps.types import ( - Coordinates, - LocationContext, - NearbyPlace, - PlacePosition, - Position, -) -from dimos.mapping.types import LatLon -from dimos.mapping.utils.distance import distance_in_meters -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class GoogleMaps: - _client: googlemaps.Client - _max_nearby_places: int - - def __init__(self, api_key: str | None = None) -> None: - api_key = api_key or os.environ.get("GOOGLE_MAPS_API_KEY") - if not api_key: - raise ValueError("GOOGLE_MAPS_API_KEY environment variable not set") - self._client = googlemaps.Client(key=api_key) - self._max_nearby_places = 6 - - def get_position(self, query: str, current_location: LatLon | None = None) -> Position | None: - # Use location bias if current location is provided - if current_location: - geocode_results = self._client.geocode( - query, - bounds={ - "southwest": { - "lat": current_location.lat - 0.5, - "lng": current_location.lon - 0.5, - }, - "northeast": { - "lat": current_location.lat + 0.5, - "lng": current_location.lon + 0.5, - }, - }, - ) - else: - geocode_results = self._client.geocode(query) - - if not geocode_results: - return None - - result = geocode_results[0] - - location = result["geometry"]["location"] - - return Position( - lat=location["lat"], - lon=location["lng"], - description=result["formatted_address"], - ) - - def get_position_with_places( - self, query: str, current_location: LatLon | None = None - ) -> PlacePosition | None: - # Use location bias if current location is provided - if current_location: - places_results = self._client.places( - query, - location=(current_location.lat, current_location.lon), - radius=50000, # 50km radius for location bias - ) - else: - places_results = self._client.places(query) - - if not places_results or "results" not in places_results: - return None - - results = places_results["results"] - if not results: - return None - - place = results[0] - - location = place["geometry"]["location"] - - return PlacePosition( - lat=location["lat"], - lon=location["lng"], - description=place.get("name", ""), - address=place.get("formatted_address", ""), - types=place.get("types", []), - ) - - def get_location_context( - self, latlon: LatLon, radius: int = 100, n_nearby_places: int = 6 - ) -> LocationContext | None: - reverse_geocode_results = self._client.reverse_geocode((latlon.lat, latlon.lon)) - - if not reverse_geocode_results: - return None - - result = reverse_geocode_results[0] - - # Extract address components - components = {} - for component in result.get("address_components", []): - types = component.get("types", []) - if "street_number" in types: - components["street_number"] = component["long_name"] - elif "route" in types: - components["street"] = component["long_name"] - elif "neighborhood" in types: - components["neighborhood"] = component["long_name"] - elif "locality" in types: - components["locality"] = component["long_name"] - elif "administrative_area_level_1" in types: - components["admin_area"] = component["long_name"] - elif "country" in types: - components["country"] = component["long_name"] - elif "postal_code" in types: - components["postal_code"] = component["long_name"] - - nearby_places, place_types_summary = self._get_nearby_places( - latlon, radius, n_nearby_places - ) - - return LocationContext( - formatted_address=result.get("formatted_address", ""), - street_number=components.get("street_number", ""), - street=components.get("street", ""), - neighborhood=components.get("neighborhood", ""), - locality=components.get("locality", ""), - admin_area=components.get("admin_area", ""), - country=components.get("country", ""), - postal_code=components.get("postal_code", ""), - nearby_places=nearby_places, - place_types_summary=place_types_summary or "No specific landmarks nearby", - coordinates=Coordinates(lat=latlon.lat, lon=latlon.lon), - ) - - def _get_nearby_places( - self, latlon: LatLon, radius: int, n_nearby_places: int - ) -> tuple[list[NearbyPlace], str]: - nearby_places = [] - place_types_count: dict[str, int] = {} - - places_nearby = self._client.places_nearby(location=(latlon.lat, latlon.lon), radius=radius) - - if places_nearby and "results" in places_nearby: - for place in places_nearby["results"][:n_nearby_places]: - place_lat = place["geometry"]["location"]["lat"] - place_lon = place["geometry"]["location"]["lng"] - place_latlon = LatLon(lat=place_lat, lon=place_lon) - - place_info = NearbyPlace( - name=place.get("name", ""), - types=place.get("types", []), - vicinity=place.get("vicinity", ""), - distance=round(distance_in_meters(place_latlon, latlon), 1), - ) - - nearby_places.append(place_info) - - for place_type in place.get("types", []): - if place_type not in ["point_of_interest", "establishment"]: - place_types_count[place_type] = place_types_count.get(place_type, 0) + 1 - nearby_places.sort(key=lambda x: x.distance) - - place_types_summary = ", ".join( - [ - f"{count} {ptype.replace('_', ' ')}{'s' if count > 1 else ''}" - for ptype, count in sorted( - place_types_count.items(), key=lambda x: x[1], reverse=True - )[:5] - ] - ) - - return nearby_places, place_types_summary diff --git a/dimos/mapping/google_maps/test_google_maps.py b/dimos/mapping/google_maps/test_google_maps.py deleted file mode 100644 index 13f7fa8eaa..0000000000 --- a/dimos/mapping/google_maps/test_google_maps.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.mapping.types import LatLon - - -def test_get_position(maps_client, maps_fixture) -> None: - maps_client._client.geocode.return_value = maps_fixture("get_position.json") - - res = maps_client.get_position("golden gate bridge") - - assert res.model_dump() == { - "description": "Golden Gate Bridge, Golden Gate Brg, San Francisco, CA, USA", - "lat": 37.8199109, - "lon": -122.4785598, - } - - -def test_get_position_with_places(maps_client, maps_fixture) -> None: - maps_client._client.places.return_value = maps_fixture("get_position_with_places.json") - - res = maps_client.get_position_with_places("golden gate bridge") - - assert res.model_dump() == { - "address": "Golden Gate Brg, San Francisco, CA, United States", - "description": "Golden Gate Bridge", - "lat": 37.8199109, - "lon": -122.4785598, - "types": [ - "tourist_attraction", - "point_of_interest", - "establishment", - ], - } - - -def test_get_location_context(maps_client, maps_fixture) -> None: - maps_client._client.reverse_geocode.return_value = maps_fixture( - "get_location_context_reverse_geocode.json" - ) - maps_client._client.places_nearby.return_value = maps_fixture( - "get_location_context_places_nearby.json" - ) - - res = maps_client.get_location_context(LatLon(lat=37.78017758753598, lon=-122.4144951709186)) - - assert res.model_dump() == { - "admin_area": "California", - "coordinates": { - "lat": 37.78017758753598, - "lon": -122.4144951709186, - }, - "country": "United States", - "formatted_address": "50 United Nations Plaza, San Francisco, CA 94102, USA", - "locality": "San Francisco", - "nearby_places": [ - { - "distance": 9.3, - "name": "U.S. General Services Administration - Pacific Rim Region", - "types": [ - "point_of_interest", - "establishment", - ], - "vicinity": "50 United Nations Plaza, San Francisco", - }, - { - "distance": 14.0, - "name": "Federal Office Building", - "types": [ - "point_of_interest", - "establishment", - ], - "vicinity": "50 United Nations Plaza, San Francisco", - }, - { - "distance": 35.7, - "name": "UN Plaza", - "types": [ - "city_hall", - "point_of_interest", - "local_government_office", - "establishment", - ], - "vicinity": "355 McAllister Street, San Francisco", - }, - { - "distance": 92.7, - "name": "McAllister Market & Deli", - "types": [ - "liquor_store", - "atm", - "grocery_or_supermarket", - "finance", - "point_of_interest", - "food", - "store", - "establishment", - ], - "vicinity": "136 McAllister Street, San Francisco", - }, - { - "distance": 95.9, - "name": "Civic Center / UN Plaza", - "types": [ - "subway_station", - "transit_station", - "point_of_interest", - "establishment", - ], - "vicinity": "1150 Market Street, San Francisco", - }, - { - "distance": 726.3, - "name": "San Francisco", - "types": [ - "locality", - "political", - ], - "vicinity": "San Francisco", - }, - ], - "neighborhood": "Civic Center", - "place_types_summary": "1 locality, 1 political, 1 subway station, 1 transit station, 1 city hall", - "postal_code": "94102", - "street": "United Nations Plaza", - "street_number": "50", - } diff --git a/dimos/mapping/google_maps/types.py b/dimos/mapping/google_maps/types.py deleted file mode 100644 index 29f9bee6eb..0000000000 --- a/dimos/mapping/google_maps/types.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from pydantic import BaseModel - - -class Coordinates(BaseModel): - """GPS coordinates.""" - - lat: float - lon: float - - -class Position(BaseModel): - """Basic position information from geocoding.""" - - lat: float - lon: float - description: str - - -class PlacePosition(BaseModel): - """Position with places API details.""" - - lat: float - lon: float - description: str - address: str - types: list[str] - - -class NearbyPlace(BaseModel): - """Information about a nearby place.""" - - name: str - types: list[str] - distance: float - vicinity: str - - -class LocationContext(BaseModel): - """Contextual information about a location.""" - - formatted_address: str | None = None - street_number: str | None = None - street: str | None = None - neighborhood: str | None = None - locality: str | None = None - admin_area: str | None = None - country: str | None = None - postal_code: str | None = None - nearby_places: list[NearbyPlace] = [] - place_types_summary: str | None = None - coordinates: Coordinates diff --git a/dimos/mapping/occupancy/conftest.py b/dimos/mapping/occupancy/conftest.py deleted file mode 100644 index f20dc1310b..0000000000 --- a/dimos/mapping/occupancy/conftest.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.mapping.occupancy.gradient import gradient -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.utils.data import get_data - - -@pytest.fixture -def occupancy() -> OccupancyGrid: - return OccupancyGrid(np.load(get_data("occupancy_simple.npy"))) - - -@pytest.fixture -def occupancy_gradient(occupancy) -> OccupancyGrid: - return gradient(occupancy, max_distance=1.5) diff --git a/dimos/mapping/occupancy/extrude_occupancy.py b/dimos/mapping/occupancy/extrude_occupancy.py deleted file mode 100644 index 799319cbf6..0000000000 --- a/dimos/mapping/occupancy/extrude_occupancy.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - -# Rectangle type: (x, y, width, height) -Rect = tuple[int, int, int, int] - - -def identify_convex_shapes(occupancy_grid: OccupancyGrid) -> list[Rect]: - """Identify occupied zones and decompose them into convex rectangles. - - This function finds all occupied cells in the occupancy grid and - decomposes them into axis-aligned rectangles suitable for MuJoCo - collision geometry. - - Args: - occupancy_grid: The input occupancy grid. - output_path: Path to save the visualization image. - - Returns: - List of rectangles as (x, y, width, height) tuples in grid coords. - """ - grid = occupancy_grid.grid - - # Create binary mask of occupied cells (treat UNKNOWN as OCCUPIED) - occupied_mask = ((grid == CostValues.OCCUPIED) | (grid == CostValues.UNKNOWN)).astype( - np.uint8 - ) * 255 - - return _decompose_to_rectangles(occupied_mask) - - -def _decompose_to_rectangles(mask: NDArray[np.uint8]) -> list[Rect]: - """Decompose a binary mask into rectangles using greedy maximal rectangles. - - Iteratively finds and removes the largest rectangle until the mask is empty. - - Args: - mask: Binary mask of the shape (255 for occupied, 0 for free). - - Returns: - List of rectangles as (x, y, width, height) tuples. - """ - rectangles: list[Rect] = [] - remaining = mask.copy() - - max_iterations = 10000 # Safety limit - - for _ in range(max_iterations): - # Find the largest rectangle in the remaining mask - rect = _find_largest_rectangle(remaining) - - if rect is None: - break - - x_start, y_start, x_end, y_end = rect - - # Add rectangle to shapes - # Store as (x, y, width, height) - # x_end and y_end are exclusive (like Python slicing) - rectangles.append((x_start, y_start, x_end - x_start, y_end - y_start)) - - # Remove this rectangle from the mask - remaining[y_start:y_end, x_start:x_end] = 0 - - return rectangles - - -def _find_largest_rectangle(mask: NDArray[np.uint8]) -> tuple[int, int, int, int] | None: - """Find the largest rectangle of 1s in a binary mask. - - Uses the histogram method for O(rows * cols) complexity. - - Args: - mask: Binary mask (non-zero = occupied). - - Returns: - (x_start, y_start, x_end, y_end) or None if no rectangle found. - Coordinates are exclusive on the end (like Python slicing). - """ - if not np.any(mask): - return None - - rows, cols = mask.shape - binary = (mask > 0).astype(np.int32) - - # Build histogram of heights for each row - heights = np.zeros((rows, cols), dtype=np.int32) - heights[0] = binary[0] - for i in range(1, rows): - heights[i] = np.where(binary[i] > 0, heights[i - 1] + 1, 0) - - best_area = 0 - best_rect: tuple[int, int, int, int] | None = None - - # For each row, find largest rectangle in histogram - for row_idx in range(rows): - hist = heights[row_idx] - rect = _largest_rect_in_histogram(hist, row_idx) - if rect is not None: - x_start, y_start, x_end, y_end = rect - area = (x_end - x_start) * (y_end - y_start) - if area > best_area: - best_area = area - best_rect = rect - - return best_rect - - -def _largest_rect_in_histogram( - hist: NDArray[np.int32], bottom_row: int -) -> tuple[int, int, int, int] | None: - """Find largest rectangle in a histogram. - - Args: - hist: Array of heights. - bottom_row: The row index this histogram ends at. - - Returns: - (x_start, y_start, x_end, y_end) or None. - """ - n = len(hist) - if n == 0: - return None - - # Stack-based algorithm for largest rectangle in histogram - stack: list[int] = [] # Stack of indices - best_area = 0 - best_rect: tuple[int, int, int, int] | None = None - - for i in range(n + 1): - h = hist[i] if i < n else 0 - - while stack and hist[stack[-1]] > h: - height = hist[stack.pop()] - width_start = stack[-1] + 1 if stack else 0 - width_end = i - area = height * (width_end - width_start) - - if area > best_area: - best_area = area - # Convert to rectangle coordinates - y_start = bottom_row - height + 1 - y_end = bottom_row + 1 - best_rect = (width_start, y_start, width_end, y_end) - - stack.append(i) - - return best_rect - - -def generate_mujoco_scene( - occupancy_grid: OccupancyGrid, -) -> str: - """Generate a MuJoCo scene XML from an occupancy grid. - - Creates a scene with a flat floor and extruded boxes for each occupied - region. All boxes are red and used for collision. - - Args: - occupancy_grid: The input occupancy grid. - - Returns: - Path to the generated XML file. - """ - extrude_height = 0.5 - - # Get rectangles from the occupancy grid - rectangles = identify_convex_shapes(occupancy_grid) - - resolution = occupancy_grid.resolution - origin_x = occupancy_grid.origin.position.x - origin_y = occupancy_grid.origin.position.y - - # Build XML - xml_lines = [ - '', - '', - ' ', - ' ', - " ", - ' ', - ' ', - ' ', - ' ', - " ", - " ", - ' ', - ' ', - ] - - # Add each rectangle as a box geom - for i, (gx, gy, gw, gh) in enumerate(rectangles): - # Convert grid coordinates to world coordinates - # Grid origin is top-left, world origin is at occupancy_grid.origin - # gx, gy are in grid cells, need to convert to meters - world_x = origin_x + (gx + gw / 2) * resolution - world_y = origin_y + (gy + gh / 2) * resolution - world_z = extrude_height / 2 # Center of the box - - # Box half-sizes - half_x = (gw * resolution) / 2 - half_y = (gh * resolution) / 2 - half_z = extrude_height / 2 - - xml_lines.append( - f' ' - ) - - xml_lines.append(" ") - xml_lines.append(' ') - xml_lines.append("\n") - - xml_content = "\n".join(xml_lines) - - return xml_content diff --git a/dimos/mapping/occupancy/gradient.py b/dimos/mapping/occupancy/gradient.py deleted file mode 100644 index 880f2692da..0000000000 --- a/dimos/mapping/occupancy/gradient.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from scipy import ndimage # type: ignore[import-untyped] - -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - - -def gradient( - occupancy_grid: OccupancyGrid, obstacle_threshold: int = 50, max_distance: float = 2.0 -) -> OccupancyGrid: - """Create a gradient OccupancyGrid for path planning. - - Creates a gradient where free space has value 0 and values increase near obstacles. - This can be used as a cost map for path planning algorithms like A*. - - Args: - obstacle_threshold: Cell values >= this are considered obstacles (default: 50) - max_distance: Maximum distance to compute gradient in meters (default: 2.0) - - Returns: - New OccupancyGrid with gradient values: - - -1: Unknown cells (preserved as-is) - - 0: Free space far from obstacles - - 1-99: Increasing cost as you approach obstacles - - 100: At obstacles - - Note: Unknown cells remain as unknown (-1) and do not receive gradient values. - """ - - # Remember which cells are unknown - unknown_mask = occupancy_grid.grid == CostValues.UNKNOWN - - # Create binary obstacle map - # Consider cells >= threshold as obstacles (1), everything else as free (0) - # Unknown cells are not considered obstacles for distance calculation - obstacle_map = (occupancy_grid.grid >= obstacle_threshold).astype(np.float32) - - # Compute distance transform (distance to nearest obstacle in cells) - # Unknown cells are treated as if they don't exist for distance calculation - distance_cells = ndimage.distance_transform_edt(1 - obstacle_map) - - # Convert to meters and clip to max distance - distance_meters = np.clip(distance_cells * occupancy_grid.resolution, 0, max_distance) - - # Invert and scale to 0-100 range - # Far from obstacles (max_distance) -> 0 - # At obstacles (0 distance) -> 100 - gradient_values = (1 - distance_meters / max_distance) * 100 - - # Ensure obstacles are exactly 100 - gradient_values[obstacle_map > 0] = CostValues.OCCUPIED - - # Convert to int8 for OccupancyGrid - gradient_data = gradient_values.astype(np.int8) - - # Preserve unknown cells as unknown (don't apply gradient to them) - gradient_data[unknown_mask] = CostValues.UNKNOWN - - # Create new OccupancyGrid with gradient - gradient_grid = OccupancyGrid( - grid=gradient_data, - resolution=occupancy_grid.resolution, - origin=occupancy_grid.origin, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) - - return gradient_grid - - -def voronoi_gradient( - occupancy_grid: OccupancyGrid, obstacle_threshold: int = 50, max_distance: float = 2.0 -) -> OccupancyGrid: - """Create a Voronoi-based gradient OccupancyGrid for path planning. - - Unlike the regular gradient which can result in suboptimal paths in narrow - corridors (where the center still has high cost), this method creates a cost - map based on the Voronoi diagram of obstacles. Cells on Voronoi edges - (equidistant from multiple obstacles) have minimum cost, encouraging paths - that stay maximally far from all obstacles. - - For a corridor of width 10 cells: - - Regular gradient: center cells might be 95 (still high cost) - - Voronoi gradient: center cells are 0 (optimal path) - - The cost is interpolated based on relative position between the nearest - obstacle and the nearest Voronoi edge: - - At obstacle: cost = 100 - - At Voronoi edge: cost = 0 - - In between: cost = 99 * d_voronoi / (d_obstacle + d_voronoi) - - Args: - obstacle_threshold: Cell values >= this are considered obstacles (default: 50) - max_distance: Maximum distance in meters beyond which cost is 0 (default: 2.0) - - Returns: - New OccupancyGrid with gradient values: - - -1: Unknown cells (preserved as-is) - - 0: On Voronoi edges (equidistant from obstacles) or far from obstacles - - 1-99: Increasing cost closer to obstacles - - 100: At obstacles - """ - # Remember which cells are unknown - unknown_mask = occupancy_grid.grid == CostValues.UNKNOWN - - # Create binary obstacle map - obstacle_map = (occupancy_grid.grid >= obstacle_threshold).astype(np.float32) - - # Check if there are any obstacles - if not np.any(obstacle_map): - # No obstacles - everything is free - gradient_data = np.zeros_like(occupancy_grid.grid, dtype=np.int8) - gradient_data[unknown_mask] = CostValues.UNKNOWN - return OccupancyGrid( - grid=gradient_data, - resolution=occupancy_grid.resolution, - origin=occupancy_grid.origin, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) - - # Label connected obstacle regions (clusters) - # This groups all cells of the same wall/obstacle together - obstacle_labels, num_obstacles = ndimage.label(obstacle_map) - - # If only one obstacle cluster, Voronoi edges don't make sense - # Fall back to regular gradient behavior - if num_obstacles <= 1: - return gradient(occupancy_grid, obstacle_threshold, max_distance) - - # Compute distance transform with indices to nearest obstacle - # indices[0][i,j], indices[1][i,j] = row,col of nearest obstacle to (i,j) - distance_cells, indices = ndimage.distance_transform_edt(1 - obstacle_map, return_indices=True) - - # For each cell, find which obstacle cluster it belongs to (Voronoi region) - # by looking up the label of its nearest obstacle cell - nearest_obstacle_cluster = obstacle_labels[indices[0], indices[1]] - - # Find Voronoi edges: cells where neighbors belong to different obstacle clusters - # Using max/min filters: an edge exists where max != min in the 3x3 neighborhood - footprint = np.ones((3, 3), dtype=bool) - local_max = ndimage.maximum_filter( - nearest_obstacle_cluster, footprint=footprint, mode="nearest" - ) - local_min = ndimage.minimum_filter( - nearest_obstacle_cluster, footprint=footprint, mode="nearest" - ) - voronoi_edges = local_max != local_min - - # Don't count obstacle cells as Voronoi edges - voronoi_edges &= obstacle_map == 0 - - # Compute distance to nearest Voronoi edge - if not np.any(voronoi_edges): - # No Voronoi edges found - fall back to regular gradient - return gradient(occupancy_grid, obstacle_threshold, max_distance) - - voronoi_distance = ndimage.distance_transform_edt(~voronoi_edges) - - # Calculate cost based on position between obstacle and Voronoi edge - # cost = 99 * d_voronoi / (d_obstacle + d_voronoi) - # At Voronoi edge: d_voronoi = 0, cost = 0 - # Near obstacle: d_obstacle small, d_voronoi large, cost high - total_distance = distance_cells + voronoi_distance - with np.errstate(divide="ignore", invalid="ignore"): - cost_ratio = np.where(total_distance > 0, voronoi_distance / total_distance, 0) - - gradient_values = cost_ratio * 99 - - # Ensure obstacles are exactly 100 - gradient_values[obstacle_map > 0] = CostValues.OCCUPIED - - # Apply max_distance clipping - cells beyond max_distance from obstacles get cost 0 - max_distance_cells = max_distance / occupancy_grid.resolution - gradient_values[distance_cells > max_distance_cells] = 0 - - # Convert to int8 - gradient_data = gradient_values.astype(np.int8) - - # Preserve unknown cells - gradient_data[unknown_mask] = CostValues.UNKNOWN - - return OccupancyGrid( - grid=gradient_data, - resolution=occupancy_grid.resolution, - origin=occupancy_grid.origin, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) diff --git a/dimos/mapping/occupancy/inflation.py b/dimos/mapping/occupancy/inflation.py deleted file mode 100644 index a9ef628cd6..0000000000 --- a/dimos/mapping/occupancy/inflation.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from scipy import ndimage # type: ignore[import-untyped] - -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - - -def simple_inflate(occupancy_grid: OccupancyGrid, radius: float) -> OccupancyGrid: - """Inflate obstacles by a given radius (binary inflation). - Args: - radius: Inflation radius in meters - Returns: - New OccupancyGrid with inflated obstacles - """ - # Convert radius to grid cells - cell_radius = int(np.ceil(radius / occupancy_grid.resolution)) - - # Get grid as numpy array - grid_array = occupancy_grid.grid - - # Create circular kernel for binary inflation - y, x = np.ogrid[-cell_radius : cell_radius + 1, -cell_radius : cell_radius + 1] - kernel = (x**2 + y**2 <= cell_radius**2).astype(np.uint8) - - # Find occupied cells - occupied_mask = grid_array >= CostValues.OCCUPIED - - # Binary inflation - inflated = ndimage.binary_dilation(occupied_mask, structure=kernel) - result_grid = grid_array.copy() - result_grid[inflated] = CostValues.OCCUPIED - - # Create new OccupancyGrid with inflated data using numpy constructor - return OccupancyGrid( - grid=result_grid, - resolution=occupancy_grid.resolution, - origin=occupancy_grid.origin, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) diff --git a/dimos/mapping/occupancy/operations.py b/dimos/mapping/occupancy/operations.py deleted file mode 100644 index be17670a6a..0000000000 --- a/dimos/mapping/occupancy/operations.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from scipy import ndimage # type: ignore[import-untyped] - -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - - -def smooth_occupied( - occupancy_grid: OccupancyGrid, min_neighbor_fraction: float = 0.4 -) -> OccupancyGrid: - """Smooth occupied zones by removing unsupported protrusions. - - Removes occupied cells that don't have sufficient neighboring occupied - cells. - - Args: - occupancy_grid: Input occupancy grid - min_neighbor_fraction: Minimum fraction of 8-connected neighbors - that must be occupied for a cell to remain occupied. - Returns: - New OccupancyGrid with smoothed occupied zones - """ - grid_array = occupancy_grid.grid - occupied_mask = grid_array >= CostValues.OCCUPIED - - # Count occupied neighbors for each cell (8-connectivity). - kernel = np.array([[1, 1, 1], [1, 0, 1], [1, 1, 1]], dtype=np.uint8) - neighbor_count = ndimage.convolve( - occupied_mask.astype(np.uint8), kernel, mode="constant", cval=0 - ) - - # Remove cells with too few occupied neighbors. - min_neighbors = int(np.ceil(8 * min_neighbor_fraction)) - unsupported = occupied_mask & (neighbor_count < min_neighbors) - - result_grid = grid_array.copy() - result_grid[unsupported] = CostValues.FREE - - return OccupancyGrid( - grid=result_grid, - resolution=occupancy_grid.resolution, - origin=occupancy_grid.origin, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) - - -def overlay_occupied(base: OccupancyGrid, overlay: OccupancyGrid) -> OccupancyGrid: - """Overlay occupied zones from one grid onto another. - - Marks cells as occupied in the base grid wherever they are occupied - in the overlay grid. - - Args: - base: The base occupancy grid - overlay: The grid whose occupied zones will be overlaid onto base - Returns: - New OccupancyGrid with combined occupied zones - """ - if base.grid.shape != overlay.grid.shape: - raise ValueError( - f"Grid shapes must match: base {base.grid.shape} vs overlay {overlay.grid.shape}" - ) - - result_grid = base.grid.copy() - overlay_occupied_mask = overlay.grid >= CostValues.OCCUPIED - result_grid[overlay_occupied_mask] = CostValues.OCCUPIED - - return OccupancyGrid( - grid=result_grid, - resolution=base.resolution, - origin=base.origin, - frame_id=base.frame_id, - ts=base.ts, - ) diff --git a/dimos/mapping/occupancy/path_map.py b/dimos/mapping/occupancy/path_map.py deleted file mode 100644 index a99a423de8..0000000000 --- a/dimos/mapping/occupancy/path_map.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Literal, TypeAlias - -from dimos.mapping.occupancy.gradient import voronoi_gradient -from dimos.mapping.occupancy.inflation import simple_inflate -from dimos.mapping.occupancy.operations import overlay_occupied, smooth_occupied -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid - -NavigationStrategy: TypeAlias = Literal["simple", "mixed"] - - -def make_navigation_map( - occupancy_grid: OccupancyGrid, robot_width: float, strategy: NavigationStrategy -) -> OccupancyGrid: - half_width = robot_width / 2 - gradient_distance = 1.5 - - if strategy == "simple": - costmap = simple_inflate(occupancy_grid, half_width) - elif strategy == "mixed": - costmap = smooth_occupied(occupancy_grid) - costmap = simple_inflate(costmap, half_width) - costmap = overlay_occupied(costmap, occupancy_grid) - else: - raise ValueError(f"Unknown strategy: {strategy}") - - return voronoi_gradient(costmap, max_distance=gradient_distance) diff --git a/dimos/mapping/occupancy/path_mask.py b/dimos/mapping/occupancy/path_mask.py deleted file mode 100644 index 5ad3010111..0000000000 --- a/dimos/mapping/occupancy/path_mask.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import cv2 -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.nav_msgs import Path -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - - -def make_path_mask( - occupancy_grid: OccupancyGrid, - path: Path, - robot_width: float, - pose_index: int = 0, - max_length: float = float("inf"), -) -> NDArray[np.bool_]: - """Generate a numpy mask of path cells the robot will travel through. - - Creates a boolean mask where True indicates cells that the robot will - occupy while following the path, accounting for the robot's width. - - Args: - occupancy_grid: The occupancy grid providing dimensions and resolution. - path: The path containing poses the robot will follow. - robot_width: The width of the robot in meters. - pose_index: The index in path.poses to start drawing from. Defaults to 0. - max_length: Maximum cumulative length to draw. Defaults to infinity. - - Returns: - A 2D boolean numpy array (height x width) where True indicates - cells the robot will pass through. - """ - mask = np.zeros((occupancy_grid.height, occupancy_grid.width), dtype=np.uint8) - - line_width_pixels = max(1, int(robot_width / occupancy_grid.resolution)) - - poses = path.poses - if len(poses) < pose_index + 2: - return mask.astype(np.bool_) - - # Draw lines between consecutive points - cumulative_length = 0.0 - for i in range(pose_index, len(poses) - 1): - pos1 = poses[i].position - pos2 = poses[i + 1].position - - segment_length = np.sqrt( - (pos2.x - pos1.x) ** 2 + (pos2.y - pos1.y) ** 2 + (pos2.z - pos1.z) ** 2 - ) - - if cumulative_length + segment_length > max_length: - break - - cumulative_length += segment_length - - grid_pt1 = occupancy_grid.world_to_grid(pos1) - grid_pt2 = occupancy_grid.world_to_grid(pos2) - - pt1 = (round(grid_pt1.x), round(grid_pt1.y)) - pt2 = (round(grid_pt2.x), round(grid_pt2.y)) - - cv2.line(mask, pt1, pt2, (255.0,), thickness=line_width_pixels) - - bool_mask = mask.astype(np.bool_) - - total_points = np.sum(bool_mask) - - if total_points == 0: - return bool_mask - - occupied_mask = occupancy_grid.grid >= CostValues.OCCUPIED - occupied_in_path = bool_mask & occupied_mask - occupied_count = np.sum(occupied_in_path) - - if occupied_count / total_points > 0.05: - raise ValueError( - f"More than 5% of path points are occupied: " - f"{occupied_count}/{total_points} ({100 * occupied_count / total_points:.1f}%)" - ) - - # Some of the points on the edge of the path may be occupied due to - # rounding. Remove them. - bool_mask = bool_mask & ~occupied_mask # type: ignore[assignment] - - return bool_mask diff --git a/dimos/mapping/occupancy/path_resampling.py b/dimos/mapping/occupancy/path_resampling.py deleted file mode 100644 index 2090bf8f04..0000000000 --- a/dimos/mapping/occupancy/path_resampling.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import math - -import numpy as np -from scipy.ndimage import uniform_filter1d # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Vector3 -from dimos.msgs.nav_msgs import Path -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion - -logger = setup_logger() - - -def _add_orientations_to_path(path: Path, goal_orientation: Quaternion) -> None: - """Add orientations to path poses based on direction of movement. - - Args: - path: Path with poses to add orientations to - goal_orientation: Desired orientation for the final pose - - Returns: - Path with orientations added to all poses - """ - if not path.poses or len(path.poses) < 2: - return - - # Calculate orientations for all poses except the last one - for i in range(len(path.poses) - 1): - current_pose = path.poses[i] - next_pose = path.poses[i + 1] - - # Calculate direction to next point - dx = next_pose.position.x - current_pose.position.x - dy = next_pose.position.y - current_pose.position.y - - # Calculate yaw angle - yaw = math.atan2(dy, dx) - - # Convert to quaternion (roll=0, pitch=0, yaw) - orientation = euler_to_quaternion(Vector3(0, 0, yaw)) - current_pose.orientation = orientation - - # Set last pose orientation - identity_quat = Quaternion(0, 0, 0, 1) - if goal_orientation != identity_quat: - # Use the provided goal orientation if it's not the identity - path.poses[-1].orientation = goal_orientation - elif len(path.poses) > 1: - # Use the previous pose's orientation - path.poses[-1].orientation = path.poses[-2].orientation - else: - # Single pose with identity goal orientation - path.poses[-1].orientation = identity_quat - - -# TODO: replace goal_pose with just goal_orientation -def simple_resample_path(path: Path, goal_pose: Pose, spacing: float) -> Path: - """Resample a path to have approximately uniform spacing between poses. - - Args: - path: The original Path - spacing: Desired distance between consecutive poses - - Returns: - A new Path with resampled poses - """ - if len(path) < 2 or spacing <= 0: - return path - - resampled = [] - resampled.append(path.poses[0]) - - accumulated_distance = 0.0 - - for i in range(1, len(path.poses)): - current = path.poses[i] - prev = path.poses[i - 1] - - # Calculate segment distance - dx = current.x - prev.x - dy = current.y - prev.y - segment_length = (dx**2 + dy**2) ** 0.5 - - if segment_length < 1e-10: - continue - - # Direction vector - dir_x = dx / segment_length - dir_y = dy / segment_length - - # Add points along this segment - while accumulated_distance + segment_length >= spacing: - # Distance along segment for next point - dist_along = spacing - accumulated_distance - if dist_along < 0: - break - - # Create new pose - new_x = prev.x + dir_x * dist_along - new_y = prev.y + dir_y * dist_along - new_pose = PoseStamped( - frame_id=path.frame_id, - position=[new_x, new_y, 0.0], - orientation=prev.orientation, # Keep same orientation - ) - resampled.append(new_pose) - - # Update for next iteration - accumulated_distance = 0 - segment_length -= dist_along - prev = new_pose - - accumulated_distance += segment_length - - # Add last pose if not already there - if len(path.poses) > 1: - last = path.poses[-1] - if not resampled or (resampled[-1].x != last.x or resampled[-1].y != last.y): - resampled.append(last) - - ret = Path(frame_id=path.frame_id, poses=resampled) - - _add_orientations_to_path(ret, goal_pose.orientation) - - return ret - - -def smooth_resample_path( - path: Path, goal_pose: Pose, spacing: float, smoothing_window: int = 100 -) -> Path: - """Resample a path with smoothing to reduce jagged corners and abrupt turns. - - This produces smoother paths than simple_resample_path by: - - First upsampling the path to have many points - - Applying a moving average filter to smooth the coordinates - - Resampling at the desired spacing - - Keeping start and end points fixed - - Args: - path: The original Path - goal_pose: Goal pose with desired final orientation - spacing: Desired approximate distance between consecutive poses - smoothing_window: Size of the smoothing window (larger = smoother) - - Returns: - A new Path with smoothly resampled poses - """ - - if len(path.poses) == 1: - p = path.poses[0].position - o = goal_pose.orientation - new_pose = PoseStamped( - frame_id=path.frame_id, - position=[p.x, p.y, p.z], - orientation=[o.x, o.y, o.z, o.w], - ) - return Path(frame_id=path.frame_id, poses=[new_pose]) - - if len(path) < 2 or spacing <= 0: - return path - - # Extract x, y coordinates from path - xs = np.array([p.x for p in path.poses]) - ys = np.array([p.y for p in path.poses]) - - # Remove duplicate consecutive points - diffs = np.sqrt(np.diff(xs) ** 2 + np.diff(ys) ** 2) - valid_mask = np.concatenate([[True], diffs > 1e-10]) - xs = xs[valid_mask] - ys = ys[valid_mask] - - if len(xs) < 2: - return path - - # Calculate total path length - dx = np.diff(xs) - dy = np.diff(ys) - segment_lengths = np.sqrt(dx**2 + dy**2) - total_length = np.sum(segment_lengths) - - if total_length < spacing: - return path - - # Upsample: create many points along the original path using linear interpolation - # This gives us enough points for effective smoothing - upsample_factor = 10 - num_upsampled = max(len(xs) * upsample_factor, 100) - - arc_length = np.concatenate([[0], np.cumsum(segment_lengths)]) - upsample_distances = np.linspace(0, total_length, num_upsampled) - - # Linear interpolation along arc length - xs_upsampled = np.interp(upsample_distances, arc_length, xs) - ys_upsampled = np.interp(upsample_distances, arc_length, ys) - - # Apply moving average smoothing - # Use 'nearest' mode to avoid shrinking at boundaries - window = min(smoothing_window, len(xs_upsampled) // 3) - if window >= 3: - xs_smooth = uniform_filter1d(xs_upsampled, size=window, mode="nearest") - ys_smooth = uniform_filter1d(ys_upsampled, size=window, mode="nearest") - else: - xs_smooth = xs_upsampled - ys_smooth = ys_upsampled - - # Keep start and end points exactly as original - xs_smooth[0] = xs[0] - ys_smooth[0] = ys[0] - xs_smooth[-1] = xs[-1] - ys_smooth[-1] = ys[-1] - - # Recalculate arc length on smoothed path - dx_smooth = np.diff(xs_smooth) - dy_smooth = np.diff(ys_smooth) - segment_lengths_smooth = np.sqrt(dx_smooth**2 + dy_smooth**2) - arc_length_smooth = np.concatenate([[0], np.cumsum(segment_lengths_smooth)]) - total_length_smooth = arc_length_smooth[-1] - - # Resample at desired spacing - num_samples = max(2, int(np.ceil(total_length_smooth / spacing)) + 1) - sample_distances = np.linspace(0, total_length_smooth, num_samples) - - # Interpolate to get final points - sampled_x = np.interp(sample_distances, arc_length_smooth, xs_smooth) - sampled_y = np.interp(sample_distances, arc_length_smooth, ys_smooth) - - # Create resampled poses - resampled = [] - for i in range(len(sampled_x)): - new_pose = PoseStamped( - frame_id=path.frame_id, - position=[float(sampled_x[i]), float(sampled_y[i]), 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - resampled.append(new_pose) - - ret = Path(frame_id=path.frame_id, poses=resampled) - - _add_orientations_to_path(ret, goal_pose.orientation) - - return ret diff --git a/dimos/mapping/occupancy/test_extrude_occupancy.py b/dimos/mapping/occupancy/test_extrude_occupancy.py deleted file mode 100644 index 88f05d7780..0000000000 --- a/dimos/mapping/occupancy/test_extrude_occupancy.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.mapping.occupancy.extrude_occupancy import generate_mujoco_scene -from dimos.utils.data import get_data - - -@pytest.mark.integration -def test_generate_mujoco_scene(occupancy) -> None: - with open(get_data("expected_occupancy_scene.xml")) as f: - expected = f.read() - - actual = generate_mujoco_scene(occupancy) - - assert actual == expected diff --git a/dimos/mapping/occupancy/test_gradient.py b/dimos/mapping/occupancy/test_gradient.py deleted file mode 100644 index a097873aae..0000000000 --- a/dimos/mapping/occupancy/test_gradient.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.mapping.occupancy.gradient import gradient, voronoi_gradient -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.msgs.sensor_msgs.Image import Image -from dimos.utils.data import get_data - - -@pytest.mark.parametrize("method", ["simple", "voronoi"]) -def test_gradient(occupancy, method) -> None: - expected = Image.from_file(get_data(f"gradient_{method}.png")) - - match method: - case "simple": - og = gradient(occupancy, max_distance=1.5) - case "voronoi": - og = voronoi_gradient(occupancy, max_distance=1.5) - case _: - raise ValueError(f"Unknown resampling method: {method}") - - actual = visualize_occupancy_grid(og, "rainbow") - np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_inflation.py b/dimos/mapping/occupancy/test_inflation.py deleted file mode 100644 index a30ad413b1..0000000000 --- a/dimos/mapping/occupancy/test_inflation.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import cv2 -import numpy as np - -from dimos.mapping.occupancy.inflation import simple_inflate -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.utils.data import get_data - - -def test_inflation(occupancy) -> None: - expected = cv2.imread(get_data("inflation_simple.png"), cv2.IMREAD_COLOR) - - og = simple_inflate(occupancy, 0.2) - - result = visualize_occupancy_grid(og, "rainbow") - np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_operations.py b/dimos/mapping/occupancy/test_operations.py deleted file mode 100644 index 89332d0bdd..0000000000 --- a/dimos/mapping/occupancy/test_operations.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import cv2 -import numpy as np - -from dimos.mapping.occupancy.operations import overlay_occupied, smooth_occupied -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.utils.data import get_data - - -def test_smooth_occupied(occupancy) -> None: - expected = cv2.imread(get_data("smooth_occupied.png"), cv2.IMREAD_COLOR) - - result = visualize_occupancy_grid(smooth_occupied(occupancy), "rainbow") - - np.testing.assert_array_equal(result.data, expected) - - -def test_overlay_occupied(occupancy) -> None: - expected = cv2.imread(get_data("overlay_occupied.png"), cv2.IMREAD_COLOR) - overlay = occupancy.copy() - overlay.grid[50:100, 50:100] = 100 - - result = visualize_occupancy_grid(overlay_occupied(occupancy, overlay), "rainbow") - - np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_path_map.py b/dimos/mapping/occupancy/test_path_map.py deleted file mode 100644 index b3e250db9d..0000000000 --- a/dimos/mapping/occupancy/test_path_map.py +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import cv2 -import numpy as np -import pytest - -from dimos.mapping.occupancy.path_map import make_navigation_map -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.utils.data import get_data - - -@pytest.mark.parametrize("strategy", ["simple", "mixed"]) -def test_make_navigation_map(occupancy, strategy) -> None: - expected = cv2.imread(get_data(f"make_navigation_map_{strategy}.png"), cv2.IMREAD_COLOR) - robot_width = 0.4 - - og = make_navigation_map(occupancy, robot_width, strategy=strategy) - - result = visualize_occupancy_grid(og, "rainbow") - np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/test_path_mask.py b/dimos/mapping/occupancy/test_path_mask.py deleted file mode 100644 index dede997946..0000000000 --- a/dimos/mapping/occupancy/test_path_mask.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import numpy as np -import pytest - -from dimos.mapping.occupancy.path_mask import make_path_mask -from dimos.mapping.occupancy.path_resampling import smooth_resample_path -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.msgs.geometry_msgs import Pose -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar -from dimos.utils.data import get_data - - -@pytest.mark.parametrize( - "pose_index,max_length,expected_image", - [ - (0, float("inf"), "make_path_mask_full.png"), - (50, 2, "make_path_mask_two_meters.png"), - ], -) -def test_make_path_mask(occupancy_gradient, pose_index, max_length, expected_image) -> None: - start = Vector3(4.0, 2.0, 0) - goal_pose = Pose(6.15, 10.0, 0, 0, 0, 0, 1) - expected = Image.from_file(get_data(expected_image)) - path = min_cost_astar(occupancy_gradient, goal_pose.position, start, use_cpp=False) - path = smooth_resample_path(path, goal_pose, 0.1) - robot_width = 0.4 - path_mask = make_path_mask(occupancy_gradient, path, robot_width, pose_index, max_length) - actual = visualize_occupancy_grid(occupancy_gradient, "rainbow") - - actual.data[path_mask] = [0, 100, 0] - - np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_path_resampling.py b/dimos/mapping/occupancy/test_path_resampling.py deleted file mode 100644 index c23f71cf89..0000000000 --- a/dimos/mapping/occupancy/test_path_resampling.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.mapping.occupancy.gradient import gradient -from dimos.mapping.occupancy.path_resampling import simple_resample_path, smooth_resample_path -from dimos.mapping.occupancy.visualize_path import visualize_path -from dimos.msgs.geometry_msgs import Pose -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.msgs.sensor_msgs.Image import Image -from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar -from dimos.utils.data import get_data - - -@pytest.fixture -def costmap() -> OccupancyGrid: - return gradient(OccupancyGrid(np.load(get_data("occupancy_simple.npy"))), max_distance=1.5) - - -@pytest.mark.parametrize("method", ["simple", "smooth"]) -def test_resample_path(costmap, method) -> None: - start = Vector3(4.0, 2.0, 0) - goal_pose = Pose(6.15, 10.0, 0, 0, 0, 0, 1) - expected = Image.from_file(get_data(f"resample_path_{method}.png")) - path = min_cost_astar(costmap, goal_pose.position, start, use_cpp=False) - - match method: - case "simple": - resampled = simple_resample_path(path, goal_pose, 0.1) - case "smooth": - resampled = smooth_resample_path(path, goal_pose, 0.1) - case _: - raise ValueError(f"Unknown resampling method: {method}") - - actual = visualize_path(costmap, resampled, 0.2, 0.4) - np.testing.assert_array_equal(actual.data, expected.data) diff --git a/dimos/mapping/occupancy/test_visualizations.py b/dimos/mapping/occupancy/test_visualizations.py deleted file mode 100644 index 17b2629e80..0000000000 --- a/dimos/mapping/occupancy/test_visualizations.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import cv2 -import numpy as np -import pytest - -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.utils.data import get_data - - -@pytest.mark.parametrize("palette", ["rainbow", "turbo"]) -def test_visualize_occupancy_grid(occupancy_gradient, palette) -> None: - expected = cv2.imread(get_data(f"visualize_occupancy_{palette}.png"), cv2.IMREAD_COLOR) - - result = visualize_occupancy_grid(occupancy_gradient, palette) - - np.testing.assert_array_equal(result.data, expected) diff --git a/dimos/mapping/occupancy/visualizations.py b/dimos/mapping/occupancy/visualizations.py deleted file mode 100644 index 2ed0364257..0000000000 --- a/dimos/mapping/occupancy/visualizations.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import lru_cache -from typing import Literal, TypeAlias - -import cv2 -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.nav_msgs import Path -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat - -Palette: TypeAlias = Literal["rainbow", "turbo"] - - -def visualize_occupancy_grid( - occupancy_grid: OccupancyGrid, palette: Palette, path: Path | None = None -) -> Image: - match palette: - case "rainbow": - bgr_image = rainbow_image(occupancy_grid.grid) - case "turbo": - bgr_image = turbo_image(occupancy_grid.grid) - case _: - raise NotImplementedError() - - if path is not None and len(path.poses) > 0: - _draw_path(occupancy_grid, bgr_image, path) - - return Image( - data=bgr_image, - format=ImageFormat.BGR, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) - - -def _draw_path(occupancy_grid: OccupancyGrid, bgr_image: NDArray[np.uint8], path: Path) -> None: - points = [] - for pose in path.poses: - grid_coord = occupancy_grid.world_to_grid([pose.x, pose.y, pose.z]) - pixel_x = int(grid_coord.x) - pixel_y = int(grid_coord.y) - - if 0 <= pixel_x < occupancy_grid.width and 0 <= pixel_y < occupancy_grid.height: - points.append((pixel_x, pixel_y)) - - if len(points) > 1: - points_array = np.array(points, dtype=np.int32) - cv2.polylines(bgr_image, [points_array], isClosed=False, color=(0, 0, 0), thickness=1) - - -def rainbow_image(grid: NDArray[np.int8]) -> NDArray[np.uint8]: - """Convert the occupancy grid to a rainbow-colored Image. - - Color scheme: - - -1 (unknown): black - - 100 (occupied): magenta - - 0-99: rainbow from blue (0) to red (99) - - Returns: - Image with rainbow visualization of the occupancy grid - """ - - # Create a copy of the grid for visualization - # Map values to 0-255 range for colormap - height, width = grid.shape - vis_grid = np.zeros((height, width), dtype=np.uint8) - - # Handle 0-99: map to colormap range - gradient_mask = (grid >= 0) & (grid < 100) - vis_grid[gradient_mask] = ((grid[gradient_mask] / 99.0) * 255).astype(np.uint8) - - # Apply JET colormap (blue to red) - returns BGR - bgr_image = cv2.applyColorMap(vis_grid, cv2.COLORMAP_JET) - - unknown_mask = grid == -1 - bgr_image[unknown_mask] = [0, 0, 0] - - occupied_mask = grid == 100 - bgr_image[occupied_mask] = [255, 0, 255] - - return bgr_image.astype(np.uint8) - - -def turbo_image(grid: NDArray[np.int8]) -> NDArray[np.uint8]: - """Convert the occupancy grid to a turbo-colored Image. - - Returns: - Image with turbo visualization of the occupancy grid - """ - color_lut = _turbo_lut() - - # Map grid values to lookup indices - # Values: -1 -> 255, 0-100 -> 0-100, clipped to valid range - lookup_indices = np.where(grid == -1, 255, np.clip(grid, 0, 100)).astype(np.uint8) - - # Create BGR image using lookup table (vectorized operation) - return color_lut[lookup_indices] - - -def _interpolate_turbo(t: float) -> tuple[int, int, int]: - """D3's interpolateTurbo colormap implementation. - - Based on Anton Mikhailov's Turbo colormap using polynomial approximations. - - Args: - t: Value in [0, 1] - - Returns: - RGB tuple (0-255 range) - """ - t = max(0.0, min(1.0, t)) - - r = 34.61 + t * (1172.33 - t * (10793.56 - t * (33300.12 - t * (38394.49 - t * 14825.05)))) - g = 23.31 + t * (557.33 + t * (1225.33 - t * (3574.96 - t * (1073.77 + t * 707.56)))) - b = 27.2 + t * (3211.1 - t * (15327.97 - t * (27814.0 - t * (22569.18 - t * 6838.66)))) - - return ( - max(0, min(255, round(r))), - max(0, min(255, round(g))), - max(0, min(255, round(b))), - ) - - -@lru_cache(maxsize=1) -def _turbo_lut() -> NDArray[np.uint8]: - # Pre-compute lookup table for all possible values (-1 to 100) - color_lut = np.zeros((256, 3), dtype=np.uint8) - - for value in range(-1, 101): - # Normalize to [0, 1] range based on domain [-1, 100] - t = (value + 1) / 101.0 - - if value == -1: - rgb = (34, 24, 28) - elif value == 100: - rgb = (0, 0, 0) - else: - rgb = _interpolate_turbo(t * 2 - 1) - - # Map -1 to index 255, 0-100 to indices 0-100 - idx = 255 if value == -1 else value - color_lut[idx] = [rgb[2], rgb[1], rgb[0]] - - return color_lut diff --git a/dimos/mapping/occupancy/visualize_path.py b/dimos/mapping/occupancy/visualize_path.py deleted file mode 100644 index 0662582f72..0000000000 --- a/dimos/mapping/occupancy/visualize_path.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import cv2 -import numpy as np - -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.msgs.nav_msgs import Path -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat - - -def visualize_path( - occupancy_grid: OccupancyGrid, - path: Path, - robot_width: float, - robot_length: float, - thickness: int = 1, - scale: int = 8, -) -> Image: - image = visualize_occupancy_grid(occupancy_grid, "rainbow") - bgr = image.data - - bgr = cv2.resize( - bgr, - (bgr.shape[1] * scale, bgr.shape[0] * scale), - interpolation=cv2.INTER_NEAREST, - ) - - # Convert robot dimensions from meters to grid cells, then to scaled pixels - resolution = occupancy_grid.resolution - robot_width_px = int((robot_width / resolution) * scale) - robot_length_px = int((robot_length / resolution) * scale) - - # Draw robot rectangle at each path point - for pose in path.poses: - # Convert world coordinates to grid coordinates - grid_coord = occupancy_grid.world_to_grid([pose.x, pose.y, pose.z]) - cx = int(grid_coord.x * scale) - cy = int(grid_coord.y * scale) - - # Get yaw angle from pose orientation - yaw = pose.yaw - - # Define rectangle corners centered at origin (length along x, width along y) - half_length = robot_length_px / 2 - half_width = robot_width_px / 2 - corners = np.array( - [ - [-half_length, -half_width], - [half_length, -half_width], - [half_length, half_width], - [-half_length, half_width], - ], - dtype=np.float32, - ) - - # Rotate corners by yaw angle - cos_yaw = np.cos(yaw) - sin_yaw = np.sin(yaw) - rotation_matrix = np.array([[cos_yaw, -sin_yaw], [sin_yaw, cos_yaw]]) - rotated_corners = corners @ rotation_matrix.T - - # Translate to center position - rotated_corners[:, 0] += cx - rotated_corners[:, 1] += cy - - # Draw the rotated rectangle - pts = rotated_corners.astype(np.int32).reshape((-1, 1, 2)) - cv2.polylines(bgr, [pts], isClosed=True, color=(0, 0, 0), thickness=thickness) - - return Image( - data=bgr, - format=ImageFormat.BGR, - frame_id=occupancy_grid.frame_id, - ts=occupancy_grid.ts, - ) diff --git a/dimos/mapping/osm/README.md b/dimos/mapping/osm/README.md deleted file mode 100644 index cb94c0160b..0000000000 --- a/dimos/mapping/osm/README.md +++ /dev/null @@ -1,43 +0,0 @@ -# OpenStreetMap (OSM) - -This provides functionality to fetch and work with OpenStreetMap tiles, including coordinate conversions and location-based VLM queries. - -## Getting a MapImage - -```python -map_image = get_osm_map(LatLon(lat=..., lon=...), zoom_level=18, n_tiles=4)` -``` - -OSM tiles are 256x256 pixels so with 4 tiles you get a 1024x1024 map. - -You can translate pixel coordinates on the map to GPS location and back. - -```python ->>> map_image.pixel_to_latlon((300, 500)) -LatLon(lat=43.58571248, lon=12.23423511) ->>> map_image.latlon_to_pixel(LatLon(lat=43.58571248, lon=12.23423511)) -(300, 500) -``` - -## CurrentLocationMap - -This class maintains an appropriate context map for your current location so you can VLM queries. - -You have to update it with your current location and when you stray too far from the center it fetches a new map. - -```python -curr_map = CurrentLocationMap(QwenVlModel()) - -# Set your latest position. -curr_map.update_position(LatLon(lat=..., lon=...)) - -# If you want to get back a GPS position of a feature (Qwen gets your current position). -curr_map.query_for_one_position('Where is the closest farmacy?') -# Returns: -# LatLon(lat=..., lon=...) - -# If you also want to get back a description of the result. -curr_map.query_for_one_position_and_context('Where is the closest pharmacy?') -# Returns: -# (LatLon(lat=..., lon=...), "Lloyd's Pharmacy on Main Street") -``` diff --git a/dimos/mapping/osm/__init__.py b/dimos/mapping/osm/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/mapping/osm/current_location_map.py b/dimos/mapping/osm/current_location_map.py deleted file mode 100644 index ef0a832cd6..0000000000 --- a/dimos/mapping/osm/current_location_map.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from PIL import Image as PILImage, ImageDraw - -from dimos.mapping.osm.osm import MapImage, get_osm_map -from dimos.mapping.osm.query import query_for_one_position, query_for_one_position_and_context -from dimos.mapping.types import LatLon -from dimos.models.vl.base import VlModel -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class CurrentLocationMap: - _vl_model: VlModel - _position: LatLon | None - _map_image: MapImage | None - - def __init__(self, vl_model: VlModel) -> None: - self._vl_model = vl_model - self._position = None - self._map_image = None - self._zoom_level = 15 - self._n_tiles = 6 - # What ratio of the width is considered the center. 1.0 means the entire map is the center. - self._center_width = 0.4 - - def update_position(self, position: LatLon) -> None: - self._position = position - - def query_for_one_position(self, query: str) -> LatLon | None: - return query_for_one_position(self._vl_model, self._get_current_map(), query) # type: ignore[no-untyped-call] - - def query_for_one_position_and_context( - self, query: str, robot_position: LatLon - ) -> tuple[LatLon, str] | None: - return query_for_one_position_and_context( - self._vl_model, - self._get_current_map(), # type: ignore[no-untyped-call] - query, - robot_position, - ) - - def _get_current_map(self): # type: ignore[no-untyped-def] - if not self._position: - raise ValueError("Current position has not been set.") - - if not self._map_image or self._position_is_too_far_off_center(): - self._fetch_new_map() - return self._map_image - - return self._map_image - - def _fetch_new_map(self) -> None: - logger.info( - f"Getting a new OSM map, position={self._position}, zoom={self._zoom_level} n_tiles={self._n_tiles}" - ) - self._map_image = get_osm_map(self._position, self._zoom_level, self._n_tiles) # type: ignore[arg-type] - - # Add position marker - import numpy as np - - assert self._map_image is not None - assert self._position is not None - pil_image = PILImage.fromarray(self._map_image.image.data) - draw = ImageDraw.Draw(pil_image) - x, y = self._map_image.latlon_to_pixel(self._position) - radius = 20 - draw.ellipse( - [x - radius, y - radius, x + radius, y + radius], - fill=(255, 0, 0), - outline=(0, 0, 0), - width=3, - ) - - self._map_image.image.data[:] = np.array(pil_image) - - def _position_is_too_far_off_center(self) -> bool: - x, y = self._map_image.latlon_to_pixel(self._position) # type: ignore[arg-type, union-attr] - width = self._map_image.image.width # type: ignore[union-attr] - size_min = width * (0.5 - self._center_width / 2) - size_max = width * (0.5 + self._center_width / 2) - - return x < size_min or x > size_max or y < size_min or y > size_max - - def save_current_map_image(self, filepath: str = "osm_debug_map.png") -> str: - """Save the current OSM map image to a file for debugging. - - Args: - filepath: Path where to save the image - - Returns: - The filepath where the image was saved - """ - if not self._map_image: - self._get_current_map() # type: ignore[no-untyped-call] - - if self._map_image is not None: - self._map_image.image.save(filepath) - logger.info(f"Saved OSM map image to {filepath}") - return filepath diff --git a/dimos/mapping/osm/demo_osm.py b/dimos/mapping/osm/demo_osm.py deleted file mode 100644 index 97622cfaf2..0000000000 --- a/dimos/mapping/osm/demo_osm.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.agents.skills.demo_robot import demo_robot -from dimos.agents.skills.osm import osm_skill -from dimos.core.blueprints import autoconnect - -demo_osm = autoconnect( - demo_robot(), - osm_skill(), - agent(), -) diff --git a/dimos/mapping/osm/osm.py b/dimos/mapping/osm/osm.py deleted file mode 100644 index 31fb044087..0000000000 --- a/dimos/mapping/osm/osm.py +++ /dev/null @@ -1,183 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from concurrent.futures import ThreadPoolExecutor, as_completed -from dataclasses import dataclass -import io -import math - -import numpy as np -from PIL import Image as PILImage -import requests # type: ignore[import-untyped] - -from dimos.mapping.types import ImageCoord, LatLon -from dimos.msgs.sensor_msgs import Image, ImageFormat - - -@dataclass(frozen=True) -class MapImage: - image: Image - position: LatLon - zoom_level: int - n_tiles: int - - def pixel_to_latlon(self, position: ImageCoord) -> LatLon: - """Convert pixel coordinates to latitude/longitude. - - Args: - position: (x, y) pixel coordinates in the image - - Returns: - LatLon object with the corresponding latitude and longitude - """ - pixel_x, pixel_y = position - tile_size = 256 - - # Get the center tile coordinates - center_tile_x, center_tile_y = _lat_lon_to_tile( - self.position.lat, self.position.lon, self.zoom_level - ) - - # Calculate the actual top-left tile indices (integers) - start_tile_x = int(center_tile_x - self.n_tiles / 2.0) - start_tile_y = int(center_tile_y - self.n_tiles / 2.0) - - # Convert pixel position to exact tile coordinates - tile_x = start_tile_x + pixel_x / tile_size - tile_y = start_tile_y + pixel_y / tile_size - - # Convert tile coordinates to lat/lon - n = 2**self.zoom_level - lon = tile_x / n * 360.0 - 180.0 - lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * tile_y / n))) - lat = math.degrees(lat_rad) - - return LatLon(lat=lat, lon=lon) - - def latlon_to_pixel(self, position: LatLon) -> ImageCoord: - """Convert latitude/longitude to pixel coordinates. - - Args: - position: LatLon object with latitude and longitude - - Returns: - (x, y) pixel coordinates in the image - Note: Can return negative values if position is outside the image bounds - """ - tile_size = 256 - - # Convert the input lat/lon to tile coordinates - tile_x, tile_y = _lat_lon_to_tile(position.lat, position.lon, self.zoom_level) - - # Get the center tile coordinates - center_tile_x, center_tile_y = _lat_lon_to_tile( - self.position.lat, self.position.lon, self.zoom_level - ) - - # Calculate the actual top-left tile indices (integers) - start_tile_x = int(center_tile_x - self.n_tiles / 2.0) - start_tile_y = int(center_tile_y - self.n_tiles / 2.0) - - # Calculate pixel position relative to top-left corner - pixel_x = int((tile_x - start_tile_x) * tile_size) - pixel_y = int((tile_y - start_tile_y) * tile_size) - - return (pixel_x, pixel_y) - - -def _lat_lon_to_tile(lat: float, lon: float, zoom: int) -> tuple[float, float]: - """Convert latitude/longitude to tile coordinates at given zoom level.""" - n = 2**zoom - x_tile = (lon + 180.0) / 360.0 * n - lat_rad = math.radians(lat) - y_tile = (1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n - return x_tile, y_tile - - -def _download_tile( - args: tuple[int, int, int, int, int], -) -> tuple[int, int, PILImage.Image | None]: - """Download a single tile. - - Args: - args: Tuple of (row, col, tile_x, tile_y, zoom_level) - - Returns: - Tuple of (row, col, tile_image or None if failed) - """ - row, col, tile_x, tile_y, zoom_level = args - url = f"https://tile.openstreetmap.org/{zoom_level}/{tile_x}/{tile_y}.png" - headers = {"User-Agent": "Dimos OSM Client/1.0"} - - try: - response = requests.get(url, headers=headers, timeout=10) - response.raise_for_status() - tile_img = PILImage.open(io.BytesIO(response.content)) - return row, col, tile_img - except Exception: - return row, col, None - - -def get_osm_map(position: LatLon, zoom_level: int = 18, n_tiles: int = 4) -> MapImage: - """ - Tiles are always 256x256 pixels. With n_tiles=4, this should produce a 1024x1024 image. - Downloads tiles in parallel with a maximum of 5 concurrent downloads. - - Args: - position (LatLon): center position - zoom_level (int, optional): Defaults to 18. - n_tiles (int, optional): generate a map of n_tiles by n_tiles. - """ - center_x, center_y = _lat_lon_to_tile(position.lat, position.lon, zoom_level) - - start_x = int(center_x - n_tiles / 2.0) - start_y = int(center_y - n_tiles / 2.0) - - tile_size = 256 - output_size = tile_size * n_tiles - output_img = PILImage.new("RGB", (output_size, output_size)) - - n_failed_tiles = 0 - - # Prepare all tile download tasks - download_tasks = [] - for row in range(n_tiles): - for col in range(n_tiles): - tile_x = start_x + col - tile_y = start_y + row - download_tasks.append((row, col, tile_x, tile_y, zoom_level)) - - # Download tiles in parallel with max 5 workers - with ThreadPoolExecutor(max_workers=5) as executor: - futures = [executor.submit(_download_tile, task) for task in download_tasks] - - for future in as_completed(futures): - row, col, tile_img = future.result() - - if tile_img is not None: - paste_x = col * tile_size - paste_y = row * tile_size - output_img.paste(tile_img, (paste_x, paste_y)) - else: - n_failed_tiles += 1 - - if n_failed_tiles > 3: - raise ValueError("Failed to download all tiles for the requested map.") - - return MapImage( - image=Image.from_numpy(np.array(output_img), format=ImageFormat.RGB), - position=position, - zoom_level=zoom_level, - n_tiles=n_tiles, - ) diff --git a/dimos/mapping/osm/query.py b/dimos/mapping/osm/query.py deleted file mode 100644 index 410f879c20..0000000000 --- a/dimos/mapping/osm/query.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from dimos.mapping.osm.osm import MapImage -from dimos.mapping.types import LatLon -from dimos.models.vl.base import VlModel -from dimos.utils.generic import extract_json_from_llm_response -from dimos.utils.logging_config import setup_logger - -_PROLOGUE = "This is an image of an open street map I'm on." -_JSON = "Please only respond with valid JSON." -logger = setup_logger() - - -def query_for_one_position(vl_model: VlModel, map_image: MapImage, query: str) -> LatLon | None: - full_query = f"{_PROLOGUE} {query} {_JSON} If there's a match return the x, y coordinates from the image. Example: `[123, 321]`. If there's no match return `null`." - response = vl_model.query(map_image.image, full_query) - coords = tuple(map(int, re.findall(r"\d+", response))) - if len(coords) != 2: - return None - return map_image.pixel_to_latlon(coords) - - -def query_for_one_position_and_context( - vl_model: VlModel, map_image: MapImage, query: str, robot_position: LatLon -) -> tuple[LatLon, str] | None: - example = '{"coordinates": [123, 321], "description": "A Starbucks on 27th Street"}' - x, y = map_image.latlon_to_pixel(robot_position) - my_location = f"I'm currently at x={x}, y={y}." - full_query = f"{_PROLOGUE} {my_location} {query} {_JSON} If there's a match return the x, y coordinates from the image and what is there. Example response: `{example}`. If there's no match return `null`." - logger.info(f"Qwen query: `{full_query}`") - response = vl_model.query(map_image.image, full_query) - - try: - doc = extract_json_from_llm_response(response) - return map_image.pixel_to_latlon(tuple(doc["coordinates"])), str(doc["description"]) - except Exception: - pass - - # TODO: Try more simplictic methods to parse. - return None diff --git a/dimos/mapping/osm/test_osm.py b/dimos/mapping/osm/test_osm.py deleted file mode 100644 index 475e2b40fc..0000000000 --- a/dimos/mapping/osm/test_osm.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -from typing import Any - -import cv2 -import numpy as np -import pytest -from requests import Request -import requests_mock - -from dimos.mapping.osm.osm import get_osm_map -from dimos.mapping.types import LatLon -from dimos.utils.data import get_data - -_fixture_dir = get_data("osm_map_test") - - -def _tile_callback(request: Request, context: Any) -> bytes: - parts = (request.url or "").split("/") - zoom, x, y_png = parts[-3], parts[-2], parts[-1] - y = y_png.removesuffix(".png") - tile_path = _fixture_dir / f"{zoom}_{x}_{y}.png" - context.headers["Content-Type"] = "image/png" - return tile_path.read_bytes() - - -@pytest.fixture -def mock_openstreetmap_org() -> Generator[None, None, None]: - with requests_mock.Mocker() as m: - m.get(requests_mock.ANY, content=_tile_callback) - yield - - -def test_get_osm_map(mock_openstreetmap_org: None) -> None: - position = LatLon(lat=37.751857, lon=-122.431265) - map_image = get_osm_map(position, 18, 4) - - assert map_image.position == position - assert map_image.n_tiles == 4 - - expected_image = cv2.imread(str(_fixture_dir / "full.png")) - expected_image_rgb = cv2.cvtColor(expected_image, cv2.COLOR_BGR2RGB) - assert np.array_equal(map_image.image.data, expected_image_rgb), "Map is not the same." - - -def test_pixel_to_latlon(mock_openstreetmap_org: None) -> None: - position = LatLon(lat=37.751857, lon=-122.431265) - map_image = get_osm_map(position, 18, 4) - latlon = map_image.pixel_to_latlon((100, 100)) - assert abs(latlon.lat - 37.7540056) < 0.0000001 - assert abs(latlon.lon - (-122.43385076)) < 0.0000001 - - -def test_latlon_to_pixel(mock_openstreetmap_org: None) -> None: - position = LatLon(lat=37.751857, lon=-122.431265) - map_image = get_osm_map(position, 18, 4) - coords = map_image.latlon_to_pixel(LatLon(lat=37.751, lon=-122.431)) - assert coords == (631, 808) diff --git a/dimos/mapping/pointclouds/accumulators/general.py b/dimos/mapping/pointclouds/accumulators/general.py deleted file mode 100644 index d0d4668dc3..0000000000 --- a/dimos/mapping/pointclouds/accumulators/general.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from open3d.geometry import PointCloud # type: ignore[import-untyped] -from open3d.io import read_point_cloud # type: ignore[import-untyped] - -from dimos.core.global_config import GlobalConfig - - -class GeneralPointCloudAccumulator: - _point_cloud: PointCloud - _voxel_size: float - - def __init__(self, voxel_size: float, global_config: GlobalConfig) -> None: - self._point_cloud = PointCloud() - self._voxel_size = voxel_size - - if global_config.mujoco_global_map_from_pointcloud: - path = global_config.mujoco_global_map_from_pointcloud - self._point_cloud = read_point_cloud(path) - - def get_point_cloud(self) -> PointCloud: - return self._point_cloud - - def add(self, point_cloud: PointCloud) -> None: - """Voxelise *frame* and splice it into the running map.""" - new_pct = point_cloud.voxel_down_sample(voxel_size=self._voxel_size) - - # Skip for empty pointclouds. - if len(new_pct.points) == 0: - return - - self._point_cloud = _splice_cylinder(self._point_cloud, new_pct, shrink=0.5) - - -def _splice_cylinder( - map_pcd: PointCloud, - patch_pcd: PointCloud, - axis: int = 2, - shrink: float = 0.95, -) -> PointCloud: - center = patch_pcd.get_center() - patch_pts = np.asarray(patch_pcd.points) - - # Axes perpendicular to cylinder - axes = [0, 1, 2] - axes.remove(axis) - - planar_dists = np.linalg.norm(patch_pts[:, axes] - center[axes], axis=1) - radius = planar_dists.max() * shrink - - axis_min = (patch_pts[:, axis].min() - center[axis]) * shrink + center[axis] - axis_max = (patch_pts[:, axis].max() - center[axis]) * shrink + center[axis] - - map_pts = np.asarray(map_pcd.points) - planar_dists_map = np.linalg.norm(map_pts[:, axes] - center[axes], axis=1) - - victims = np.nonzero( - (planar_dists_map < radius) - & (map_pts[:, axis] >= axis_min) - & (map_pts[:, axis] <= axis_max) - )[0] - - survivors = map_pcd.select_by_index(victims, invert=True) - return survivors + patch_pcd diff --git a/dimos/mapping/pointclouds/accumulators/protocol.py b/dimos/mapping/pointclouds/accumulators/protocol.py deleted file mode 100644 index f453165816..0000000000 --- a/dimos/mapping/pointclouds/accumulators/protocol.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from open3d.geometry import PointCloud # type: ignore[import-untyped] - - -class PointCloudAccumulator(Protocol): - def get_point_cloud(self) -> PointCloud: - """Get the accumulated pointcloud.""" - ... - - def add(self, point_cloud: PointCloud) -> None: - """Add a pointcloud to the accumulator.""" - ... diff --git a/dimos/mapping/pointclouds/demo.py b/dimos/mapping/pointclouds/demo.py deleted file mode 100644 index 5251fc3406..0000000000 --- a/dimos/mapping/pointclouds/demo.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import cv2 -from open3d.geometry import PointCloud # type: ignore[import-untyped] -import typer - -from dimos.mapping.occupancy.gradient import gradient -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.mapping.pointclouds.occupancy import simple_occupancy -from dimos.mapping.pointclouds.util import ( - height_colorize, - read_pointcloud, - visualize, -) -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.utils.data import get_data - -app = typer.Typer() - - -def _get_sum_map() -> PointCloud: - return read_pointcloud(get_data("apartment") / "sum.ply") - - -def _get_occupancy_grid() -> OccupancyGrid: - resolution = 0.05 - min_height = 0.15 - max_height = 0.6 - occupancygrid = simple_occupancy( - PointCloud2(_get_sum_map()), - resolution=resolution, - min_height=min_height, - max_height=max_height, - ) - return occupancygrid - - -def _show_occupancy_grid(og: OccupancyGrid) -> None: - cost_map = visualize_occupancy_grid(og, "turbo").to_opencv() - cost_map = cv2.flip(cost_map, 0) - - # Resize to make the image larger (scale by 4x) - height, width = cost_map.shape[:2] - cost_map = cv2.resize(cost_map, (width * 4, height * 4), interpolation=cv2.INTER_NEAREST) - - cv2.namedWindow("Occupancy Grid", cv2.WINDOW_NORMAL) - cv2.imshow("Occupancy Grid", cost_map) - cv2.waitKey(0) - cv2.destroyAllWindows() - - -@app.command() -def view_sum() -> None: - pointcloud = _get_sum_map() - height_colorize(pointcloud) - visualize(pointcloud) - - -@app.command() -def view_map() -> None: - og = _get_occupancy_grid() - _show_occupancy_grid(og) - - -@app.command() -def view_map_inflated() -> None: - og = gradient(_get_occupancy_grid(), max_distance=1.5) - _show_occupancy_grid(og) - - -if __name__ == "__main__": - app() diff --git a/dimos/mapping/pointclouds/occupancy.py b/dimos/mapping/pointclouds/occupancy.py deleted file mode 100644 index 0f6ad8c0de..0000000000 --- a/dimos/mapping/pointclouds/occupancy.py +++ /dev/null @@ -1,501 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, Protocol, TypeVar - -from numba import njit, prange # type: ignore[import-untyped] -import numpy as np -from scipy import ndimage # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import Pose -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid - -if TYPE_CHECKING: - from numpy.typing import NDArray - - -@njit(cache=True) # type: ignore[untyped-decorator] -def _height_map_kernel( - points: NDArray[np.floating[Any]], - min_height_map: NDArray[np.floating[Any]], - max_height_map: NDArray[np.floating[Any]], - min_x: float, - min_y: float, - inv_res: float, - width: int, - height: int, -) -> None: - """Build min/max height maps from points (faster than np.fmax/fmin.at).""" - n = points.shape[0] - for i in range(n): - x = points[i, 0] - y = points[i, 1] - z = points[i, 2] - - gx = int((x - min_x) * inv_res + 0.5) - gy = int((y - min_y) * inv_res + 0.5) - - if 0 <= gx < width and 0 <= gy < height: - cur_min = min_height_map[gy, gx] - cur_max = max_height_map[gy, gx] - # NaN comparisons are always False, so first point sets the value - if z < cur_min or cur_min != cur_min: # cur_min != cur_min checks for NaN - min_height_map[gy, gx] = z - if z > cur_max or cur_max != cur_max: - max_height_map[gy, gx] = z - - -@njit(cache=True, parallel=True) # type: ignore[untyped-decorator] -def _simple_occupancy_kernel( - points: NDArray[np.floating[Any]], - grid: NDArray[np.signedinteger[Any]], - min_x: float, - min_y: float, - inv_res: float, - width: int, - height: int, - min_height: float, - max_height: float, -) -> None: - """Numba-accelerated kernel for simple_occupancy grid population.""" - n = points.shape[0] - # Pass 1: Mark ground as free - for i in prange(n): - x = points[i, 0] - y = points[i, 1] - z = points[i, 2] - if z < min_height: - gx = int((x - min_x) * inv_res + 0.5) - gy = int((y - min_y) * inv_res + 0.5) - if 0 <= gx < width and 0 <= gy < height: - grid[gy, gx] = 0 - - # Pass 2: Mark obstacles (overwrites ground) - for i in prange(n): - x = points[i, 0] - y = points[i, 1] - z = points[i, 2] - if min_height <= z <= max_height: - gx = int((x - min_x) * inv_res + 0.5) - gy = int((y - min_y) * inv_res + 0.5) - if 0 <= gx < width and 0 <= gy < height: - grid[gy, gx] = 100 - - -if TYPE_CHECKING: - from collections.abc import Callable - - from dimos.msgs.sensor_msgs import PointCloud2 - - -@dataclass(frozen=True) -class OccupancyConfig: - """Base config for all occupancy grid generators.""" - - resolution: float = 0.05 - frame_id: str | None = None - - -ConfigT = TypeVar("ConfigT", bound=OccupancyConfig, covariant=True) - - -class OccupancyFn(Protocol[ConfigT]): - """Protocol for pointcloud-to-occupancy conversion functions. - - Functions matching this protocol take a PointCloud2 and config kwargs, - returning an OccupancyGrid. Call with: fn(cloud, resolution=0.1, ...) - """ - - @property - def config_class(self) -> type[ConfigT]: ... - - def __call__(self, cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: ... - - -# Populated after function definitions below -OCCUPANCY_ALGOS: dict[str, Callable[..., OccupancyGrid]] = {} - - -@dataclass(frozen=True) -class HeightCostConfig(OccupancyConfig): - """Config for height-cost based occupancy (terrain slope analysis).""" - - can_pass_under: float = 0.6 - can_climb: float = 0.15 - ignore_noise: float = 0.05 - smoothing: float = 1.0 - - -def height_cost_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: - """Create a costmap based on terrain slope (rate of change of height). - - Costs are assigned based on the gradient magnitude of the terrain height. - Steeper slopes get higher costs, with max_step height change mapping to cost 100. - Cells without observations are marked unknown (-1). - - Args: - cloud: PointCloud2 message containing 3D points - **kwargs: HeightCostConfig fields - resolution, can_pass_under, can_climb, - ignore_noise, smoothing, frame_id - - Returns: - OccupancyGrid with costs 0-100 based on terrain slope, -1 for unknown - """ - cfg = HeightCostConfig(**kwargs) - points, _ = cloud.as_numpy() - points = points.astype(np.float64) # Upcast to avoid float32 rounding - ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 - - if len(points) == 0: - return OccupancyGrid( - width=1, - height=1, - resolution=cfg.resolution, - frame_id=cfg.frame_id or cloud.frame_id, - ) - - # Find bounds of the point cloud in X-Y plane (use all points) - min_x = np.min(points[:, 0]) - max_x = np.max(points[:, 0]) - min_y = np.min(points[:, 1]) - max_y = np.max(points[:, 1]) - - # Add padding - padding = 1.0 - min_x -= padding - max_x += padding - min_y -= padding - max_y += padding - - # Calculate grid dimensions - width = int(np.ceil((max_x - min_x) / cfg.resolution)) - height = int(np.ceil((max_y - min_y) / cfg.resolution)) - - # Create origin pose - origin = Pose() - origin.position.x = min_x - origin.position.y = min_y - origin.position.z = 0.0 - origin.orientation.w = 1.0 - - # Step 1: Build min and max height maps for each cell - # Initialize with NaN to track which cells have observations - min_height_map = np.full((height, width), np.nan, dtype=np.float32) - max_height_map = np.full((height, width), np.nan, dtype=np.float32) - - # Use numba kernel (faster than np.fmax/fmin.at) - _height_map_kernel( - points, - min_height_map, - max_height_map, - min_x, - min_y, - 1.0 / cfg.resolution, - width, - height, - ) - - # Step 2: Determine effective height for each cell - # If gap between min and max > can_pass_under, robot can pass under - use min (ground) - # Otherwise use max (solid obstacle) - height_gap = max_height_map - min_height_map - height_map = np.where(height_gap > cfg.can_pass_under, min_height_map, max_height_map) - - # Track which cells have observations - observed_mask = ~np.isnan(height_map) - - # Step 3: Apply smoothing to fill gaps while preserving unknown space - if cfg.smoothing > 0 and np.any(observed_mask): - # Use a weighted smoothing approach that only interpolates from known cells - # Create a weight map (1 for observed, 0 for unknown) - weights = observed_mask.astype(np.float32) - height_map_filled = np.where(observed_mask, height_map, 0.0) - - # Smooth both height values and weights - smoothed_heights = ndimage.gaussian_filter(height_map_filled, sigma=cfg.smoothing) - smoothed_weights = ndimage.gaussian_filter(weights, sigma=cfg.smoothing) - - # Avoid division by zero (use np.divide with where to prevent warning) - valid_smooth = smoothed_weights > 0.01 - height_map_smoothed = np.full_like(smoothed_heights, np.nan) - np.divide(smoothed_heights, smoothed_weights, out=height_map_smoothed, where=valid_smooth) - - # Keep original values where we had observations, use smoothed elsewhere - height_map = np.where(observed_mask, height_map, height_map_smoothed) - - # Update observed mask to include smoothed cells - observed_mask = ~np.isnan(height_map) - - # Step 4: Calculate rate of change (gradient magnitude) - # Use Sobel filters for gradient calculation - if np.any(observed_mask): - # Replace NaN with 0 for gradient calculation - height_for_grad = np.where(observed_mask, height_map, 0.0) - - # Calculate gradients (Sobel gives gradient in pixels, scale by resolution) - grad_x = ndimage.sobel(height_for_grad, axis=1) / (8.0 * cfg.resolution) - grad_y = ndimage.sobel(height_for_grad, axis=0) / (8.0 * cfg.resolution) - - # Gradient magnitude = height change per meter - gradient_magnitude = np.sqrt(grad_x**2 + grad_y**2) - - # Map gradient to cost: can_climb height change over one cell maps to cost 100 - # gradient_magnitude is in m/m, so multiply by resolution to get height change per cell - height_change_per_cell = gradient_magnitude * cfg.resolution - - # Ignore height changes below noise threshold (lidar floor noise) - height_change_per_cell = np.where( - height_change_per_cell < cfg.ignore_noise, 0.0, height_change_per_cell - ) - - cost_float = (height_change_per_cell / cfg.can_climb) * 100.0 - cost_float = np.clip(cost_float, 0, 100) - - # Erode observed mask - only trust gradients where all neighbors are observed - # This prevents false high costs at boundaries with unknown regions - structure = ndimage.generate_binary_structure(2, 1) # 4-connectivity - valid_gradient_mask = ndimage.binary_erosion(observed_mask, structure=structure) - - # Convert to int8, marking cells without valid gradients as -1 - cost = np.where(valid_gradient_mask, cost_float.astype(np.int8), -1) - else: - cost = np.full((height, width), -1, dtype=np.int8) - - return OccupancyGrid( - grid=cost, - resolution=cfg.resolution, - origin=origin, - frame_id=cfg.frame_id or cloud.frame_id, - ts=ts, - ) - - -@dataclass(frozen=True) -class GeneralOccupancyConfig(OccupancyConfig): - """Config for general obstacle-based occupancy.""" - - min_height: float = 0.1 - max_height: float = 2.0 - mark_free_radius: float = 0.4 - - -# can remove, just needs pulling out of unitree type/map.py -def general_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: - """Create an OccupancyGrid from a PointCloud2 message. - - Args: - cloud: PointCloud2 message containing 3D points - **kwargs: GeneralOccupancyConfig fields - resolution, min_height, max_height, - frame_id, mark_free_radius - - Returns: - OccupancyGrid with occupied cells where points were projected - """ - cfg = GeneralOccupancyConfig(**kwargs) - points, _ = cloud.as_numpy() - points = points.astype(np.float64) # Upcast to avoid float32 rounding - - if len(points) == 0: - return OccupancyGrid( - width=1, - height=1, - resolution=cfg.resolution, - frame_id=cfg.frame_id or cloud.frame_id, - ) - - # Filter points by height for obstacles - obstacle_mask = (points[:, 2] >= cfg.min_height) & (points[:, 2] <= cfg.max_height) - obstacle_points = points[obstacle_mask] - - # Get points below min_height for marking as free space - ground_mask = points[:, 2] < cfg.min_height - ground_points = points[ground_mask] - - # Find bounds of the point cloud in X-Y plane (use all points) - min_x = np.min(points[:, 0]) - max_x = np.max(points[:, 0]) - min_y = np.min(points[:, 1]) - max_y = np.max(points[:, 1]) - - # Add some padding around the bounds - padding = 1.0 # 1 meter padding - min_x -= padding - max_x += padding - min_y -= padding - max_y += padding - - # Calculate grid dimensions - width = int(np.ceil((max_x - min_x) / cfg.resolution)) - height = int(np.ceil((max_y - min_y) / cfg.resolution)) - - # Create origin pose (bottom-left corner of the grid) - origin = Pose() - origin.position.x = min_x - origin.position.y = min_y - origin.position.z = 0.0 - origin.orientation.w = 1.0 # No rotation - - # Initialize grid (all unknown) - grid = np.full((height, width), -1, dtype=np.int8) - - # First, mark ground points as free space - if len(ground_points) > 0: - ground_x = ((ground_points[:, 0] - min_x) / cfg.resolution).astype(np.int32) - ground_y = ((ground_points[:, 1] - min_y) / cfg.resolution).astype(np.int32) - - # Clip indices to grid bounds - ground_x = np.clip(ground_x, 0, width - 1) - ground_y = np.clip(ground_y, 0, height - 1) - - # Mark ground cells as free - grid[ground_y, ground_x] = 0 # Free space - - # Then mark obstacle points (will override ground if at same location) - if len(obstacle_points) > 0: - obs_x = ((obstacle_points[:, 0] - min_x) / cfg.resolution).astype(np.int32) - obs_y = ((obstacle_points[:, 1] - min_y) / cfg.resolution).astype(np.int32) - - # Clip indices to grid bounds - obs_x = np.clip(obs_x, 0, width - 1) - obs_y = np.clip(obs_y, 0, height - 1) - - # Mark cells as occupied - grid[obs_y, obs_x] = 100 # Lethal obstacle - - # Apply mark_free_radius to expand free space areas - if cfg.mark_free_radius > 0: - # Expand existing free space areas by the specified radius - # This will NOT expand from obstacles, only from free space - - free_mask = grid == 0 # Current free space - free_radius_cells = int(np.ceil(cfg.mark_free_radius / cfg.resolution)) - - # Create circular kernel - y, x = np.ogrid[ - -free_radius_cells : free_radius_cells + 1, - -free_radius_cells : free_radius_cells + 1, - ] - kernel = x**2 + y**2 <= free_radius_cells**2 - - # Dilate free space areas - expanded_free = ndimage.binary_dilation(free_mask, structure=kernel, iterations=1) - - # Mark expanded areas as free, but don't override obstacles - grid[expanded_free & (grid != 100)] = 0 - - # Create and return OccupancyGrid - # Get timestamp from cloud if available - ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 - - return OccupancyGrid( - grid=grid, - resolution=cfg.resolution, - origin=origin, - frame_id=cfg.frame_id or cloud.frame_id, - ts=ts, - ) - - -@dataclass(frozen=True) -class SimpleOccupancyConfig(OccupancyConfig): - """Config for simple occupancy with morphological closing.""" - - min_height: float = 0.1 - max_height: float = 2.0 - closing_iterations: int = 1 - closing_connectivity: int = 2 - can_pass_under: float = 0.6 - can_climb: float = 0.15 - ignore_noise: float = 0.05 - smoothing: float = 1.0 - - -def simple_occupancy(cloud: PointCloud2, **kwargs: Any) -> OccupancyGrid: - """Create a simple occupancy grid with morphological closing. - - Args: - cloud: PointCloud2 message containing 3D points - **kwargs: SimpleOccupancyConfig fields - resolution, min_height, max_height, - frame_id, closing_iterations, closing_connectivity - - Returns: - OccupancyGrid with occupied/free cells - """ - cfg = SimpleOccupancyConfig(**kwargs) - points, _ = cloud.as_numpy() - points = points.astype(np.float64) # Upcast to avoid float32 rounding - - if len(points) == 0: - return OccupancyGrid( - width=1, - height=1, - resolution=cfg.resolution, - frame_id=cfg.frame_id or cloud.frame_id, - ) - - # Find bounds of the point cloud in X-Y plane - min_x = float(np.min(points[:, 0])) - 1.0 - max_x = float(np.max(points[:, 0])) + 1.0 - min_y = float(np.min(points[:, 1])) - 1.0 - max_y = float(np.max(points[:, 1])) + 1.0 - - # Calculate grid dimensions - width = int(np.ceil((max_x - min_x) / cfg.resolution)) - height = int(np.ceil((max_y - min_y) / cfg.resolution)) - - # Create origin pose (bottom-left corner of the grid) - origin = Pose() - origin.position.x = min_x - origin.position.y = min_y - origin.position.z = 0.0 - origin.orientation.w = 1.0 - - # Initialize grid (all unknown) - grid = np.full((height, width), -1, dtype=np.int8) - - # Use numba kernel for fast grid population - _simple_occupancy_kernel( - points, - grid, - min_x, - min_y, - 1.0 / cfg.resolution, - width, - height, - cfg.min_height, - cfg.max_height, - ) - - ts = cloud.ts if hasattr(cloud, "ts") and cloud.ts is not None else 0.0 - - return OccupancyGrid( - grid=grid, - resolution=cfg.resolution, - origin=origin, - frame_id=cfg.frame_id or cloud.frame_id, - ts=ts, - ) - - -# Populate algorithm registry -OCCUPANCY_ALGOS.update( - { - "height_cost": height_cost_occupancy, - "general": general_occupancy, - "simple": simple_occupancy, - } -) diff --git a/dimos/mapping/pointclouds/test_occupancy.py b/dimos/mapping/pointclouds/test_occupancy.py deleted file mode 100644 index 2e301c772d..0000000000 --- a/dimos/mapping/pointclouds/test_occupancy.py +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import cv2 -import numpy as np -from open3d.geometry import PointCloud -import pytest - -from dimos.core import LCMTransport -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.mapping.pointclouds.occupancy import ( - height_cost_occupancy, - simple_occupancy, -) -from dimos.mapping.pointclouds.util import read_pointcloud -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.msgs.sensor_msgs.Image import Image -from dimos.utils.data import get_data -from dimos.utils.testing.moment import OutputMoment -from dimos.utils.testing.test_moment import Go2Moment - - -@pytest.fixture -def apartment() -> PointCloud: - return read_pointcloud(get_data("apartment") / "sum.ply") - - -@pytest.fixture -def big_office() -> PointCloud: - return read_pointcloud(get_data("big_office.ply")) - - -@pytest.mark.parametrize( - "occupancy_fn,output_name", - [ - (simple_occupancy, "occupancy_simple.png"), - ], -) -def test_occupancy(apartment: PointCloud, occupancy_fn, output_name: str) -> None: - expected_image = cv2.imread(str(get_data(output_name)), cv2.IMREAD_GRAYSCALE) - cloud = PointCloud2.from_numpy(np.asarray(apartment.points), frame_id="map") - - occupancy_grid = occupancy_fn(cloud) - - # Convert grid from -1..100 to 0..101 for PNG - computed_image = (occupancy_grid.grid + 1).astype(np.uint8) - - np.testing.assert_array_equal(computed_image, expected_image) - - -@pytest.mark.parametrize( - "occupancy_fn,output_name", - [ - (height_cost_occupancy, "big_office_height_cost_occupancy.png"), - (simple_occupancy, "big_office_simple_occupancy.png"), - ], -) -def test_occupancy2(big_office, occupancy_fn, output_name): - expected_image = Image.from_file(get_data(output_name)) - cloud = PointCloud2.from_numpy(np.asarray(big_office.points), frame_id="") - - occupancy_grid = occupancy_fn(cloud) - - actual = visualize_occupancy_grid(occupancy_grid, "rainbow") - actual.ts = expected_image.ts - np.testing.assert_array_equal(actual, expected_image) - - -class HeightCostMoment(Go2Moment): - costmap: OutputMoment[OccupancyGrid] = OutputMoment(LCMTransport("/costmap", OccupancyGrid)) - - -@pytest.fixture -def height_cost_moment(): - moment = HeightCostMoment() - - def get_moment(ts: float, publish: bool = True) -> HeightCostMoment: - moment.seek(ts) - if moment.lidar.value is not None: - costmap = height_cost_occupancy( - moment.lidar.value, - resolution=0.05, - can_pass_under=0.6, - can_climb=0.15, - ) - moment.costmap.set(costmap) - if publish: - moment.publish() - return moment - - yield get_moment - - moment.stop() - - -def test_height_cost_occupancy_from_lidar(height_cost_moment) -> None: - """Test height_cost_occupancy with real lidar data.""" - moment = height_cost_moment(1.0) - - costmap = moment.costmap.value - assert costmap is not None - - # Basic sanity checks - assert costmap.grid is not None - assert costmap.width > 0 - assert costmap.height > 0 - - # Costs should be in range -1 to 100 (-1 = unknown) - assert costmap.grid.min() >= -1 - assert costmap.grid.max() <= 100 - - # Check we have some unknown, some known - known_mask = costmap.grid >= 0 - assert known_mask.sum() > 0, "Expected some known cells" - assert (~known_mask).sum() > 0, "Expected some unknown cells" diff --git a/dimos/mapping/pointclouds/test_occupancy_speed.py b/dimos/mapping/pointclouds/test_occupancy_speed.py deleted file mode 100644 index 2def839dd5..0000000000 --- a/dimos/mapping/pointclouds/test_occupancy_speed.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pickle -import time - -import pytest - -from dimos.mapping.pointclouds.occupancy import OCCUPANCY_ALGOS -from dimos.mapping.voxels import VoxelGridMapper -from dimos.utils.cli.plot import bar -from dimos.utils.data import get_data, get_data_dir -from dimos.utils.testing import TimedSensorReplay - - -@pytest.mark.tool -def test_build_map(): - mapper = VoxelGridMapper(publish_interval=-1) - - for _ts, frame in TimedSensorReplay("unitree_go2_bigoffice/lidar").iterate(): - mapper.add_frame(frame) - - pickle_file = get_data_dir() / "unitree_go2_bigoffice_map.pickle" - global_pcd = mapper.get_global_pointcloud2() - - with open(pickle_file, "wb") as f: - pickle.dump(global_pcd, f) - - mapper.stop() - - -def test_costmap_calc(): - path = get_data("unitree_go2_bigoffice_map.pickle") - pointcloud = pickle.loads(path.read_bytes()) - - names = [] - times_ms = [] - for name, algo in OCCUPANCY_ALGOS.items(): - start = time.perf_counter() - result = algo(pointcloud) - elapsed = time.perf_counter() - start - names.append(name) - times_ms.append(elapsed * 1000) - print(f"{name}: {elapsed * 1000:.1f}ms - {result}") - - bar(names, times_ms, title="Occupancy Algorithm Speed", ylabel="ms") diff --git a/dimos/mapping/pointclouds/util.py b/dimos/mapping/pointclouds/util.py deleted file mode 100644 index f85b2520eb..0000000000 --- a/dimos/mapping/pointclouds/util.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Iterable -import colorsys -from pathlib import Path - -import numpy as np -import open3d as o3d # type: ignore[import-untyped] -from open3d.geometry import PointCloud # type: ignore[import-untyped] - - -def read_pointcloud(path: Path) -> PointCloud: - return o3d.io.read_point_cloud(path) - - -def sum_pointclouds(pointclouds: Iterable[PointCloud]) -> PointCloud: - it = iter(pointclouds) - ret = next(it) - for x in it: - ret += x - return ret.remove_duplicated_points() - - -def height_colorize(pointcloud: PointCloud) -> None: - points = np.asarray(pointcloud.points) - z_values = points[:, 2] - z_min = z_values.min() - z_max = z_values.max() - - z_normalized = (z_values - z_min) / (z_max - z_min) - - # Create rainbow color map. - colors = np.array([colorsys.hsv_to_rgb(0.7 * (1 - h), 1.0, 1.0) for h in z_normalized]) - - pointcloud.colors = o3d.utility.Vector3dVector(colors) - - -def visualize(pointcloud: PointCloud) -> None: - voxel_size = 0.05 # 0.05m voxels - voxel_grid = o3d.geometry.VoxelGrid.create_from_point_cloud(pointcloud, voxel_size=voxel_size) - o3d.visualization.draw_geometries( - [voxel_grid], - window_name="Combined Point Clouds (Voxelized)", - width=1024, - height=768, - ) diff --git a/dimos/mapping/test_voxels.py b/dimos/mapping/test_voxels.py deleted file mode 100644 index 8fdb1f2827..0000000000 --- a/dimos/mapping/test_voxels.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Generator -import time - -import numpy as np -import pytest - -from dimos.core import LCMTransport -from dimos.mapping.voxels import VoxelGridMapper -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.utils.data import get_data -from dimos.utils.testing.moment import OutputMoment -from dimos.utils.testing.replay import TimedSensorReplay -from dimos.utils.testing.test_moment import Go2Moment - - -@pytest.fixture -def mapper() -> Generator[VoxelGridMapper, None, None]: - mapper = VoxelGridMapper() - yield mapper - mapper.stop() - - -class Go2MapperMoment(Go2Moment): - global_map: OutputMoment[PointCloud2] = OutputMoment(LCMTransport("/global_map", PointCloud2)) - - -MomentFactory = Callable[[float, bool], Go2MapperMoment] - - -@pytest.fixture -def moment() -> Generator[MomentFactory, None, None]: - instances: list[Go2MapperMoment] = [] - - def get_moment(ts: float, publish: bool = True) -> Go2MapperMoment: - m = Go2MapperMoment() - m.seek(ts) - if publish: - m.publish() - instances.append(m) - return m - - yield get_moment - for m in instances: - m.stop() - - -@pytest.fixture -def moment1(moment: MomentFactory) -> Go2MapperMoment: - return moment(10, False) - - -@pytest.fixture -def moment2(moment: MomentFactory) -> Go2MapperMoment: - return moment(85, False) - - -@pytest.mark.tool -def two_perspectives_loop(moment: MomentFactory) -> None: - while True: - moment(10, True) - time.sleep(1) - moment(85, True) - time.sleep(1) - - -def test_carving( - mapper: VoxelGridMapper, moment1: Go2MapperMoment, moment2: Go2MapperMoment -) -> None: - lidar_frame1 = moment1.lidar.value - assert lidar_frame1 is not None - lidar_frame1_transport: LCMTransport[PointCloud2] = LCMTransport("/prev_lidar", PointCloud2) - lidar_frame1_transport.publish(lidar_frame1) - lidar_frame1_transport.stop() - - lidar_frame2 = moment2.lidar.value - assert lidar_frame2 is not None - - # Debug: check XY overlap - pts1 = np.asarray(lidar_frame1.pointcloud.points) - pts2 = np.asarray(lidar_frame2.pointcloud.points) - - voxel_size = mapper.config.voxel_size - xy1 = set(map(tuple, (pts1[:, :2] / voxel_size).astype(int))) - xy2 = set(map(tuple, (pts2[:, :2] / voxel_size).astype(int))) - - overlap = xy1 & xy2 - print(f"\nFrame1 XY columns: {len(xy1)}") - print(f"Frame2 XY columns: {len(xy2)}") - print(f"Overlapping XY columns: {len(overlap)}") - - # Carving mapper (default, carve_columns=True) - mapper.add_frame(lidar_frame1) - mapper.add_frame(lidar_frame2) - - moment2.global_map.set(mapper.get_global_pointcloud2()) - moment2.publish() - - count_carving = mapper.size() - # Additive mapper (carve_columns=False) - additive_mapper = VoxelGridMapper(carve_columns=False) - additive_mapper.add_frame(lidar_frame1) - additive_mapper.add_frame(lidar_frame2) - count_additive = additive_mapper.size() - - print("\n=== Carving comparison ===") - print(f"Additive (no carving): {count_additive}") - print(f"With carving: {count_carving}") - print(f"Voxels carved: {count_additive - count_carving}") - - # Carving should result in fewer voxels - assert count_carving < count_additive, ( - f"Carving should remove some voxels. Additive: {count_additive}, Carving: {count_carving}" - ) - - additive_global_map: LCMTransport[PointCloud2] = LCMTransport( - "additive_global_map", PointCloud2 - ) - additive_global_map.publish(additive_mapper.get_global_pointcloud2()) - additive_global_map.stop() - additive_mapper.stop() - - -def test_injest_a_few(mapper: VoxelGridMapper) -> None: - data_dir = get_data("unitree_go2_office_walk2") - lidar_store = TimedSensorReplay(f"{data_dir}/lidar") - - for i in [1, 4, 8]: - frame = lidar_store.find_closest_seek(i) - assert frame is not None - print("add", frame) - mapper.add_frame(frame) - - assert len(mapper.get_global_pointcloud2()) == 30136 - - -@pytest.mark.parametrize( - "voxel_size, expected_points", - [ - (0.5, 277), - (0.1, 7290), - (0.05, 28199), - ], -) -def test_roundtrip(moment1: Go2MapperMoment, voxel_size: float, expected_points: int) -> None: - lidar_frame = moment1.lidar.value - assert lidar_frame is not None - - mapper = VoxelGridMapper(voxel_size=voxel_size) - mapper.add_frame(lidar_frame) - - global1 = mapper.get_global_pointcloud2() - assert len(global1) == expected_points - - # loseless roundtrip - if voxel_size == 0.05: - assert len(global1) == len(lidar_frame) - # TODO: we want __eq__ on PointCloud2 - should actually compare - # all points in both frames - - mapper.add_frame(global1) - # no new information, no global map change - assert len(mapper.get_global_pointcloud2()) == len(global1) - - moment1.publish() - mapper.stop() - - -def test_roundtrip_range_preserved(mapper: VoxelGridMapper) -> None: - """Test that input coordinate ranges are preserved in output.""" - data_dir = get_data("unitree_go2_office_walk2") - lidar_store = TimedSensorReplay(f"{data_dir}/lidar") - - frame = lidar_store.find_closest_seek(1.0) - assert frame is not None - input_pts = np.asarray(frame.pointcloud.points) - - mapper.add_frame(frame) - - out_pcd = mapper.get_global_pointcloud().to_legacy() - out_pts = np.asarray(out_pcd.points) - - voxel_size = mapper.config.voxel_size - tolerance = voxel_size # Allow one voxel of difference at boundaries - - # TODO: we want __eq__ on PointCloud2 - should actually compare - # all points in both frames - - for axis, name in enumerate(["X", "Y", "Z"]): - in_min, in_max = input_pts[:, axis].min(), input_pts[:, axis].max() - out_min, out_max = out_pts[:, axis].min(), out_pts[:, axis].max() - - assert abs(in_min - out_min) < tolerance, f"{name} min mismatch: in={in_min}, out={out_min}" - assert abs(in_max - out_max) < tolerance, f"{name} max mismatch: in={in_max}, out={out_max}" diff --git a/dimos/mapping/types.py b/dimos/mapping/types.py deleted file mode 100644 index 9584e8e8ba..0000000000 --- a/dimos/mapping/types.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dataclasses import dataclass -from typing import TypeAlias - - -@dataclass(frozen=True) -class LatLon: - lat: float - lon: float - alt: float | None = None - - -ImageCoord: TypeAlias = tuple[int, int] diff --git a/dimos/mapping/utils/distance.py b/dimos/mapping/utils/distance.py deleted file mode 100644 index 6e8c48c205..0000000000 --- a/dimos/mapping/utils/distance.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - -from dimos.mapping.types import LatLon - - -def distance_in_meters(location1: LatLon, location2: LatLon) -> float: - """Calculate the great circle distance between two points on Earth using Haversine formula. - - Args: - location1: First location with latitude and longitude - location2: Second location with latitude and longitude - - Returns: - Distance in meters between the two points - """ - # Earth's radius in meters - EARTH_RADIUS_M = 6371000 - - # Convert degrees to radians - lat1_rad = math.radians(location1.lat) - lat2_rad = math.radians(location2.lat) - lon1_rad = math.radians(location1.lon) - lon2_rad = math.radians(location2.lon) - - # Haversine formula - dlat = lat2_rad - lat1_rad - dlon = lon2_rad - lon1_rad - - a = math.sin(dlat / 2) ** 2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2 - c = 2 * math.asin(math.sqrt(a)) - - distance = EARTH_RADIUS_M * c - - return distance diff --git a/dimos/mapping/voxels.py b/dimos/mapping/voxels.py deleted file mode 100644 index 4c1805e059..0000000000 --- a/dimos/mapping/voxels.py +++ /dev/null @@ -1,244 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -import time - -import numpy as np -import open3d as o3d # type: ignore[import-untyped] -import open3d.core as o3c # type: ignore[import-untyped] -from reactivex import interval, operators as ops -from reactivex.disposable import Disposable -from reactivex.subject import Subject - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.core.module import ModuleConfig -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.utils.decorators import simple_mcache -from dimos.utils.logging_config import setup_logger -from dimos.utils.reactive import backpressure - -logger = setup_logger() - - -@dataclass -class Config(ModuleConfig): - frame_id: str = "world" - # -1 never publishes, 0 publishes on every frame, >0 publishes at interval in seconds - publish_interval: float = 0 - voxel_size: float = 0.05 - block_count: int = 2_000_000 - device: str = "CUDA:0" - carve_columns: bool = True - - -class VoxelGridMapper(Module): - default_config = Config - config: Config - - lidar: In[PointCloud2] - global_map: Out[PointCloud2] - - def __init__(self, cfg: GlobalConfig = global_config, **kwargs: object) -> None: - super().__init__(**kwargs) - self._global_config = cfg - - dev = ( - o3c.Device(self.config.device) - if (self.config.device.startswith("CUDA") and o3c.cuda.is_available()) - else o3c.Device("CPU:0") - ) - - logger.info(f"VoxelGridMapper using device: {dev}") - - self.vbg = o3d.t.geometry.VoxelBlockGrid( - attr_names=("dummy",), - attr_dtypes=(o3c.uint8,), - attr_channels=(o3c.SizeVector([1]),), - voxel_size=self.config.voxel_size, - block_resolution=1, - block_count=self.config.block_count, - device=dev, - ) - - self._dev = dev - self._voxel_hashmap = self.vbg.hashmap() - self._key_dtype = self._voxel_hashmap.key_tensor().dtype - self._latest_frame_ts: float = 0.0 - - @rpc - def start(self) -> None: - super().start() - - # Subject to trigger publishing, with backpressure to drop if busy - self._publish_trigger: Subject[None] = Subject() - self._disposables.add( - backpressure(self._publish_trigger) - .pipe(ops.map(lambda _: self.publish_global_map())) - .subscribe() - ) - - lidar_unsub = self.lidar.subscribe(self._on_frame) - self._disposables.add(Disposable(lidar_unsub)) - - # If publish_interval > 0, publish on timer; otherwise publish on each frame - if self.config.publish_interval > 0: - self._disposables.add( - interval(self.config.publish_interval).subscribe( - lambda _: self._publish_trigger.on_next(None) - ) - ) - - @rpc - def stop(self) -> None: - super().stop() - - def _on_frame(self, frame: PointCloud2) -> None: - self.add_frame(frame) - if self.config.publish_interval == 0: - self._publish_trigger.on_next(None) - - def publish_global_map(self) -> None: - pc = self.get_global_pointcloud2() - self.global_map.publish(pc) - - def size(self) -> int: - return self._voxel_hashmap.size() # type: ignore[no-any-return] - - def __len__(self) -> int: - return self.size() - - # @timed() # TODO: fix thread leak in timed decorator - def add_frame(self, frame: PointCloud2) -> None: - # Track latest frame timestamp for proper latency measurement - if hasattr(frame, "ts") and frame.ts: - self._latest_frame_ts = frame.ts - - # we are potentially moving into CUDA here - pcd = ensure_tensor_pcd(frame.pointcloud, self._dev) - - if pcd.is_empty(): - return - - pts = pcd.point["positions"].to(self._dev, o3c.float32) - vox = (pts / self.config.voxel_size).floor().to(self._key_dtype) - keys_Nx3 = vox.contiguous() - - if self.config.carve_columns: - self._carve_and_insert(keys_Nx3) - else: - self._voxel_hashmap.activate(keys_Nx3) - - self.get_global_pointcloud.invalidate_cache(self) # type: ignore[attr-defined] - self.get_global_pointcloud2.invalidate_cache(self) # type: ignore[attr-defined] - - def _carve_and_insert(self, new_keys: o3c.Tensor) -> None: - """Column carving: remove all existing voxels sharing (X,Y) with new_keys, then insert.""" - if new_keys.shape[0] == 0: - self._voxel_hashmap.activate(new_keys) - return - - # Extract (X, Y) from incoming keys - xy_keys = new_keys[:, :2].contiguous() - - # Build temp hashmap for O(1) (X,Y) membership lookup - xy_hashmap = o3c.HashMap( - init_capacity=xy_keys.shape[0], - key_dtype=self._key_dtype, - key_element_shape=o3c.SizeVector([2]), - value_dtypes=[o3c.uint8], - value_element_shapes=[o3c.SizeVector([1])], - device=self._dev, - ) - dummy_vals = o3c.Tensor.zeros((xy_keys.shape[0], 1), o3c.uint8, self._dev) - xy_hashmap.insert(xy_keys, dummy_vals) - - # Get existing keys from main hashmap - active_indices = self._voxel_hashmap.active_buf_indices() - if active_indices.shape[0] == 0: - self._voxel_hashmap.activate(new_keys) - return - - existing_keys = self._voxel_hashmap.key_tensor()[active_indices] - existing_xy = existing_keys[:, :2].contiguous() - - # Find which existing keys have (X,Y) in the incoming set - _, found_mask = xy_hashmap.find(existing_xy) - - # Erase those columns - to_erase = existing_keys[found_mask] - if to_erase.shape[0] > 0: - self._voxel_hashmap.erase(to_erase) - - # Insert new keys - self._voxel_hashmap.activate(new_keys) - - # returns PointCloud2 message (ready to send off down the pipeline) - @simple_mcache - def get_global_pointcloud2(self) -> PointCloud2: - return PointCloud2( - # we are potentially moving out of CUDA here - ensure_legacy_pcd(self.get_global_pointcloud()), - frame_id=self.frame_id, - ts=self._latest_frame_ts if self._latest_frame_ts else time.time(), - ) - - @simple_mcache - # @timed() - def get_global_pointcloud(self) -> o3d.t.geometry.PointCloud: - voxel_coords, _ = self.vbg.voxel_coordinates_and_flattened_indices() - pts = voxel_coords + (self.config.voxel_size * 0.5) - out = o3d.t.geometry.PointCloud(device=self._dev) - out.point["positions"] = pts - return out - - -def ensure_tensor_pcd( - pcd_any: o3d.t.geometry.PointCloud | o3d.geometry.PointCloud, - device: o3c.Device, -) -> o3d.t.geometry.PointCloud: - """Convert legacy / cuda.pybind point clouds into o3d.t.geometry.PointCloud on `device`.""" - - if isinstance(pcd_any, o3d.t.geometry.PointCloud): - return pcd_any.to(device) - - assert isinstance(pcd_any, o3d.geometry.PointCloud), ( - "Input must be a legacy PointCloud or a tensor PointCloud" - ) - - # Legacy CPU point cloud -> tensor - if isinstance(pcd_any, o3d.geometry.PointCloud): - return o3d.t.geometry.PointCloud.from_legacy(pcd_any, o3c.float32, device) - - pts = np.asarray(pcd_any.points, dtype=np.float32) - pcd_t = o3d.t.geometry.PointCloud(device=device) - pcd_t.point["positions"] = o3c.Tensor(pts, o3c.float32, device) - return pcd_t - - -def ensure_legacy_pcd( - pcd_any: o3d.t.geometry.PointCloud | o3d.geometry.PointCloud, -) -> o3d.geometry.PointCloud: - if isinstance(pcd_any, o3d.geometry.PointCloud): - return pcd_any - - assert isinstance(pcd_any, o3d.t.geometry.PointCloud), ( - "Input must be a legacy PointCloud or a tensor PointCloud" - ) - - return pcd_any.to_legacy() - - -voxel_mapper = VoxelGridMapper.blueprint diff --git a/dimos/memory/embedding.py b/dimos/memory/embedding.py deleted file mode 100644 index 20dd82422c..0000000000 --- a/dimos/memory/embedding.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from dataclasses import dataclass, field -from typing import cast - -import reactivex as rx -from reactivex import operators as ops -from reactivex.observable import Observable - -from dimos.core import In, rpc -from dimos.core.module import Module, ModuleConfig -from dimos.models.embedding.base import Embedding, EmbeddingModel -from dimos.models.embedding.clip import CLIPModel -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier -from dimos.utils.reactive import getter_hot - - -@dataclass -class Config(ModuleConfig): - embedding_model: EmbeddingModel = field(default_factory=CLIPModel) - - -@dataclass -class SpatialEntry: - image: Image - pose: PoseStamped - - -@dataclass -class SpatialEmbedding(SpatialEntry): - embedding: Embedding - - -class EmbeddingMemory(Module[Config]): - default_config = Config - config: Config - color_image: In[Image] - global_costmap: In[OccupancyGrid] - - _costmap_getter: Callable[[], OccupancyGrid] | None = None - - def get_costmap(self) -> OccupancyGrid: - if self._costmap_getter is None: - self._costmap_getter = getter_hot(self.global_costmap.pure_observable()) - self._disposables.add(self._costmap_getter) - return self._costmap_getter() - - @rpc - def query_costmap(self, text: str) -> OccupancyGrid: - costmap = self.get_costmap() - # overlay costmap with embedding heat - return costmap - - @rpc - def start(self) -> None: - # would be cool if this sharpness_barrier was somehow self-calibrating - # - # we need a Governor system, sharpness_barrier frequency shouldn't - # be a fixed float but an observable that adjusts based on downstream load - # - # (also voxel size for mapper for example would benefit from this) - self.color_image.pure_observable().pipe( - sharpness_barrier(0.5), - ops.flat_map(self._try_create_spatial_entry), - ops.map(self._embed_spatial_entry), - ops.map(self._store_spatial_entry), - ).subscribe(print) - - def _try_create_spatial_entry(self, img: Image) -> Observable[SpatialEntry]: - pose = self.tf.get_pose("world", "base_link") - if not pose: - return rx.empty() - return rx.of(SpatialEntry(image=img, pose=pose)) - - def _embed_spatial_entry(self, spatial_entry: SpatialEntry) -> SpatialEmbedding: - embedding = cast("Embedding", self.config.embedding_model.embed(spatial_entry.image)) - return SpatialEmbedding( - image=spatial_entry.image, - pose=spatial_entry.pose, - embedding=embedding, - ) - - def _store_spatial_entry(self, spatial_embedding: SpatialEmbedding) -> SpatialEmbedding: - return spatial_embedding - - def query_text(self, query: str) -> list[SpatialEmbedding]: - self.config.embedding_model.embed_text(query) - results: list[SpatialEmbedding] = [] - return results diff --git a/dimos/memory/test_embedding.py b/dimos/memory/test_embedding.py deleted file mode 100644 index b7e7fbb294..0000000000 --- a/dimos/memory/test_embedding.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.memory.embedding import EmbeddingMemory, SpatialEntry -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.utils.data import get_data -from dimos.utils.testing import TimedSensorReplay - -dir_name = "unitree_go2_bigoffice" - - -@pytest.mark.skip -def test_embed_frame() -> None: - """Test embedding a single frame.""" - # Load a frame from recorded data - video = TimedSensorReplay(get_data(dir_name) / "video") - frame = video.find_closest_seek(10) - - # Create memory and embed - memory = EmbeddingMemory() - - try: - # Create a spatial entry with dummy pose (no TF needed for this test) - dummy_pose = PoseStamped( - position=[0, 0, 0], - orientation=[0, 0, 0, 1], # identity quaternion - ) - spatial_entry = SpatialEntry(image=frame, pose=dummy_pose) - - # Embed the frame - result = memory._embed_spatial_entry(spatial_entry) - - # Verify - assert result is not None - assert result.embedding is not None - assert result.embedding.vector is not None - print(f"Embedding shape: {result.embedding.vector.shape}") - print(f"Embedding vector (first 5): {result.embedding.vector[:5]}") - finally: - memory.stop() diff --git a/dimos/memory/timeseries/__init__.py b/dimos/memory/timeseries/__init__.py deleted file mode 100644 index debc14ab3a..0000000000 --- a/dimos/memory/timeseries/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Time series storage and replay.""" - -from dimos.memory.timeseries.base import TimeSeriesStore -from dimos.memory.timeseries.inmemory import InMemoryStore -from dimos.memory.timeseries.pickledir import PickleDirStore -from dimos.memory.timeseries.sqlite import SqliteStore - - -def __getattr__(name: str): # type: ignore[no-untyped-def] - if name == "PostgresStore": - from dimos.memory.timeseries.postgres import PostgresStore - - return PostgresStore - if name == "reset_db": - from dimos.memory.timeseries.postgres import reset_db - - return reset_db - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") - - -__all__ = [ - "InMemoryStore", - "PickleDirStore", - "PostgresStore", - "SqliteStore", - "TimeSeriesStore", - "reset_db", -] diff --git a/dimos/memory/timeseries/base.py b/dimos/memory/timeseries/base.py deleted file mode 100644 index 0d88355b5b..0000000000 --- a/dimos/memory/timeseries/base.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Unified time series storage and replay.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -import time -from typing import TYPE_CHECKING, Generic, TypeVar - -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import CompositeDisposable, Disposable -from reactivex.scheduler import TimeoutScheduler - -if TYPE_CHECKING: - from collections.abc import Iterator - - from reactivex.observable import Observable - - from dimos.types.timestamped import Timestamped - -T = TypeVar("T", bound="Timestamped") - - -class TimeSeriesStore(Generic[T], ABC): - """Unified storage + replay for sensor data. - - Implement abstract methods for your backend (in-memory, pickle, sqlite, etc.). - All iteration, streaming, and seek logic comes free from the base class. - - T must be a Timestamped subclass — timestamps are taken from .ts attribute. - """ - - @abstractmethod - def _save(self, timestamp: float, data: T) -> None: - """Save data at timestamp.""" - ... - - @abstractmethod - def _load(self, timestamp: float) -> T | None: - """Load data at exact timestamp. Returns None if not found.""" - ... - - @abstractmethod - def _delete(self, timestamp: float) -> T | None: - """Delete data at exact timestamp. Returns the deleted item or None.""" - ... - - @abstractmethod - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - """Lazy iteration of (timestamp, data) in range.""" - ... - - @abstractmethod - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - """Find closest timestamp. Backend can optimize (binary search, db index, etc.).""" - ... - - @abstractmethod - def _count(self) -> int: - """Return number of stored items.""" - ... - - @abstractmethod - def _last_timestamp(self) -> float | None: - """Return the last (largest) timestamp, or None if empty.""" - ... - - @abstractmethod - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - """Find the last (ts, data) strictly before the given timestamp.""" - ... - - @abstractmethod - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - """Find the first (ts, data) strictly after the given timestamp.""" - ... - - # --- Collection API (built on abstract methods) --- - - def __len__(self) -> int: - return self._count() - - def __iter__(self) -> Iterator[T]: - """Iterate over data items in timestamp order.""" - for _, data in self._iter_items(): - yield data - - def last_timestamp(self) -> float | None: - """Get the last timestamp in the store.""" - return self._last_timestamp() - - def last(self) -> T | None: - """Get the last data item in the store.""" - ts = self._last_timestamp() - if ts is None: - return None - return self._load(ts) - - @property - def start_ts(self) -> float | None: - """Get the start timestamp of the store.""" - return self.first_timestamp() - - @property - def end_ts(self) -> float | None: - """Get the end timestamp of the store.""" - return self._last_timestamp() - - def time_range(self) -> tuple[float, float] | None: - """Get the time range (start, end) of the store.""" - s = self.first_timestamp() - e = self._last_timestamp() - if s is None or e is None: - return None - return (s, e) - - def duration(self) -> float: - """Get the duration of the store in seconds.""" - r = self.time_range() - return (r[1] - r[0]) if r else 0.0 - - def find_before(self, timestamp: float) -> T | None: - """Find the last item strictly before the given timestamp.""" - result = self._find_before(timestamp) - return result[1] if result else None - - def find_after(self, timestamp: float) -> T | None: - """Find the first item strictly after the given timestamp.""" - result = self._find_after(timestamp) - return result[1] if result else None - - def slice_by_time(self, start: float, end: float) -> list[T]: - """Return items in [start, end) range.""" - return [data for _, data in self._iter_items(start=start, end=end)] - - def save(self, *data: T) -> None: - """Save one or more Timestamped items.""" - for item in data: - self._save(item.ts, item) - - def pipe_save(self, source: Observable[T]) -> Observable[T]: - """Operator for Observable.pipe() — saves items using .ts. - - Usage: - observable.pipe(store.pipe_save).subscribe(...) - """ - - def _save_and_return(data: T) -> T: - self._save(data.ts, data) - return data - - return source.pipe(ops.map(_save_and_return)) - - def consume_stream(self, observable: Observable[T]) -> rx.abc.DisposableBase: - """Subscribe to an observable and save items using .ts. - - Usage: - disposable = store.consume_stream(observable) - """ - return observable.subscribe(on_next=lambda data: self._save(data.ts, data)) - - def load(self, timestamp: float) -> T | None: - """Load data at exact timestamp.""" - return self._load(timestamp) - - def prune_old(self, cutoff: float) -> None: - """Prune items older than cutoff timestamp.""" - to_delete = [ts for ts, _ in self._iter_items(end=cutoff)] - for ts in to_delete: - self._delete(ts) - - def find_closest( - self, - timestamp: float, - tolerance: float | None = None, - ) -> T | None: - """Find data closest to the given absolute timestamp.""" - closest_ts = self._find_closest_timestamp(timestamp, tolerance) - if closest_ts is None: - return None - return self._load(closest_ts) - - def find_closest_seek( - self, - relative_seconds: float, - tolerance: float | None = None, - ) -> T | None: - """Find data closest to a time relative to the start.""" - first = self.first_timestamp() - if first is None: - return None - return self.find_closest(first + relative_seconds, tolerance) - - def first_timestamp(self) -> float | None: - """Get the first timestamp in the store.""" - for ts, _ in self._iter_items(): - return ts - return None - - def first(self) -> T | None: - """Get the first data item in the store.""" - for _, data in self._iter_items(): - return data - return None - - def iterate_items( - self, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Iterator[tuple[float, T]]: - """Iterate over (timestamp, data) tuples with optional seek/duration.""" - first = self.first_timestamp() - if first is None: - return - - if from_timestamp is not None: - start = from_timestamp - elif seek is not None: - start = first + seek - else: - start = None - - end = None - if duration is not None: - start_ts = start if start is not None else first - end = start_ts + duration - - while True: - yield from self._iter_items(start=start, end=end) - if not loop: - break - - def iterate( - self, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Iterator[T]: - """Iterate over data items with optional seek/duration.""" - for _, data in self.iterate_items( - seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop - ): - yield data - - def iterate_realtime( - self, - speed: float = 1.0, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Iterator[T]: - """Iterate data, sleeping to match original timing.""" - prev_ts: float | None = None - for ts, data in self.iterate_items( - seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop - ): - if prev_ts is not None: - delay = (ts - prev_ts) / speed - if delay > 0: - time.sleep(delay) - prev_ts = ts - yield data - - def stream( - self, - speed: float = 1.0, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Observable[T]: - """Stream data as Observable with timing control. - - Uses scheduler-based timing with absolute time reference to prevent drift. - """ - - def subscribe( - observer: rx.abc.ObserverBase[T], - scheduler: rx.abc.SchedulerBase | None = None, - ) -> rx.abc.DisposableBase: - sched = scheduler or TimeoutScheduler() - disp = CompositeDisposable() - is_disposed = False - - iterator = self.iterate_items( - seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop - ) - - try: - first_ts, first_data = next(iterator) - except StopIteration: - observer.on_completed() - return Disposable() - - start_local_time = time.time() - start_replay_time = first_ts - - observer.on_next(first_data) - - try: - next_message: tuple[float, T] | None = next(iterator) - except StopIteration: - observer.on_completed() - return disp - - def schedule_emission(message: tuple[float, T]) -> None: - nonlocal next_message, is_disposed - - if is_disposed: - return - - msg_ts, msg_data = message - - try: - next_message = next(iterator) - except StopIteration: - next_message = None - - target_time = start_local_time + (msg_ts - start_replay_time) / speed - delay = max(0.0, target_time - time.time()) - - def emit( - _scheduler: rx.abc.SchedulerBase, _state: object - ) -> rx.abc.DisposableBase | None: - if is_disposed: - return None - observer.on_next(msg_data) - if next_message is not None: - schedule_emission(next_message) - else: - observer.on_completed() - return None - - sched.schedule_relative(delay, emit) - - if next_message is not None: - schedule_emission(next_message) - - def dispose() -> None: - nonlocal is_disposed - is_disposed = True - disp.dispose() - - return Disposable(dispose) - - return rx.create(subscribe) diff --git a/dimos/memory/timeseries/inmemory.py b/dimos/memory/timeseries/inmemory.py deleted file mode 100644 index b67faca644..0000000000 --- a/dimos/memory/timeseries/inmemory.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""In-memory backend for TimeSeriesStore.""" - -from collections.abc import Iterator - -from sortedcontainers import SortedKeyList # type: ignore[import-untyped] - -from dimos.memory.timeseries.base import T, TimeSeriesStore - - -class InMemoryStore(TimeSeriesStore[T]): - """In-memory storage using SortedKeyList. O(log n) insert, lookup, and range queries.""" - - def __init__(self) -> None: - self._entries: SortedKeyList = SortedKeyList(key=lambda e: e.ts) - - def _bisect_exact(self, timestamp: float) -> int | None: - """Return index of entry with exact timestamp, or None.""" - pos = self._entries.bisect_key_left(timestamp) - if pos < len(self._entries) and self._entries[pos].ts == timestamp: - return pos # type: ignore[no-any-return] - return None - - def _save(self, timestamp: float, data: T) -> None: - self._entries.add(data) - - def _load(self, timestamp: float) -> T | None: - idx = self._bisect_exact(timestamp) - if idx is not None: - return self._entries[idx] # type: ignore[no-any-return] - return None - - def _delete(self, timestamp: float) -> T | None: - idx = self._bisect_exact(timestamp) - if idx is not None: - data = self._entries[idx] - del self._entries[idx] - return data # type: ignore[no-any-return] - return None - - def __iter__(self) -> Iterator[T]: - yield from self._entries - - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - if start is not None and end is not None: - it = self._entries.irange_key(start, end, (True, False)) - elif start is not None: - it = self._entries.irange_key(min_key=start) - elif end is not None: - it = self._entries.irange_key(max_key=end, inclusive=(True, False)) - else: - it = iter(self._entries) - for e in it: - yield (e.ts, e) - - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - if not self._entries: - return None - - pos = self._entries.bisect_key_left(timestamp) - - candidates: list[float] = [] - if pos > 0: - candidates.append(self._entries[pos - 1].ts) - if pos < len(self._entries): - candidates.append(self._entries[pos].ts) - - if not candidates: - return None - - # On ties, prefer the later timestamp (more recent data) - closest = max(candidates, key=lambda ts: (-abs(ts - timestamp), ts)) - - if tolerance is not None and abs(closest - timestamp) > tolerance: - return None - - return closest - - def _count(self) -> int: - return len(self._entries) - - def _last_timestamp(self) -> float | None: - if not self._entries: - return None - return self._entries[-1].ts # type: ignore[no-any-return] - - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - if not self._entries: - return None - pos = self._entries.bisect_key_left(timestamp) - if pos > 0: - e = self._entries[pos - 1] - return (e.ts, e) - return None - - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - if not self._entries: - return None - pos = self._entries.bisect_key_right(timestamp) - if pos < len(self._entries): - e = self._entries[pos] - return (e.ts, e) - return None diff --git a/dimos/memory/timeseries/legacy.py b/dimos/memory/timeseries/legacy.py deleted file mode 100644 index 821d306d2d..0000000000 --- a/dimos/memory/timeseries/legacy.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Legacy pickle directory backend for TimeSeriesStore. - -Compatible with TimedSensorReplay/TimedSensorStorage file format. -""" - -from collections.abc import Callable, Iterator -import glob -import os -from pathlib import Path -import pickle -import re -import time -from typing import Any, cast - -import reactivex as rx -from reactivex.disposable import CompositeDisposable, Disposable -from reactivex.observable import Observable -from reactivex.scheduler import TimeoutScheduler - -from dimos.memory.timeseries.base import T, TimeSeriesStore -from dimos.utils.data import get_data, get_data_dir - - -class LegacyPickleStore(TimeSeriesStore[T]): - """Legacy pickle backend compatible with TimedSensorReplay/TimedSensorStorage. - - File format: - {name}/ - 000.pickle # contains (timestamp, data) tuple - 001.pickle - ... - - Files are assumed to be in chronological order (timestamps increase with file number). - No index is built - iteration is lazy and memory-efficient for large datasets. - - Usage: - # Load existing recording (auto-downloads from LFS if needed) - store = LegacyPickleStore("unitree_go2_bigoffice/lidar") - data = store.find_closest_seek(10.0) - - # Create new recording (directory created on first save) - store = LegacyPickleStore("my_recording/images") - store.save_ts(image) # uses image.ts for timestamp - - Backward compatibility: - This class also supports the old TimedSensorReplay/SensorReplay API: - - iterate_ts() - iterate returning (timestamp, data) tuples - - files - property returning list of file paths - - load_one() - load a single pickle file - """ - - def __init__(self, name: str | Path, autocast: Callable[[Any], T] | None = None) -> None: - """ - Args: - name: Data directory name (e.g. "unitree_go2_bigoffice/lidar") or absolute path. - autocast: Optional function to transform data after loading (for replay) or - before saving (for storage). E.g., `Odometry.from_msg`. - """ - self._name = str(name) - self._root_dir: Path | None = None - self._counter: int = 0 - self._autocast = autocast - - def _get_root_dir(self, for_write: bool = False) -> Path: - """Get root directory, creating on first write if needed.""" - if self._root_dir is not None: - # Ensure directory exists if writing - if for_write: - self._root_dir.mkdir(parents=True, exist_ok=True) - return self._root_dir - - # If absolute path, use directly - if Path(self._name).is_absolute(): - self._root_dir = Path(self._name) - if for_write: - self._root_dir.mkdir(parents=True, exist_ok=True) - elif for_write: - # For writing: use get_data_dir and create if needed - self._root_dir = get_data_dir(self._name) - self._root_dir.mkdir(parents=True, exist_ok=True) - else: - # For reading: use get_data (handles LFS download) - self._root_dir = get_data(self._name) - - return self._root_dir - - def _iter_files(self) -> Iterator[Path]: - """Iterate pickle files in sorted order (by number in filename).""" - - def extract_number(filepath: str) -> int: - basename = os.path.basename(filepath) - match = re.search(r"(\d+)\.pickle$", basename) - return int(match.group(1)) if match else 0 - - root_dir = self._get_root_dir() - files = sorted( - glob.glob(os.path.join(root_dir, "*.pickle")), - key=extract_number, - ) - for f in files: - yield Path(f) - - def _save(self, timestamp: float, data: T) -> None: - root_dir = self._get_root_dir(for_write=True) - - # Initialize counter from existing files if needed - if self._counter == 0: - existing = list(root_dir.glob("*.pickle")) - if existing: - # Find highest existing counter - max_num = 0 - for filepath in existing: - match = re.search(r"(\d+)\.pickle$", filepath.name) - if match: - max_num = max(max_num, int(match.group(1))) - self._counter = max_num + 1 - - full_path = root_dir / f"{self._counter:03d}.pickle" - - if full_path.exists(): - raise RuntimeError(f"File {full_path} already exists") - - # Save as (timestamp, data) tuple for legacy compatibility - with open(full_path, "wb") as f: - pickle.dump((timestamp, data), f) - - self._counter += 1 - - def _load(self, timestamp: float) -> T | None: - """Load data at exact timestamp (linear scan).""" - for ts, data in self._iter_items(): - if ts == timestamp: - return data - return None - - def _delete(self, timestamp: float) -> T | None: - """Delete not supported for legacy pickle format.""" - raise NotImplementedError("LegacyPickleStore does not support deletion") - - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - """Lazy iteration - loads one file at a time. - - Handles both timed format (timestamp, data) and non-timed format (just data). - For non-timed data, uses file index as synthetic timestamp. - """ - for idx, filepath in enumerate(self._iter_files()): - try: - with open(filepath, "rb") as f: - raw = pickle.load(f) - - # Handle both timed (timestamp, data) and non-timed (just data) formats - if isinstance(raw, tuple) and len(raw) == 2: - ts, data = raw - ts = float(ts) - else: - # Non-timed format: use index as synthetic timestamp - ts = float(idx) - data = raw - except Exception: - continue - - if start is not None and ts < start: - continue - if end is not None and ts >= end: - break - - if self._autocast is not None: - data = self._autocast(data) - yield (ts, cast("T", data)) - - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - """Linear scan with early exit (assumes timestamps are monotonically increasing).""" - closest_ts: float | None = None - closest_diff = float("inf") - - for ts, _ in self._iter_items(): - diff = abs(ts - timestamp) - - if diff < closest_diff: - closest_diff = diff - closest_ts = ts - elif diff > closest_diff: - # Moving away from target, can stop - break - - if closest_ts is None: - return None - - if tolerance is not None and closest_diff > tolerance: - return None - - return closest_ts - - def _count(self) -> int: - return sum(1 for _ in self._iter_files()) - - def _last_timestamp(self) -> float | None: - last_ts: float | None = None - for ts, _ in self._iter_items(): - last_ts = ts - return last_ts - - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - result: tuple[float, T] | None = None - for ts, data in self._iter_items(): - if ts < timestamp: - result = (ts, data) - else: - break - return result - - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - for ts, data in self._iter_items(): - if ts > timestamp: - return (ts, data) - return None - - # === Backward-compatible API (TimedSensorReplay/SensorReplay) === - - @property - def files(self) -> list[Path]: - """Return list of pickle files (backward compatibility with SensorReplay).""" - return list(self._iter_files()) - - def load_one(self, name: int | str | Path) -> T | Any: - """Load a single pickle file (backward compatibility with SensorReplay). - - Args: - name: File index (int), filename without extension (str), or full path (Path) - - Returns: - For TimedSensorReplay: (timestamp, data) tuple - For SensorReplay: just the data - """ - root_dir = self._get_root_dir() - - if isinstance(name, int): - full_path = root_dir / f"{name:03d}.pickle" - elif isinstance(name, Path): - full_path = name - else: - full_path = root_dir / Path(f"{name}.pickle") - - with open(full_path, "rb") as f: - data = pickle.load(f) - - # Legacy format: (timestamp, data) tuple - if isinstance(data, tuple) and len(data) == 2: - ts, payload = data - if self._autocast is not None: - payload = self._autocast(payload) - return (ts, payload) - - # Non-timed format: just data - if self._autocast is not None: - data = self._autocast(data) - return data - - def iterate_ts( - self, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Iterator[tuple[float, T]]: - """Iterate with timestamps (backward compatibility with TimedSensorReplay). - - Args: - seek: Relative seconds from start - duration: Duration window in seconds - from_timestamp: Absolute timestamp to start from - loop: Whether to loop the data - - Yields: - (timestamp, data) tuples - """ - first = self.first_timestamp() - if first is None: - return - - # Calculate start timestamp - start: float | None = None - if from_timestamp is not None: - start = from_timestamp - elif seek is not None: - start = first + seek - - # Calculate end timestamp - end: float | None = None - if duration is not None: - start_ts = start if start is not None else first - end = start_ts + duration - - while True: - yield from self._iter_items(start=start, end=end) - if not loop: - break - - def stream( - self, - speed: float = 1.0, - seek: float | None = None, - duration: float | None = None, - from_timestamp: float | None = None, - loop: bool = False, - ) -> Observable[T]: - """Stream data as Observable with timing control. - - Uses stored timestamps from pickle files for timing (not data.ts). - """ - - def subscribe( - observer: rx.abc.ObserverBase[T], - scheduler: rx.abc.SchedulerBase | None = None, - ) -> rx.abc.DisposableBase: - sched = scheduler or TimeoutScheduler() - disp = CompositeDisposable() - is_disposed = False - - iterator = self.iterate_ts( - seek=seek, duration=duration, from_timestamp=from_timestamp, loop=loop - ) - - try: - first_ts, first_data = next(iterator) - except StopIteration: - observer.on_completed() - return Disposable() - - start_local_time = time.time() - start_replay_time = first_ts - - observer.on_next(first_data) - - try: - next_message: tuple[float, T] | None = next(iterator) - except StopIteration: - observer.on_completed() - return disp - - def schedule_emission(message: tuple[float, T]) -> None: - nonlocal next_message, is_disposed - - if is_disposed: - return - - ts, data = message - - try: - next_message = next(iterator) - except StopIteration: - next_message = None - - target_time = start_local_time + (ts - start_replay_time) / speed - delay = max(0.0, target_time - time.time()) - - def emit( - _scheduler: rx.abc.SchedulerBase, _state: object - ) -> rx.abc.DisposableBase | None: - if is_disposed: - return None - observer.on_next(data) - if next_message is not None: - schedule_emission(next_message) - else: - observer.on_completed() - return None - - sched.schedule_relative(delay, emit) - - if next_message is not None: - schedule_emission(next_message) - - def dispose() -> None: - nonlocal is_disposed - is_disposed = True - disp.dispose() - - return Disposable(dispose) - - return rx.create(subscribe) diff --git a/dimos/memory/timeseries/pickledir.py b/dimos/memory/timeseries/pickledir.py deleted file mode 100644 index 9e8cd5a249..0000000000 --- a/dimos/memory/timeseries/pickledir.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Pickle directory backend for TimeSeriesStore.""" - -import bisect -from collections.abc import Iterator -import glob -import os -from pathlib import Path -import pickle - -from dimos.memory.timeseries.base import T, TimeSeriesStore -from dimos.utils.data import get_data, get_data_dir - - -class PickleDirStore(TimeSeriesStore[T]): - """Pickle directory backend. Files named by timestamp. - - Directory structure: - {name}/ - 1704067200.123.pickle - 1704067200.456.pickle - ... - - Usage: - # Load existing recording (auto-downloads from LFS if needed) - store = PickleDirStore("unitree_go2_bigoffice/lidar") - data = store.find_closest_seek(10.0) - - # Create new recording (directory created on first save) - store = PickleDirStore("my_recording/images") - store.save(image) # saves using image.ts - """ - - def __init__(self, name: str) -> None: - """ - Args: - name: Data directory name (e.g. "unitree_go2_bigoffice/lidar") - """ - self._name = name - self._root_dir: Path | None = None - - # Cached sorted timestamps for find_closest - self._timestamps: list[float] | None = None - - def _get_root_dir(self, for_write: bool = False) -> Path: - """Get root directory, creating on first write if needed.""" - if self._root_dir is not None: - return self._root_dir - - # If absolute path, use directly - if Path(self._name).is_absolute(): - self._root_dir = Path(self._name) - if for_write: - self._root_dir.mkdir(parents=True, exist_ok=True) - elif for_write: - # For writing: use get_data_dir and create if needed - self._root_dir = get_data_dir(self._name) - self._root_dir.mkdir(parents=True, exist_ok=True) - else: - # For reading: use get_data (handles LFS download) - self._root_dir = get_data(self._name) - - return self._root_dir - - def _save(self, timestamp: float, data: T) -> None: - root_dir = self._get_root_dir(for_write=True) - full_path = root_dir / f"{timestamp}.pickle" - - if full_path.exists(): - raise RuntimeError(f"File {full_path} already exists") - - with open(full_path, "wb") as f: - pickle.dump(data, f) - - self._timestamps = None # Invalidate cache - - def _load(self, timestamp: float) -> T | None: - filepath = self._get_root_dir() / f"{timestamp}.pickle" - if filepath.exists(): - return self._load_file(filepath) - return None - - def _delete(self, timestamp: float) -> T | None: - filepath = self._get_root_dir() / f"{timestamp}.pickle" - if filepath.exists(): - data = self._load_file(filepath) - filepath.unlink() - self._timestamps = None # Invalidate cache - return data - return None - - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - for ts in self._get_timestamps(): - if start is not None and ts < start: - continue - if end is not None and ts >= end: - break - data = self._load(ts) - if data is not None: - yield (ts, data) - - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - timestamps = self._get_timestamps() - if not timestamps: - return None - - pos = bisect.bisect_left(timestamps, timestamp) - - # Check neighbors - candidates = [] - if pos > 0: - candidates.append(timestamps[pos - 1]) - if pos < len(timestamps): - candidates.append(timestamps[pos]) - - if not candidates: - return None - - closest = min(candidates, key=lambda ts: abs(ts - timestamp)) - - if tolerance is not None and abs(closest - timestamp) > tolerance: - return None - - return closest - - def _get_timestamps(self) -> list[float]: - """Get sorted list of all timestamps.""" - if self._timestamps is not None: - return self._timestamps - - timestamps: list[float] = [] - root_dir = self._get_root_dir() - for filepath in glob.glob(os.path.join(root_dir, "*.pickle")): - try: - ts = float(Path(filepath).stem) - timestamps.append(ts) - except ValueError: - continue - - timestamps.sort() - self._timestamps = timestamps - return timestamps - - def _count(self) -> int: - return len(self._get_timestamps()) - - def _last_timestamp(self) -> float | None: - timestamps = self._get_timestamps() - return timestamps[-1] if timestamps else None - - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - timestamps = self._get_timestamps() - if not timestamps: - return None - pos = bisect.bisect_left(timestamps, timestamp) - if pos > 0: - ts = timestamps[pos - 1] - data = self._load(ts) - if data is not None: - return (ts, data) - return None - - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - timestamps = self._get_timestamps() - if not timestamps: - return None - pos = bisect.bisect_right(timestamps, timestamp) - if pos < len(timestamps): - ts = timestamps[pos] - data = self._load(ts) - if data is not None: - return (ts, data) - return None - - def _load_file(self, filepath: Path) -> T | None: - """Load data from a pickle file (LRU cached).""" - try: - with open(filepath, "rb") as f: - data: T = pickle.load(f) - return data - except Exception: - return None diff --git a/dimos/memory/timeseries/postgres.py b/dimos/memory/timeseries/postgres.py deleted file mode 100644 index 0daae44adb..0000000000 --- a/dimos/memory/timeseries/postgres.py +++ /dev/null @@ -1,312 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""PostgreSQL backend for TimeSeriesStore.""" - -from collections.abc import Iterator -import pickle -import re - -import psycopg2 # type: ignore[import-untyped] -import psycopg2.extensions # type: ignore[import-untyped] - -from dimos.core.resource import Resource -from dimos.memory.timeseries.base import T, TimeSeriesStore - -# Valid SQL identifier: alphanumeric and underscores, not starting with digit -_VALID_IDENTIFIER = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") - - -def _validate_identifier(name: str) -> str: - """Validate SQL identifier to prevent injection.""" - if not _VALID_IDENTIFIER.match(name): - raise ValueError( - f"Invalid identifier '{name}': must be alphanumeric/underscore, not start with digit" - ) - if len(name) > 128: - raise ValueError(f"Identifier too long: {len(name)} > 128") - return name - - -class PostgresStore(TimeSeriesStore[T], Resource): - """PostgreSQL backend for sensor data. - - Multiple stores can share the same database with different tables. - Implements Resource for lifecycle management (start/stop/dispose). - - Usage: - # Create store - store = PostgresStore("lidar") - store.start() # open connection - - # Use store - store.save(data) # saves using data.ts - data = store.find_closest_seek(10.0) - - # Cleanup - store.stop() # close connection - - # Multiple sensors in same db - lidar = PostgresStore("lidar") - images = PostgresStore("images") - - # Manual run management via table naming - run1_lidar = PostgresStore("run1_lidar") - """ - - def __init__( - self, - table: str, - db: str = "dimensional", - host: str = "localhost", - port: int = 5432, - user: str | None = None, - ) -> None: - """ - Args: - table: Table name for this sensor's data (alphanumeric/underscore only). - db: Database name (alphanumeric/underscore only). - host: PostgreSQL host. - port: PostgreSQL port. - user: PostgreSQL user. Defaults to current system user. - """ - self._table = _validate_identifier(table) - self._db = _validate_identifier(db) - self._host = host - self._port = port - self._user = user - self._conn: psycopg2.extensions.connection | None = None - self._table_created = False - - def start(self) -> None: - """Open database connection.""" - if self._conn is not None: - return - self._conn = psycopg2.connect( - dbname=self._db, - host=self._host, - port=self._port, - user=self._user, - ) - - def stop(self) -> None: - """Close database connection.""" - if self._conn is not None: - self._conn.close() - self._conn = None - - def _get_conn(self) -> psycopg2.extensions.connection: - """Get connection, starting if needed.""" - if self._conn is None: - self.start() - assert self._conn is not None - return self._conn - - def _ensure_table(self) -> None: - """Create table if it doesn't exist.""" - if self._table_created: - return - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute(f""" - CREATE TABLE IF NOT EXISTS {self._table} ( - timestamp DOUBLE PRECISION PRIMARY KEY, - data BYTEA NOT NULL - ) - """) - cur.execute(f""" - CREATE INDEX IF NOT EXISTS idx_{self._table}_ts - ON {self._table}(timestamp) - """) - conn.commit() - self._table_created = True - - def _save(self, timestamp: float, data: T) -> None: - self._ensure_table() - conn = self._get_conn() - blob = pickle.dumps(data) - with conn.cursor() as cur: - cur.execute( - f""" - INSERT INTO {self._table} (timestamp, data) VALUES (%s, %s) - ON CONFLICT (timestamp) DO UPDATE SET data = EXCLUDED.data - """, - (timestamp, psycopg2.Binary(blob)), - ) - conn.commit() - - def _load(self, timestamp: float) -> T | None: - self._ensure_table() - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute(f"SELECT data FROM {self._table} WHERE timestamp = %s", (timestamp,)) - row = cur.fetchone() - if row is None: - return None - data: T = pickle.loads(row[0]) - return data - - def _delete(self, timestamp: float) -> T | None: - data = self._load(timestamp) - if data is not None: - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute(f"DELETE FROM {self._table} WHERE timestamp = %s", (timestamp,)) - conn.commit() - return data - - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - self._ensure_table() - conn = self._get_conn() - - query = f"SELECT timestamp, data FROM {self._table}" - params: list[float] = [] - conditions = [] - - if start is not None: - conditions.append("timestamp >= %s") - params.append(start) - if end is not None: - conditions.append("timestamp < %s") - params.append(end) - - if conditions: - query += " WHERE " + " AND ".join(conditions) - query += " ORDER BY timestamp" - - with conn.cursor() as cur: - cur.execute(query, params) - for row in cur: - ts: float = row[0] - data: T = pickle.loads(row[1]) - yield (ts, data) - - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - self._ensure_table() - conn = self._get_conn() - - with conn.cursor() as cur: - # Get closest timestamp <= target - cur.execute( - f""" - SELECT timestamp FROM {self._table} - WHERE timestamp <= %s - ORDER BY timestamp DESC LIMIT 1 - """, - (timestamp,), - ) - before = cur.fetchone() - - # Get closest timestamp >= target - cur.execute( - f""" - SELECT timestamp FROM {self._table} - WHERE timestamp >= %s - ORDER BY timestamp ASC LIMIT 1 - """, - (timestamp,), - ) - after = cur.fetchone() - - candidates: list[float] = [] - if before: - candidates.append(before[0]) - if after: - candidates.append(after[0]) - - if not candidates: - return None - - closest = min(candidates, key=lambda ts: abs(ts - timestamp)) - - if tolerance is not None and abs(closest - timestamp) > tolerance: - return None - - return closest - - def _count(self) -> int: - self._ensure_table() - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute(f"SELECT COUNT(*) FROM {self._table}") - row = cur.fetchone() - return row[0] if row else 0 # type: ignore[no-any-return] - - def _last_timestamp(self) -> float | None: - self._ensure_table() - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute(f"SELECT MAX(timestamp) FROM {self._table}") - row = cur.fetchone() - if row is None or row[0] is None: - return None - return row[0] # type: ignore[no-any-return] - - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - self._ensure_table() - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute( - f"SELECT timestamp, data FROM {self._table} WHERE timestamp < %s ORDER BY timestamp DESC LIMIT 1", - (timestamp,), - ) - row = cur.fetchone() - if row is None: - return None - return (row[0], pickle.loads(row[1])) - - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - self._ensure_table() - conn = self._get_conn() - with conn.cursor() as cur: - cur.execute( - f"SELECT timestamp, data FROM {self._table} WHERE timestamp > %s ORDER BY timestamp ASC LIMIT 1", - (timestamp,), - ) - row = cur.fetchone() - if row is None: - return None - return (row[0], pickle.loads(row[1])) - - -def reset_db(db: str = "dimensional", host: str = "localhost", port: int = 5432) -> None: - """Drop and recreate database. Simple migration strategy. - - WARNING: This deletes all data in the database! - - Args: - db: Database name to reset (alphanumeric/underscore only). - host: PostgreSQL host. - port: PostgreSQL port. - """ - db = _validate_identifier(db) - # Connect to 'postgres' database to drop/create - conn = psycopg2.connect(dbname="postgres", host=host, port=port) - conn.autocommit = True - with conn.cursor() as cur: - # Terminate existing connections - cur.execute( - """ - SELECT pg_terminate_backend(pid) - FROM pg_stat_activity - WHERE datname = %s AND pid <> pg_backend_pid() - """, - (db,), - ) - cur.execute(f"DROP DATABASE IF EXISTS {db}") - cur.execute(f"CREATE DATABASE {db}") - conn.close() diff --git a/dimos/memory/timeseries/sqlite.py b/dimos/memory/timeseries/sqlite.py deleted file mode 100644 index 6e2ac7a7f5..0000000000 --- a/dimos/memory/timeseries/sqlite.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""SQLite backend for TimeSeriesStore.""" - -from collections.abc import Iterator -from pathlib import Path -import pickle -import re -import sqlite3 - -from dimos.memory.timeseries.base import T, TimeSeriesStore -from dimos.utils.data import get_data, get_data_dir - -# Valid SQL identifier: alphanumeric and underscores, not starting with digit -_VALID_IDENTIFIER = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") - - -def _validate_identifier(name: str) -> str: - """Validate SQL identifier to prevent injection.""" - if not _VALID_IDENTIFIER.match(name): - raise ValueError( - f"Invalid identifier '{name}': must be alphanumeric/underscore, not start with digit" - ) - if len(name) > 128: - raise ValueError(f"Identifier too long: {len(name)} > 128") - return name - - -class SqliteStore(TimeSeriesStore[T]): - """SQLite backend for sensor data. Good for indexed queries and single-file storage. - - Data is stored as pickled BLOBs with timestamp as indexed column. - - Usage: - # Named store (uses data/ directory, auto-downloads from LFS if needed) - store = SqliteStore("recordings/lidar") # -> data/recordings/lidar.db - store.save(data) # saves using data.ts - - # Absolute path - store = SqliteStore("/path/to/sensors.db") - - # In-memory (for testing) - store = SqliteStore(":memory:") - - # Multiple tables in one DB - store = SqliteStore("recordings/sensors", table="lidar") - """ - - def __init__(self, name: str | Path, table: str = "sensor_data") -> None: - """ - Args: - name: Data name (e.g. "recordings/lidar") resolved via get_data, - absolute path, or ":memory:" for in-memory. - table: Table name for this sensor's data (alphanumeric/underscore only). - """ - self._name = str(name) - self._table = _validate_identifier(table) - self._db_path: str | None = None - self._conn: sqlite3.Connection | None = None - - def _get_db_path(self, for_write: bool = False) -> str: - """Get database path, resolving via get_data if needed.""" - if self._db_path is not None: - return self._db_path - - # Special case for in-memory - if self._name == ":memory:": - self._db_path = ":memory:" - return self._db_path - - # If absolute path, use directly - if Path(self._name).is_absolute(): - self._db_path = self._name - elif for_write: - # For writing: use get_data_dir - db_file = get_data_dir(self._name + ".db") - db_file.parent.mkdir(parents=True, exist_ok=True) - self._db_path = str(db_file) - else: - # For reading: use get_data (handles LFS download) - # Try with .db extension first - try: - db_file = get_data(self._name + ".db") - self._db_path = str(db_file) - except FileNotFoundError: - # Fall back to get_data_dir for new databases - db_file = get_data_dir(self._name + ".db") - db_file.parent.mkdir(parents=True, exist_ok=True) - self._db_path = str(db_file) - - return self._db_path - - def _get_conn(self) -> sqlite3.Connection: - """Get or create database connection.""" - if self._conn is None: - db_path = self._get_db_path(for_write=True) - self._conn = sqlite3.connect(db_path, check_same_thread=False) - self._create_table() - return self._conn - - def _create_table(self) -> None: - """Create table if it doesn't exist.""" - conn = self._conn - assert conn is not None - conn.execute(f""" - CREATE TABLE IF NOT EXISTS {self._table} ( - timestamp REAL PRIMARY KEY, - data BLOB NOT NULL - ) - """) - conn.execute(f""" - CREATE INDEX IF NOT EXISTS idx_{self._table}_timestamp - ON {self._table}(timestamp) - """) - conn.commit() - - def _save(self, timestamp: float, data: T) -> None: - conn = self._get_conn() - blob = pickle.dumps(data) - conn.execute( - f"INSERT OR REPLACE INTO {self._table} (timestamp, data) VALUES (?, ?)", - (timestamp, blob), - ) - conn.commit() - - def _load(self, timestamp: float) -> T | None: - conn = self._get_conn() - cursor = conn.execute(f"SELECT data FROM {self._table} WHERE timestamp = ?", (timestamp,)) - row = cursor.fetchone() - if row is None: - return None - data: T = pickle.loads(row[0]) - return data - - def _delete(self, timestamp: float) -> T | None: - data = self._load(timestamp) - if data is not None: - conn = self._get_conn() - conn.execute(f"DELETE FROM {self._table} WHERE timestamp = ?", (timestamp,)) - conn.commit() - return data - - def _iter_items( - self, start: float | None = None, end: float | None = None - ) -> Iterator[tuple[float, T]]: - conn = self._get_conn() - - # Build query with optional range filters - query = f"SELECT timestamp, data FROM {self._table}" - params: list[float] = [] - conditions = [] - - if start is not None: - conditions.append("timestamp >= ?") - params.append(start) - if end is not None: - conditions.append("timestamp < ?") - params.append(end) - - if conditions: - query += " WHERE " + " AND ".join(conditions) - query += " ORDER BY timestamp" - - cursor = conn.execute(query, params) - for row in cursor: - ts: float = row[0] - data: T = pickle.loads(row[1]) - yield (ts, data) - - def _find_closest_timestamp( - self, timestamp: float, tolerance: float | None = None - ) -> float | None: - conn = self._get_conn() - - # Find closest timestamp using SQL - # Get the closest timestamp <= target - cursor = conn.execute( - f""" - SELECT timestamp FROM {self._table} - WHERE timestamp <= ? - ORDER BY timestamp DESC LIMIT 1 - """, - (timestamp,), - ) - before = cursor.fetchone() - - # Get the closest timestamp >= target - cursor = conn.execute( - f""" - SELECT timestamp FROM {self._table} - WHERE timestamp >= ? - ORDER BY timestamp ASC LIMIT 1 - """, - (timestamp,), - ) - after = cursor.fetchone() - - # Find the closest of the two - candidates: list[float] = [] - if before: - candidates.append(before[0]) - if after: - candidates.append(after[0]) - - if not candidates: - return None - - closest = min(candidates, key=lambda ts: abs(ts - timestamp)) - - if tolerance is not None and abs(closest - timestamp) > tolerance: - return None - - return closest - - def _count(self) -> int: - conn = self._get_conn() - cursor = conn.execute(f"SELECT COUNT(*) FROM {self._table}") - return cursor.fetchone()[0] # type: ignore[no-any-return] - - def _last_timestamp(self) -> float | None: - conn = self._get_conn() - cursor = conn.execute(f"SELECT MAX(timestamp) FROM {self._table}") - row = cursor.fetchone() - if row is None or row[0] is None: - return None - return row[0] # type: ignore[no-any-return] - - def _find_before(self, timestamp: float) -> tuple[float, T] | None: - conn = self._get_conn() - cursor = conn.execute( - f"SELECT timestamp, data FROM {self._table} WHERE timestamp < ? ORDER BY timestamp DESC LIMIT 1", - (timestamp,), - ) - row = cursor.fetchone() - if row is None: - return None - return (row[0], pickle.loads(row[1])) - - def _find_after(self, timestamp: float) -> tuple[float, T] | None: - conn = self._get_conn() - cursor = conn.execute( - f"SELECT timestamp, data FROM {self._table} WHERE timestamp > ? ORDER BY timestamp ASC LIMIT 1", - (timestamp,), - ) - row = cursor.fetchone() - if row is None: - return None - return (row[0], pickle.loads(row[1])) - - def close(self) -> None: - """Close the database connection.""" - if self._conn is not None: - self._conn.close() - self._conn = None - - def __del__(self) -> None: - self.close() diff --git a/dimos/memory/timeseries/test_base.py b/dimos/memory/timeseries/test_base.py deleted file mode 100644 index 9491d2c93c..0000000000 --- a/dimos/memory/timeseries/test_base.py +++ /dev/null @@ -1,468 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Tests for TimeSeriesStore implementations.""" - -from dataclasses import dataclass -from pathlib import Path -import tempfile -import uuid - -import pytest - -from dimos.memory.timeseries.base import TimeSeriesStore -from dimos.memory.timeseries.inmemory import InMemoryStore -from dimos.memory.timeseries.legacy import LegacyPickleStore -from dimos.memory.timeseries.pickledir import PickleDirStore -from dimos.memory.timeseries.sqlite import SqliteStore -from dimos.types.timestamped import Timestamped - - -@dataclass -class SampleData(Timestamped): - """Simple timestamped data for testing.""" - - value: str - - def __init__(self, value: str, ts: float) -> None: - super().__init__(ts) - self.value = value - - def __eq__(self, other: object) -> bool: - if isinstance(other, SampleData): - return self.value == other.value and self.ts == other.ts - return False - - -@pytest.fixture -def temp_dir(): - """Create a temporary directory for file-based store tests.""" - with tempfile.TemporaryDirectory() as tmpdir: - yield tmpdir - - -def make_in_memory_store() -> TimeSeriesStore[SampleData]: - return InMemoryStore[SampleData]() - - -def make_pickle_dir_store(tmpdir: str) -> TimeSeriesStore[SampleData]: - return PickleDirStore[SampleData](tmpdir) - - -def make_sqlite_store(tmpdir: str) -> TimeSeriesStore[SampleData]: - return SqliteStore[SampleData](Path(tmpdir) / "test.db") - - -def make_legacy_pickle_store(tmpdir: str) -> TimeSeriesStore[SampleData]: - return LegacyPickleStore[SampleData](Path(tmpdir) / "legacy") - - -# Base test data (always available) -testdata: list[tuple[object, str]] = [ - (lambda _: make_in_memory_store(), "InMemoryStore"), - (lambda tmpdir: make_pickle_dir_store(tmpdir), "PickleDirStore"), - (lambda tmpdir: make_sqlite_store(tmpdir), "SqliteStore"), - (lambda tmpdir: make_legacy_pickle_store(tmpdir), "LegacyPickleStore"), -] - -# Track postgres tables to clean up -_postgres_tables: list[str] = [] - -try: - import psycopg2 - - from dimos.memory.timeseries.postgres import PostgresStore - - # Test connection - _test_conn = psycopg2.connect(dbname="dimensional") - _test_conn.close() - - def make_postgres_store(_tmpdir: str) -> TimeSeriesStore[SampleData]: - """Create PostgresStore with unique table name.""" - table = f"test_{uuid.uuid4().hex[:8]}" - _postgres_tables.append(table) - store = PostgresStore[SampleData](table) - store.start() - return store - - testdata.append((lambda tmpdir: make_postgres_store(tmpdir), "PostgresStore")) - - @pytest.fixture(autouse=True) - def cleanup_postgres_tables(): - """Clean up postgres test tables after each test.""" - yield - if _postgres_tables: - try: - conn = psycopg2.connect(dbname="dimensional") - conn.autocommit = True - with conn.cursor() as cur: - for table in _postgres_tables: - cur.execute(f"DROP TABLE IF EXISTS {table}") - conn.close() - except Exception: - pass # Ignore cleanup errors - _postgres_tables.clear() - -except Exception: - print("PostgreSQL not available") - - -@pytest.mark.parametrize("store_factory,store_name", testdata) -class TestTimeSeriesStore: - """Parametrized tests for all TimeSeriesStore implementations.""" - - def test_save_and_load(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("data_at_1", 1.0)) - store.save(SampleData("data_at_2", 2.0)) - - assert store.load(1.0) == SampleData("data_at_1", 1.0) - assert store.load(2.0) == SampleData("data_at_2", 2.0) - assert store.load(3.0) is None - - def test_find_closest_timestamp(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Exact match - assert store._find_closest_timestamp(2.0) == 2.0 - - # Closest to 1.4 is 1.0 - assert store._find_closest_timestamp(1.4) == 1.0 - - # Closest to 1.6 is 2.0 - assert store._find_closest_timestamp(1.6) == 2.0 - - # With tolerance - assert store._find_closest_timestamp(1.4, tolerance=0.5) == 1.0 - assert store._find_closest_timestamp(1.4, tolerance=0.3) is None - - def test_iter_items(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Should iterate in timestamp order - items = list(store._iter_items()) - assert items == [ - (1.0, SampleData("a", 1.0)), - (2.0, SampleData("b", 2.0)), - (3.0, SampleData("c", 3.0)), - ] - - def test_iter_items_with_range(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save( - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - SampleData("d", 4.0), - ) - - # Start only - items = list(store._iter_items(start=2.0)) - assert items == [ - (2.0, SampleData("b", 2.0)), - (3.0, SampleData("c", 3.0)), - (4.0, SampleData("d", 4.0)), - ] - - # End only - items = list(store._iter_items(end=3.0)) - assert items == [(1.0, SampleData("a", 1.0)), (2.0, SampleData("b", 2.0))] - - # Both - items = list(store._iter_items(start=2.0, end=4.0)) - assert items == [(2.0, SampleData("b", 2.0)), (3.0, SampleData("c", 3.0))] - - def test_empty_store(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - - assert store.load(1.0) is None - assert store._find_closest_timestamp(1.0) is None - assert list(store._iter_items()) == [] - - def test_first_and_first_timestamp(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - - # Empty store - assert store.first() is None - assert store.first_timestamp() is None - - # Add data (in chronological order) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Should return first by timestamp - assert store.first_timestamp() == 1.0 - assert store.first() == SampleData("a", 1.0) - - def test_find_closest(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Exact match - assert store.find_closest(2.0) == SampleData("b", 2.0) - - # Closest to 1.4 is 1.0 - assert store.find_closest(1.4) == SampleData("a", 1.0) - - # Closest to 1.6 is 2.0 - assert store.find_closest(1.6) == SampleData("b", 2.0) - - # With tolerance - assert store.find_closest(1.4, tolerance=0.5) == SampleData("a", 1.0) - assert store.find_closest(1.4, tolerance=0.3) is None - - def test_find_closest_seek(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 10.0), SampleData("b", 11.0), SampleData("c", 12.0)) - - # Seek 0 = first item (10.0) - assert store.find_closest_seek(0.0) == SampleData("a", 10.0) - - # Seek 1.0 = 11.0 - assert store.find_closest_seek(1.0) == SampleData("b", 11.0) - - # Seek 1.4 -> closest to 11.4 is 11.0 - assert store.find_closest_seek(1.4) == SampleData("b", 11.0) - - # Seek 1.6 -> closest to 11.6 is 12.0 - assert store.find_closest_seek(1.6) == SampleData("c", 12.0) - - # With tolerance - assert store.find_closest_seek(1.4, tolerance=0.5) == SampleData("b", 11.0) - assert store.find_closest_seek(1.4, tolerance=0.3) is None - - def test_iterate(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Should iterate in timestamp order, returning data only (not tuples) - items = list(store.iterate()) - assert items == [ - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ] - - def test_iterate_with_seek_and_duration(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save( - SampleData("a", 10.0), - SampleData("b", 11.0), - SampleData("c", 12.0), - SampleData("d", 13.0), - ) - - # Seek from start - items = list(store.iterate(seek=1.0)) - assert items == [ - SampleData("b", 11.0), - SampleData("c", 12.0), - SampleData("d", 13.0), - ] - - # Duration - items = list(store.iterate(duration=2.0)) - assert items == [SampleData("a", 10.0), SampleData("b", 11.0)] - - # Seek + duration - items = list(store.iterate(seek=1.0, duration=2.0)) - assert items == [SampleData("b", 11.0), SampleData("c", 12.0)] - - # from_timestamp - items = list(store.iterate(from_timestamp=12.0)) - assert items == [SampleData("c", 12.0), SampleData("d", 13.0)] - - def test_variadic_save(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - - # Save multiple items at once - store.save( - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ) - - assert store.load(1.0) == SampleData("a", 1.0) - assert store.load(2.0) == SampleData("b", 2.0) - assert store.load(3.0) == SampleData("c", 3.0) - - def test_pipe_save(self, store_factory, store_name, temp_dir): - import reactivex as rx - - store = store_factory(temp_dir) - - # Create observable with test data - source = rx.of( - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ) - - # Pipe through store.pipe_save — should save and pass through - results: list[SampleData] = [] - source.pipe(store.pipe_save).subscribe(results.append) - - # Data should be saved - assert store.load(1.0) == SampleData("a", 1.0) - assert store.load(2.0) == SampleData("b", 2.0) - assert store.load(3.0) == SampleData("c", 3.0) - - # Data should also pass through - assert results == [ - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ] - - def test_consume_stream(self, store_factory, store_name, temp_dir): - import reactivex as rx - - store = store_factory(temp_dir) - - # Create observable with test data - source = rx.of( - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ) - - # Consume stream — should save all items - disposable = store.consume_stream(source) - - # Data should be saved - assert store.load(1.0) == SampleData("a", 1.0) - assert store.load(2.0) == SampleData("b", 2.0) - assert store.load(3.0) == SampleData("c", 3.0) - - disposable.dispose() - - def test_iterate_items(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - items = list(store.iterate_items()) - assert items == [ - (1.0, SampleData("a", 1.0)), - (2.0, SampleData("b", 2.0)), - (3.0, SampleData("c", 3.0)), - ] - - # With seek - items = list(store.iterate_items(seek=1.0)) - assert len(items) == 2 - assert items[0] == (2.0, SampleData("b", 2.0)) - - def test_stream_basic(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - # Stream at high speed (essentially instant) - results: list[SampleData] = [] - store.stream(speed=1000.0).subscribe( - on_next=results.append, - on_completed=lambda: None, - ) - - # Give it a moment to complete - import time - - time.sleep(0.1) - - assert results == [ - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - ] - - -@pytest.mark.parametrize("store_factory,store_name", testdata) -class TestCollectionAPI: - """Test new collection API methods on all backends.""" - - def test_len(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert len(store) == 0 - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - assert len(store) == 3 - - def test_iter(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0)) - items = list(store) - assert items == [SampleData("a", 1.0), SampleData("b", 2.0)] - - def test_last_timestamp(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert store.last_timestamp() is None - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - assert store.last_timestamp() == 3.0 - - def test_last(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert store.last() is None - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - assert store.last() == SampleData("c", 3.0) - - def test_start_end_ts(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert store.start_ts is None - assert store.end_ts is None - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - assert store.start_ts == 1.0 - assert store.end_ts == 3.0 - - def test_time_range(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert store.time_range() is None - store.save(SampleData("a", 1.0), SampleData("b", 5.0)) - assert store.time_range() == (1.0, 5.0) - - def test_duration(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - assert store.duration() == 0.0 - store.save(SampleData("a", 1.0), SampleData("b", 5.0)) - assert store.duration() == 4.0 - - def test_find_before(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - assert store.find_before(0.5) is None - assert store.find_before(1.0) is None # strictly before - assert store.find_before(1.5) == SampleData("a", 1.0) - assert store.find_before(2.5) == SampleData("b", 2.0) - assert store.find_before(10.0) == SampleData("c", 3.0) - - def test_find_after(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save(SampleData("a", 1.0), SampleData("b", 2.0), SampleData("c", 3.0)) - - assert store.find_after(0.5) == SampleData("a", 1.0) - assert store.find_after(1.0) == SampleData("b", 2.0) # strictly after - assert store.find_after(2.5) == SampleData("c", 3.0) - assert store.find_after(3.0) is None # strictly after - assert store.find_after(10.0) is None - - def test_slice_by_time(self, store_factory, store_name, temp_dir): - store = store_factory(temp_dir) - store.save( - SampleData("a", 1.0), - SampleData("b", 2.0), - SampleData("c", 3.0), - SampleData("d", 4.0), - ) - - # [2.0, 4.0) should include b, c - result = store.slice_by_time(2.0, 4.0) - assert result == [SampleData("b", 2.0), SampleData("c", 3.0)] diff --git a/dimos/memory/timeseries/test_legacy.py b/dimos/memory/timeseries/test_legacy.py deleted file mode 100644 index aaad962a95..0000000000 --- a/dimos/memory/timeseries/test_legacy.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Tests specific to LegacyPickleStore.""" - -from dimos.memory.timeseries.legacy import LegacyPickleStore - - -class TestLegacyPickleStoreRealData: - """Test LegacyPickleStore with real recorded data.""" - - def test_read_lidar_recording(self) -> None: - """Test reading from unitree_go2_bigoffice/lidar recording.""" - store = LegacyPickleStore("unitree_go2_bigoffice/lidar") - - # Check first timestamp exists - first_ts = store.first_timestamp() - assert first_ts is not None - assert first_ts > 0 - - # Check first data - first = store.first() - assert first is not None - assert hasattr(first, "ts") - - # Check find_closest_seek works - data_at_10s = store.find_closest_seek(10.0) - assert data_at_10s is not None - - # Check iteration returns monotonically increasing timestamps - prev_ts = None - for i, item in enumerate(store.iterate()): - assert item.ts is not None - if prev_ts is not None: - assert item.ts >= prev_ts, "Timestamps should be monotonically increasing" - prev_ts = item.ts - if i >= 10: # Only check first 10 items - break diff --git a/dimos/models/__init__.py b/dimos/models/__init__.py deleted file mode 100644 index d8e2e14341..0000000000 --- a/dimos/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from dimos.models.base import HuggingFaceModel, LocalModel - -__all__ = ["HuggingFaceModel", "LocalModel"] diff --git a/dimos/models/base.py b/dimos/models/base.py deleted file mode 100644 index 2269a6d0b8..0000000000 --- a/dimos/models/base.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base classes for local GPU models.""" - -from __future__ import annotations - -from dataclasses import dataclass -from functools import cached_property -from typing import Annotated, Any - -import torch - -from dimos.core.resource import Resource -from dimos.protocol.service import Configurable # type: ignore[attr-defined] - -# Device string type - 'cuda', 'cpu', 'cuda:0', 'cuda:1', etc. -DeviceType = Annotated[str, "Device identifier (e.g., 'cuda', 'cpu', 'cuda:0')"] - - -@dataclass -class LocalModelConfig: - device: DeviceType = "cuda" if torch.cuda.is_available() else "cpu" - dtype: torch.dtype = torch.float32 - warmup: bool = False - autostart: bool = False - - -class LocalModel(Resource, Configurable[LocalModelConfig]): - """Base class for all local GPU/CPU models. - - Implements Resource interface for lifecycle management. - - Subclasses MUST override: - - _model: @cached_property that loads and returns the model - - Subclasses MAY override: - - start() for custom initialization logic - - stop() for custom cleanup logic - """ - - default_config = LocalModelConfig - config: LocalModelConfig - - def __init__(self, **kwargs: object) -> None: - """Initialize local model with device and dtype configuration. - - Args: - device: Device to run on ('cuda', 'cpu', 'cuda:0', etc.). - Auto-detects CUDA availability if None. - dtype: Model dtype (torch.float16, torch.bfloat16, etc.). - Uses class _default_dtype if None. - autostart: If True, immediately load the model. - If False (default), model loads lazily on first use. - """ - super().__init__(**kwargs) - if self.config.warmup or self.config.autostart: - self.start() - - @property - def device(self) -> str: - """The device this model runs on.""" - return self.config.device - - @property - def dtype(self) -> torch.dtype: - """The dtype used by this model.""" - return self.config.dtype - - @cached_property - def _model(self) -> Any: - """Lazily loaded model. Subclasses must override this property.""" - raise NotImplementedError(f"{self.__class__.__name__} must override _model property") - - def start(self) -> None: - """Load the model (Resource interface). - - Subclasses should override to add custom initialization. - """ - _ = self._model - - def stop(self) -> None: - """Release model and free GPU memory (Resource interface). - - Subclasses should override and call super().stop() for custom cleanup. - """ - import gc - - if "_model" in self.__dict__: - del self.__dict__["_model"] - - # Reset torch.compile caches to free memory from compiled models - # See: https://github.com/pytorch/pytorch/issues/105181 - try: - import torch._dynamo - - torch._dynamo.reset() - except (ImportError, AttributeError): - pass - - gc.collect() - if self.config.device.startswith("cuda") and torch.cuda.is_available(): - torch.cuda.empty_cache() - - def _ensure_cuda_initialized(self) -> None: - """Initialize CUDA context to prevent cuBLAS allocation failures. - - Some models (CLIP, TorchReID) fail if they are the first to use CUDA. - Call this before model loading if needed. - """ - if self.config.device.startswith("cuda") and torch.cuda.is_available(): - try: - _ = torch.zeros(1, 1, device="cuda") @ torch.zeros(1, 1, device="cuda") - torch.cuda.synchronize() - except Exception: - pass - - -@dataclass -class HuggingFaceModelConfig(LocalModelConfig): - model_name: str = "" - trust_remote_code: bool = True - dtype: torch.dtype = torch.float16 - - -class HuggingFaceModel(LocalModel): - """Base class for HuggingFace transformers-based models. - - Provides common patterns for loading models from the HuggingFace Hub - using from_pretrained(). - - Subclasses SHOULD set: - - _model_class: The AutoModel class to use (e.g., AutoModelForCausalLM) - - Subclasses MAY override: - - _model: @cached_property for custom model loading - """ - - default_config = HuggingFaceModelConfig - config: HuggingFaceModelConfig - _model_class: Any = None # e.g., AutoModelForCausalLM - - @property - def model_name(self) -> str: - """The HuggingFace model identifier.""" - return self.config.model_name - - @cached_property - def _model(self) -> Any: - """Load the HuggingFace model using _model_class. - - Override this property for custom loading logic. - """ - if self._model_class is None: - raise NotImplementedError( - f"{self.__class__.__name__} must set _model_class or override _model property" - ) - model = self._model_class.from_pretrained( - self.config.model_name, - trust_remote_code=self.config.trust_remote_code, - torch_dtype=self.config.dtype, - ) - return model.to(self.config.device) - - def _move_inputs_to_device( - self, - inputs: dict[str, torch.Tensor], - apply_dtype: bool = True, - ) -> dict[str, torch.Tensor]: - """Move input tensors to model device with appropriate dtype. - - Args: - inputs: Dictionary of input tensors - apply_dtype: Whether to apply model dtype to floating point tensors - - Returns: - Dictionary with tensors moved to device - """ - result = {} - for k, v in inputs.items(): - if isinstance(v, torch.Tensor): - if apply_dtype and v.is_floating_point(): - result[k] = v.to(self.config.device, dtype=self.config.dtype) - else: - result[k] = v.to(self.config.device) - else: - result[k] = v - return result diff --git a/dimos/models/depth/__init__.py b/dimos/models/depth/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/models/depth/metric3d.py b/dimos/models/depth/metric3d.py deleted file mode 100644 index a668ea321e..0000000000 --- a/dimos/models/depth/metric3d.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass, field -from functools import cached_property -from typing import Any - -import cv2 -import torch - -from dimos.models.base import LocalModel, LocalModelConfig - - -@dataclass -class Metric3DConfig(LocalModelConfig): - """Configuration for Metric3D depth estimation model.""" - - camera_intrinsics: list[float] = field(default_factory=lambda: [500.0, 500.0, 320.0, 240.0]) - """Camera intrinsics [fx, fy, cx, cy].""" - - gt_depth_scale: float = 256.0 - """Scale factor for ground truth depth.""" - - device: str = "cuda" if torch.cuda.is_available() else "cpu" - """Device to run the model on.""" - - -class Metric3D(LocalModel): - default_config = Metric3DConfig - config: Metric3DConfig - - def __init__(self, **kwargs: object) -> None: - super().__init__(**kwargs) - self.intrinsic = self.config.camera_intrinsics - self.intrinsic_scaled: list[float] | None = None - self.gt_depth_scale = self.config.gt_depth_scale - self.pad_info: list[int] | None = None - self.rgb_origin: Any = None - - @cached_property - def _model(self) -> Any: - model = torch.hub.load( # type: ignore[no-untyped-call] - "yvanyin/metric3d", "metric3d_vit_small", pretrain=True - ) - model = model.to(self.device) - model.eval() - return model - - """ - Input: Single image in RGB format - Output: Depth map - """ - - def update_intrinsic(self, intrinsic): # type: ignore[no-untyped-def] - """ - Update the intrinsic parameters dynamically. - Ensure that the input intrinsic is valid. - """ - if len(intrinsic) != 4: - raise ValueError("Intrinsic must be a list or tuple with 4 values: [fx, fy, cx, cy]") - self.intrinsic = intrinsic - print(f"Intrinsics updated to: {self.intrinsic}") - - def infer_depth(self, img, debug: bool = False): # type: ignore[no-untyped-def] - if debug: - print(f"Input image: {img}") - try: - if isinstance(img, str): - print(f"Image type string: {type(img)}") - img_data = cv2.imread(img) - if img_data is None: - raise ValueError(f"Failed to load image from {img}") - self.rgb_origin = img_data[:, :, ::-1] - else: - # print(f"Image type not string: {type(img)}, cv2 conversion assumed to be handled. If not, this will throw an error") - self.rgb_origin = img - except Exception as e: - print(f"Error parsing into infer_depth: {e}") - - img = self.rescale_input(img, self.rgb_origin) # type: ignore[no-untyped-call] - - with torch.no_grad(): - pred_depth, confidence, output_dict = self._model.inference({"input": img}) - - # Convert to PIL format - depth_image = self.unpad_transform_depth(pred_depth) # type: ignore[no-untyped-call] - - return depth_image.cpu().numpy() - - def save_depth(self, pred_depth) -> None: # type: ignore[no-untyped-def] - # Save the depth map to a file - pred_depth_np = pred_depth.cpu().numpy() - output_depth_file = "output_depth_map.png" - cv2.imwrite(output_depth_file, pred_depth_np) - print(f"Depth map saved to {output_depth_file}") - - # Adjusts input size to fit pretrained ViT model - def rescale_input(self, rgb, rgb_origin): # type: ignore[no-untyped-def] - #### ajust input size to fit pretrained model - # keep ratio resize - input_size = (616, 1064) # for vit model - # input_size = (544, 1216) # for convnext model - h, w = rgb_origin.shape[:2] - scale = min(input_size[0] / h, input_size[1] / w) - rgb = cv2.resize( - rgb_origin, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_LINEAR - ) - # remember to scale intrinsic, hold depth - self.intrinsic_scaled = [ - self.intrinsic[0] * scale, - self.intrinsic[1] * scale, - self.intrinsic[2] * scale, - self.intrinsic[3] * scale, - ] - # padding to input_size - padding = [123.675, 116.28, 103.53] - h, w = rgb.shape[:2] - pad_h = input_size[0] - h - pad_w = input_size[1] - w - pad_h_half = pad_h // 2 - pad_w_half = pad_w // 2 - rgb = cv2.copyMakeBorder( - rgb, - pad_h_half, - pad_h - pad_h_half, - pad_w_half, - pad_w - pad_w_half, - cv2.BORDER_CONSTANT, - value=padding, - ) - self.pad_info = [pad_h_half, pad_h - pad_h_half, pad_w_half, pad_w - pad_w_half] - - #### normalize - mean = torch.tensor([123.675, 116.28, 103.53]).float()[:, None, None] - std = torch.tensor([58.395, 57.12, 57.375]).float()[:, None, None] - rgb = torch.from_numpy(rgb.transpose((2, 0, 1))).float() - rgb = torch.div((rgb - mean), std) - rgb = rgb[None, :, :, :].to(self.device) - return rgb - - def unpad_transform_depth(self, pred_depth): # type: ignore[no-untyped-def] - # un pad - pred_depth = pred_depth.squeeze() - pred_depth = pred_depth[ - self.pad_info[0] : pred_depth.shape[0] - self.pad_info[1], # type: ignore[index] - self.pad_info[2] : pred_depth.shape[1] - self.pad_info[3], # type: ignore[index] - ] - - # upsample to original size - pred_depth = torch.nn.functional.interpolate( - pred_depth[None, None, :, :], - self.rgb_origin.shape[:2], - mode="bilinear", - ).squeeze() - ###################### canonical camera space ###################### - - #### de-canonical transform - canonical_to_real_scale = ( - self.intrinsic_scaled[0] / 1000.0 # type: ignore[index] - ) # 1000.0 is the focal length of canonical camera - pred_depth = pred_depth * canonical_to_real_scale # now the depth is metric - pred_depth = torch.clamp(pred_depth, 0, 1000) - return pred_depth - - def eval_predicted_depth(self, depth_file, pred_depth) -> None: # type: ignore[no-untyped-def] - if depth_file is not None: - gt_depth_np = cv2.imread(depth_file, -1) - if gt_depth_np is None: - raise ValueError(f"Failed to load depth file from {depth_file}") - gt_depth_scaled = gt_depth_np / self.gt_depth_scale - gt_depth = torch.from_numpy(gt_depth_scaled).float().to(self.device) - assert gt_depth.shape == pred_depth.shape - - mask = gt_depth > 1e-8 # type: ignore[operator] - abs_rel_err = (torch.abs(pred_depth[mask] - gt_depth[mask]) / gt_depth[mask]).mean() # type: ignore[index] - print("abs_rel_err:", abs_rel_err.item()) diff --git a/dimos/models/depth/test_metric3d.py b/dimos/models/depth/test_metric3d.py deleted file mode 100644 index 33e39f6a29..0000000000 --- a/dimos/models/depth/test_metric3d.py +++ /dev/null @@ -1,102 +0,0 @@ -from contextlib import contextmanager - -import numpy as np -import pytest - -from dimos.models.depth.metric3d import Metric3D -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -@contextmanager -def skip_xformers_unsupported(): - try: - yield - except NotImplementedError as e: - if "memory_efficient_attention" in str(e): - pytest.skip(f"xformers not supported on this GPU: {e}") - raise - - -@pytest.fixture -def sample_intrinsics() -> list[float]: - """Sample camera intrinsics [fx, fy, cx, cy].""" - return [500.0, 500.0, 320.0, 240.0] - -@pytest.mark.cuda -@pytest.mark.gpu -def test_metric3d_init(sample_intrinsics: list[float]) -> None: - """Test Metric3D initialization.""" - model = Metric3D(camera_intrinsics=sample_intrinsics) - assert model.config.camera_intrinsics == sample_intrinsics - assert model.config.gt_depth_scale == 256.0 - assert model.device == "cuda" - - -@pytest.mark.gpu -def test_metric3d_update_intrinsic(sample_intrinsics: list[float]) -> None: - """Test updating camera intrinsics.""" - model = Metric3D(camera_intrinsics=sample_intrinsics) - - new_intrinsics = [600.0, 600.0, 400.0, 300.0] - model.update_intrinsic(new_intrinsics) - assert model.intrinsic == new_intrinsics - -@pytest.mark.gpu -def test_metric3d_update_intrinsic_invalid(sample_intrinsics: list[float]) -> None: - """Test that invalid intrinsics raise an error.""" - model = Metric3D(camera_intrinsics=sample_intrinsics) - - with pytest.raises(ValueError, match="Intrinsic must be a list"): - model.update_intrinsic([1.0, 2.0]) # Only 2 values - - -@pytest.mark.cuda -@pytest.mark.gpu -def test_metric3d_infer_depth(sample_intrinsics: list[float]) -> None: - """Test depth inference on a sample image.""" - model = Metric3D(camera_intrinsics=sample_intrinsics) - model.start() - - # Load test image - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - rgb_array = image.data - - # Run inference - with skip_xformers_unsupported(): - depth_map = model.infer_depth(rgb_array) - - # Verify output - assert isinstance(depth_map, np.ndarray) - assert depth_map.shape[:2] == rgb_array.shape[:2] # Same spatial dimensions - assert depth_map.dtype in [np.float32, np.float64] - assert depth_map.min() >= 0 # Depth should be non-negative - - print(f"Depth map shape: {depth_map.shape}") - print(f"Depth range: [{depth_map.min():.2f}, {depth_map.max():.2f}]") - - model.stop() - - -@pytest.mark.cuda -@pytest.mark.gpu -def test_metric3d_multiple_inferences(sample_intrinsics: list[float]) -> None: - """Test multiple depth inferences.""" - model = Metric3D(camera_intrinsics=sample_intrinsics) - model.start() - - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - rgb_array = image.data - - # Run multiple inferences - depths = [] - for _ in range(3): - with skip_xformers_unsupported(): - depth = model.infer_depth(rgb_array) - depths.append(depth) - - # Results should be consistent - for i in range(1, len(depths)): - assert np.allclose(depths[0], depths[i], rtol=1e-5) - - model.stop() diff --git a/dimos/models/embedding/__init__.py b/dimos/models/embedding/__init__.py deleted file mode 100644 index 050d35467e..0000000000 --- a/dimos/models/embedding/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -from dimos.models.embedding.base import Embedding, EmbeddingModel - -__all__ = [ - "Embedding", - "EmbeddingModel", -] - -# Optional: CLIP support -try: - from dimos.models.embedding.clip import CLIPModel - - __all__.append("CLIPModel") -except ImportError: - pass - -# Optional: MobileCLIP support -try: - from dimos.models.embedding.mobileclip import MobileCLIPModel - - __all__.append("MobileCLIPModel") -except ImportError: - pass - -# Optional: TorchReID support -try: - from dimos.models.embedding.treid import TorchReIDModel - - __all__.append("TorchReIDModel") -except ImportError: - pass diff --git a/dimos/models/embedding/base.py b/dimos/models/embedding/base.py deleted file mode 100644 index c6b78fcf2c..0000000000 --- a/dimos/models/embedding/base.py +++ /dev/null @@ -1,165 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from dataclasses import dataclass -import time -from typing import TYPE_CHECKING - -import numpy as np -import torch - -from dimos.models.base import HuggingFaceModelConfig, LocalModelConfig -from dimos.types.timestamped import Timestamped - -if TYPE_CHECKING: - from dimos.msgs.sensor_msgs import Image - - -@dataclass -class EmbeddingModelConfig(LocalModelConfig): - """Base config for embedding models.""" - - normalize: bool = True - - -@dataclass -class HuggingFaceEmbeddingModelConfig(HuggingFaceModelConfig): - """Base config for HuggingFace-based embedding models.""" - - normalize: bool = True - - -class Embedding(Timestamped): - """Base class for embeddings with vector data. - - Supports both torch.Tensor (for GPU-accelerated comparisons) and np.ndarray. - Embeddings are kept as torch.Tensor on device by default for efficiency. - """ - - vector: torch.Tensor | np.ndarray # type: ignore[type-arg] - - def __init__(self, vector: torch.Tensor | np.ndarray, timestamp: float | None = None) -> None: # type: ignore[type-arg] - self.vector = vector - if timestamp: - self.timestamp = timestamp - else: - self.timestamp = time.time() - - def __matmul__(self, other: Embedding) -> float: - """Compute cosine similarity via @ operator.""" - if isinstance(self.vector, torch.Tensor): - other_tensor = other.to_torch(self.vector.device) - result = self.vector @ other_tensor - return result.item() - return float(self.vector @ other.to_numpy()) - - def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] - """Convert to numpy array (moves to CPU if needed).""" - if isinstance(self.vector, torch.Tensor): - return self.vector.detach().cpu().numpy() - return self.vector - - def to_torch(self, device: str | torch.device | None = None) -> torch.Tensor: - """Convert to torch tensor on specified device.""" - if isinstance(self.vector, np.ndarray): - tensor = torch.from_numpy(self.vector) - return tensor.to(device) if device else tensor - - if device is not None and self.vector.device != torch.device(device): - return self.vector.to(device) - return self.vector - - def to_cpu(self) -> Embedding: - """Move embedding to CPU, returning self for chaining.""" - if isinstance(self.vector, torch.Tensor): - self.vector = self.vector.cpu() - return self - - -class EmbeddingModel(ABC): - """Abstract base class for embedding models supporting vision and language.""" - - device: str - - @abstractmethod - def embed(self, *images: Image) -> Embedding | list[Embedding]: - """ - Embed one or more images. - Returns single Embedding if one image, list if multiple. - """ - pass - - @abstractmethod - def embed_text(self, *texts: str) -> Embedding | list[Embedding]: - """ - Embed one or more text strings. - Returns single Embedding if one text, list if multiple. - """ - pass - - def compare_one_to_many(self, query: Embedding, candidates: list[Embedding]) -> torch.Tensor: - """ - Efficiently compare one query against many candidates on GPU. - - Args: - query: Query embedding - candidates: List of candidate embeddings - - Returns: - torch.Tensor of similarities (N,) - """ - query_tensor = query.to_torch(self.device) - candidate_tensors = torch.stack([c.to_torch(self.device) for c in candidates]) - return query_tensor @ candidate_tensors.T - - def compare_many_to_many( - self, queries: list[Embedding], candidates: list[Embedding] - ) -> torch.Tensor: - """ - Efficiently compare all queries against all candidates on GPU. - - Args: - queries: List of query embeddings - candidates: List of candidate embeddings - - Returns: - torch.Tensor of similarities (M, N) where M=len(queries), N=len(candidates) - """ - query_tensors = torch.stack([q.to_torch(self.device) for q in queries]) - candidate_tensors = torch.stack([c.to_torch(self.device) for c in candidates]) - return query_tensors @ candidate_tensors.T - - def query( - self, query_emb: Embedding, candidates: list[Embedding], top_k: int = 5 - ) -> list[tuple[int, float]]: - """ - Find top-k most similar candidates to query (GPU accelerated). - - Args: - query_emb: Query embedding - candidates: List of candidate embeddings - top_k: Number of top results to return - - Returns: - List of (index, similarity) tuples sorted by similarity (descending) - """ - similarities = self.compare_one_to_many(query_emb, candidates) - top_values, top_indices = similarities.topk(k=min(top_k, len(candidates))) - return [(idx.item(), val.item()) for idx, val in zip(top_indices, top_values, strict=False)] - - - ... diff --git a/dimos/models/embedding/clip.py b/dimos/models/embedding/clip.py deleted file mode 100644 index 1b8d3e68bb..0000000000 --- a/dimos/models/embedding/clip.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -from functools import cached_property - -from PIL import Image as PILImage -import torch -import torch.nn.functional as functional -from transformers import CLIPModel as HFCLIPModel, CLIPProcessor # type: ignore[import-untyped] - -from dimos.models.base import HuggingFaceModel -from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig -from dimos.msgs.sensor_msgs import Image - - -@dataclass -class CLIPModelConfig(HuggingFaceEmbeddingModelConfig): - model_name: str = "openai/clip-vit-base-patch32" - dtype: torch.dtype = torch.float32 - - -class CLIPModel(EmbeddingModel, HuggingFaceModel): - """CLIP embedding model for vision-language re-identification.""" - - default_config = CLIPModelConfig - config: CLIPModelConfig - _model_class = HFCLIPModel - - @cached_property - def _model(self) -> HFCLIPModel: - self._ensure_cuda_initialized() - return HFCLIPModel.from_pretrained(self.config.model_name).eval().to(self.config.device) - - @cached_property - def _processor(self) -> CLIPProcessor: - return CLIPProcessor.from_pretrained(self.config.model_name) - - def embed(self, *images: Image) -> Embedding | list[Embedding]: - """Embed one or more images. - - Returns embeddings as torch.Tensor on device for efficient GPU comparisons. - """ - # Convert to PIL images - pil_images = [PILImage.fromarray(img.to_opencv()) for img in images] - - # Process images - with torch.inference_mode(): - inputs = self._processor(images=pil_images, return_tensors="pt").to(self.config.device) - image_features = self._model.get_image_features(**inputs) - - if self.config.normalize: - image_features = functional.normalize(image_features, dim=-1) - - # Create embeddings (keep as torch.Tensor on device) - embeddings: list[Embedding] = [] - for i, feat in enumerate(image_features): - timestamp = images[i].ts - embeddings.append(Embedding(vector=feat, timestamp=timestamp)) - - return embeddings[0] if len(images) == 1 else embeddings - - def embed_text(self, *texts: str) -> Embedding | list[Embedding]: - """Embed one or more text strings. - - Returns embeddings as torch.Tensor on device for efficient GPU comparisons. - """ - with torch.inference_mode(): - inputs = self._processor(text=list(texts), return_tensors="pt", padding=True).to( - self.config.device - ) - text_features = self._model.get_text_features(**inputs) - - if self.config.normalize: - text_features = functional.normalize(text_features, dim=-1) - - # Create embeddings (keep as torch.Tensor on device) - embeddings: list[Embedding] = [] - for feat in text_features: - embeddings.append(Embedding(vector=feat)) - - return embeddings[0] if len(texts) == 1 else embeddings - - def start(self) -> None: - """Start the model with a dummy forward pass.""" - super().start() - - dummy_image = torch.randn(1, 3, 224, 224).to(self.config.device) - dummy_text_inputs = self._processor(text=["warmup"], return_tensors="pt", padding=True).to( - self.config.device - ) - - with torch.inference_mode(): - self._model.get_image_features(pixel_values=dummy_image) - self._model.get_text_features(**dummy_text_inputs) - - def stop(self) -> None: - """Release model and free GPU memory.""" - if "_processor" in self.__dict__: - del self.__dict__["_processor"] - super().stop() diff --git a/dimos/models/embedding/mobileclip.py b/dimos/models/embedding/mobileclip.py deleted file mode 100644 index c02361b367..0000000000 --- a/dimos/models/embedding/mobileclip.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -from functools import cached_property -from typing import Any - -import open_clip -from PIL import Image as PILImage -import torch -import torch.nn.functional as F - -from dimos.models.base import LocalModel -from dimos.models.embedding.base import Embedding, EmbeddingModel, EmbeddingModelConfig -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -@dataclass -class MobileCLIPModelConfig(EmbeddingModelConfig): - model_name: str = "MobileCLIP2-S4" - - -class MobileCLIPModel(EmbeddingModel, LocalModel): - """MobileCLIP embedding model for vision-language re-identification.""" - - default_config = MobileCLIPModelConfig - config: MobileCLIPModelConfig - - @cached_property - def _model_and_preprocess(self) -> tuple[Any, Any]: - """Load model and transforms (open_clip returns them together).""" - model_path = get_data("models_mobileclip") / (self.config.model_name + ".pt") - model, _, preprocess = open_clip.create_model_and_transforms( - self.config.model_name, pretrained=str(model_path) - ) - return model.eval().to(self.config.device), preprocess - - @cached_property - def _model(self) -> Any: - return self._model_and_preprocess[0] - - @cached_property - def _preprocess(self) -> Any: - return self._model_and_preprocess[1] - - @cached_property - def _tokenizer(self) -> Any: - return open_clip.get_tokenizer(self.config.model_name) - - def embed(self, *images: Image) -> Embedding | list[Embedding]: - """Embed one or more images. - - Returns embeddings as torch.Tensor on device for efficient GPU comparisons. - """ - # Convert to PIL images - pil_images = [PILImage.fromarray(img.to_opencv()) for img in images] - - # Preprocess and batch - with torch.inference_mode(): - batch = torch.stack([self._preprocess(img) for img in pil_images]).to( - self.config.device - ) - feats = self._model.encode_image(batch) - if self.config.normalize: - feats = F.normalize(feats, dim=-1) - - # Create embeddings (keep as torch.Tensor on device) - embeddings = [] - for i, feat in enumerate(feats): - timestamp = images[i].ts - embeddings.append(Embedding(vector=feat, timestamp=timestamp)) - - return embeddings[0] if len(images) == 1 else embeddings - - def embed_text(self, *texts: str) -> Embedding | list[Embedding]: - """Embed one or more text strings. - - Returns embeddings as torch.Tensor on device for efficient GPU comparisons. - """ - with torch.inference_mode(): - text_tokens = self._tokenizer(list(texts)).to(self.config.device) - feats = self._model.encode_text(text_tokens) - if self.config.normalize: - feats = F.normalize(feats, dim=-1) - - # Create embeddings (keep as torch.Tensor on device) - embeddings = [] - for feat in feats: - embeddings.append(Embedding(vector=feat)) - - return embeddings[0] if len(texts) == 1 else embeddings - - def start(self) -> None: - """Start the model with a dummy forward pass.""" - super().start() - dummy_image = torch.randn(1, 3, 224, 224).to(self.config.device) - dummy_text = self._tokenizer(["warmup"]).to(self.config.device) - with torch.inference_mode(): - self._model.encode_image(dummy_image) - self._model.encode_text(dummy_text) - - def stop(self) -> None: - """Release model and free GPU memory.""" - for attr in ("_model_and_preprocess", "_model", "_preprocess", "_tokenizer"): - if attr in self.__dict__: - del self.__dict__[attr] - super().stop() diff --git a/dimos/models/embedding/test_embedding.py b/dimos/models/embedding/test_embedding.py deleted file mode 100644 index a87a2f5a57..0000000000 --- a/dimos/models/embedding/test_embedding.py +++ /dev/null @@ -1,152 +0,0 @@ -import time -from typing import Any - -import pytest -import torch - -from dimos.models.embedding.clip import CLIPModel -from dimos.models.embedding.mobileclip import MobileCLIPModel -from dimos.models.embedding.treid import TorchReIDModel -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -@pytest.mark.parametrize( - "model_class,model_name,supports_text", - [ - (CLIPModel, "CLIP", True), - pytest.param(MobileCLIPModel, "MobileCLIP", True), - (TorchReIDModel, "TorchReID", False), - ], - ids=["clip", "mobileclip", "treid"], -) -@pytest.mark.gpu -def test_embedding_model(model_class: type, model_name: str, supports_text: bool) -> None: - """Test embedding functionality across different model types.""" - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"\nTesting {model_name} embedding model") - - # Initialize model - print(f"Loading {model_name} model...") - model: Any = model_class() - model.start() - - # Test single image embedding - print("Embedding single image...") - start_time = time.time() - embedding = model.embed(image) - embed_time = time.time() - start_time - - print(f" Vector shape: {embedding.vector.shape}") - print(f" Time: {embed_time:.3f}s") - - assert embedding.vector is not None - assert len(embedding.vector.shape) == 1 # Should be 1D vector - - # Test batch embedding - print("\nTesting batch embedding (3 images)...") - start_time = time.time() - embeddings = model.embed(image, image, image) - batch_time = time.time() - start_time - - print(f" Batch size: {len(embeddings)}") - print(f" Total time: {batch_time:.3f}s") - print(f" Per image: {batch_time / 3:.3f}s") - - assert len(embeddings) == 3 - assert all(e.vector is not None for e in embeddings) - - # Test similarity computation - print("\nTesting similarity computation...") - sim = embedding @ embeddings[0] - print(f" Self-similarity: {sim:.4f}") - # Self-similarity should be ~1.0 for normalized embeddings - assert sim > 0.99, "Self-similarity should be ~1.0 for normalized embeddings" - - # Test text embedding if supported - if supports_text: - print("\nTesting text embedding...") - start_time = time.time() - text_embedding = model.embed_text("a photo of a cafe") - text_time = time.time() - start_time - - print(f" Text vector shape: {text_embedding.vector.shape}") - print(f" Time: {text_time:.3f}s") - - # Test cross-modal similarity - cross_sim = embedding @ text_embedding - print(f" Image-text similarity: {cross_sim:.4f}") - - assert text_embedding.vector is not None - assert embedding.vector.shape == text_embedding.vector.shape - else: - print(f"\nSkipping text embedding (not supported by {model_name})") - - print(f"\n{model_name} embedding test passed!") - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (CLIPModel, "CLIP"), - pytest.param(MobileCLIPModel, "MobileCLIP"), - ], - ids=["clip", "mobileclip"], -) -@pytest.mark.gpu -def test_text_image_retrieval(model_class: type, model_name: str) -> None: - """Test text-to-image retrieval using embedding similarity.""" - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"\nTesting {model_name} text-image retrieval") - - model: Any = model_class(normalize=True) - model.start() - - # Embed images - image_embeddings = model.embed(image, image, image) - - # Embed text queries - queries = ["a cafe", "a dog", "a car"] - text_embeddings = model.embed_text(*queries) - - # Compute similarities - print("\nSimilarity matrix (text x image):") - for query, text_emb in zip(queries, text_embeddings, strict=False): - sims = [text_emb @ img_emb for img_emb in image_embeddings] - print(f" '{query}': {[f'{s:.3f}' for s in sims]}") - - # The cafe query should have highest similarity - cafe_sims = [text_embeddings[0] @ img_emb for img_emb in image_embeddings] - other_sims = [text_embeddings[1] @ img_emb for img_emb in image_embeddings] - - assert cafe_sims[0] > other_sims[0], "Cafe query should match cafe image better than dog query" - - print(f"\n{model_name} retrieval test passed!") - - -@pytest.mark.gpu -def test_embedding_device_transfer() -> None: - """Test embedding device transfer operations.""" - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - model = CLIPModel() - embedding = model.embed(image) - assert not isinstance(embedding, list) - - # Test to_numpy - np_vec = embedding.to_numpy() - assert not isinstance(np_vec, torch.Tensor) - print(f"NumPy vector shape: {np_vec.shape}") - - # Test to_torch - torch_vec = embedding.to_torch() - assert isinstance(torch_vec, torch.Tensor) - print(f"Torch vector shape: {torch_vec.shape}, device: {torch_vec.device}") - - # Test to_cpu - embedding.to_cpu() - assert isinstance(embedding.vector, torch.Tensor) - assert embedding.vector.device == torch.device("cpu") - print("Successfully moved to CPU") diff --git a/dimos/models/embedding/treid.py b/dimos/models/embedding/treid.py deleted file mode 100644 index 85e32cd39b..0000000000 --- a/dimos/models/embedding/treid.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import warnings - -warnings.filterwarnings("ignore", message="Cython evaluation.*unavailable", category=UserWarning) - -from dataclasses import dataclass -from functools import cached_property - -import torch -import torch.nn.functional as functional -from torchreid import utils as torchreid_utils - -from dimos.models.base import LocalModel -from dimos.models.embedding.base import Embedding, EmbeddingModel, EmbeddingModelConfig -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -# osnet models downloaded from https://kaiyangzhou.github.io/deep-person-reid/MODEL_ZOO.html -# into dimos/data/models_torchreid/ -# feel free to add more -@dataclass -class TorchReIDModelConfig(EmbeddingModelConfig): - model_name: str = "osnet_x1_0" - - -class TorchReIDModel(EmbeddingModel, LocalModel): - """TorchReID embedding model for person re-identification.""" - - default_config = TorchReIDModelConfig - config: TorchReIDModelConfig - - @cached_property - def _model(self) -> torchreid_utils.FeatureExtractor: - self._ensure_cuda_initialized() - return torchreid_utils.FeatureExtractor( - model_name=self.config.model_name, - model_path=str(get_data("models_torchreid") / (self.config.model_name + ".pth")), - device=self.config.device, - ) - - def embed(self, *images: Image) -> Embedding | list[Embedding]: - """Embed one or more images. - - Returns embeddings as torch.Tensor on device for efficient GPU comparisons. - """ - # Convert to numpy arrays - torchreid expects numpy arrays or file paths - np_images = [img.to_opencv() for img in images] - - # Extract features - with torch.inference_mode(): - features = self._model(np_images) - - # torchreid may return either numpy array or torch tensor depending on configuration - if isinstance(features, torch.Tensor): - features_tensor = features.to(self.config.device) - else: - features_tensor = torch.from_numpy(features).to(self.config.device) - - if self.config.normalize: - features_tensor = functional.normalize(features_tensor, dim=-1) - - # Create embeddings (keep as torch.Tensor on device) - embeddings = [] - for i, feat in enumerate(features_tensor): - timestamp = images[i].ts - embeddings.append(Embedding(vector=feat, timestamp=timestamp)) - - return embeddings[0] if len(images) == 1 else embeddings - - def embed_text(self, *texts: str) -> Embedding | list[Embedding]: - """Text embedding not supported for ReID models. - - TorchReID models are vision-only person re-identification models - and do not support text embeddings. - """ - raise NotImplementedError( - "TorchReID models are vision-only and do not support text embeddings. " - "Use CLIP or MobileCLIP for text-image similarity." - ) - - def start(self) -> None: - """Start the model with a dummy forward pass.""" - super().start() - - # Create a dummy 256x128 image (typical person ReID input size) as numpy array - import numpy as np - - dummy_image = np.random.randint(0, 256, (256, 128, 3), dtype=np.uint8) - with torch.inference_mode(): - _ = self._model([dummy_image]) - - def stop(self) -> None: - """Release model and free GPU memory.""" - super().stop() diff --git a/dimos/models/qwen/video_query.py b/dimos/models/qwen/video_query.py deleted file mode 100644 index 7ba80ae069..0000000000 --- a/dimos/models/qwen/video_query.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Utility functions for one-off video frame queries using Qwen model.""" - -import json -import os - -import numpy as np -from openai import OpenAI -from reactivex import Observable, operators as ops -from reactivex.subject import Subject - -from dimos.agents_deprecated.agent import OpenAIAgent -from dimos.agents_deprecated.tokenizer.huggingface_tokenizer import HuggingFaceTokenizer -from dimos.utils.threadpool import get_scheduler - -BBox = tuple[float, float, float, float] # (x1, y1, x2, y2) - - -def query_single_frame_observable( - video_observable: Observable, # type: ignore[type-arg] - query: str, - api_key: str | None = None, - model_name: str = "qwen2.5-vl-72b-instruct", -) -> Observable: # type: ignore[type-arg] - """Process a single frame from a video observable with Qwen model. - - Args: - video_observable: An observable that emits video frames - query: The query to ask about the frame - api_key: Alibaba API key. If None, will try to get from ALIBABA_API_KEY env var - model_name: The Qwen model to use. Defaults to qwen2.5-vl-72b-instruct - - Returns: - Observable: An observable that emits a single response string - - Example: - ```python - video_obs = video_provider.capture_video_as_observable() - single_frame = video_obs.pipe(ops.take(1)) - response = query_single_frame_observable(single_frame, "What objects do you see?") - response.subscribe(print) - ``` - """ - # Get API key from env if not provided - api_key = api_key or os.getenv("ALIBABA_API_KEY") - if not api_key: - raise ValueError( - "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" - ) - - # Create Qwen client - qwen_client = OpenAI( - base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - ) - - # Create response subject - response_subject = Subject() # type: ignore[var-annotated] - - # Create temporary agent for processing - agent = OpenAIAgent( - dev_name="QwenSingleFrameAgent", - openai_client=qwen_client, - model_name=model_name, - tokenizer=HuggingFaceTokenizer(model_name=f"Qwen/{model_name}"), - max_output_tokens_per_request=100, - system_query=query, - pool_scheduler=get_scheduler(), - ) - - # Take only first frame - single_frame = video_observable.pipe(ops.take(1)) - - # Subscribe to frame processing and forward response to our subject - agent.subscribe_to_image_processing(single_frame) - - # Forward agent responses to our response subject - agent.get_response_observable().subscribe( - on_next=lambda x: response_subject.on_next(x), - on_error=lambda e: response_subject.on_error(e), - on_completed=lambda: response_subject.on_completed(), - ) - - # Clean up agent when response subject completes - response_subject.subscribe(on_completed=lambda: agent.dispose_all()) - - return response_subject - - -def query_single_frame( - image: np.ndarray, # type: ignore[type-arg] - query: str = "Return the center coordinates of the fridge handle as a tuple (x,y)", - api_key: str | None = None, - model_name: str = "qwen2.5-vl-72b-instruct", -) -> str: - """Process a single numpy image array with Qwen model. - - Args: - image: A numpy array image to process (H, W, 3) in RGB format - query: The query to ask about the image - api_key: Alibaba API key. If None, will try to get from ALIBABA_API_KEY env var - model_name: The Qwen model to use. Defaults to qwen2.5-vl-72b-instruct - - Returns: - str: The model's response - - Example: - ```python - import cv2 - image = cv2.imread('image.jpg') - image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) # Convert to RGB - response = query_single_frame(image, "Return the center coordinates of the object _____ as a tuple (x,y)") - print(response) - ``` - """ - # Get API key from env if not provided - api_key = api_key or os.getenv("ALIBABA_API_KEY") - if not api_key: - raise ValueError( - "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" - ) - - # Create Qwen client - qwen_client = OpenAI( - base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - ) - - # Create temporary agent for processing - agent = OpenAIAgent( - dev_name="QwenSingleFrameAgent", - openai_client=qwen_client, - model_name=model_name, - tokenizer=HuggingFaceTokenizer(model_name=f"Qwen/{model_name}"), - max_output_tokens_per_request=8192, - system_query=query, - pool_scheduler=get_scheduler(), - ) - - # Use the numpy array directly (no conversion needed) - frame = image - - # Create a Subject that will emit the image once - frame_subject = Subject() # type: ignore[var-annotated] - - # Subscribe to frame processing - agent.subscribe_to_image_processing(frame_subject) - - # Create response observable - response_observable = agent.get_response_observable() - - # Emit the image - frame_subject.on_next(frame) - frame_subject.on_completed() - - # Take first response and run synchronously - response = response_observable.pipe(ops.take(1)).run() - - # Clean up - agent.dispose_all() - - return response # type: ignore[no-any-return] - - -def get_bbox_from_qwen( - video_stream: Observable, object_name: str | None = None # type: ignore[type-arg] -) -> tuple[BBox, float] | None: - """Get bounding box coordinates from Qwen for a specific object or any object. - - Args: - video_stream: Observable video stream - object_name: Optional name of object to detect - - Returns: - Tuple of (bbox, size) where bbox is (x1, y1, x2, y2) and size is height in meters, - or None if no detection - """ - prompt = ( - f"Look at this image and find the {object_name if object_name else 'most prominent object'}. Estimate the approximate height of the subject." - "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2], 'size': height_in_meters} " - "where x1,y1 is the top-left and x2,y2 is the bottom-right corner of the bounding box. If not found, return None." - ) - - response = query_single_frame_observable(video_stream, prompt).pipe(ops.take(1)).run() - - try: - # Extract JSON from response - start_idx = response.find("{") - end_idx = response.rfind("}") + 1 - if start_idx >= 0 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - result = json.loads(json_str) - - # Extract and validate bbox - if "bbox" in result and len(result["bbox"]) == 4: - bbox = tuple(result["bbox"]) # Convert list to tuple - return (bbox, result["size"]) - except Exception as e: - print(f"Error parsing Qwen response: {e}") - print(f"Raw response: {response}") - - return None - - -def get_bbox_from_qwen_frame(frame, object_name: str | None = None) -> BBox | None: # type: ignore[no-untyped-def] - """Get bounding box coordinates from Qwen for a specific object or any object using a single frame. - - Args: - frame: A single image frame (numpy array in RGB format) - object_name: Optional name of object to detect - - Returns: - BBox: Bounding box as (x1, y1, x2, y2) or None if no detection - """ - # Ensure frame is numpy array - if not isinstance(frame, np.ndarray): - raise ValueError("Frame must be a numpy array") - - prompt = ( - f"Look at this image and find the {object_name if object_name else 'most prominent object'}. " - "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2]} " - "where x1,y1 is the top-left and x2,y2 is the bottom-right corner of the bounding box. If not found, return None." - ) - - response = query_single_frame(frame, prompt) - - try: - # Extract JSON from response - start_idx = response.find("{") - end_idx = response.rfind("}") + 1 - if start_idx >= 0 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - result = json.loads(json_str) - - # Extract and validate bbox - if "bbox" in result and len(result["bbox"]) == 4: - return tuple(result["bbox"]) # Convert list to tuple - except Exception as e: - print(f"Error parsing Qwen response: {e}") - print(f"Raw response: {response}") - - return None diff --git a/dimos/models/segmentation/configs/edgetam.yaml b/dimos/models/segmentation/configs/edgetam.yaml deleted file mode 100644 index 6fe21c99df..0000000000 --- a/dimos/models/segmentation/configs/edgetam.yaml +++ /dev/null @@ -1,138 +0,0 @@ -# @package _global_ - -# Model -model: - _target_: sam2.sam2_video_predictor.SAM2VideoPredictor - image_encoder: - _target_: sam2.modeling.backbones.image_encoder.ImageEncoder - scalp: 1 - trunk: - _target_: sam2.modeling.backbones.timm.TimmBackbone - name: repvit_m1.dist_in1k - features: - - layer0 - - layer1 - - layer2 - - layer3 - neck: - _target_: sam2.modeling.backbones.image_encoder.FpnNeck - position_encoding: - _target_: sam2.modeling.position_encoding.PositionEmbeddingSine - num_pos_feats: 256 - normalize: true - scale: null - temperature: 10000 - d_model: 256 - backbone_channel_list: [384, 192, 96, 48] - fpn_top_down_levels: [2, 3] # output level 0 and 1 directly use the backbone features - fpn_interp_model: nearest - - memory_attention: - _target_: sam2.modeling.memory_attention.MemoryAttention - d_model: 256 - pos_enc_at_input: true - layer: - _target_: sam2.modeling.memory_attention.MemoryAttentionLayer - activation: relu - dim_feedforward: 2048 - dropout: 0.1 - pos_enc_at_attn: false - self_attention: - _target_: sam2.modeling.sam.transformer.RoPEAttention - rope_theta: 10000.0 - feat_sizes: [32, 32] - embedding_dim: 256 - num_heads: 1 - downsample_rate: 1 - dropout: 0.1 - d_model: 256 - pos_enc_at_cross_attn_keys: true - pos_enc_at_cross_attn_queries: false - cross_attention: - _target_: sam2.modeling.sam.transformer.RoPEAttentionv2 - rope_theta: 10000.0 - q_sizes: [64, 64] - k_sizes: [16, 16] - embedding_dim: 256 - num_heads: 1 - downsample_rate: 1 - dropout: 0.1 - kv_in_dim: 64 - num_layers: 2 - - memory_encoder: - _target_: sam2.modeling.memory_encoder.MemoryEncoder - out_dim: 64 - position_encoding: - _target_: sam2.modeling.position_encoding.PositionEmbeddingSine - num_pos_feats: 64 - normalize: true - scale: null - temperature: 10000 - mask_downsampler: - _target_: sam2.modeling.memory_encoder.MaskDownSampler - kernel_size: 3 - stride: 2 - padding: 1 - fuser: - _target_: sam2.modeling.memory_encoder.Fuser - layer: - _target_: sam2.modeling.memory_encoder.CXBlock - dim: 256 - kernel_size: 7 - padding: 3 - layer_scale_init_value: 1e-6 - use_dwconv: True # depth-wise convs - num_layers: 2 - - spatial_perceiver: - _target_: sam2.modeling.perceiver.PerceiverResampler - depth: 2 - dim: 64 - dim_head: 64 - heads: 1 - ff_mult: 4 - hidden_dropout_p: 0. - attention_dropout_p: 0. - pos_enc_at_key_value: true # implicit pos - concat_kv_latents: false - num_latents: 256 - num_latents_2d: 256 - position_encoding: - _target_: sam2.modeling.position_encoding.PositionEmbeddingSine - num_pos_feats: 64 - normalize: true - scale: null - temperature: 10000 - use_self_attn: true - - num_maskmem: 7 - image_size: 1024 - # apply scaled sigmoid on mask logits for memory encoder, and directly feed input mask as output mask - sigmoid_scale_for_mem_enc: 20.0 - sigmoid_bias_for_mem_enc: -10.0 - use_mask_input_as_output_without_sam: true - # Memory - directly_add_no_mem_embed: true - # use high-resolution feature map in the SAM mask decoder - use_high_res_features_in_sam: true - # output 3 masks on the first click on initial conditioning frames - multimask_output_in_sam: true - # SAM heads - iou_prediction_use_sigmoid: True - # cross-attend to object pointers from other frames (based on SAM output tokens) in the encoder - use_obj_ptrs_in_encoder: true - add_tpos_enc_to_obj_ptrs: false - only_obj_ptrs_in_the_past_for_eval: true - # object occlusion prediction - pred_obj_scores: true - pred_obj_scores_mlp: true - fixed_no_obj_ptr: true - # multimask tracking settings - multimask_output_for_tracking: true - use_multimask_token_for_obj_ptr: true - multimask_min_pt_num: 0 - multimask_max_pt_num: 1 - use_mlp_for_obj_ptr_proj: true - # Compilation flag - compile_image_encoder: false diff --git a/dimos/models/segmentation/edge_tam.py b/dimos/models/segmentation/edge_tam.py deleted file mode 100644 index 54158b2b92..0000000000 --- a/dimos/models/segmentation/edge_tam.py +++ /dev/null @@ -1,269 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -from contextlib import contextmanager -import os -from pathlib import Path -import shutil -import tempfile -from typing import TYPE_CHECKING, Any, TypedDict - -import cv2 -from hydra.utils import instantiate # type: ignore[import-not-found] -import numpy as np -from numpy.typing import NDArray -from omegaconf import OmegaConf # type: ignore[import-not-found] -from PIL import Image as PILImage -import torch - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.types import Detector -from dimos.perception.detection.type import ImageDetections2D -from dimos.perception.detection.type.detection2d.seg import Detection2DSeg -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from sam2.sam2_video_predictor import SAM2VideoPredictor - -os.environ['TQDM_DISABLE'] = '1' - -logger = setup_logger() - - -class SAM2InferenceState(TypedDict): - images: list[torch.Tensor | None] - num_frames: int - cached_features: dict[int, Any] - - -class EdgeTAMProcessor(Detector): - _predictor: "SAM2VideoPredictor" - _inference_state: SAM2InferenceState | None - _frame_count: int - _is_tracking: bool - _buffer_size: int - - def __init__( - self, - ) -> None: - local_config_path = Path(__file__).parent / "configs" / "edgetam.yaml" - - if not local_config_path.exists(): - raise FileNotFoundError(f"EdgeTAM config not found at {local_config_path}") - - if not torch.cuda.is_available(): - raise RuntimeError("EdgeTAM requires a CUDA-capable GPU") - - cfg = OmegaConf.load(local_config_path) - - overrides = { - "model.sam_mask_decoder_extra_args.dynamic_multimask_via_stability": True, - "model.sam_mask_decoder_extra_args.dynamic_multimask_stability_delta": 0.05, - "model.sam_mask_decoder_extra_args.dynamic_multimask_stability_thresh": 0.98, - "model.binarize_mask_from_pts_for_mem_enc": True, - "model.fill_hole_area": 8, - } - - for key, value in overrides.items(): - OmegaConf.update(cfg, key, value) - - if cfg.model._target_ != "sam2.sam2_video_predictor.SAM2VideoPredictor": - logger.warning( - f"Config target is {cfg.model._target_}, forcing SAM2VideoPredictor" - ) - cfg.model._target_ = "sam2.sam2_video_predictor.SAM2VideoPredictor" - - self._predictor = instantiate(cfg.model, _recursive_=True) - - ckpt_path = str(get_data("models_edgetam") / "edgetam.pt") - - sd = torch.load(ckpt_path, map_location="cpu", weights_only=True)["model"] - missing_keys, unexpected_keys = self._predictor.load_state_dict(sd) - if missing_keys: - raise RuntimeError("Missing keys in checkpoint") - if unexpected_keys: - raise RuntimeError("Unexpected keys in checkpoint") - - self._predictor = self._predictor.to("cuda") - self._predictor.eval() - - self._inference_state = None - self._frame_count = 0 - self._is_tracking = False - self._buffer_size = 100 # Keep last N frames in memory to avoid OOM - - def _prepare_frame(self, image: Image) -> torch.Tensor: - """Prepare frame for SAM2 (resize, normalize, convert to tensor).""" - - cv_image = image.to_opencv() - rgb_image = cv2.cvtColor(cv_image, cv2.COLOR_BGR2RGB) - pil_image = PILImage.fromarray(rgb_image) - - img_np = np.array( - pil_image.resize((self._predictor.image_size, self._predictor.image_size)) - ) - img_np = img_np.astype(np.float32) / 255.0 - - img_mean = np.array([0.485, 0.456, 0.406], dtype=np.float32).reshape(1, 1, 3) - img_std = np.array([0.229, 0.224, 0.225], dtype=np.float32).reshape(1, 1, 3) - img_np -= img_mean - img_np /= img_std - - img_tensor = torch.from_numpy(img_np).permute(2, 0, 1).float() - img_tensor = img_tensor.cuda() - - return img_tensor - - def init_track( - self, - image: Image, - points: NDArray[np.floating[Any]] | None = None, - labels: NDArray[np.integer[Any]] | None = None, - box: NDArray[np.floating[Any]] | None = None, - obj_id: int = 1, - ) -> ImageDetections2D: - """Initialize tracking with a prompt (points or box). - - Args: - image: Initial frame to start tracking from - points: Point prompts for segmentation (Nx2 array of [x, y] coordinates) - labels: Labels for points (1 = foreground, 0 = background) - box: Bounding box prompt in [x1, y1, x2, y2] format - obj_id: Object ID for tracking - - Returns: - ImageDetections2D with initial segmentation mask - """ - if self._inference_state is not None: - self.stop() - - self._frame_count = 0 - - with _temp_dir_context(image) as video_path: - self._inference_state = self._predictor.init_state(video_path=video_path) - - self._predictor.reset_state(self._inference_state) - - if torch.is_tensor(self._inference_state["images"]): - self._inference_state["images"] = [self._inference_state["images"][0]] - - self._is_tracking = True - - if points is not None: - points = points.astype(np.float32) - if labels is not None: - labels = labels.astype(np.int32) - if box is not None: - box = box.astype(np.float32) - - with torch.no_grad(): - _, out_obj_ids, out_mask_logits = self._predictor.add_new_points_or_box( - inference_state=self._inference_state, - frame_idx=0, - obj_id=obj_id, - points=points, - labels=labels, - box=box, - ) - - return self._process_results(image, out_obj_ids, out_mask_logits) - - def process_image(self, image: Image) -> ImageDetections2D: - """Process a new video frame and propagate tracking. - - Args: - image: New frame to process - - Returns: - ImageDetections2D with tracked object segmentation masks - """ - if not self._is_tracking or self._inference_state is None: - return ImageDetections2D(image=image) - - self._frame_count += 1 - - # Append new frame to inference state - new_frame_tensor = self._prepare_frame(image) - self._inference_state["images"].append(new_frame_tensor) - self._inference_state["num_frames"] += 1 - - # Memory management - cached_features = self._inference_state["cached_features"] - if len(cached_features) > self._buffer_size: - oldest_frame = min(cached_features.keys()) - if oldest_frame < self._frame_count - self._buffer_size: - del cached_features[oldest_frame] - - if len(self._inference_state["images"]) > self._buffer_size + 10: - idx_to_drop = self._frame_count - self._buffer_size - 5 - if idx_to_drop >= 0 and idx_to_drop < len(self._inference_state["images"]): - if self._inference_state["images"][idx_to_drop] is not None: - self._inference_state["images"][idx_to_drop] = None - - detections: ImageDetections2D = ImageDetections2D(image=image) - - with torch.no_grad(): - for out_frame_idx, out_obj_ids, out_mask_logits in self._predictor.propagate_in_video( - self._inference_state, start_frame_idx=self._frame_count, max_frame_num_to_track=1 - ): - if out_frame_idx == self._frame_count: - return self._process_results(image, out_obj_ids, out_mask_logits) - - return detections - - def _process_results( - self, - image: Image, - obj_ids: list[int], - mask_logits: torch.Tensor | NDArray[np.floating[Any]], - ) -> ImageDetections2D: - detections: ImageDetections2D = ImageDetections2D(image=image) - - if len(obj_ids) == 0: - return detections - - if isinstance(mask_logits, torch.Tensor): - mask_logits = mask_logits.cpu().numpy() - - for i, obj_id in enumerate(obj_ids): - mask = mask_logits[i] - seg = Detection2DSeg.from_sam2_result( - mask=mask, - obj_id=obj_id, - image=image, - name="object", - ) - - if seg.is_valid(): - detections.detections.append(seg) - - return detections - - def stop(self) -> None: - self._is_tracking = False - self._inference_state = None - - -@contextmanager -def _temp_dir_context(image: Image) -> Generator[str, None, None]: - path = tempfile.mkdtemp() - - image.save(f"{path}/00000.jpg") - - try: - yield path - finally: - shutil.rmtree(path) diff --git a/dimos/models/test_base.py b/dimos/models/test_base.py deleted file mode 100644 index 3ae6f116ac..0000000000 --- a/dimos/models/test_base.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for LocalModel and HuggingFaceModel base classes.""" - -from functools import cached_property - -import torch - -from dimos.models.base import HuggingFaceModel, LocalModel - - -class ConcreteLocalModel(LocalModel): - """Concrete implementation for testing.""" - - @cached_property - def _model(self) -> str: - return "loaded_model" - - -class ConcreteHuggingFaceModel(HuggingFaceModel): - """Concrete implementation for testing.""" - - @cached_property - def _model(self) -> str: - return f"hf_model:{self.model_name}" - - -def test_local_model_device_auto_detection() -> None: - """Test that device is auto-detected based on CUDA availability.""" - model = ConcreteLocalModel() - expected = "cuda" if torch.cuda.is_available() else "cpu" - assert model.device == expected - - -def test_local_model_explicit_device() -> None: - """Test that explicit device is respected.""" - model = ConcreteLocalModel(device="cpu") - assert model.device == "cpu" - - -def test_local_model_default_dtype() -> None: - """Test that default dtype is float32 for LocalModel.""" - model = ConcreteLocalModel() - assert model.dtype == torch.float32 - - -def test_local_model_explicit_dtype() -> None: - """Test that explicit dtype is respected.""" - model = ConcreteLocalModel(dtype=torch.float16) - assert model.dtype == torch.float16 - - -def test_local_model_lazy_loading() -> None: - """Test that model is lazily loaded.""" - model = ConcreteLocalModel() - # Model not loaded yet - assert "_model" not in model.__dict__ - # Access triggers loading - _ = model._model - # Now it's cached - assert "_model" in model.__dict__ - assert model._model == "loaded_model" - - -def test_local_model_start_triggers_loading() -> None: - """Test that start() triggers model loading.""" - model = ConcreteLocalModel() - assert "_model" not in model.__dict__ - model.start() - assert "_model" in model.__dict__ - - -def test_huggingface_model_inherits_local_model() -> None: - """Test that HuggingFaceModel inherits from LocalModel.""" - assert issubclass(HuggingFaceModel, LocalModel) - - -def test_huggingface_model_default_dtype() -> None: - """Test that default dtype is float16 for HuggingFaceModel.""" - model = ConcreteHuggingFaceModel(model_name="test/model") - assert model.dtype == torch.float16 - - -def test_huggingface_model_name() -> None: - """Test model_name property.""" - model = ConcreteHuggingFaceModel(model_name="microsoft/Florence-2-large") - assert model.model_name == "microsoft/Florence-2-large" - - -def test_huggingface_model_trust_remote_code() -> None: - """Test trust_remote_code defaults to True.""" - model = ConcreteHuggingFaceModel(model_name="test/model") - assert model.config.trust_remote_code is True - - model2 = ConcreteHuggingFaceModel(model_name="test/model", trust_remote_code=False) - assert model2.config.trust_remote_code is False - - -def test_huggingface_start_loads_model() -> None: - """Test that start() loads model.""" - model = ConcreteHuggingFaceModel(model_name="test/model") - assert "_model" not in model.__dict__ - model.start() - assert "_model" in model.__dict__ - - -def test_move_inputs_to_device() -> None: - """Test _move_inputs_to_device helper.""" - model = ConcreteHuggingFaceModel(model_name="test/model", device="cpu") - - inputs = { - "input_ids": torch.tensor([1, 2, 3]), - "attention_mask": torch.tensor([1, 1, 1]), - "pixel_values": torch.randn(1, 3, 224, 224), - "labels": "not_a_tensor", - } - - moved = model._move_inputs_to_device(inputs) - - assert moved["input_ids"].device.type == "cpu" - assert moved["attention_mask"].device.type == "cpu" - assert moved["pixel_values"].device.type == "cpu" - assert moved["pixel_values"].dtype == torch.float16 # dtype applied - assert moved["labels"] == "not_a_tensor" # non-tensor unchanged diff --git a/dimos/models/vl/README.md b/dimos/models/vl/README.md deleted file mode 100644 index c252d47957..0000000000 --- a/dimos/models/vl/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Vision Language Models - -This provides vision language model implementations for processing images and text queries. - -## QwenVL Model - -The `QwenVlModel` class provides access to Alibaba's Qwen2.5-VL model for vision-language tasks. - -### Example Usage - -```python -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.sensor_msgs.Image import Image - -# Initialize the model (requires ALIBABA_API_KEY environment variable) -model = QwenVlModel() - -image = Image.from_file("path/to/your/image.jpg") - -response = model.query(image.data, "What do you see in this image?") -print(response) -``` - -## Moondream Hosted Model - -The `MoondreamHostedVlModel` class provides access to the hosted Moondream API for fast vision-language tasks. - -**Prerequisites:** - -You must export your API key before using the model: -```bash -export MOONDREAM_API_KEY="your_api_key_here" -``` - -### Capabilities - -The model supports four modes of operation: - -1. **Caption**: Generate a description of the image. -2. **Query**: Ask natural language questions about the image. -3. **Detect**: Find bounding boxes for specific objects. -4. **Point**: Locate the center points of specific objects. - -### Example Usage - -```python -from dimos.models.vl.moondream_hosted import MoondreamHostedVlModel -from dimos.msgs.sensor_msgs import Image - -model = MoondreamHostedVlModel() -image = Image.from_file("path/to/image.jpg") - -# 1. Caption -print(f"Caption: {model.caption(image)}") - -# 2. Query -print(f"Answer: {model.query(image, 'Is there a person in the image?')}") - -# 3. Detect (returns ImageDetections2D) -detections = model.query_detections(image, "person") -for det in detections.detections: - print(f"Found person at {det.bbox}") - -# 4. Point (returns list of (x, y) coordinates) -points = model.point(image, "person") -print(f"Person centers: {points}") -``` diff --git a/dimos/models/vl/__init__.py b/dimos/models/vl/__init__.py deleted file mode 100644 index 482a907cbd..0000000000 --- a/dimos/models/vl/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "base": ["Captioner", "VlModel"], - "florence": ["Florence2Model"], - "moondream": ["MoondreamVlModel"], - "moondream_hosted": ["MoondreamHostedVlModel"], - "openai": ["OpenAIVlModel"], - "qwen": ["QwenVlModel"], - }, -) diff --git a/dimos/models/vl/base.py b/dimos/models/vl/base.py deleted file mode 100644 index 93caba4de7..0000000000 --- a/dimos/models/vl/base.py +++ /dev/null @@ -1,342 +0,0 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -import json -import logging -import warnings - -from dimos.core.resource import Resource -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2DBBox, Detection2DPoint, ImageDetections2D -from dimos.protocol.service import Configurable # type: ignore[attr-defined] -from dimos.utils.data import get_data -from dimos.utils.decorators import retry -from dimos.utils.llm_utils import extract_json - -logger = logging.getLogger(__name__) - - -class Captioner(ABC): - """Interface for models that can generate image captions.""" - - @abstractmethod - def caption(self, image: Image) -> str: - """Generate a text description of the image. - - Args: - image: Input image to caption - - Returns: - Text description of the image - """ - ... - - def caption_batch(self, *images: Image) -> list[str]: - """Generate captions for multiple images. - - Default implementation calls caption() for each image. - Subclasses may override for more efficient batching. - - Args: - images: Input images to caption - - Returns: - List of text descriptions - """ - return [self.caption(img) for img in images] - - -# Type alias for VLM detection format: [label, x1, y1, x2, y2] -VlmDetection = tuple[str, float, float, float, float] - - -def vlm_detection_to_detection2d( - vlm_detection: VlmDetection | list[str | float], - track_id: int, - image: Image, -) -> Detection2DBBox | None: - """Convert a single VLM detection [label, x1, y1, x2, y2] to Detection2DBBox. - - Args: - vlm_detection: Single detection tuple/list containing [label, x1, y1, x2, y2] - track_id: Track ID to assign to this detection - image: Source image for the detection - - Returns: - Detection2DBBox instance or None if invalid - """ - # Validate list/tuple structure - if not isinstance(vlm_detection, (list, tuple)): - logger.debug(f"VLM detection is not a list/tuple: {type(vlm_detection)}") - return None - - if len(vlm_detection) != 5: - logger.debug( - f"Invalid VLM detection length: {len(vlm_detection)}, expected 5. Got: {vlm_detection}" - ) - return None - - # Extract label - name = str(vlm_detection[0]) - - # Validate and convert coordinates - try: - coords = [float(vlm_detection[i]) for i in range(1, 5)] - except (ValueError, TypeError) as e: - logger.debug(f"Invalid VLM detection coordinates: {vlm_detection[1:]}. Error: {e}") - return None - - bbox = (coords[0], coords[1], coords[2], coords[3]) - - # Use -1 for class_id since VLM doesn't provide it - # confidence defaults to 1.0 for VLM - return Detection2DBBox( - bbox=bbox, - track_id=track_id, - class_id=-1, - confidence=1.0, - name=name, - ts=image.ts, - image=image, - ) - - -# Type alias for VLM point format: [label, x, y] -VlmPoint = tuple[str, float, float] - - -def vlm_point_to_detection2d_point( - vlm_point: VlmPoint | list[str | float], - track_id: int, - image: Image, -) -> Detection2DPoint | None: - """Convert a single VLM point [label, x, y] to Detection2DPoint. - - Args: - vlm_point: Single point tuple/list containing [label, x, y] - track_id: Track ID to assign to this detection - image: Source image for the detection - - Returns: - Detection2DPoint instance or None if invalid - """ - # Validate list/tuple structure - if not isinstance(vlm_point, (list, tuple)): - logger.debug(f"VLM point is not a list/tuple: {type(vlm_point)}") - return None - - if len(vlm_point) != 3: - logger.debug(f"Invalid VLM point length: {len(vlm_point)}, expected 3. Got: {vlm_point}") - return None - - # Extract label - name = str(vlm_point[0]) - - # Validate and convert coordinates - try: - x = float(vlm_point[1]) - y = float(vlm_point[2]) - except (ValueError, TypeError) as e: - logger.debug(f"Invalid VLM point coordinates: {vlm_point[1:]}. Error: {e}") - return None - - return Detection2DPoint( - x=x, - y=y, - name=name, - ts=image.ts, - image=image, - track_id=track_id, - ) - - -@dataclass -class VlModelConfig: - """Configuration for VlModel.""" - - auto_resize: tuple[int, int] | None = None - """Optional (width, height) tuple. If set, images are resized to fit.""" - - -class VlModel(Captioner, Resource, Configurable[VlModelConfig]): - """Vision-language model that can answer questions about images. - - Inherits from Captioner, providing a default caption() implementation - that uses query() with a standard captioning prompt. - - Implements Resource interface for lifecycle management. - """ - - default_config = VlModelConfig - config: VlModelConfig - - def _prepare_image(self, image: Image) -> tuple[Image, float]: - """Prepare image for inference, applying any configured transformations. - - Returns: - Tuple of (prepared_image, scale_factor). Scale factor is 1.0 if no resize. - """ - if self.config.auto_resize is not None: - max_w, max_h = self.config.auto_resize - return image.resize_to_fit(max_w, max_h) - return image, 1.0 - - @abstractmethod - def query(self, image: Image, query: str, **kwargs) -> str: ... # type: ignore[no-untyped-def] - - def query_batch(self, images: list[Image], query: str, **kwargs) -> list[str]: # type: ignore[no-untyped-def] - """Query multiple images with the same question. - - Default implementation calls query() for each image sequentially. - Subclasses may override for more efficient batched inference. - - Args: - images: List of input images - query: Question to ask about each image - - Returns: - List of responses, one per image - """ - warnings.warn( - f"{self.__class__.__name__}.query_batch() is using default sequential implementation. " - "Override for efficient batched inference.", - stacklevel=2, - ) - return [self.query(image, query, **kwargs) for image in images] - - def query_multi(self, image: Image, queries: list[str], **kwargs) -> list[str]: # type: ignore[no-untyped-def] - """Query a single image with multiple different questions. - - Default implementation calls query() for each question sequentially. - Subclasses may override for more efficient inference (e.g., by - encoding the image once and reusing it for all queries). - - Args: - image: Input image - queries: List of questions to ask about the image - - Returns: - List of responses, one per query - """ - warnings.warn( - f"{self.__class__.__name__}.query_multi() is using default sequential implementation. " - "Override for efficient batched inference.", - stacklevel=2, - ) - return [self.query(image, q, **kwargs) for q in queries] - - def caption(self, image: Image) -> str: - """Generate a caption by querying the VLM with a standard prompt.""" - return self.query(image, "Describe this image concisely.") - - def start(self) -> None: - """Start the model by running a simple query (Resource interface).""" - try: - image = Image.from_file(get_data("cafe-smol.jpg")).to_rgb() - self.query(image, "What is this?") - except Exception: - pass - - # requery once if JSON parsing fails - @retry(max_retries=2, on_exception=json.JSONDecodeError, delay=0.0) # type: ignore[untyped-decorator] - def query_json(self, image: Image, query: str) -> dict: # type: ignore[type-arg] - response = self.query(image, query) - return extract_json(response) # type: ignore[return-value] - - def query_detections( - self, image: Image, query: str, **kwargs: object - ) -> ImageDetections2D[Detection2DBBox]: - full_query = f"""show me bounding boxes in pixels for this query: `{query}` - - format should be: - ```json - [ - ["label1", x1, y1, x2, y2] - ["label2", x1, y1, x2, y2] - ... - ]` - - (etc, multiple matches are possible) - - If there's no match return `[]`. Label is whatever you think is appropriate - Only respond with JSON, no other text. - """ - - image_detections = ImageDetections2D(image) - - # Get scaled image and scale factor for coordinate rescaling - scaled_image, scale = self._prepare_image(image) - - try: - detection_tuples = self.query_json(scaled_image, full_query) - except Exception: - return image_detections - - for track_id, detection_tuple in enumerate(detection_tuples): - # Scale coordinates back to original image size if resized - if ( - scale != 1.0 - and isinstance(detection_tuple, (list, tuple)) - and len(detection_tuple) == 5 - ): - detection_tuple = [ - detection_tuple[0], # label - detection_tuple[1] / scale, # x1 - detection_tuple[2] / scale, # y1 - detection_tuple[3] / scale, # x2 - detection_tuple[4] / scale, # y2 - ] - detection2d = vlm_detection_to_detection2d(detection_tuple, track_id, image) - if detection2d is not None and detection2d.is_valid(): - image_detections.detections.append(detection2d) - - return image_detections - - def query_points( - self, image: Image, query: str, **kwargs: object - ) -> ImageDetections2D[Detection2DPoint]: - """Query the VLM for point locations matching the query. - - Args: - image: Input image to query - query: Description of what points to find (e.g., "center of the red ball") - - Returns: - ImageDetections2D containing Detection2DPoint instances - """ - full_query = f"""Show me point coordinates in pixels for this query: `{query}` - - The format should be: - ```json - [ - ["label 1", x, y], - ["label 2", x, y], - ... - ] - - If there's no match return `[]`. Label is whatever you think is appropriate. - Only respond with the JSON, no other text. - """ - - image_detections: ImageDetections2D[Detection2DPoint] = ImageDetections2D(image) - - # Get scaled image and scale factor for coordinate rescaling - scaled_image, scale = self._prepare_image(image) - - try: - point_tuples = self.query_json(scaled_image, full_query) - except Exception: - return image_detections - - for track_id, point_tuple in enumerate(point_tuples): - # Scale coordinates back to original image size if resized - if scale != 1.0 and isinstance(point_tuple, (list, tuple)) and len(point_tuple) == 3: - point_tuple = [ - point_tuple[0], # label - point_tuple[1] / scale, # x - point_tuple[2] / scale, # y - ] - point2d = vlm_point_to_detection2d_point(point_tuple, track_id, image) - if point2d is not None and point2d.is_valid(): - image_detections.detections.append(point2d) - - return image_detections diff --git a/dimos/models/vl/florence.py b/dimos/models/vl/florence.py deleted file mode 100644 index 2e6cf822a8..0000000000 --- a/dimos/models/vl/florence.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import cached_property - -from PIL import Image as PILImage -import torch -from transformers import AutoModelForCausalLM, AutoProcessor # type: ignore[import-untyped] - -from dimos.models.base import HuggingFaceModel -from dimos.models.vl.base import Captioner -from dimos.msgs.sensor_msgs import Image - - -class Florence2Model(HuggingFaceModel, Captioner): - """Florence-2 captioning model from Microsoft. - - A lightweight, fast captioning model optimized for generating image descriptions - without requiring a text prompt. Supports multiple caption detail levels. - """ - - _model_class = AutoModelForCausalLM - - def __init__( - self, - model_name: str = "microsoft/Florence-2-base", - **kwargs: object, - ) -> None: - """Initialize Florence-2 model. - - Args: - model_name: HuggingFace model name. Options: - - "microsoft/Florence-2-base" (~0.2B, fastest) - - "microsoft/Florence-2-large" (~0.8B, better quality) - **kwargs: Additional config options (device, dtype, warmup, etc.) - """ - super().__init__(model_name=model_name, **kwargs) - - @cached_property - def _processor(self) -> AutoProcessor: - return AutoProcessor.from_pretrained( - self.config.model_name, trust_remote_code=self.config.trust_remote_code - ) - - def caption(self, image: Image, detail: str = "normal") -> str: - """Generate a caption for the image. - - Args: - image: Input image to caption - detail: Level of detail for caption: - - "brief": Short, concise caption - - "normal": Standard caption (default) - - "detailed": More detailed description - - Returns: - Text description of the image - """ - # Map detail level to Florence-2 task prompts - task_prompts = { - "brief": "", - "normal": "", - "detailed": "", - "more_detailed": "", - } - task_prompt = task_prompts.get(detail, "") - - # Convert to PIL - pil_image = PILImage.fromarray(image.to_rgb().data) - - # Process inputs - inputs = self._processor(text=task_prompt, images=pil_image, return_tensors="pt") - inputs = self._move_inputs_to_device(inputs) - - # Generate - with torch.inference_mode(): - generated_ids = self._model.generate( - **inputs, - max_new_tokens=256, - num_beams=3, - do_sample=False, - ) - - # Decode - generated_text = self._processor.batch_decode(generated_ids, skip_special_tokens=False)[0] - - # Parse output - Florence returns structured output - parsed = self._processor.post_process_generation( - generated_text, task=task_prompt, image_size=pil_image.size - ) - - # Extract caption from parsed output - caption: str = parsed.get(task_prompt, generated_text) - return caption.strip() - - def caption_batch(self, *images: Image) -> list[str]: - """Generate captions for multiple images efficiently. - - Args: - images: Input images to caption - - Returns: - List of text descriptions - """ - if not images: - return [] - - task_prompt = "" - - # Convert all to PIL - pil_images = [PILImage.fromarray(img.to_rgb().data) for img in images] - - # Process batch - inputs = self._processor( - text=[task_prompt] * len(images), images=pil_images, return_tensors="pt", padding=True - ) - inputs = self._move_inputs_to_device(inputs) - - # Generate - with torch.inference_mode(): - generated_ids = self._model.generate( - **inputs, - max_new_tokens=256, - num_beams=3, - do_sample=False, - ) - - # Decode all - generated_texts = self._processor.batch_decode(generated_ids, skip_special_tokens=False) - - # Parse outputs - captions = [] - for text, pil_img in zip(generated_texts, pil_images, strict=True): - parsed = self._processor.post_process_generation( - text, task=task_prompt, image_size=pil_img.size - ) - captions.append(parsed.get(task_prompt, text).strip()) - - return captions - - def start(self) -> None: - """Start the model with a dummy forward pass.""" - # Load model and processor via base class - super().start() - - # Run a small inference - dummy = PILImage.new("RGB", (224, 224), color="gray") - inputs = self._processor(text="", images=dummy, return_tensors="pt") - inputs = self._move_inputs_to_device(inputs) - - with torch.inference_mode(): - self._model.generate(**inputs, max_new_tokens=10) - - def stop(self) -> None: - """Release model and free GPU memory.""" - # Clean up processor cached property - if "_processor" in self.__dict__: - del self.__dict__["_processor"] - # Call parent which handles _model cleanup - super().stop() diff --git a/dimos/models/vl/moondream.py b/dimos/models/vl/moondream.py deleted file mode 100644 index f31611e867..0000000000 --- a/dimos/models/vl/moondream.py +++ /dev/null @@ -1,220 +0,0 @@ -from dataclasses import dataclass -from functools import cached_property -from typing import Any -import warnings - -import numpy as np -from PIL import Image as PILImage -import torch -from transformers import AutoModelForCausalLM # type: ignore[import-untyped] - -from dimos.models.base import HuggingFaceModel, HuggingFaceModelConfig -from dimos.models.vl.base import VlModel -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2DBBox, Detection2DPoint, ImageDetections2D - -# Moondream works well with 512x512 max -MOONDREAM_DEFAULT_AUTO_RESIZE = (512, 512) - - -@dataclass -class MoondreamConfig(HuggingFaceModelConfig): - """Configuration for MoondreamVlModel.""" - - model_name: str = "vikhyatk/moondream2" - dtype: torch.dtype = torch.bfloat16 - auto_resize: tuple[int, int] | None = MOONDREAM_DEFAULT_AUTO_RESIZE - - -class MoondreamVlModel(HuggingFaceModel, VlModel): - _model_class = AutoModelForCausalLM - default_config = MoondreamConfig # type: ignore[assignment] - config: MoondreamConfig # type: ignore[assignment] - - @cached_property - def _model(self) -> AutoModelForCausalLM: - """Load model with compile() for optimization.""" - model = AutoModelForCausalLM.from_pretrained( - self.config.model_name, - trust_remote_code=self.config.trust_remote_code, - torch_dtype=self.config.dtype, - ).to(self.config.device) - model.compile() - return model - - def _to_pil(self, image: Image | np.ndarray[Any, Any]) -> PILImage.Image: - """Convert dimos Image or numpy array to PIL Image, applying auto_resize.""" - if isinstance(image, np.ndarray): - warnings.warn( - "MoondreamVlModel should receive standard dimos Image type, not a numpy array", - DeprecationWarning, - stacklevel=2, - ) - image = Image.from_numpy(image) - - image, _ = self._prepare_image(image) - rgb_image = image.to_rgb() - return PILImage.fromarray(rgb_image.data) - - def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: # type: ignore[no-untyped-def, type-arg] - pil_image = self._to_pil(image) - - # Query the model - result = self._model.query(image=pil_image, question=query, reasoning=False) - - # Handle both dict and string responses - if isinstance(result, dict): - return result.get("answer", str(result)) # type: ignore[no-any-return] - - return str(result) - - def query_batch(self, images: list[Image], query: str, **kwargs) -> list[str]: # type: ignore[no-untyped-def] - """Query multiple images with the same question. - - Note: moondream2's batch_answer is not truly batched - it processes - images sequentially. No speedup over sequential calls. - - Args: - images: List of input images - query: Question to ask about each image - - Returns: - List of responses, one per image - """ - warnings.warn( - "MoondreamVlModel.query_batch() uses moondream's batch_answer which is not " - "truly batched - images are processed sequentially with no speedup.", - stacklevel=2, - ) - if not images: - return [] - - pil_images = [self._to_pil(img) for img in images] - prompts = [query] * len(images) - result: list[str] = self._model.batch_answer(pil_images, prompts) - return result - - def query_multi(self, image: Image, queries: list[str], **kwargs) -> list[str]: # type: ignore[no-untyped-def] - """Query a single image with multiple different questions. - - Optimized implementation that encodes the image once and reuses - the encoded representation for all queries. - - Args: - image: Input image - queries: List of questions to ask about the image - - Returns: - List of responses, one per query - """ - if not queries: - return [] - - # Encode image once - pil_image = self._to_pil(image) - encoded_image = self._model.encode_image(pil_image) - - # Query with each question, reusing the encoded image - results = [] - for query in queries: - result = self._model.query(image=encoded_image, question=query, reasoning=False) - if isinstance(result, dict): - results.append(result.get("answer", str(result))) - else: - results.append(str(result)) - - return results - - def query_detections( - self, image: Image, query: str, **kwargs: object - ) -> ImageDetections2D[Detection2DBBox]: - """Detect objects using Moondream's native detect method. - - Args: - image: Input image - query: Object query (e.g., "person", "car") - max_objects: Maximum number of objects to detect - - Returns: - ImageDetections2D containing detected bounding boxes - """ - pil_image = self._to_pil(image) - - settings = {"max_objects": kwargs.get("max_objects", 5)} - result = self._model.detect(pil_image, query, settings=settings) - - # Convert to ImageDetections2D - image_detections = ImageDetections2D(image) - - # Get image dimensions for converting normalized coords to pixels - height, width = image.height, image.width - - for track_id, obj in enumerate(result.get("objects", [])): - # Convert normalized coordinates (0-1) to pixel coordinates - x_min_norm = obj["x_min"] - y_min_norm = obj["y_min"] - x_max_norm = obj["x_max"] - y_max_norm = obj["y_max"] - - x1 = x_min_norm * width - y1 = y_min_norm * height - x2 = x_max_norm * width - y2 = y_max_norm * height - - bbox = (x1, y1, x2, y2) - - detection = Detection2DBBox( - bbox=bbox, - track_id=track_id, - class_id=-1, # Moondream doesn't provide class IDs - confidence=1.0, # Moondream doesn't provide confidence scores - name=query, # Use the query as the object name - ts=image.ts, - image=image, - ) - - if detection.is_valid(): - image_detections.detections.append(detection) - - return image_detections - - def query_points( - self, image: Image, query: str, **kwargs: object - ) -> ImageDetections2D[Detection2DPoint]: - """Detect point locations using Moondream's native point method. - - Args: - image: Input image - query: Object query (e.g., "person's head", "center of the ball") - - Returns: - ImageDetections2D containing detected points - """ - pil_image = self._to_pil(image) - - result = self._model.point(pil_image, query) - - # Convert to ImageDetections2D - image_detections: ImageDetections2D[Detection2DPoint] = ImageDetections2D(image) - - # Get image dimensions for converting normalized coords to pixels - height, width = image.height, image.width - - for track_id, point in enumerate(result.get("points", [])): - # Convert normalized coordinates (0-1) to pixel coordinates - x = point["x"] * width - y = point["y"] * height - - detection = Detection2DPoint( - x=x, - y=y, - name=query, - ts=image.ts, - image=image, - track_id=track_id, - ) - - if detection.is_valid(): - image_detections.detections.append(detection) - - return image_detections diff --git a/dimos/models/vl/moondream_hosted.py b/dimos/models/vl/moondream_hosted.py deleted file mode 100644 index fc1f8b7a17..0000000000 --- a/dimos/models/vl/moondream_hosted.py +++ /dev/null @@ -1,148 +0,0 @@ -from functools import cached_property -import os -import warnings - -import moondream as md # type: ignore[import-untyped] -import numpy as np -from PIL import Image as PILImage - -from dimos.models.vl.base import VlModel -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2DBBox, Detection2DPoint, ImageDetections2D - - -class MoondreamHostedVlModel(VlModel): - _api_key: str | None - - def __init__(self, api_key: str | None = None) -> None: - self._api_key = api_key - - @cached_property - def _client(self) -> md.vl: - api_key = self._api_key or os.getenv("MOONDREAM_API_KEY") - if not api_key: - raise ValueError( - "Moondream API key must be provided or set in MOONDREAM_API_KEY environment variable" - ) - return md.vl(api_key=api_key) - - def _to_pil_image(self, image: Image | np.ndarray) -> PILImage.Image: # type: ignore[type-arg] - if isinstance(image, np.ndarray): - warnings.warn( - "MoondreamHostedVlModel should receive standard dimos Image type, not a numpy array", - DeprecationWarning, - stacklevel=3, - ) - image = Image.from_numpy(image) - - rgb_image = image.to_rgb() - return PILImage.fromarray(rgb_image.data) - - def query(self, image: Image | np.ndarray, query: str, **kwargs) -> str: # type: ignore[no-untyped-def, type-arg] - pil_image = self._to_pil_image(image) - - result = self._client.query(pil_image, query) - return result.get("answer", str(result)) # type: ignore[no-any-return] - - def caption(self, image: Image | np.ndarray, length: str = "normal") -> str: # type: ignore[type-arg] - """Generate a caption for the image. - - Args: - image: Input image - length: Caption length ("normal", "short", "long") - """ - pil_image = self._to_pil_image(image) - result = self._client.caption(pil_image, length=length) - return result.get("caption", str(result)) # type: ignore[no-any-return] - - def query_detections(self, image: Image, query: str, **kwargs) -> ImageDetections2D[Detection2DBBox]: # type: ignore[no-untyped-def] - """Detect objects using Moondream's hosted detect method. - - Args: - image: Input image - query: Object query (e.g., "person", "car") - max_objects: Maximum number of objects to detect (not directly supported by hosted API args in docs, - but we handle the output) - - Returns: - ImageDetections2D containing detected bounding boxes - """ - pil_image = self._to_pil_image(image) - - # API docs: detect(image, object) -> {"objects": [...]} - result = self._client.detect(pil_image, query) - objects = result.get("objects", []) - - # Convert to ImageDetections2D - image_detections = ImageDetections2D(image) - height, width = image.height, image.width - - for track_id, obj in enumerate(objects): - # Expected format from docs: Region with x_min, y_min, x_max, y_max - # Assuming normalized coordinates as per local model and standard VLM behavior - x_min_norm = obj.get("x_min", 0.0) - y_min_norm = obj.get("y_min", 0.0) - x_max_norm = obj.get("x_max", 1.0) - y_max_norm = obj.get("y_max", 1.0) - - x1 = x_min_norm * width - y1 = y_min_norm * height - x2 = x_max_norm * width - y2 = y_max_norm * height - - bbox = (x1, y1, x2, y2) - - detection = Detection2DBBox( - bbox=bbox, - track_id=track_id, - class_id=-1, - confidence=1.0, - name=query, - ts=image.ts, - image=image, - ) - - if detection.is_valid(): - image_detections.detections.append(detection) - - return image_detections - - def query_points( - self, image: Image, query: str, **kwargs: object - ) -> ImageDetections2D[Detection2DPoint]: - """Detect point locations using Moondream's hosted point method. - - Args: - image: Input image - query: Object query (e.g., "person's head", "center of the ball") - - Returns: - ImageDetections2D containing detected points - """ - pil_image = self._to_pil_image(image) - result = self._client.point(pil_image, query) - - image_detections: ImageDetections2D[Detection2DPoint] = ImageDetections2D(image) - height, width = image.height, image.width - - for track_id, point in enumerate(result.get("points", [])): - x = point.get("x", 0.0) * width - y = point.get("y", 0.0) * height - - detection = Detection2DPoint( - x=x, - y=y, - name=query, - ts=image.ts, - image=image, - track_id=track_id, - ) - - if detection.is_valid(): - image_detections.detections.append(detection) - - return image_detections - - def stop(self) -> None: - pass - diff --git a/dimos/models/vl/openai.py b/dimos/models/vl/openai.py deleted file mode 100644 index f596f1ee1e..0000000000 --- a/dimos/models/vl/openai.py +++ /dev/null @@ -1,106 +0,0 @@ -from dataclasses import dataclass -from functools import cached_property -import os -from typing import Any - -import numpy as np -from openai import OpenAI - -from dimos.models.vl.base import VlModel, VlModelConfig -from dimos.msgs.sensor_msgs import Image -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class OpenAIVlModelConfig(VlModelConfig): - model_name: str = "gpt-4o-mini" - api_key: str | None = None - - -class OpenAIVlModel(VlModel): - default_config = OpenAIVlModelConfig - config: OpenAIVlModelConfig - - @cached_property - def _client(self) -> OpenAI: - api_key = self.config.api_key or os.getenv("OPENAI_API_KEY") - if not api_key: - raise ValueError( - "OpenAI API key must be provided or set in OPENAI_API_KEY environment variable" - ) - - return OpenAI(api_key=api_key) - - def query(self, image: Image | np.ndarray, query: str, response_format: dict | None = None, **kwargs) -> str: # type: ignore[override, type-arg, no-untyped-def] - if isinstance(image, np.ndarray): - import warnings - - warnings.warn( - "OpenAIVlModel.query should receive standard dimos Image type, not a numpy array", - DeprecationWarning, - stacklevel=2, - ) - - image = Image.from_numpy(image) - - # Apply auto_resize if configured - image, _ = self._prepare_image(image) - - img_base64 = image.to_base64() - - api_kwargs: dict[str, Any] = { - "model": self.config.model_name, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{img_base64}"}, - }, - {"type": "text", "text": query}, - ], - } - ], - } - - if response_format: - api_kwargs["response_format"] = response_format - - response = self._client.chat.completions.create(**api_kwargs) - - return response.choices[0].message.content # type: ignore[return-value,no-any-return] - - def query_batch( - self, images: list[Image], query: str, response_format: dict[str, Any] | None = None, **kwargs: Any - ) -> list[str]: # type: ignore[override] - """Query VLM with multiple images using a single API call.""" - if not images: - return [] - - content: list[dict[str, Any]] = [ - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{self._prepare_image(img)[0].to_base64()}"}, - } - for img in images - ] - content.append({"type": "text", "text": query}) - - messages = [{"role": "user", "content": content}] - api_kwargs: dict[str, Any] = {"model": self.config.model_name, "messages": messages} - if response_format: - api_kwargs["response_format"] = response_format - - response = self._client.chat.completions.create(**api_kwargs) - response_text = response.choices[0].message.content or "" - # Return one response per image (same response since API analyzes all images together) - return [response_text] * len(images) - - def stop(self) -> None: - """Release the OpenAI client.""" - if "_client" in self.__dict__: - del self.__dict__["_client"] - diff --git a/dimos/models/vl/qwen.py b/dimos/models/vl/qwen.py deleted file mode 100644 index 93b31bf74c..0000000000 --- a/dimos/models/vl/qwen.py +++ /dev/null @@ -1,102 +0,0 @@ -from dataclasses import dataclass -from functools import cached_property -import os -from typing import Any - -import numpy as np -from openai import OpenAI - -from dimos.models.vl.base import VlModel, VlModelConfig -from dimos.msgs.sensor_msgs import Image - - -@dataclass -class QwenVlModelConfig(VlModelConfig): - """Configuration for Qwen VL model.""" - - model_name: str = "qwen2.5-vl-72b-instruct" - api_key: str | None = None - - -class QwenVlModel(VlModel): - default_config = QwenVlModelConfig - config: QwenVlModelConfig - - @cached_property - def _client(self) -> OpenAI: - api_key = self.config.api_key or os.getenv("ALIBABA_API_KEY") - if not api_key: - raise ValueError( - "Alibaba API key must be provided or set in ALIBABA_API_KEY environment variable" - ) - - return OpenAI( - base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1", - api_key=api_key, - ) - - def query(self, image: Image | np.ndarray, query: str) -> str: # type: ignore[override, type-arg] - if isinstance(image, np.ndarray): - import warnings - - warnings.warn( - "QwenVlModel.query should receive standard dimos Image type, not a numpy array", - DeprecationWarning, - stacklevel=2, - ) - - image = Image.from_numpy(image) - - # Apply auto_resize if configured - image, _ = self._prepare_image(image) - - img_base64 = image.to_base64() - - response = self._client.chat.completions.create( - model=self.config.model_name, - messages=[ - { - "role": "user", - "content": [ - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{img_base64}"}, - }, - {"type": "text", "text": query}, - ], - } - ], - ) - - return response.choices[0].message.content # type: ignore[return-value] - - def query_batch( - self, images: list[Image], query: str, response_format: dict[str, Any] | None = None, **kwargs: Any - ) -> list[str]: # type: ignore[override] - """Query VLM with multiple images using a single API call.""" - if not images: - return [] - - content: list[dict[str, Any]] = [ - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{self._prepare_image(img)[0].to_base64()}"}, - } - for img in images - ] - content.append({"type": "text", "text": query}) - - messages = [{"role": "user", "content": content}] - api_kwargs: dict[str, Any] = {"model": self.config.model_name, "messages": messages} - if response_format: - api_kwargs["response_format"] = response_format - - response = self._client.chat.completions.create(**api_kwargs) - response_text = response.choices[0].message.content or "" - # Return one response per image (same response since API analyzes all images together) - return [response_text] * len(images) - - def stop(self) -> None: - """Release the OpenAI client.""" - if "_client" in self.__dict__: - del self.__dict__["_client"] diff --git a/dimos/models/vl/test_base.py b/dimos/models/vl/test_base.py deleted file mode 100644 index a7296bd87b..0000000000 --- a/dimos/models/vl/test_base.py +++ /dev/null @@ -1,146 +0,0 @@ -import os -from unittest.mock import MagicMock - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -import pytest - -from dimos.core import LCMTransport -from dimos.models.vl.moondream import MoondreamVlModel -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.data import get_data - -# Captured actual response from Qwen API for cafe.jpg with query "humans" -# Added garbage around JSON to ensure we are robustly extracting it -MOCK_QWEN_RESPONSE = """ - Locating humans for you 😊😊 - - [ - ["humans", 76, 368, 219, 580], - ["humans", 354, 372, 512, 525], - ["humans", 409, 370, 615, 748], - ["humans", 628, 350, 762, 528], - ["humans", 785, 323, 960, 650] - ] - - Here is some trash at the end of the response :) - Let me know if you need anything else 😀😊 - """ - - -def test_query_detections_mocked() -> None: - """Test query_detections with mocked API response (no API key required).""" - # Load test image - image = Image.from_file(get_data("cafe.jpg")) - - # Create model and mock the query method - model = QwenVlModel() - model.query = MagicMock(return_value=MOCK_QWEN_RESPONSE) - - # Query for humans in the image - query = "humans" - detections = model.query_detections(image, query) - - # Verify the return type - assert isinstance(detections, ImageDetections2D) - - # Should have 5 detections based on our mock data - assert len(detections.detections) == 5, ( - f"Expected 5 detections, got {len(detections.detections)}" - ) - - # Verify each detection - img_height, img_width = image.shape[:2] - - for i, detection in enumerate(detections.detections): - # Verify attributes - assert detection.name == "humans" - assert detection.confidence == 1.0 - assert detection.class_id == -1 # VLM detections use -1 for class_id - assert detection.track_id == i - assert len(detection.bbox) == 4 - - assert detection.is_valid() - - # Verify bbox coordinates are valid (out-of-bounds detections are discarded) - x1, y1, x2, y2 = detection.bbox - assert x2 > x1, f"Detection {i}: Invalid x coordinates: x1={x1}, x2={x2}" - assert y2 > y1, f"Detection {i}: Invalid y coordinates: y1={y1}, y2={y2}" - - # Check bounds (out-of-bounds detections would have been discarded) - assert 0 <= x1 <= img_width, f"Detection {i}: x1={x1} out of bounds" - assert 0 <= x2 <= img_width, f"Detection {i}: x2={x2} out of bounds" - assert 0 <= y1 <= img_height, f"Detection {i}: y1={y1} out of bounds" - assert 0 <= y2 <= img_height, f"Detection {i}: y2={y2} out of bounds" - - print(f"✓ Successfully processed {len(detections.detections)} mocked detections") - - -@pytest.mark.tool -@pytest.mark.skipif(not os.getenv("ALIBABA_API_KEY"), reason="ALIBABA_API_KEY not set") -def test_query_detections_real() -> None: - """Test query_detections with real API calls (requires API key).""" - # Load test image - image = Image.from_file(get_data("cafe.jpg")) - - # Initialize the model (will use real API) - model = QwenVlModel() - - # Query for humans in the image - query = "humans" - detections = model.query_detections(image, query) - - assert isinstance(detections, ImageDetections2D) - print(detections) - - # Check that detections were found - if detections.detections: - for detection in detections.detections: - # Verify each detection has expected attributes - assert detection.bbox is not None - assert len(detection.bbox) == 4 - assert detection.name - assert detection.confidence == 1.0 - assert detection.class_id == -1 # VLM detections use -1 for class_id - assert detection.is_valid() - - print(f"Found {len(detections.detections)} detections for query '{query}'") - - -@pytest.mark.tool -def test_query_points() -> None: - """Test query_points with real API calls (requires API key).""" - # Load test image - image = Image.from_file(get_data("cafe.jpg"), format=ImageFormat.RGB).to_rgb() - - # Initialize the model (will use real API) - model = MoondreamVlModel() - - # Query for points in the image - query = "center of each person's head" - detections = model.query_points(image, query) - - assert isinstance(detections, ImageDetections2D) - print(detections) - - # Check that detections were found - if detections.detections: - for point in detections.detections: - # Verify each point has expected attributes - assert hasattr(point, "x") - assert hasattr(point, "y") - assert point.name - assert point.confidence == 1.0 - assert point.class_id == -1 # VLM detections use -1 for class_id - assert point.is_valid() - - print(f"Found {len(detections.detections)} points for query '{query}'") - - image_topic: LCMTransport[Image] = LCMTransport("/image", Image) - image_topic.publish(image) - image_topic.lcm.stop() - - annotations: LCMTransport[ImageAnnotations] = LCMTransport("/annotations", ImageAnnotations) - annotations.publish(detections.to_foxglove_annotations()) - annotations.lcm.stop() diff --git a/dimos/models/vl/test_captioner.py b/dimos/models/vl/test_captioner.py deleted file mode 100644 index 081f3bcefc..0000000000 --- a/dimos/models/vl/test_captioner.py +++ /dev/null @@ -1,90 +0,0 @@ -from collections.abc import Generator -import time -from typing import Protocol, TypeVar - -import pytest - -from dimos.models.vl.florence import Florence2Model -from dimos.models.vl.moondream import MoondreamVlModel -from dimos.msgs.sensor_msgs import Image -from dimos.utils.data import get_data - - -class CaptionerModel(Protocol): - """Intersection of Captioner and Resource for testing.""" - - def caption(self, image: Image) -> str: ... - def caption_batch(self, *images: Image) -> list[str]: ... - def start(self) -> None: ... - def stop(self) -> None: ... - - -M = TypeVar("M", bound=CaptionerModel) - - -@pytest.fixture(scope="module") -def test_image() -> Image: - return Image.from_file(get_data("cafe.jpg")).to_rgb() - - -def generic_model_fixture(model_type: type[M]) -> Generator[M, None, None]: - model_instance = model_type() - model_instance.start() - yield model_instance - model_instance.stop() - - -@pytest.fixture(params=[Florence2Model, MoondreamVlModel]) -def captioner_model(request: pytest.FixtureRequest) -> Generator[CaptionerModel, None, None]: - yield from generic_model_fixture(request.param) - - -@pytest.fixture(params=[Florence2Model]) -def florence2_model(request: pytest.FixtureRequest) -> Generator[Florence2Model, None, None]: - yield from generic_model_fixture(request.param) - - -@pytest.mark.gpu -def test_captioner(captioner_model: CaptionerModel, test_image: Image) -> None: - """Test captioning functionality across different model types.""" - # Test single caption - start_time = time.time() - caption = captioner_model.caption(test_image) - caption_time = time.time() - start_time - - print(f" Caption: {caption}") - print(f" Time: {caption_time:.3f}s") - - assert isinstance(caption, str) - assert len(caption) > 0 - - # Test batch captioning - print("\nTesting batch captioning (3 images)...") - start_time = time.time() - captions = captioner_model.caption_batch(test_image, test_image, test_image) - batch_time = time.time() - start_time - - print(f" Captions: {captions}") - print(f" Total time: {batch_time:.3f}s") - print(f" Per image: {batch_time / 3:.3f}s") - - assert len(captions) == 3 - assert all(isinstance(c, str) and len(c) > 0 for c in captions) - - -@pytest.mark.gpu -def test_florence2_detail_levels(florence2_model: Florence2Model, test_image: Image) -> None: - """Test Florence-2 different detail levels.""" - detail_levels = ["brief", "normal", "detailed", "more_detailed"] - - for detail in detail_levels: - print(f"\nDetail level: {detail}") - start_time = time.time() - caption = florence2_model.caption(test_image, detail=detail) - caption_time = time.time() - start_time - - print(f" Caption ({len(caption)} chars): {caption[:100]}...") - print(f" Time: {caption_time:.3f}s") - - assert isinstance(caption, str) - assert len(caption) > 0 diff --git a/dimos/models/vl/test_models.py b/dimos/models/vl/test_models.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/models/vl/test_vlm.py b/dimos/models/vl/test_vlm.py deleted file mode 100644 index 741e0dede2..0000000000 --- a/dimos/models/vl/test_vlm.py +++ /dev/null @@ -1,317 +0,0 @@ -import os -import time -from typing import TYPE_CHECKING - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, -) -import pytest - -from dimos.core import LCMTransport -from dimos.models.vl.moondream import MoondreamVlModel -from dimos.models.vl.moondream_hosted import MoondreamHostedVlModel -from dimos.models.vl.qwen import QwenVlModel -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.cli.plot import bar -from dimos.utils.data import get_data - -if TYPE_CHECKING: - from dimos.models.vl.base import VlModel - - -# For these tests you can run foxglove-bridge to visualize results -# You can also run lcm-spy to confirm that messages are being published - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (MoondreamVlModel, "Moondream"), - (MoondreamHostedVlModel, "Moondream Hosted"), - (QwenVlModel, "Qwen"), - ], -) -@pytest.mark.gpu -def test_vlm_bbox_detections(model_class: "type[VlModel]", model_name: str) -> None: - if model_class is MoondreamHostedVlModel and 'MOONDREAM_API_KEY' not in os.environ: - pytest.skip("Need MOONDREAM_API_KEY to run") - - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"Testing {model_name}") - - # Initialize model - print(f"Loading {model_name} model...") - model: VlModel = model_class() - model.start() - - queries = [ - "glasses", - "blue shirt", - "bulb", - "cigarette", - "reflection of a car", - "knee", - "flowers on the left table", - "shoes", - "leftmost persons ear", - "rightmost arm", - ] - - all_detections = ImageDetections2D(image) - query_times = [] - - # Publish to LCM with model-specific channel names - annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( - "/annotations", ImageAnnotations - ) - - image_transport: LCMTransport[Image] = LCMTransport("/image", Image) - - image_transport.publish(image) - - # Then run VLM queries - for query in queries: - print(f"\nQuerying for: {query}") - start_time = time.time() - detections = model.query_detections(image, query, max_objects=5) - query_time = time.time() - start_time - query_times.append(query_time) - - print(f" Found {len(detections)} detections in {query_time:.3f}s") - all_detections.detections.extend(detections.detections) - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - avg_time = sum(query_times) / len(query_times) if query_times else 0 - print(f"\n{model_name} Results:") - print(f" Average query time: {avg_time:.3f}s") - print(f" Total detections: {len(all_detections)}") - print(all_detections) - - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - annotations_transport.lcm.stop() - image_transport.lcm.stop() - model.stop() - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (MoondreamVlModel, "Moondream"), - (MoondreamHostedVlModel, "Moondream Hosted"), - (QwenVlModel, "Qwen"), - ], -) -@pytest.mark.gpu -def test_vlm_point_detections(model_class: "type[VlModel]", model_name: str) -> None: - """Test VLM point detection capabilities.""" - - if model_class is MoondreamHostedVlModel and 'MOONDREAM_API_KEY' not in os.environ: - pytest.skip("Need MOONDREAM_API_KEY to run") - - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"Testing {model_name} point detection") - - # Initialize model - print(f"Loading {model_name} model...") - model: VlModel = model_class() - model.start() - - queries = [ - "center of each person's head", - "tip of the nose", - "center of the glasses", - "cigarette tip", - "center of each light bulb", - "center of each shoe", - ] - - all_detections = ImageDetections2D(image) - query_times = [] - - # Publish to LCM with model-specific channel names - annotations_transport: LCMTransport[ImageAnnotations] = LCMTransport( - "/annotations", ImageAnnotations - ) - - image_transport: LCMTransport[Image] = LCMTransport("/image", Image) - - image_transport.publish(image) - - # Then run VLM queries - for query in queries: - print(f"\nQuerying for: {query}") - start_time = time.time() - detections = model.query_points(image, query) - query_time = time.time() - start_time - query_times.append(query_time) - - print(f" Found {len(detections)} points in {query_time:.3f}s") - all_detections.detections.extend(detections.detections) - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - avg_time = sum(query_times) / len(query_times) if query_times else 0 - print(f"\n{model_name} Results:") - print(f" Average query time: {avg_time:.3f}s") - print(f" Total points: {len(all_detections)}") - print(all_detections) - - annotations_transport.publish(all_detections.to_foxglove_annotations()) - - annotations_transport.lcm.stop() - image_transport.lcm.stop() - model.stop() - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (MoondreamVlModel, "Moondream"), - ], -) -@pytest.mark.gpu -def test_vlm_query_multi(model_class: "type[VlModel]", model_name: str) -> None: - """Test query_multi optimization - single image, multiple queries.""" - image = Image.from_file(get_data("cafe.jpg")).to_rgb() - - print(f"\nTesting {model_name} query_multi optimization") - - model: VlModel = model_class() - model.start() - - queries = [ - "How many people are in this image?", - "What color is the leftmost person's shirt?", - "Are there any glasses visible?", - "What's on the table?", - ] - - # Sequential queries - print("\nSequential queries:") - start_time = time.time() - sequential_results = [model.query(image, q) for q in queries] - sequential_time = time.time() - start_time - print(f" Time: {sequential_time:.3f}s") - - # Batched queries (encode image once) - print("\nBatched queries (query_multi):") - start_time = time.time() - batch_results = model.query_multi(image, queries) - batch_time = time.time() - start_time - print(f" Time: {batch_time:.3f}s") - - speedup_pct = (sequential_time - batch_time) / sequential_time * 100 - print(f"\nSpeedup: {speedup_pct:.1f}%") - - # Print results - for q, seq_r, batch_r in zip(queries, sequential_results, batch_results, strict=True): - print(f"\nQ: {q}") - print(f" Sequential: {seq_r[:120]}...") - print(f" Batch: {batch_r[:120]}...") - - model.stop() - - -@pytest.mark.parametrize( - "model_class,model_name", - [ - (MoondreamVlModel, "Moondream"), - ], -) -@pytest.mark.tool -@pytest.mark.gpu -def test_vlm_query_batch(model_class: "type[VlModel]", model_name: str) -> None: - """Test query_batch optimization - multiple images, same query.""" - from dimos.utils.testing import TimedSensorReplay - - # Load 5 frames at 1-second intervals using TimedSensorReplay - replay = TimedSensorReplay[Image]("unitree_go2_office_walk2/video") - images = [replay.find_closest_seek(i).to_rgb() for i in range(0, 10, 2)] - - print(f"\nTesting {model_name} query_batch with {len(images)} images") - - model: VlModel = model_class() - model.start() - - query = "Describe this image in a short sentence" - - # Sequential queries (print as they come in) - print("\nSequential queries:") - sequential_results = [] - start_time = time.time() - for i, img in enumerate(images): - result = model.query(img, query) - sequential_results.append(result) - print(f" [{i}] {result[:120]}...") - sequential_time = time.time() - start_time - print(f" Time: {sequential_time:.3f}s") - - # Batched queries (pre-encode all images) - print("\nBatched queries (query_batch):") - start_time = time.time() - batch_results = model.query_batch(images, query) - batch_time = time.time() - start_time - for i, result in enumerate(batch_results): - print(f" [{i}] {result[:120]}...") - print(f" Time: {batch_time:.3f}s") - - speedup_pct = (sequential_time - batch_time) / sequential_time * 100 - print(f"\nSpeedup: {speedup_pct:.1f}%") - - # Verify results are valid strings - assert len(batch_results) == len(images) - assert all(isinstance(r, str) and len(r) > 0 for r in batch_results) - - model.stop() - - -@pytest.mark.parametrize( - "model_class,sizes", - [ - (MoondreamVlModel, [None, (512, 512), (256, 256)]), - (QwenVlModel, [None, (512, 512), (256, 256)]), - ], -) -@pytest.mark.gpu -def test_vlm_resize( - model_class: "type[VlModel]", - sizes: list[tuple[int, int] | None], -) -> None: - """Test VLM auto_resize effect on performance.""" - from dimos.utils.testing import TimedSensorReplay - - replay = TimedSensorReplay[Image]("unitree_go2_office_walk2/video") - image = replay.find_closest_seek(0).to_rgb() - - labels: list[str] = [] - avg_times: list[float] = [] - - for auto_resize in sizes: - resize_str = f"{auto_resize[0]}x{auto_resize[1]}" if auto_resize else "full" - print(f"\nOriginal image: {image.width}x{image.height}, auto_resize: {resize_str}") - - model: VlModel = model_class(auto_resize=auto_resize) - model.start() - - times = [] - for i in range(3): - start = time.time() - result = model.query_detections(image, "box") - elapsed = time.time() - start - times.append(elapsed) - print(f" [{i}] ({elapsed:.2f}s)", result) - - avg = sum(times) / len(times) - print(f"Avg time: {avg:.2f}s") - labels.append(resize_str) - avg_times.append(avg) - - # Free GPU memory before next model - model.stop() - - # Plot results - print(f"\n{model_class.__name__} resize performance:") - bar(labels, avg_times, title=f"{model_class.__name__} Query Time", ylabel="seconds") diff --git a/dimos/msgs/__init__.py b/dimos/msgs/__init__.py deleted file mode 100644 index 4395dbcc51..0000000000 --- a/dimos/msgs/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from dimos.msgs.helpers import resolve_msg_type -from dimos.msgs.protocol import DimosMsg - -__all__ = ["DimosMsg", "resolve_msg_type"] diff --git a/dimos/msgs/foxglove_msgs/Color.py b/dimos/msgs/foxglove_msgs/Color.py deleted file mode 100644 index 954b10c8b9..0000000000 --- a/dimos/msgs/foxglove_msgs/Color.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import hashlib - -from dimos_lcm.foxglove_msgs import Color as LCMColor - - -class Color(LCMColor): # type: ignore[misc] - """Color with convenience methods.""" - - @classmethod - def from_string(cls, name: str, alpha: float = 0.2, brightness: float = 1.0) -> Color: - """Generate a consistent color from a string using hash function. - - Args: - name: String to generate color from - alpha: Transparency value (0.0-1.0) - brightness: Brightness multiplier (0.0-2.0). Values > 1.0 lighten towards white. - - Returns: - Color instance with deterministic RGB values - """ - # Hash the string to get consistent values - hash_obj = hashlib.md5(name.encode()) - hash_bytes = hash_obj.digest() - - # Use first 3 bytes for RGB (0-255) - r = hash_bytes[0] / 255.0 - g = hash_bytes[1] / 255.0 - b = hash_bytes[2] / 255.0 - - # Apply brightness adjustment - # If brightness > 1.0, mix with white to lighten - if brightness > 1.0: - mix_factor = brightness - 1.0 # 0.0 to 1.0 - r = r + (1.0 - r) * mix_factor - g = g + (1.0 - g) * mix_factor - b = b + (1.0 - b) * mix_factor - else: - # If brightness < 1.0, darken by scaling - r *= brightness - g *= brightness - b *= brightness - - # Create and return color instance - color = cls() - color.r = min(1.0, r) - color.g = min(1.0, g) - color.b = min(1.0, b) - color.a = alpha - return color diff --git a/dimos/msgs/foxglove_msgs/ImageAnnotations.py b/dimos/msgs/foxglove_msgs/ImageAnnotations.py deleted file mode 100644 index aff7c5f7cb..0000000000 --- a/dimos/msgs/foxglove_msgs/ImageAnnotations.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations as FoxgloveImageAnnotations, -) - - -class ImageAnnotations(FoxgloveImageAnnotations): # type: ignore[misc] - def __add__(self, other: "ImageAnnotations") -> "ImageAnnotations": - points = self.points + other.points - texts = self.texts + other.texts - circles = self.circles + other.circles - - return ImageAnnotations( - texts=texts, - texts_length=len(texts), - points=points, - points_length=len(points), - circles=circles, - circles_length=len(circles), - ) - - def agent_encode(self) -> str: - if len(self.texts) == 0: - return None # type: ignore[return-value] - return list(map(lambda t: t.text, self.texts)) # type: ignore[return-value] diff --git a/dimos/msgs/foxglove_msgs/__init__.py b/dimos/msgs/foxglove_msgs/__init__.py deleted file mode 100644 index 945ebf94c9..0000000000 --- a/dimos/msgs/foxglove_msgs/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from dimos.msgs.foxglove_msgs.ImageAnnotations import ImageAnnotations - -__all__ = ["ImageAnnotations"] diff --git a/dimos/msgs/geometry_msgs/Pose.py b/dimos/msgs/geometry_msgs/Pose.py deleted file mode 100644 index 261aae6452..0000000000 --- a/dimos/msgs/geometry_msgs/Pose.py +++ /dev/null @@ -1,239 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TypeAlias - -from dimos_lcm.geometry_msgs import ( - Pose as LCMPose, - Transform as LCMTransform, -) -from plum import dispatch - -from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable -from dimos.msgs.geometry_msgs.Transform import Transform -from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable - -# Types that can be converted to/from Pose -PoseConvertable: TypeAlias = ( - tuple[VectorConvertable, QuaternionConvertable] - | LCMPose - | Vector3 - | dict[str, VectorConvertable | QuaternionConvertable] -) - - -class Pose(LCMPose): # type: ignore[misc] - position: Vector3 - orientation: Quaternion - msg_name = "geometry_msgs.Pose" - - @dispatch - def __init__(self) -> None: - """Initialize a pose at origin with identity orientation.""" - self.position = Vector3(0.0, 0.0, 0.0) - self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - - @dispatch # type: ignore[no-redef] - def __init__(self, x: int | float, y: int | float, z: int | float) -> None: - """Initialize a pose with position and identity orientation.""" - self.position = Vector3(x, y, z) - self.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - x: int | float, - y: int | float, - z: int | float, - qx: int | float, - qy: int | float, - qz: int | float, - qw: int | float, - ) -> None: - """Initialize a pose with position and orientation.""" - self.position = Vector3(x, y, z) - self.orientation = Quaternion(qx, qy, qz, qw) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - position: VectorConvertable | Vector3 | None = None, - orientation: QuaternionConvertable | Quaternion | None = None, - ) -> None: - """Initialize a pose with position and orientation.""" - if orientation is None: - orientation = [0, 0, 0, 1] - if position is None: - position = [0, 0, 0] - self.position = Vector3(position) - self.orientation = Quaternion(orientation) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose_tuple: tuple[VectorConvertable, QuaternionConvertable]) -> None: - """Initialize from a tuple of (position, orientation).""" - self.position = Vector3(pose_tuple[0]) - self.orientation = Quaternion(pose_tuple[1]) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose_dict: dict[str, VectorConvertable | QuaternionConvertable]) -> None: - """Initialize from a dictionary with 'position' and 'orientation' keys.""" - self.position = Vector3(pose_dict["position"]) - self.orientation = Quaternion(pose_dict["orientation"]) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose: Pose) -> None: - """Initialize from another Pose (copy constructor).""" - self.position = Vector3(pose.position) - self.orientation = Quaternion(pose.orientation) - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_pose: LCMPose) -> None: - """Initialize from an LCM Pose.""" - self.position = Vector3(lcm_pose.position.x, lcm_pose.position.y, lcm_pose.position.z) - self.orientation = Quaternion( - lcm_pose.orientation.x, - lcm_pose.orientation.y, - lcm_pose.orientation.z, - lcm_pose.orientation.w, - ) - - @property - def x(self) -> float: - """X coordinate of position.""" - return self.position.x - - @property - def y(self) -> float: - """Y coordinate of position.""" - return self.position.y - - @property - def z(self) -> float: - """Z coordinate of position.""" - return self.position.z - - @property - def roll(self) -> float: - """Roll angle in radians.""" - return self.orientation.to_euler().roll - - @property - def pitch(self) -> float: - """Pitch angle in radians.""" - return self.orientation.to_euler().pitch - - @property - def yaw(self) -> float: - """Yaw angle in radians.""" - return self.orientation.to_euler().yaw - - def __repr__(self) -> str: - return f"Pose(position={self.position!r}, orientation={self.orientation!r})" - - def __str__(self) -> str: - return ( - f"Pose(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " - f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}]), " - f"quaternion=[{self.orientation}])" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two poses are equal.""" - if not isinstance(other, Pose): - return False - return self.position == other.position and self.orientation == other.orientation - - def __matmul__(self, transform: LCMTransform | Transform) -> Pose: - return self + transform - - def __add__(self, other: Pose | PoseConvertable | LCMTransform | Transform) -> Pose: - """Compose two poses or apply a transform (transform composition). - - The operation self + other represents applying transformation 'other' - in the coordinate frame defined by 'self'. This is equivalent to: - - First apply transformation 'self' (from world to self's frame) - - Then apply transformation 'other' (from self's frame to other's frame) - - This matches ROS tf convention where: - T_world_to_other = T_world_to_self * T_self_to_other - - Args: - other: The pose or transform to compose with this one - - Returns: - A new Pose representing the composed transformation - - Example: - robot_pose = Pose(1, 0, 0) # Robot at (1,0,0) facing forward - object_in_robot = Pose(2, 0, 0) # Object 2m in front of robot - object_in_world = robot_pose + object_in_robot # Object at (3,0,0) in world - - # Or with a Transform: - transform = Transform() - transform.translation = Vector3(2, 0, 0) - transform.rotation = Quaternion(0, 0, 0, 1) - new_pose = pose + transform - """ - # Handle Transform objects - if isinstance(other, LCMTransform | Transform): - # Convert Transform to Pose using its translation and rotation - other_position = Vector3(other.translation) - other_orientation = Quaternion(other.rotation) - elif isinstance(other, Pose): - other_position = other.position - other_orientation = other.orientation - else: - # Convert to Pose if it's a convertible type - other_pose = Pose(other) - other_position = other_pose.position - other_orientation = other_pose.orientation - - # Compose orientations: self.orientation * other.orientation - new_orientation = self.orientation * other_orientation - - # Transform other's position by self's orientation, then add to self's position - rotated_position = self.orientation.rotate_vector(other_position) - new_position = self.position + rotated_position - - return Pose(new_position, new_orientation) - - def __sub__(self, other: Pose) -> Pose: - """Compute the delta pose: self - other. - - For position: simple subtraction. - For orientation: delta_quat = self.orientation * inverse(other.orientation) - - Returns: - A new Pose representing the delta transformation - """ - delta_position = self.position - other.position - delta_orientation = self.orientation * other.orientation.inverse() - return Pose(delta_position, delta_orientation) - - -@dispatch -def to_pose(value: Pose) -> Pose: - """Pass through Pose objects.""" - return value - - -@dispatch # type: ignore[no-redef] -def to_pose(value: PoseConvertable) -> Pose: - """Convert a pose-compatible value to a Pose object.""" - return Pose(value) - - -PoseLike: TypeAlias = PoseConvertable | Pose diff --git a/dimos/msgs/geometry_msgs/PoseArray.py b/dimos/msgs/geometry_msgs/PoseArray.py deleted file mode 100644 index e27f56b6bf..0000000000 --- a/dimos/msgs/geometry_msgs/PoseArray.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""PoseArray message type for Dimos.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from dimos.msgs.std_msgs.Header import Header - -if TYPE_CHECKING: - from collections.abc import Iterator - - from dimos.msgs.geometry_msgs.Pose import Pose - - -class PoseArray: - """ - An array of poses with a header for reference frame and timestamp. - - This is commonly used for representing multiple candidate positions, - such as grasp poses, particle filter samples, or waypoints. - """ - - msg_name = "geometry_msgs.PoseArray" - - def __init__(self, header: Header | None = None, poses: list[Pose] | None = None) -> None: - """ - Initialize a PoseArray. - - Args: - header: Header with frame_id and timestamp - poses: List of Pose objects - """ - self.header = header if header is not None else Header() - self.poses = poses if poses is not None else [] - - def __repr__(self) -> str: - return f"PoseArray(header={self.header!r}, poses={len(self.poses)} poses)" - - def __str__(self) -> str: - return f"PoseArray(frame_id={self.header.frame_id}, num_poses={len(self.poses)})" - - def __len__(self) -> int: - """Return the number of poses in the array.""" - return len(self.poses) - - def __getitem__(self, index: int) -> Pose: - """Get pose at index.""" - return self.poses[index] - - def __iter__(self) -> Iterator[Pose]: - """Iterate over poses.""" - return iter(self.poses) - - def append(self, pose: Pose) -> None: - """Add a pose to the array.""" - self.poses.append(pose) - - def encode(self) -> bytes: - """ - Encode to bytes for LCM transmission. - - Note: This is a simple implementation. For production use, - consider using proper LCM encoding. - """ - import pickle - - return pickle.dumps({"header": self.header, "poses": self.poses}) - - @classmethod - def decode(cls, data: bytes) -> PoseArray: - """ - Decode from bytes. - - Args: - data: Pickled PoseArray data - - Returns: - Decoded PoseArray - """ - import pickle - - decoded = pickle.loads(data) - return cls(header=decoded["header"], poses=decoded["poses"]) diff --git a/dimos/msgs/geometry_msgs/PoseStamped.py b/dimos/msgs/geometry_msgs/PoseStamped.py deleted file mode 100644 index acf0af8b32..0000000000 --- a/dimos/msgs/geometry_msgs/PoseStamped.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import math -import time -from typing import TYPE_CHECKING, BinaryIO, TypeAlias - -if TYPE_CHECKING: - from rerun._baseclasses import Archetype - -from dimos_lcm.geometry_msgs import PoseStamped as LCMPoseStamped -from plum import dispatch - -from dimos.msgs.geometry_msgs.Pose import Pose -from dimos.msgs.geometry_msgs.Quaternion import Quaternion, QuaternionConvertable -from dimos.msgs.geometry_msgs.Transform import Transform -from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from Pose -PoseConvertable: TypeAlias = ( - tuple[VectorConvertable, QuaternionConvertable] - | LCMPoseStamped - | dict[str, VectorConvertable | QuaternionConvertable] -) - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class PoseStamped(Pose, Timestamped): - msg_name = "geometry_msgs.PoseStamped" - ts: float - frame_id: str - - @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: # type: ignore[no-untyped-def] - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - super().__init__(**kwargs) - - def lcm_encode(self) -> bytes: - lcm_mgs = LCMPoseStamped() - lcm_mgs.pose = self - [lcm_mgs.header.stamp.sec, lcm_mgs.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_mgs.header.frame_id = self.frame_id - return lcm_mgs.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> PoseStamped: - lcm_msg = LCMPoseStamped.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - position=[lcm_msg.pose.position.x, lcm_msg.pose.position.y, lcm_msg.pose.position.z], - orientation=[ - lcm_msg.pose.orientation.x, - lcm_msg.pose.orientation.y, - lcm_msg.pose.orientation.z, - lcm_msg.pose.orientation.w, - ], - ) - - def __str__(self) -> str: - return ( - f"PoseStamped(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " - f"euler=[{math.degrees(self.roll):.1f}, {math.degrees(self.pitch):.1f}, {math.degrees(self.yaw):.1f}])" - ) - - def to_rerun(self) -> Archetype: - """Convert to rerun Transform3D format. - - Returns a Transform3D that can be logged to Rerun to position - child entities in the transform hierarchy. - """ - import rerun as rr - - return rr.Transform3D( - translation=[self.x, self.y, self.z], - rotation=rr.Quaternion( - xyzw=[ - self.orientation.x, - self.orientation.y, - self.orientation.z, - self.orientation.w, - ] - ), - ) - - def to_rerun_arrow(self, length: float = 0.5): # type: ignore[no-untyped-def] - """Convert to rerun Arrows3D format for visualization.""" - import rerun as rr - - origin = [[self.x, self.y, self.z]] - forward = self.orientation.rotate_vector(Vector3(length, 0, 0)) - vector = [[forward.x, forward.y, forward.z]] - return rr.Arrows3D(origins=origin, vectors=vector) - - def new_transform_to(self, name: str) -> Transform: - return self.find_transform( - PoseStamped( - frame_id=name, - position=Vector3(0, 0, 0), - orientation=Quaternion(0, 0, 0, 1), # Identity quaternion - ) - ) - - def new_transform_from(self, name: str) -> Transform: - return self.new_transform_to(name).inverse() - - def find_transform(self, other: PoseStamped) -> Transform: - inv_orientation = self.orientation.conjugate() - - pos_diff = other.position - self.position - - local_translation = inv_orientation.rotate_vector(pos_diff) - - relative_rotation = inv_orientation * other.orientation - - return Transform( - child_frame_id=other.frame_id, - frame_id=self.frame_id, - translation=local_translation, - rotation=relative_rotation, - ) diff --git a/dimos/msgs/geometry_msgs/PoseWithCovariance.py b/dimos/msgs/geometry_msgs/PoseWithCovariance.py deleted file mode 100644 index 03ce7fd081..0000000000 --- a/dimos/msgs/geometry_msgs/PoseWithCovariance.py +++ /dev/null @@ -1,197 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, TypeAlias - -from dimos_lcm.geometry_msgs import ( - PoseWithCovariance as LCMPoseWithCovariance, -) -import numpy as np -from plum import dispatch - -from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs.Quaternion import Quaternion - from dimos.msgs.geometry_msgs.Vector3 import Vector3 - -# Types that can be converted to/from PoseWithCovariance -PoseWithCovarianceConvertable: TypeAlias = ( - tuple[PoseConvertable, list[float] | np.ndarray] # type: ignore[type-arg] - | LCMPoseWithCovariance - | dict[str, PoseConvertable | list[float] | np.ndarray] # type: ignore[type-arg] -) - - -class PoseWithCovariance(LCMPoseWithCovariance): # type: ignore[misc] - pose: Pose - covariance: np.ndarray[tuple[int], np.dtype[np.floating[Any]]] - msg_name = "geometry_msgs.PoseWithCovariance" - - @dispatch - def __init__(self) -> None: - """Initialize with default pose and zero covariance.""" - self.pose = Pose() - self.covariance = np.zeros(36) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - pose: Pose | PoseConvertable, - covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] - ) -> None: - """Initialize with pose and optional covariance.""" - self.pose = Pose(pose) if not isinstance(pose, Pose) else pose - if covariance is None: - self.covariance = np.zeros(36) - else: - self.covariance = np.array(covariance, dtype=float).reshape(36) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose_with_cov: PoseWithCovariance) -> None: - """Initialize from another PoseWithCovariance (copy constructor).""" - self.pose = Pose(pose_with_cov.pose) - self.covariance = np.array(pose_with_cov.covariance).copy() - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_pose_with_cov: LCMPoseWithCovariance) -> None: - """Initialize from an LCM PoseWithCovariance.""" - self.pose = Pose(lcm_pose_with_cov.pose) - self.covariance = np.array(lcm_pose_with_cov.covariance) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose_dict: dict[str, PoseConvertable | list[float] | np.ndarray]) -> None: # type: ignore[type-arg] - """Initialize from a dictionary with 'pose' and 'covariance' keys.""" - self.pose = Pose(pose_dict["pose"]) - covariance = pose_dict.get("covariance") - if covariance is None: - self.covariance = np.zeros(36) - else: - self.covariance = np.array(covariance, dtype=float).reshape(36) - - @dispatch # type: ignore[no-redef] - def __init__(self, pose_tuple: tuple[PoseConvertable, list[float] | np.ndarray]) -> None: # type: ignore[type-arg] - """Initialize from a tuple of (pose, covariance).""" - self.pose = Pose(pose_tuple[0]) - self.covariance = np.array(pose_tuple[1], dtype=float).reshape(36) - - def __getattribute__(self, name: str): # type: ignore[no-untyped-def] - """Override to ensure covariance is always returned as numpy array.""" - if name == "covariance": - cov = object.__getattribute__(self, "covariance") - if not isinstance(cov, np.ndarray): - return np.array(cov, dtype=float) - return cov - return super().__getattribute__(name) - - def __setattr__(self, name: str, value) -> None: # type: ignore[no-untyped-def] - """Override to ensure covariance is stored as numpy array.""" - if name == "covariance": - if not isinstance(value, np.ndarray): - value = np.array(value, dtype=float).reshape(36) - super().__setattr__(name, value) - - @property - def x(self) -> float: - """X coordinate of position.""" - return self.pose.x - - @property - def y(self) -> float: - """Y coordinate of position.""" - return self.pose.y - - @property - def z(self) -> float: - """Z coordinate of position.""" - return self.pose.z - - @property - def position(self) -> Vector3: - """Position vector.""" - return self.pose.position - - @property - def orientation(self) -> Quaternion: - """Orientation quaternion.""" - return self.pose.orientation - - @property - def roll(self) -> float: - """Roll angle in radians.""" - return self.pose.roll - - @property - def pitch(self) -> float: - """Pitch angle in radians.""" - return self.pose.pitch - - @property - def yaw(self) -> float: - """Yaw angle in radians.""" - return self.pose.yaw - - @property - def covariance_matrix(self) -> np.ndarray: # type: ignore[type-arg] - """Get covariance as 6x6 matrix.""" - return self.covariance.reshape(6, 6) # type: ignore[has-type, no-any-return] - - @covariance_matrix.setter - def covariance_matrix(self, value: np.ndarray) -> None: # type: ignore[type-arg] - """Set covariance from 6x6 matrix.""" - self.covariance = np.array(value).reshape(36) # type: ignore[has-type] - - def __repr__(self) -> str: - return f"PoseWithCovariance(pose={self.pose!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" # type: ignore[has-type] - - def __str__(self) -> str: - return ( - f"PoseWithCovariance(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " - f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}], " - f"cov_trace={np.trace(self.covariance_matrix):.3f})" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two PoseWithCovariance are equal.""" - if not isinstance(other, PoseWithCovariance): - return False - return self.pose == other.pose and np.allclose(self.covariance, other.covariance) # type: ignore[has-type] - - def lcm_encode(self) -> bytes: - """Encode to LCM binary format.""" - lcm_msg = LCMPoseWithCovariance() - lcm_msg.pose = self.pose - # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] - lcm_msg.covariance = self.covariance.tolist() # type: ignore[has-type] - else: - lcm_msg.covariance = list(self.covariance) # type: ignore[has-type] - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> PoseWithCovariance: - """Decode from LCM binary format.""" - lcm_msg = LCMPoseWithCovariance.lcm_decode(data) - pose = Pose( - position=[lcm_msg.pose.position.x, lcm_msg.pose.position.y, lcm_msg.pose.position.z], - orientation=[ - lcm_msg.pose.orientation.x, - lcm_msg.pose.orientation.y, - lcm_msg.pose.orientation.z, - lcm_msg.pose.orientation.w, - ], - ) - return cls(pose, lcm_msg.covariance) diff --git a/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py deleted file mode 100644 index 9e92aa06f0..0000000000 --- a/dimos/msgs/geometry_msgs/PoseWithCovarianceStamped.py +++ /dev/null @@ -1,110 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TypeAlias - -from dimos_lcm.geometry_msgs import ( - PoseWithCovarianceStamped as LCMPoseWithCovarianceStamped, -) -import numpy as np -from plum import dispatch - -from dimos.msgs.geometry_msgs.Pose import Pose, PoseConvertable -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from PoseWithCovarianceStamped -PoseWithCovarianceStampedConvertable: TypeAlias = ( - tuple[PoseConvertable, list[float] | np.ndarray] # type: ignore[type-arg] - | LCMPoseWithCovarianceStamped - | dict[str, PoseConvertable | list[float] | np.ndarray | float | str] # type: ignore[type-arg] -) - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class PoseWithCovarianceStamped(PoseWithCovariance, Timestamped): - msg_name = "geometry_msgs.PoseWithCovarianceStamped" - ts: float - frame_id: str - - @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: - """Initialize with timestamp and frame_id.""" - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - super().__init__(**kwargs) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - ts: float = 0.0, - frame_id: str = "", - pose: Pose | PoseConvertable | None = None, - covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] - ) -> None: - """Initialize with timestamp, frame_id, pose and covariance.""" - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - if pose is None: - super().__init__() - else: - super().__init__(pose, covariance) - - def lcm_encode(self) -> bytes: - lcm_msg = LCMPoseWithCovarianceStamped() - lcm_msg.pose.pose = self.pose - # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] - lcm_msg.pose.covariance = self.covariance.tolist() # type: ignore[has-type] - else: - lcm_msg.pose.covariance = list(self.covariance) # type: ignore[has-type] - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> PoseWithCovarianceStamped: - lcm_msg = LCMPoseWithCovarianceStamped.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - pose=Pose( - position=[ - lcm_msg.pose.pose.position.x, - lcm_msg.pose.pose.position.y, - lcm_msg.pose.pose.position.z, - ], - orientation=[ - lcm_msg.pose.pose.orientation.x, - lcm_msg.pose.pose.orientation.y, - lcm_msg.pose.pose.orientation.z, - lcm_msg.pose.pose.orientation.w, - ], - ), - covariance=lcm_msg.pose.covariance, - ) - - def __str__(self) -> str: - return ( - f"PoseWithCovarianceStamped(pos=[{self.x:.3f}, {self.y:.3f}, {self.z:.3f}], " - f"euler=[{self.roll:.3f}, {self.pitch:.3f}, {self.yaw:.3f}], " - f"cov_trace={np.trace(self.covariance_matrix):.3f})" - ) diff --git a/dimos/msgs/geometry_msgs/Quaternion.py b/dimos/msgs/geometry_msgs/Quaternion.py deleted file mode 100644 index 8e83a32e50..0000000000 --- a/dimos/msgs/geometry_msgs/Quaternion.py +++ /dev/null @@ -1,268 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections.abc import Sequence -from io import BytesIO -import struct -from typing import TYPE_CHECKING, BinaryIO, TypeAlias - -if TYPE_CHECKING: - import rerun as rr - -from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion -import numpy as np -from plum import dispatch -from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - -# Types that can be converted to/from Quaternion -QuaternionConvertable: TypeAlias = Sequence[int | float] | LCMQuaternion | np.ndarray # type: ignore[type-arg] - - -class Quaternion(LCMQuaternion): # type: ignore[misc] - x: float = 0.0 - y: float = 0.0 - z: float = 0.0 - w: float = 1.0 - msg_name = "geometry_msgs.Quaternion" - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO): # type: ignore[no-untyped-def] - if not hasattr(data, "read"): - data = BytesIO(data) - if data.read(8) != cls._get_packed_fingerprint(): - raise ValueError("Decode error") - return cls._lcm_decode_one(data) # type: ignore[no-untyped-call] - - @classmethod - def _lcm_decode_one(cls, buf): # type: ignore[no-untyped-def] - return cls(struct.unpack(">dddd", buf.read(32))) - - @dispatch - def __init__(self) -> None: ... - - @dispatch # type: ignore[no-redef] - def __init__(self, x: int | float, y: int | float, z: int | float, w: int | float) -> None: - self.x = float(x) - self.y = float(y) - self.z = float(z) - self.w = float(w) - - @dispatch # type: ignore[no-redef] - def __init__(self, sequence: Sequence[int | float] | np.ndarray) -> None: # type: ignore[type-arg] - if isinstance(sequence, np.ndarray): - if sequence.size != 4: - raise ValueError("Quaternion requires exactly 4 components [x, y, z, w]") - else: - if len(sequence) != 4: - raise ValueError("Quaternion requires exactly 4 components [x, y, z, w]") - - self.x = sequence[0] - self.y = sequence[1] - self.z = sequence[2] - self.w = sequence[3] - - @dispatch # type: ignore[no-redef] - def __init__(self, quaternion: Quaternion) -> None: - """Initialize from another Quaternion (copy constructor).""" - self.x, self.y, self.z, self.w = quaternion.x, quaternion.y, quaternion.z, quaternion.w - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_quaternion: LCMQuaternion) -> None: - """Initialize from an LCM Quaternion.""" - self.x, self.y, self.z, self.w = ( - lcm_quaternion.x, - lcm_quaternion.y, - lcm_quaternion.z, - lcm_quaternion.w, - ) - - def to_tuple(self) -> tuple[float, float, float, float]: - """Tuple representation of the quaternion (x, y, z, w).""" - return (self.x, self.y, self.z, self.w) - - def to_list(self) -> list[float]: - """List representation of the quaternion (x, y, z, w).""" - return [self.x, self.y, self.z, self.w] - - def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] - """Numpy array representation of the quaternion (x, y, z, w).""" - return np.array([self.x, self.y, self.z, self.w]) - - @property - def euler(self) -> Vector3: - return self.to_euler() - - @property - def radians(self) -> Vector3: - return self.to_euler() - - def to_radians(self) -> Vector3: - """Radians representation of the quaternion (x, y, z, w).""" - return self.to_euler() - - def to_rerun(self) -> rr.Quaternion: - import rerun as rr - - return rr.Quaternion(xyzw=[self.x, self.y, self.z, self.w]) - - @classmethod - def from_euler(cls, vector: Vector3) -> Quaternion: - """Convert Euler angles (roll, pitch, yaw) in radians to quaternion. - - Args: - vector: Vector3 containing (roll, pitch, yaw) in radians - - Returns: - Quaternion representation - """ - - # Calculate quaternion components - cy = np.cos(vector.yaw * 0.5) - sy = np.sin(vector.yaw * 0.5) - cp = np.cos(vector.pitch * 0.5) - sp = np.sin(vector.pitch * 0.5) - cr = np.cos(vector.roll * 0.5) - sr = np.sin(vector.roll * 0.5) - - w = cr * cp * cy + sr * sp * sy - x = sr * cp * cy - cr * sp * sy - y = cr * sp * cy + sr * cp * sy - z = cr * cp * sy - sr * sp * cy - - return cls(x, y, z, w) - - @classmethod - def from_rotation_matrix(cls, matrix: np.ndarray) -> Quaternion: # type: ignore[type-arg] - """Convert a 3x3 rotation matrix to quaternion. - - Args: - matrix: 3x3 rotation matrix (numpy array) - - Returns: - Quaternion representation - """ - rotation = R.from_matrix(matrix) - quat = rotation.as_quat() # Returns [x, y, z, w] - return cls(quat[0], quat[1], quat[2], quat[3]) - - def to_euler(self) -> Vector3: - """Convert quaternion to Euler angles (roll, pitch, yaw) in radians. - - Returns: - Vector3: Euler angles as (roll, pitch, yaw) in radians - """ - # Use scipy for accurate quaternion to euler conversion - quat = [self.x, self.y, self.z, self.w] - rotation = R.from_quat(quat) - euler_angles = rotation.as_euler("xyz") # roll, pitch, yaw - - return Vector3(euler_angles[0], euler_angles[1], euler_angles[2]) - - def __getitem__(self, idx: int) -> float: - """Allow indexing into quaternion components: 0=x, 1=y, 2=z, 3=w.""" - if idx == 0: - return self.x - elif idx == 1: - return self.y - elif idx == 2: - return self.z - elif idx == 3: - return self.w - else: - raise IndexError(f"Quaternion index {idx} out of range [0-3]") - - def __repr__(self) -> str: - return f"Quaternion({self.x:.6f}, {self.y:.6f}, {self.z:.6f}, {self.w:.6f})" - - def __str__(self) -> str: - return self.__repr__() - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - if not isinstance(other, Quaternion): - return False - return self.x == other.x and self.y == other.y and self.z == other.z and self.w == other.w - - def __mul__(self, other: Quaternion) -> Quaternion: - """Multiply two quaternions (Hamilton product). - - The result represents the composition of rotations: - q1 * q2 represents rotating by q2 first, then by q1. - """ - if not isinstance(other, Quaternion): - raise TypeError(f"Cannot multiply Quaternion with {type(other)}") - - # Hamilton product formula - w = self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z - x = self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y - y = self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x - z = self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w - - return Quaternion(x, y, z, w) - - def conjugate(self) -> Quaternion: - """Return the conjugate of the quaternion. - - For unit quaternions, the conjugate represents the inverse rotation. - """ - return Quaternion(-self.x, -self.y, -self.z, self.w) - - def inverse(self) -> Quaternion: - """Return the inverse of the quaternion. - - For unit quaternions, this is equivalent to the conjugate. - For non-unit quaternions, this is conjugate / norm^2. - """ - norm_sq = self.x**2 + self.y**2 + self.z**2 + self.w**2 - if norm_sq == 0: - raise ZeroDivisionError("Cannot invert zero quaternion") - - # For unit quaternions (norm_sq ≈ 1), this simplifies to conjugate - if np.isclose(norm_sq, 1.0): - return self.conjugate() - - # For non-unit quaternions - conj = self.conjugate() - return Quaternion(conj.x / norm_sq, conj.y / norm_sq, conj.z / norm_sq, conj.w / norm_sq) - - def normalize(self) -> Quaternion: - """Return a normalized (unit) quaternion.""" - norm = np.sqrt(self.x**2 + self.y**2 + self.z**2 + self.w**2) - if norm == 0: - raise ZeroDivisionError("Cannot normalize zero quaternion") - return Quaternion(self.x / norm, self.y / norm, self.z / norm, self.w / norm) - - def rotate_vector(self, vector: Vector3) -> Vector3: - """Rotate a 3D vector by this quaternion. - - Args: - vector: The vector to rotate - - Returns: - The rotated vector - """ - # For unit quaternions, conjugate equals inverse, so we use conjugate for efficiency - # The rotation formula is: q * v * q^* where q^* is the conjugate - - # Convert vector to pure quaternion (w=0) - v_quat = Quaternion(vector.x, vector.y, vector.z, 0) - - # Apply rotation: q * v * q^* (conjugate for unit quaternions) - rotated = self * v_quat * self.conjugate() - - # Extract vector components - return Vector3(rotated.x, rotated.y, rotated.z) diff --git a/dimos/msgs/geometry_msgs/Transform.py b/dimos/msgs/geometry_msgs/Transform.py deleted file mode 100644 index 5f50f9b9d1..0000000000 --- a/dimos/msgs/geometry_msgs/Transform.py +++ /dev/null @@ -1,305 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING, BinaryIO - -if TYPE_CHECKING: - import rerun as rr - - from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped - -from dimos_lcm.geometry_msgs import ( - Transform as LCMTransform, - TransformStamped as LCMTransformStamped, -) - -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.std_msgs import Header -from dimos.types.timestamped import Timestamped - - -class Transform(Timestamped): - translation: Vector3 - rotation: Quaternion - ts: float - frame_id: str - child_frame_id: str - msg_name = "tf2_msgs.TFMessage" - - def __init__( # type: ignore[no-untyped-def] - self, - translation: Vector3 | None = None, - rotation: Quaternion | None = None, - frame_id: str = "world", - child_frame_id: str = "unset", - ts: float = 0.0, - **kwargs, - ) -> None: - self.frame_id = frame_id - self.child_frame_id = child_frame_id - self.ts = ts if ts != 0.0 else time.time() - self.translation = translation if translation is not None else Vector3() - self.rotation = rotation if rotation is not None else Quaternion() - - def now(self) -> Transform: - """Return a copy of this Transform with the current timestamp.""" - return Transform( - translation=self.translation, - rotation=self.rotation, - frame_id=self.frame_id, - child_frame_id=self.child_frame_id, - ts=time.time(), - ) - - def __repr__(self) -> str: - return f"Transform(translation={self.translation!r}, rotation={self.rotation!r})" - - def __str__(self) -> str: - return f"{self.frame_id} -> {self.child_frame_id}\n Translation: {self.translation}\n Rotation: {self.rotation}" - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two transforms are equal.""" - if not isinstance(other, Transform): - return False - return self.translation == other.translation and self.rotation == other.rotation - - @classmethod - def identity(cls) -> Transform: - """Create an identity transform.""" - return cls() - - def lcm_transform(self) -> LCMTransformStamped: - return LCMTransformStamped( - child_frame_id=self.child_frame_id, - header=Header(self.ts, self.frame_id), - transform=LCMTransform( - translation=self.translation, - rotation=self.rotation, - ), - ) - - def apply(self, other: Transform) -> Transform: - return self.__add__(other) - - def __add__(self, other: Transform) -> Transform: - """Compose two transforms (transform composition). - - The operation self + other represents applying transformation 'other' - in the coordinate frame defined by 'self'. This is equivalent to: - - First apply transformation 'self' (from frame A to frame B) - - Then apply transformation 'other' (from frame B to frame C) - - Args: - other: The transform to compose with this one - - Returns: - A new Transform representing the composed transformation - - Example: - t1 = Transform(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - t2 = Transform(Vector3(2, 0, 0), Quaternion(0, 0, 0, 1)) - t3 = t1 + t2 # Combined transform: translation (3, 0, 0) - """ - if not isinstance(other, Transform): - raise TypeError(f"Cannot add Transform and {type(other).__name__}") - - # Compose orientations: self.rotation * other.rotation - new_rotation = self.rotation * other.rotation - - # Transform other's translation by self's rotation, then add to self's translation - rotated_translation = self.rotation.rotate_vector(other.translation) - new_translation = self.translation + rotated_translation - - return Transform( - translation=new_translation, - rotation=new_rotation, - frame_id=self.frame_id, - child_frame_id=other.child_frame_id, - ts=self.ts, - ) - - def inverse(self) -> Transform: - """Compute the inverse transform. - - The inverse transform reverses the direction of the transformation. - If this transform goes from frame A to frame B, the inverse goes from B to A. - - Returns: - A new Transform representing the inverse transformation - """ - # Inverse rotation - inv_rotation = self.rotation.inverse() - - # Inverse translation: -R^(-1) * t - inv_translation = inv_rotation.rotate_vector(self.translation) - inv_translation = Vector3(-inv_translation.x, -inv_translation.y, -inv_translation.z) - - return Transform( - translation=inv_translation, - rotation=inv_rotation, - frame_id=self.child_frame_id, # Swap frame references - child_frame_id=self.frame_id, - ts=self.ts, - ) - - def __neg__(self) -> Transform: - """Unary minus operator returns the inverse transform.""" - return self.inverse() - - @classmethod - def from_pose(cls, frame_id: str, pose: Pose | PoseStamped) -> Transform: # type: ignore[name-defined] - """Create a Transform from a Pose or PoseStamped. - - Args: - pose: A Pose or PoseStamped object to convert - - Returns: - A Transform with the same translation and rotation as the pose - """ - # Import locally to avoid circular imports - from dimos.msgs.geometry_msgs.Pose import Pose - from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped - - # Handle both Pose and PoseStamped - if isinstance(pose, PoseStamped): - return cls( - translation=pose.position, - rotation=pose.orientation, - frame_id=pose.frame_id, - child_frame_id=frame_id, - ts=pose.ts, - ) - elif isinstance(pose, Pose): - return cls( - translation=pose.position, - rotation=pose.orientation, - child_frame_id=frame_id, - ) - else: - raise TypeError(f"Expected Pose or PoseStamped, got {type(pose).__name__}") - - def to_pose(self, **kwargs: object) -> PoseStamped: - """Create a Transform from a Pose or PoseStamped. - - Args: - pose: A Pose or PoseStamped object to convert - - Returns: - A Transform with the same translation and rotation as the pose - """ - # Import locally to avoid circular imports - from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped as _PoseStamped - - # Handle both Pose and PoseStamped - result: PoseStamped = _PoseStamped( - **{ - "position": self.translation, - "orientation": self.rotation, - "frame_id": self.frame_id, - }, - **kwargs, - ) - return result - - def to_matrix(self) -> np.ndarray: # type: ignore[name-defined] - """Convert Transform to a 4x4 transformation matrix. - - Returns a homogeneous transformation matrix that represents both - the rotation and translation of this transform. - - Returns: - np.ndarray: A 4x4 homogeneous transformation matrix - """ - import numpy as np - - # Extract quaternion components - x, y, z, w = self.rotation.x, self.rotation.y, self.rotation.z, self.rotation.w - - # Build rotation matrix from quaternion using standard formula - # This avoids numerical issues compared to converting to axis-angle first - rotation_matrix = np.array( - [ - [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w)], - [2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w)], - [2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)], - ] - ) - - # Build 4x4 homogeneous transformation matrix - matrix = np.eye(4) - matrix[:3, :3] = rotation_matrix - matrix[:3, 3] = [self.translation.x, self.translation.y, self.translation.z] - - return matrix - - def lcm_encode(self) -> bytes: - # we get a circular import otherwise - from dimos.msgs.tf2_msgs.TFMessage import TFMessage - - return TFMessage(self).lcm_encode() - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> Transform: - """Decode from LCM TFMessage bytes.""" - from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage - - lcm_msg = LCMTFMessage.lcm_decode(data) - - if not lcm_msg.transforms: - raise ValueError("No transforms found in LCM message") - - # Get the first transform from the message - lcm_transform_stamped = lcm_msg.transforms[0] - - # Extract timestamp from header - ts = lcm_transform_stamped.header.stamp.sec + ( - lcm_transform_stamped.header.stamp.nsec / 1_000_000_000 - ) - - # Create and return Transform instance - return cls( - translation=Vector3( - lcm_transform_stamped.transform.translation.x, - lcm_transform_stamped.transform.translation.y, - lcm_transform_stamped.transform.translation.z, - ), - rotation=Quaternion( - lcm_transform_stamped.transform.rotation.x, - lcm_transform_stamped.transform.rotation.y, - lcm_transform_stamped.transform.rotation.z, - lcm_transform_stamped.transform.rotation.w, - ), - frame_id=lcm_transform_stamped.header.frame_id, - child_frame_id=lcm_transform_stamped.child_frame_id, - ts=ts, - ) - - def to_rerun(self) -> rr.Transform3D: - """Convert to rerun Transform3D format with frame IDs. - - Returns: - rr.Transform3D archetype for logging to rerun with parent/child frames - """ - import rerun as rr - - return rr.Transform3D( - translation=[self.translation.x, self.translation.y, self.translation.z], - rotation=self.rotation.to_rerun(), - parent_frame="tf#/" + self.frame_id, - child_frame="tf#/" + self.child_frame_id, - ) diff --git a/dimos/msgs/geometry_msgs/Twist.py b/dimos/msgs/geometry_msgs/Twist.py deleted file mode 100644 index be5d9a34a0..0000000000 --- a/dimos/msgs/geometry_msgs/Twist.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dimos_lcm.geometry_msgs import Twist as LCMTwist -from plum import dispatch - -# Import Quaternion at runtime for beartype compatibility -# (beartype needs to resolve forward references at runtime) -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike - - -class Twist(LCMTwist): # type: ignore[misc] - linear: Vector3 - angular: Vector3 - msg_name = "geometry_msgs.Twist" - - @dispatch - def __init__(self) -> None: - """Initialize a zero twist (no linear or angular velocity).""" - self.linear = Vector3() - self.angular = Vector3() - - @dispatch # type: ignore[no-redef] - def __init__(self, linear: VectorLike, angular: VectorLike) -> None: - """Initialize a twist from linear and angular velocities.""" - - self.linear = Vector3(linear) - self.angular = Vector3(angular) - - @dispatch # type: ignore[no-redef] - def __init__(self, linear: VectorLike, angular: Quaternion) -> None: - """Initialize a twist from linear velocity and angular as quaternion (converted to euler).""" - self.linear = Vector3(linear) - self.angular = angular.to_euler() - - @dispatch # type: ignore[no-redef] - def __init__(self, twist: Twist) -> None: - """Initialize from another Twist (copy constructor).""" - self.linear = Vector3(twist.linear) - self.angular = Vector3(twist.angular) - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_twist: LCMTwist) -> None: - """Initialize from an LCM Twist.""" - self.linear = Vector3(lcm_twist.linear) - self.angular = Vector3(lcm_twist.angular) - - @dispatch # type: ignore[no-redef] - def __init__(self, **kwargs) -> None: - """Handle keyword arguments for LCM compatibility.""" - linear = kwargs.get("linear", Vector3()) - angular = kwargs.get("angular", Vector3()) - - self.__init__(linear, angular) - - def __repr__(self) -> str: - return f"Twist(linear={self.linear!r}, angular={self.angular!r})" - - def __str__(self) -> str: - return f"Twist:\n Linear: {self.linear}\n Angular: {self.angular}" - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two twists are equal.""" - if not isinstance(other, Twist): - return False - return self.linear == other.linear and self.angular == other.angular - - @classmethod - def zero(cls) -> Twist: - """Create a zero twist (no motion).""" - return cls() - - def is_zero(self) -> bool: - """Check if this is a zero twist (no linear or angular velocity).""" - return self.linear.is_zero() and self.angular.is_zero() - - def __sub__(self, other: Twist) -> Twist: - """Component-wise subtraction: self - other.""" - if not isinstance(other, Twist): - return NotImplemented - return Twist( - linear=self.linear - other.linear, - angular=self.angular - other.angular, - ) - - def __add__(self, other: Twist) -> Twist: - """Component-wise addition: self + other.""" - if not isinstance(other, Twist): - return NotImplemented - return Twist( - linear=self.linear + other.linear, - angular=self.angular + other.angular, - ) - - def __bool__(self) -> bool: - """Boolean conversion for Twist. - - A Twist is considered False if it's a zero twist (no motion), - and True otherwise. - - Returns: - False if twist is zero, True otherwise - """ - return not self.is_zero() - - -__all__ = ["Quaternion", "Twist"] diff --git a/dimos/msgs/geometry_msgs/TwistStamped.py b/dimos/msgs/geometry_msgs/TwistStamped.py deleted file mode 100644 index ab3dc507b9..0000000000 --- a/dimos/msgs/geometry_msgs/TwistStamped.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import BinaryIO, TypeAlias - -from dimos_lcm.geometry_msgs import TwistStamped as LCMTwistStamped -from plum import dispatch - -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.Vector3 import VectorConvertable -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from TwistStamped -TwistConvertable: TypeAlias = ( - tuple[VectorConvertable, VectorConvertable] | LCMTwistStamped | dict[str, VectorConvertable] -) - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class TwistStamped(Twist, Timestamped): - msg_name = "geometry_msgs.TwistStamped" - ts: float - frame_id: str - - @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: # type: ignore[no-untyped-def] - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - super().__init__(**kwargs) - - def lcm_encode(self) -> bytes: - lcm_msg = LCMTwistStamped() - lcm_msg.twist = self - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> TwistStamped: - lcm_msg = LCMTwistStamped.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - linear=[lcm_msg.twist.linear.x, lcm_msg.twist.linear.y, lcm_msg.twist.linear.z], - angular=[lcm_msg.twist.angular.x, lcm_msg.twist.angular.y, lcm_msg.twist.angular.z], - ) - - def __str__(self) -> str: - return ( - f"TwistStamped(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " - f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}])" - ) diff --git a/dimos/msgs/geometry_msgs/TwistWithCovariance.py b/dimos/msgs/geometry_msgs/TwistWithCovariance.py deleted file mode 100644 index 90ddb94d7c..0000000000 --- a/dimos/msgs/geometry_msgs/TwistWithCovariance.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import Any, TypeAlias - -from dimos_lcm.geometry_msgs import ( - TwistWithCovariance as LCMTwistWithCovariance, -) -import numpy as np -from plum import dispatch - -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorConvertable - -# Types that can be converted to/from TwistWithCovariance -TwistWithCovarianceConvertable: TypeAlias = ( - tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] # type: ignore[type-arg] - | LCMTwistWithCovariance - | dict[str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray] # type: ignore[type-arg] -) - - -class TwistWithCovariance(LCMTwistWithCovariance): # type: ignore[misc] - twist: Twist - covariance: np.ndarray[tuple[int], np.dtype[np.floating[Any]]] - msg_name = "geometry_msgs.TwistWithCovariance" - - @dispatch - def __init__(self) -> None: - """Initialize with default twist and zero covariance.""" - self.twist = Twist() - self.covariance = np.zeros(36) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - twist: Twist | tuple[VectorConvertable, VectorConvertable], - covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] - ) -> None: - """Initialize with twist and optional covariance.""" - if isinstance(twist, Twist): - self.twist = twist - else: - # Assume it's a tuple of (linear, angular) - self.twist = Twist(twist[0], twist[1]) - - if covariance is None: - self.covariance = np.zeros(36) - else: - self.covariance = np.array(covariance, dtype=float).reshape(36) - - @dispatch # type: ignore[no-redef] - def __init__(self, twist_with_cov: TwistWithCovariance) -> None: - """Initialize from another TwistWithCovariance (copy constructor).""" - self.twist = Twist(twist_with_cov.twist) - self.covariance = np.array(twist_with_cov.covariance).copy() - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_twist_with_cov: LCMTwistWithCovariance) -> None: - """Initialize from an LCM TwistWithCovariance.""" - self.twist = Twist(lcm_twist_with_cov.twist) - self.covariance = np.array(lcm_twist_with_cov.covariance) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - twist_dict: dict[ # type: ignore[type-arg] - str, Twist | tuple[VectorConvertable, VectorConvertable] | list[float] | np.ndarray - ], - ) -> None: - """Initialize from a dictionary with 'twist' and 'covariance' keys.""" - twist = twist_dict["twist"] - if isinstance(twist, Twist): - self.twist = twist - else: - # Assume it's a tuple of (linear, angular) - self.twist = Twist(twist[0], twist[1]) - - covariance = twist_dict.get("covariance") - if covariance is None: - self.covariance = np.zeros(36) - else: - self.covariance = np.array(covariance, dtype=float).reshape(36) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - twist_tuple: tuple[ # type: ignore[type-arg] - Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray - ], - ) -> None: - """Initialize from a tuple of (twist, covariance).""" - twist = twist_tuple[0] - if isinstance(twist, Twist): - self.twist = twist - else: - # Assume it's a tuple of (linear, angular) - self.twist = Twist(twist[0], twist[1]) - self.covariance = np.array(twist_tuple[1], dtype=float).reshape(36) - - def __getattribute__(self, name: str): # type: ignore[no-untyped-def] - """Override to ensure covariance is always returned as numpy array.""" - if name == "covariance": - cov = object.__getattribute__(self, "covariance") - if not isinstance(cov, np.ndarray): - return np.array(cov, dtype=float) - return cov - return super().__getattribute__(name) - - def __setattr__(self, name: str, value) -> None: # type: ignore[no-untyped-def] - """Override to ensure covariance is stored as numpy array.""" - if name == "covariance": - if not isinstance(value, np.ndarray): - value = np.array(value, dtype=float).reshape(36) - super().__setattr__(name, value) - - @property - def linear(self) -> Vector3: - """Linear velocity vector.""" - return self.twist.linear - - @property - def angular(self) -> Vector3: - """Angular velocity vector.""" - return self.twist.angular - - @property - def covariance_matrix(self) -> np.ndarray: # type: ignore[type-arg] - """Get covariance as 6x6 matrix.""" - return self.covariance.reshape(6, 6) # type: ignore[has-type, no-any-return] - - @covariance_matrix.setter - def covariance_matrix(self, value: np.ndarray) -> None: # type: ignore[type-arg] - """Set covariance from 6x6 matrix.""" - self.covariance = np.array(value).reshape(36) # type: ignore[has-type] - - def __repr__(self) -> str: - return f"TwistWithCovariance(twist={self.twist!r}, covariance=<{self.covariance.shape[0] if isinstance(self.covariance, np.ndarray) else len(self.covariance)} elements>)" # type: ignore[has-type] - - def __str__(self) -> str: - return ( - f"TwistWithCovariance(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " - f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}], " - f"cov_trace={np.trace(self.covariance_matrix):.3f})" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two TwistWithCovariance are equal.""" - if not isinstance(other, TwistWithCovariance): - return False - return self.twist == other.twist and np.allclose(self.covariance, other.covariance) # type: ignore[has-type] - - def is_zero(self) -> bool: - """Check if this is a zero twist (no linear or angular velocity).""" - return self.twist.is_zero() - - def __bool__(self) -> bool: - """Boolean conversion - False if zero twist, True otherwise.""" - return not self.is_zero() - - def lcm_encode(self) -> bytes: - """Encode to LCM binary format.""" - lcm_msg = LCMTwistWithCovariance() - lcm_msg.twist = self.twist - # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] - lcm_msg.covariance = self.covariance.tolist() # type: ignore[has-type] - else: - lcm_msg.covariance = list(self.covariance) # type: ignore[has-type] - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> TwistWithCovariance: - """Decode from LCM binary format.""" - lcm_msg = LCMTwistWithCovariance.lcm_decode(data) - twist = Twist( - linear=[lcm_msg.twist.linear.x, lcm_msg.twist.linear.y, lcm_msg.twist.linear.z], - angular=[lcm_msg.twist.angular.x, lcm_msg.twist.angular.y, lcm_msg.twist.angular.z], - ) - return cls(twist, lcm_msg.covariance) diff --git a/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py deleted file mode 100644 index 82d0ba7eb2..0000000000 --- a/dimos/msgs/geometry_msgs/TwistWithCovarianceStamped.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TypeAlias - -from dimos_lcm.geometry_msgs import ( - TwistWithCovarianceStamped as LCMTwistWithCovarianceStamped, -) -import numpy as np -from plum import dispatch - -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.msgs.geometry_msgs.Vector3 import VectorConvertable -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from TwistWithCovarianceStamped -TwistWithCovarianceStampedConvertable: TypeAlias = ( - tuple[Twist | tuple[VectorConvertable, VectorConvertable], list[float] | np.ndarray] # type: ignore[type-arg] - | LCMTwistWithCovarianceStamped - | dict[ - str, - Twist - | tuple[VectorConvertable, VectorConvertable] - | list[float] - | np.ndarray # type: ignore[type-arg] - | float - | str, - ] -) - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class TwistWithCovarianceStamped(TwistWithCovariance, Timestamped): - msg_name = "geometry_msgs.TwistWithCovarianceStamped" - ts: float - frame_id: str - - @dispatch - def __init__(self, ts: float = 0.0, frame_id: str = "", **kwargs) -> None: - """Initialize with timestamp and frame_id.""" - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - super().__init__(**kwargs) - - @dispatch # type: ignore[no-redef] - def __init__( - self, - ts: float = 0.0, - frame_id: str = "", - twist: Twist | tuple[VectorConvertable, VectorConvertable] | None = None, - covariance: list[float] | np.ndarray | None = None, # type: ignore[type-arg] - ) -> None: - """Initialize with timestamp, frame_id, twist and covariance.""" - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - if twist is None: - super().__init__() - else: - super().__init__(twist, covariance) - - def lcm_encode(self) -> bytes: - lcm_msg = LCMTwistWithCovarianceStamped() - lcm_msg.twist.twist = self.twist - # LCM expects list, not numpy array - if isinstance(self.covariance, np.ndarray): # type: ignore[has-type] - lcm_msg.twist.covariance = self.covariance.tolist() # type: ignore[has-type] - else: - lcm_msg.twist.covariance = list(self.covariance) # type: ignore[has-type] - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> TwistWithCovarianceStamped: - lcm_msg = LCMTwistWithCovarianceStamped.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - twist=Twist( - linear=[ - lcm_msg.twist.twist.linear.x, - lcm_msg.twist.twist.linear.y, - lcm_msg.twist.twist.linear.z, - ], - angular=[ - lcm_msg.twist.twist.angular.x, - lcm_msg.twist.twist.angular.y, - lcm_msg.twist.twist.angular.z, - ], - ), - covariance=lcm_msg.twist.covariance, - ) - - def __str__(self) -> str: - return ( - f"TwistWithCovarianceStamped(linear=[{self.linear.x:.3f}, {self.linear.y:.3f}, {self.linear.z:.3f}], " - f"angular=[{self.angular.x:.3f}, {self.angular.y:.3f}, {self.angular.z:.3f}], " - f"cov_trace={np.trace(self.covariance_matrix):.3f})" - ) diff --git a/dimos/msgs/geometry_msgs/Vector3.py b/dimos/msgs/geometry_msgs/Vector3.py deleted file mode 100644 index 8be8cdf2ae..0000000000 --- a/dimos/msgs/geometry_msgs/Vector3.py +++ /dev/null @@ -1,434 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections.abc import Sequence -from typing import Any, TypeAlias - -from dimos_lcm.geometry_msgs import Vector3 as LCMVector3 -import numpy as np - -# Types that can be converted to/from Vector -VectorConvertable: TypeAlias = Sequence[int | float] | LCMVector3 | np.ndarray # type: ignore[type-arg] - - -def _ensure_3d(data: np.ndarray) -> np.ndarray: # type: ignore[type-arg] - """Ensure the data array is exactly 3D by padding with zeros or raising an exception if too long.""" - if len(data) == 3: - return data - elif len(data) < 3: - padded = np.zeros(3, dtype=float) - padded[: len(data)] = data - return padded - else: - raise ValueError( - f"Vector3 cannot be initialized with more than 3 components. Got {len(data)} components." - ) - - -class Vector3(LCMVector3): # type: ignore[misc] - x: float = 0.0 - y: float = 0.0 - z: float = 0.0 - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize a 3D vector. - - Supported forms: - Vector3() # zero vector - Vector3(x) # (x, 0, 0) - Vector3(x, y) # (x, y, 0) - Vector3(x, y, z) # (x, y, z) - Vector3(x=1, y=2, z=3) # keyword args - Vector3([x, y, z]) # sequence - Vector3(np.array([x, y, z])) # numpy array - Vector3(other_vector3) # copy constructor - Vector3(lcm_vector3) # from LCM message - """ - if kwargs and not args: - # Keyword arguments: Vector3(x=1, y=2, z=3) - self.x = float(kwargs.get("x", 0.0)) - self.y = float(kwargs.get("y", 0.0)) - self.z = float(kwargs.get("z", 0.0)) - elif not args: - # No arguments: zero vector - self.x = 0.0 - self.y = 0.0 - self.z = 0.0 - elif len(args) == 1: - arg = args[0] - if isinstance(arg, Vector3): - # Copy constructor - self.x = arg.x - self.y = arg.y - self.z = arg.z - elif isinstance(arg, LCMVector3): - # From LCM Vector3 - self.x = float(arg.x) - self.y = float(arg.y) - self.z = float(arg.z) - elif isinstance(arg, np.ndarray): - # From numpy array - data = _ensure_3d(np.array(arg, dtype=float)) - self.x = float(data[0]) - self.y = float(data[1]) - self.z = float(data[2]) - elif isinstance(arg, (list, tuple)): - # From sequence - data = _ensure_3d(np.array(arg, dtype=float)) - self.x = float(data[0]) - self.y = float(data[1]) - self.z = float(data[2]) - elif isinstance(arg, (int, float)): - # Single numeric value: (x, 0, 0) - self.x = float(arg) - self.y = 0.0 - self.z = 0.0 - else: - raise TypeError(f"Cannot initialize Vector3 from {type(arg)}") - elif len(args) == 2: - # Two numeric values: (x, y, 0) - self.x = float(args[0]) - self.y = float(args[1]) - self.z = 0.0 - elif len(args) == 3: - # Three numeric values: (x, y, z) - self.x = float(args[0]) - self.y = float(args[1]) - self.z = float(args[2]) - else: - raise TypeError(f"Vector3 takes at most 3 positional arguments ({len(args)} given)") - - @property - def as_tuple(self) -> tuple[float, float, float]: - return (self.x, self.y, self.z) - - @property - def yaw(self) -> float: - return self.z - - @property - def pitch(self) -> float: - return self.y - - @property - def roll(self) -> float: - return self.x - - @property - def data(self) -> np.ndarray: # type: ignore[type-arg] - """Get the underlying numpy array.""" - return np.array([self.x, self.y, self.z], dtype=float) - - def __getitem__(self, idx: int) -> float: - if idx == 0: - return self.x - elif idx == 1: - return self.y - elif idx == 2: - return self.z - else: - raise IndexError(f"Vector3 index {idx} out of range [0-2]") - - def __repr__(self) -> str: - return f"Vector({self.data})" - - def __str__(self) -> str: - def getArrow() -> str: - repr = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"] - - if self.x == 0 and self.y == 0: - return "·" - - # Calculate angle in radians and convert to directional index - angle = np.arctan2(self.y, self.x) - # Map angle to 0-7 index (8 directions) with proper orientation - dir_index = int(((angle + np.pi) * 4 / np.pi) % 8) - # Get directional arrow symbol - return repr[dir_index] - - return f"{getArrow()} Vector {self.__repr__()}" - - def agent_encode(self) -> dict[str, float]: - """Encode the vector for agent communication.""" - return {"x": self.x, "y": self.y, "z": self.z} - - def serialize(self) -> dict[str, Any]: - """Serialize the vector to a tuple.""" - return {"type": "vector", "c": (self.x, self.y, self.z)} - - def __eq__(self, other: object) -> bool: - """Check if two vectors are equal using numpy's allclose for floating point comparison.""" - if not isinstance(other, Vector3): - return False - return bool(np.allclose([self.x, self.y, self.z], [other.x, other.y, other.z])) - - def __add__(self, other: VectorConvertable | Vector3) -> Vector3: - other_vector: Vector3 = to_vector(other) - return self.__class__( - self.x + other_vector.x, self.y + other_vector.y, self.z + other_vector.z - ) - - def __sub__(self, other: VectorConvertable | Vector3) -> Vector3: - other_vector = to_vector(other) - return self.__class__( - self.x - other_vector.x, self.y - other_vector.y, self.z - other_vector.z - ) - - def __mul__(self, scalar: float) -> Vector3: - return self.__class__(self.x * scalar, self.y * scalar, self.z * scalar) - - def __rmul__(self, scalar: float) -> Vector3: - return self.__mul__(scalar) - - def __truediv__(self, scalar: float) -> Vector3: - return self.__class__(self.x / scalar, self.y / scalar, self.z / scalar) - - def __neg__(self) -> Vector3: - return self.__class__(-self.x, -self.y, -self.z) - - def dot(self, other: VectorConvertable | Vector3) -> float: - """Compute dot product.""" - other_vector = to_vector(other) - return float(self.x * other_vector.x + self.y * other_vector.y + self.z * other_vector.z) - - def cross(self, other: VectorConvertable | Vector3) -> Vector3: - """Compute cross product (3D vectors only).""" - other_vector = to_vector(other) - return self.__class__( - self.y * other_vector.z - self.z * other_vector.y, - self.z * other_vector.x - self.x * other_vector.z, - self.x * other_vector.y - self.y * other_vector.x, - ) - - def magnitude(self) -> float: - """Alias for length().""" - return self.length() - - def length(self) -> float: - """Compute the Euclidean length (magnitude) of the vector.""" - return float(np.sqrt(self.x * self.x + self.y * self.y + self.z * self.z)) - - def length_squared(self) -> float: - """Compute the squared length of the vector (faster than length()).""" - return float(self.x * self.x + self.y * self.y + self.z * self.z) - - def normalize(self) -> Vector3: - """Return a normalized unit vector in the same direction.""" - length = self.length() - if length < 1e-10: # Avoid division by near-zero - return self.__class__(0.0, 0.0, 0.0) - return self.__class__(self.x / length, self.y / length, self.z / length) - - def to_2d(self) -> Vector3: - """Convert a vector to a 2D vector by taking only the x and y components (z=0).""" - return self.__class__(self.x, self.y, 0.0) - - def distance(self, other: VectorConvertable | Vector3) -> float: - """Compute Euclidean distance to another vector.""" - other_vector = to_vector(other) - dx = self.x - other_vector.x - dy = self.y - other_vector.y - dz = self.z - other_vector.z - return float(np.sqrt(dx * dx + dy * dy + dz * dz)) - - def distance_squared(self, other: VectorConvertable | Vector3) -> float: - """Compute squared Euclidean distance to another vector (faster than distance()).""" - other_vector = to_vector(other) - dx = self.x - other_vector.x - dy = self.y - other_vector.y - dz = self.z - other_vector.z - return float(dx * dx + dy * dy + dz * dz) - - def angle(self, other: VectorConvertable | Vector3) -> float: - """Compute the angle (in radians) between this vector and another.""" - other_vector = to_vector(other) - this_length = self.length() - other_length = other_vector.length() - - if this_length < 1e-10 or other_length < 1e-10: - return 0.0 - - cos_angle = np.clip( - self.dot(other_vector) / (this_length * other_length), - -1.0, - 1.0, - ) - return float(np.arccos(cos_angle)) - - def project(self, onto: VectorConvertable | Vector3) -> Vector3: - """Project this vector onto another vector.""" - onto_vector = to_vector(onto) - onto_length_sq = ( - onto_vector.x * onto_vector.x - + onto_vector.y * onto_vector.y - + onto_vector.z * onto_vector.z - ) - if onto_length_sq < 1e-10: - return self.__class__(0.0, 0.0, 0.0) - - scalar_projection = self.dot(onto_vector) / onto_length_sq - return self.__class__( - scalar_projection * onto_vector.x, - scalar_projection * onto_vector.y, - scalar_projection * onto_vector.z, - ) - - @classmethod - def zeros(cls) -> Vector3: - """Create a zero 3D vector.""" - return cls() - - @classmethod - def ones(cls) -> Vector3: - """Create a 3D vector of ones.""" - return cls(1.0, 1.0, 1.0) - - @classmethod - def unit_x(cls) -> Vector3: - """Create a unit vector in the x direction.""" - return cls(1.0, 0.0, 0.0) - - @classmethod - def unit_y(cls) -> Vector3: - """Create a unit vector in the y direction.""" - return cls(0.0, 1.0, 0.0) - - @classmethod - def unit_z(cls) -> Vector3: - """Create a unit vector in the z direction.""" - return cls(0.0, 0.0, 1.0) - - def to_list(self) -> list[float]: - """Convert the vector to a list.""" - return [self.x, self.y, self.z] - - def to_tuple(self) -> tuple[float, float, float]: - """Convert the vector to a tuple.""" - return (self.x, self.y, self.z) - - def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] - """Convert the vector to a numpy array.""" - return np.array([self.x, self.y, self.z], dtype=float) - - def is_zero(self) -> bool: - """Check if this is a zero vector (all components are zero). - - Returns: - True if all components are zero, False otherwise - """ - return bool(np.allclose([self.x, self.y, self.z], 0.0)) - - @property - def quaternion(self) -> Quaternion: # type: ignore[name-defined] - return self.to_quaternion() - - def to_quaternion(self) -> Quaternion: # type: ignore[name-defined] - """Convert Vector3 representing Euler angles (roll, pitch, yaw) to a Quaternion. - - Assumes this Vector3 contains Euler angles in radians: - - x component: roll (rotation around x-axis) - - y component: pitch (rotation around y-axis) - - z component: yaw (rotation around z-axis) - - Returns: - Quaternion: The equivalent quaternion representation - """ - # Import here to avoid circular imports - from dimos.msgs.geometry_msgs.Quaternion import Quaternion - - # Extract Euler angles - roll = self.x - pitch = self.y - yaw = self.z - - # Convert Euler angles to quaternion using ZYX convention - # Source: https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles - - # Compute half angles - cy = np.cos(yaw * 0.5) - sy = np.sin(yaw * 0.5) - cp = np.cos(pitch * 0.5) - sp = np.sin(pitch * 0.5) - cr = np.cos(roll * 0.5) - sr = np.sin(roll * 0.5) - - # Compute quaternion components - w = cr * cp * cy + sr * sp * sy - x = sr * cp * cy - cr * sp * sy - y = cr * sp * cy + sr * cp * sy - z = cr * cp * sy - sr * sp * cy - - return Quaternion(x, y, z, w) - - def __bool__(self) -> bool: - """Boolean conversion for Vector. - - A Vector is considered False if it's a zero vector (all components are zero), - and True otherwise. - - Returns: - False if vector is zero, True otherwise - """ - return not self.is_zero() - - -def to_numpy(value: Vector3 | np.ndarray | Sequence[int | float]) -> np.ndarray: # type: ignore[type-arg] - """Convert a value to a numpy array.""" - if isinstance(value, Vector3): - return value.to_numpy() - elif isinstance(value, np.ndarray): - return value - else: - return np.array(value, dtype=float) - - -def to_vector(value: VectorConvertable | Vector3) -> Vector3: - """Convert a vector-compatible value to a Vector3 object.""" - if isinstance(value, Vector3): - return value - return Vector3(value) - - -def to_tuple(value: Vector3 | np.ndarray | Sequence[int | float]) -> tuple[float, ...]: # type: ignore[type-arg] - """Convert a value to a tuple.""" - if isinstance(value, Vector3): - return value.to_tuple() - elif isinstance(value, np.ndarray): - return tuple(value.tolist()) - elif isinstance(value, tuple): - return value - else: - return tuple(value) - - -def to_list(value: Vector3 | np.ndarray | Sequence[int | float]) -> list[float]: # type: ignore[type-arg] - """Convert a value to a list.""" - if isinstance(value, Vector3): - return value.to_list() - elif isinstance(value, np.ndarray): - result: list[float] = value.tolist() - return result - elif isinstance(value, list): - return value - else: - return list(value) - - -VectorLike: TypeAlias = VectorConvertable | Vector3 - - -def make_vector3(x: float, y: float, z: float) -> Vector3: - return Vector3(x, y, z) diff --git a/dimos/msgs/geometry_msgs/Wrench.py b/dimos/msgs/geometry_msgs/Wrench.py deleted file mode 100644 index c0e1273771..0000000000 --- a/dimos/msgs/geometry_msgs/Wrench.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass - -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -@dataclass -class Wrench: - """ - Represents a force and torque in 3D space. - - This is equivalent to ROS geometry_msgs/Wrench. - """ - - force: Vector3 = None # type: ignore[assignment] # Force vector (N) - torque: Vector3 = None # type: ignore[assignment] # Torque vector (Nm) - - def __post_init__(self) -> None: - if self.force is None: - self.force = Vector3(0.0, 0.0, 0.0) - if self.torque is None: - self.torque = Vector3(0.0, 0.0, 0.0) - - def __repr__(self) -> str: - return f"Wrench(force={self.force}, torque={self.torque})" diff --git a/dimos/msgs/geometry_msgs/WrenchStamped.py b/dimos/msgs/geometry_msgs/WrenchStamped.py deleted file mode 100644 index d01d663194..0000000000 --- a/dimos/msgs/geometry_msgs/WrenchStamped.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -import time - -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.geometry_msgs.Wrench import Wrench -from dimos.types.timestamped import Timestamped - - -@dataclass -class WrenchStamped(Timestamped): - """ - Represents a stamped force/torque measurement. - - This is equivalent to ROS geometry_msgs/WrenchStamped. - """ - - msg_name = "geometry_msgs.WrenchStamped" - ts: float = 0.0 - frame_id: str = "" - wrench: Wrench = None # type: ignore[assignment] - - def __post_init__(self) -> None: - if self.ts == 0.0: - self.ts = time.time() - if self.wrench is None: - self.wrench = Wrench() - - @classmethod - def from_force_torque_array( # type: ignore[no-untyped-def] - cls, - ft_data: list, # type: ignore[type-arg] - frame_id: str = "ft_sensor", - ts: float | None = None, - ): - """ - Create WrenchStamped from a 6-element force/torque array. - - Args: - ft_data: [fx, fy, fz, tx, ty, tz] - frame_id: Reference frame - ts: Timestamp (defaults to current time) - - Returns: - WrenchStamped instance - """ - if len(ft_data) != 6: - raise ValueError(f"Expected 6 elements, got {len(ft_data)}") - - return cls( - ts=ts if ts is not None else time.time(), - frame_id=frame_id, - wrench=Wrench( - force=Vector3(x=ft_data[0], y=ft_data[1], z=ft_data[2]), - torque=Vector3(x=ft_data[3], y=ft_data[4], z=ft_data[5]), - ), - ) - - def __repr__(self) -> str: - return f"WrenchStamped(ts={self.ts}, frame_id='{self.frame_id}', wrench={self.wrench})" diff --git a/dimos/msgs/geometry_msgs/__init__.py b/dimos/msgs/geometry_msgs/__init__.py deleted file mode 100644 index 3c6a742fec..0000000000 --- a/dimos/msgs/geometry_msgs/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -from dimos.msgs.geometry_msgs.Pose import Pose, PoseLike, to_pose -from dimos.msgs.geometry_msgs.PoseArray import PoseArray -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance -from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import PoseWithCovarianceStamped -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.geometry_msgs.Transform import Transform -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistStamped import TwistStamped -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import TwistWithCovarianceStamped -from dimos.msgs.geometry_msgs.Vector3 import Vector3, VectorLike -from dimos.msgs.geometry_msgs.Wrench import Wrench -from dimos.msgs.geometry_msgs.WrenchStamped import WrenchStamped - -__all__ = [ - "Pose", - "PoseArray", - "PoseLike", - "PoseStamped", - "PoseWithCovariance", - "PoseWithCovarianceStamped", - "Quaternion", - "Transform", - "Twist", - "TwistStamped", - "TwistWithCovariance", - "TwistWithCovarianceStamped", - "Vector3", - "VectorLike", - "Wrench", - "WrenchStamped", - "to_pose", -] diff --git a/dimos/msgs/geometry_msgs/test_Pose.py b/dimos/msgs/geometry_msgs/test_Pose.py deleted file mode 100644 index dac5ed6207..0000000000 --- a/dimos/msgs/geometry_msgs/test_Pose.py +++ /dev/null @@ -1,749 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pickle - -from dimos_lcm.geometry_msgs import Pose as LCMPose -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.Pose import Pose, to_pose -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -def test_pose_default_init() -> None: - """Test that default initialization creates a pose at origin with identity orientation.""" - pose = Pose() - - # Position should be at origin - assert pose.position.x == 0.0 - assert pose.position.y == 0.0 - assert pose.position.z == 0.0 - - # Orientation should be identity quaternion - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - # Test convenience properties - assert pose.x == 0.0 - assert pose.y == 0.0 - assert pose.z == 0.0 - - -def test_pose_pose_init() -> None: - """Test initialization with position coordinates only (identity orientation).""" - pose_data = Pose(1.0, 2.0, 3.0) - - pose = to_pose(pose_data) - - # Position should be as specified - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should be identity quaternion - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - # Test convenience properties - assert pose.x == 1.0 - assert pose.y == 2.0 - assert pose.z == 3.0 - - -def test_pose_position_init() -> None: - """Test initialization with position coordinates only (identity orientation).""" - pose = Pose(1.0, 2.0, 3.0) - - # Position should be as specified - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should be identity quaternion - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - # Test convenience properties - assert pose.x == 1.0 - assert pose.y == 2.0 - assert pose.z == 3.0 - - -def test_pose_full_init() -> None: - """Test initialization with position and orientation coordinates.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - - # Position should be as specified - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should be as specified - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - # Test convenience properties - assert pose.x == 1.0 - assert pose.y == 2.0 - assert pose.z == 3.0 - - -def test_pose_vector_position_init() -> None: - """Test initialization with Vector3 position (identity orientation).""" - position = Vector3(4.0, 5.0, 6.0) - pose = Pose(position) - - # Position should match the vector - assert pose.position.x == 4.0 - assert pose.position.y == 5.0 - assert pose.position.z == 6.0 - - # Orientation should be identity - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - -def test_pose_vector_quaternion_init() -> None: - """Test initialization with Vector3 position and Quaternion orientation.""" - position = Vector3(1.0, 2.0, 3.0) - orientation = Quaternion(0.1, 0.2, 0.3, 0.9) - pose = Pose(position, orientation) - - # Position should match the vector - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match the quaternion - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_list_init() -> None: - """Test initialization with lists for position and orientation.""" - position_list = [1.0, 2.0, 3.0] - orientation_list = [0.1, 0.2, 0.3, 0.9] - pose = Pose(position_list, orientation_list) - - # Position should match the list - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match the list - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_tuple_init() -> None: - """Test initialization from a tuple of (position, orientation).""" - position = [1.0, 2.0, 3.0] - orientation = [0.1, 0.2, 0.3, 0.9] - pose_tuple = (position, orientation) - pose = Pose(pose_tuple) - - # Position should match - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_dict_init() -> None: - """Test initialization from a dictionary with 'position' and 'orientation' keys.""" - pose_dict = {"position": [1.0, 2.0, 3.0], "orientation": [0.1, 0.2, 0.3, 0.9]} - pose = Pose(pose_dict) - - # Position should match - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_copy_init() -> None: - """Test initialization from another Pose (copy constructor).""" - original = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - copy = Pose(original) - - # Position should match - assert copy.position.x == 1.0 - assert copy.position.y == 2.0 - assert copy.position.z == 3.0 - - # Orientation should match - assert copy.orientation.x == 0.1 - assert copy.orientation.y == 0.2 - assert copy.orientation.z == 0.3 - assert copy.orientation.w == 0.9 - - # Should be a copy, not the same object - assert copy is not original - assert copy == original - - -def test_pose_lcm_init() -> None: - """Test initialization from an LCM Pose.""" - # Create LCM pose - lcm_pose = LCMPose() - lcm_pose.position.x = 1.0 - lcm_pose.position.y = 2.0 - lcm_pose.position.z = 3.0 - lcm_pose.orientation.x = 0.1 - lcm_pose.orientation.y = 0.2 - lcm_pose.orientation.z = 0.3 - lcm_pose.orientation.w = 0.9 - - pose = Pose(lcm_pose) - - # Position should match - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_properties() -> None: - """Test pose property access.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - - # Test position properties - assert pose.x == 1.0 - assert pose.y == 2.0 - assert pose.z == 3.0 - - # Test orientation properties (through quaternion's to_euler method) - euler = pose.orientation.to_euler() - assert pose.roll == euler.x - assert pose.pitch == euler.y - assert pose.yaw == euler.z - - -def test_pose_euler_properties_identity() -> None: - """Test pose Euler angle properties with identity orientation.""" - pose = Pose(1.0, 2.0, 3.0) # Identity orientation - - # Identity quaternion should give zero Euler angles - assert np.isclose(pose.roll, 0.0, atol=1e-10) - assert np.isclose(pose.pitch, 0.0, atol=1e-10) - assert np.isclose(pose.yaw, 0.0, atol=1e-10) - - # Euler property should also be zeros - assert np.isclose(pose.orientation.euler.x, 0.0, atol=1e-10) - assert np.isclose(pose.orientation.euler.y, 0.0, atol=1e-10) - assert np.isclose(pose.orientation.euler.z, 0.0, atol=1e-10) - - -def test_pose_repr() -> None: - """Test pose string representation.""" - pose = Pose(1.234, 2.567, 3.891, 0.1, 0.2, 0.3, 0.9) - - repr_str = repr(pose) - - # Should contain position and orientation info - assert "Pose" in repr_str - assert "position" in repr_str - assert "orientation" in repr_str - - # Should contain the actual values (approximately) - assert "1.234" in repr_str or "1.23" in repr_str - assert "2.567" in repr_str or "2.57" in repr_str - - -def test_pose_str() -> None: - """Test pose string formatting.""" - pose = Pose(1.234, 2.567, 3.891, 0.1, 0.2, 0.3, 0.9) - - str_repr = str(pose) - - # Should contain position coordinates - assert "1.234" in str_repr - assert "2.567" in str_repr - assert "3.891" in str_repr - - # Should contain Euler angles - assert "euler" in str_repr - - # Should be formatted with specified precision - assert str_repr.count("Pose") == 1 - - -def test_pose_equality() -> None: - """Test pose equality comparison.""" - pose1 = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - pose2 = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - pose3 = Pose(1.1, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) # Different position - pose4 = Pose(1.0, 2.0, 3.0, 0.11, 0.2, 0.3, 0.9) # Different orientation - - # Equal poses - assert pose1 == pose2 - assert pose2 == pose1 - - # Different poses - assert pose1 != pose3 - assert pose1 != pose4 - assert pose3 != pose4 - - # Different types - assert pose1 != "not a pose" - assert pose1 != [1.0, 2.0, 3.0] - assert pose1 is not None - - -def test_pose_with_numpy_arrays() -> None: - """Test pose initialization with numpy arrays.""" - position_array = np.array([1.0, 2.0, 3.0]) - orientation_array = np.array([0.1, 0.2, 0.3, 0.9]) - - pose = Pose(position_array, orientation_array) - - # Position should match - assert pose.position.x == 1.0 - assert pose.position.y == 2.0 - assert pose.position.z == 3.0 - - # Orientation should match - assert pose.orientation.x == 0.1 - assert pose.orientation.y == 0.2 - assert pose.orientation.z == 0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_with_mixed_types() -> None: - """Test pose initialization with mixed input types.""" - # Position as tuple, orientation as list - pose1 = Pose((1.0, 2.0, 3.0), [0.1, 0.2, 0.3, 0.9]) - - # Position as numpy array, orientation as Vector3/Quaternion - position = np.array([1.0, 2.0, 3.0]) - orientation = Quaternion(0.1, 0.2, 0.3, 0.9) - pose2 = Pose(position, orientation) - - # Both should result in the same pose - assert pose1.position.x == pose2.position.x - assert pose1.position.y == pose2.position.y - assert pose1.position.z == pose2.position.z - assert pose1.orientation.x == pose2.orientation.x - assert pose1.orientation.y == pose2.orientation.y - assert pose1.orientation.z == pose2.orientation.z - assert pose1.orientation.w == pose2.orientation.w - - -def test_to_pose_passthrough() -> None: - """Test to_pose function with Pose input (passthrough).""" - original = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - result = to_pose(original) - - # Should be the same object (passthrough) - assert result is original - - -def test_to_pose_conversion() -> None: - """Test to_pose function with convertible inputs.""" - # Note: The to_pose conversion function has type checking issues in the current implementation - # Test direct construction instead to verify the intended functionality - - # Test the intended functionality by creating poses directly - pose_tuple = ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3, 0.9]) - result1 = Pose(pose_tuple) - - assert isinstance(result1, Pose) - assert result1.position.x == 1.0 - assert result1.position.y == 2.0 - assert result1.position.z == 3.0 - assert result1.orientation.x == 0.1 - assert result1.orientation.y == 0.2 - assert result1.orientation.z == 0.3 - assert result1.orientation.w == 0.9 - - # Test with dictionary - pose_dict = {"position": [1.0, 2.0, 3.0], "orientation": [0.1, 0.2, 0.3, 0.9]} - result2 = Pose(pose_dict) - - assert isinstance(result2, Pose) - assert result2.position.x == 1.0 - assert result2.position.y == 2.0 - assert result2.position.z == 3.0 - assert result2.orientation.x == 0.1 - assert result2.orientation.y == 0.2 - assert result2.orientation.z == 0.3 - assert result2.orientation.w == 0.9 - - -def test_pose_euler_roundtrip() -> None: - """Test conversion from Euler angles to quaternion and back.""" - # Start with known Euler angles (small angles to avoid gimbal lock) - roll = 0.1 - pitch = 0.2 - yaw = 0.3 - - # Create quaternion from Euler angles - euler_vector = Vector3(roll, pitch, yaw) - quaternion = euler_vector.to_quaternion() - - # Create pose with this quaternion - pose = Pose(Vector3(0, 0, 0), quaternion) - - # Convert back to Euler angles - result_euler = pose.orientation.euler - - # Should get back the original Euler angles (within tolerance) - assert np.isclose(result_euler.x, roll, atol=1e-6) - assert np.isclose(result_euler.y, pitch, atol=1e-6) - assert np.isclose(result_euler.z, yaw, atol=1e-6) - - -def test_pose_zero_position() -> None: - """Test pose with zero position vector.""" - # Use manual construction since Vector3.zeros has signature issues - pose = Pose(0.0, 0.0, 0.0) # Position at origin with identity orientation - - assert pose.x == 0.0 - assert pose.y == 0.0 - assert pose.z == 0.0 - assert np.isclose(pose.roll, 0.0, atol=1e-10) - assert np.isclose(pose.pitch, 0.0, atol=1e-10) - assert np.isclose(pose.yaw, 0.0, atol=1e-10) - - -def test_pose_unit_vectors() -> None: - """Test pose with unit vector positions.""" - # Test unit x vector position - pose_x = Pose(Vector3.unit_x()) - assert pose_x.x == 1.0 - assert pose_x.y == 0.0 - assert pose_x.z == 0.0 - - # Test unit y vector position - pose_y = Pose(Vector3.unit_y()) - assert pose_y.x == 0.0 - assert pose_y.y == 1.0 - assert pose_y.z == 0.0 - - # Test unit z vector position - pose_z = Pose(Vector3.unit_z()) - assert pose_z.x == 0.0 - assert pose_z.y == 0.0 - assert pose_z.z == 1.0 - - -def test_pose_negative_coordinates() -> None: - """Test pose with negative coordinates.""" - pose = Pose(-1.0, -2.0, -3.0, -0.1, -0.2, -0.3, 0.9) - - # Position should be negative - assert pose.x == -1.0 - assert pose.y == -2.0 - assert pose.z == -3.0 - - # Orientation should be as specified - assert pose.orientation.x == -0.1 - assert pose.orientation.y == -0.2 - assert pose.orientation.z == -0.3 - assert pose.orientation.w == 0.9 - - -def test_pose_large_coordinates() -> None: - """Test pose with large coordinate values.""" - large_value = 1000.0 - pose = Pose(large_value, large_value, large_value) - - assert pose.x == large_value - assert pose.y == large_value - assert pose.z == large_value - - # Orientation should still be identity - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - -@pytest.mark.parametrize( - "x,y,z", - [(0.0, 0.0, 0.0), (1.0, 2.0, 3.0), (-1.0, -2.0, -3.0), (0.5, -0.5, 1.5), (100.0, -100.0, 0.0)], -) -def test_pose_parametrized_positions(x, y, z) -> None: - """Parametrized test for various position values.""" - pose = Pose(x, y, z) - - assert pose.x == x - assert pose.y == y - assert pose.z == z - - # Should have identity orientation - assert pose.orientation.x == 0.0 - assert pose.orientation.y == 0.0 - assert pose.orientation.z == 0.0 - assert pose.orientation.w == 1.0 - - -@pytest.mark.parametrize( - "qx,qy,qz,qw", - [ - (0.0, 0.0, 0.0, 1.0), # Identity - (1.0, 0.0, 0.0, 0.0), # 180° around x - (0.0, 1.0, 0.0, 0.0), # 180° around y - (0.0, 0.0, 1.0, 0.0), # 180° around z - (0.5, 0.5, 0.5, 0.5), # Equal components - ], -) -def test_pose_parametrized_orientations(qx, qy, qz, qw) -> None: - """Parametrized test for various orientation values.""" - pose = Pose(0.0, 0.0, 0.0, qx, qy, qz, qw) - - # Position should be at origin - assert pose.x == 0.0 - assert pose.y == 0.0 - assert pose.z == 0.0 - - # Orientation should match - assert pose.orientation.x == qx - assert pose.orientation.y == qy - assert pose.orientation.z == qz - assert pose.orientation.w == qw - - -def test_lcm_encode_decode() -> None: - """Test encoding and decoding of Pose to/from binary LCM format.""" - - def encodepass() -> None: - pose_source = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - binary_msg = pose_source.lcm_encode() - pose_dest = Pose.lcm_decode(binary_msg) - assert isinstance(pose_dest, Pose) - assert pose_dest is not pose_source - assert pose_dest == pose_source - # Verify we get our custom types back - assert isinstance(pose_dest.position, Vector3) - assert isinstance(pose_dest.orientation, Quaternion) - - import timeit - - print(f"{timeit.timeit(encodepass, number=1000)} ms per cycle") - - -def test_pickle_encode_decode() -> None: - """Test encoding and decoding of Pose to/from binary LCM format.""" - - def encodepass() -> None: - pose_source = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - binary_msg = pickle.dumps(pose_source) - pose_dest = pickle.loads(binary_msg) - assert isinstance(pose_dest, Pose) - assert pose_dest is not pose_source - assert pose_dest == pose_source - - import timeit - - print(f"{timeit.timeit(encodepass, number=1000)} ms per cycle") - - -def test_pose_addition_translation_only() -> None: - """Test pose addition with translation only (identity rotations).""" - # Two poses with only translations - pose1 = Pose(1.0, 2.0, 3.0) # First translation - pose2 = Pose(4.0, 5.0, 6.0) # Second translation - - # Adding should combine translations - result = pose1 + pose2 - - assert result.position.x == 5.0 # 1 + 4 - assert result.position.y == 7.0 # 2 + 5 - assert result.position.z == 9.0 # 3 + 6 - - # Orientation should remain identity - assert result.orientation.x == 0.0 - assert result.orientation.y == 0.0 - assert result.orientation.z == 0.0 - assert result.orientation.w == 1.0 - - -def test_pose_addition_with_rotation() -> None: - """Test pose addition with rotation applied to translation.""" - # First pose: at origin, rotated 90 degrees around Z (yaw) - # 90 degree rotation quaternion around Z: (0, 0, sin(pi/4), cos(pi/4)) - angle = np.pi / 2 # 90 degrees - pose1 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)) - - # Second pose: 1 unit forward (along X in its frame) - pose2 = Pose(1.0, 0.0, 0.0) - - # After rotation, the forward direction should be along Y - result = pose1 + pose2 - - # Position should be rotated - assert np.isclose(result.position.x, 0.0, atol=1e-10) - assert np.isclose(result.position.y, 1.0, atol=1e-10) - assert np.isclose(result.position.z, 0.0, atol=1e-10) - - # Orientation should be same as pose1 (pose2 has identity rotation) - assert np.isclose(result.orientation.x, 0.0, atol=1e-10) - assert np.isclose(result.orientation.y, 0.0, atol=1e-10) - assert np.isclose(result.orientation.z, np.sin(angle / 2), atol=1e-10) - assert np.isclose(result.orientation.w, np.cos(angle / 2), atol=1e-10) - - -def test_pose_addition_rotation_composition() -> None: - """Test that rotations are properly composed.""" - # First pose: 45 degrees around Z - angle1 = np.pi / 4 # 45 degrees - pose1 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle1 / 2), np.cos(angle1 / 2)) - - # Second pose: another 45 degrees around Z - angle2 = np.pi / 4 # 45 degrees - pose2 = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle2 / 2), np.cos(angle2 / 2)) - - # Result should be 90 degrees around Z - result = pose1 + pose2 - - # Check final angle is 90 degrees - expected_angle = angle1 + angle2 # 90 degrees - expected_qz = np.sin(expected_angle / 2) - expected_qw = np.cos(expected_angle / 2) - - assert np.isclose(result.orientation.z, expected_qz, atol=1e-10) - assert np.isclose(result.orientation.w, expected_qw, atol=1e-10) - - -def test_pose_addition_full_transform() -> None: - """Test full pose composition with translation and rotation.""" - # Robot pose: at (2, 1, 0), facing 90 degrees left (positive yaw) - robot_yaw = np.pi / 2 # 90 degrees - robot_pose = Pose(2.0, 1.0, 0.0, 0.0, 0.0, np.sin(robot_yaw / 2), np.cos(robot_yaw / 2)) - - # Object in robot frame: 3 units forward, 1 unit right - object_in_robot = Pose(3.0, -1.0, 0.0) - - # Compose to get object in world frame - object_in_world = robot_pose + object_in_robot - - # Robot is facing left (90 degrees), so: - # - Robot's forward (X) is world's negative Y - # - Robot's right (negative Y) is world's X - # So object should be at: robot_pos + rotated_offset - # rotated_offset: (3, -1) rotated 90° CCW = (1, 3) - assert np.isclose(object_in_world.position.x, 3.0, atol=1e-10) # 2 + 1 - assert np.isclose(object_in_world.position.y, 4.0, atol=1e-10) # 1 + 3 - assert np.isclose(object_in_world.position.z, 0.0, atol=1e-10) - - # Orientation should match robot's orientation (object has no rotation) - assert np.isclose(object_in_world.yaw, robot_yaw, atol=1e-10) - - -def test_pose_addition_chain() -> None: - """Test chaining multiple pose additions.""" - # Create a chain of transformations - pose1 = Pose(1.0, 0.0, 0.0) # Move 1 unit in X - pose2 = Pose(0.0, 1.0, 0.0) # Move 1 unit in Y (relative to pose1) - pose3 = Pose(0.0, 0.0, 1.0) # Move 1 unit in Z (relative to pose1+pose2) - - # Chain them together - result = pose1 + pose2 + pose3 - - # Should accumulate all translations - assert result.position.x == 1.0 - assert result.position.y == 1.0 - assert result.position.z == 1.0 - - -def test_pose_addition_with_convertible() -> None: - """Test pose addition with convertible types.""" - pose1 = Pose(1.0, 2.0, 3.0) - - # Add with tuple - pose_tuple = ([4.0, 5.0, 6.0], [0.0, 0.0, 0.0, 1.0]) - result1 = pose1 + pose_tuple - assert result1.position.x == 5.0 - assert result1.position.y == 7.0 - assert result1.position.z == 9.0 - - # Add with dict - pose_dict = {"position": [1.0, 0.0, 0.0], "orientation": [0.0, 0.0, 0.0, 1.0]} - result2 = pose1 + pose_dict - assert result2.position.x == 2.0 - assert result2.position.y == 2.0 - assert result2.position.z == 3.0 - - -def test_pose_identity_addition() -> None: - """Test that adding identity pose leaves pose unchanged.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - identity = Pose() # Identity pose at origin - - result = pose + identity - - # Should be unchanged - assert result.position.x == pose.position.x - assert result.position.y == pose.position.y - assert result.position.z == pose.position.z - assert result.orientation.x == pose.orientation.x - assert result.orientation.y == pose.orientation.y - assert result.orientation.z == pose.orientation.z - assert result.orientation.w == pose.orientation.w - - -def test_pose_addition_3d_rotation() -> None: - """Test pose addition with 3D rotations.""" - # First pose: rotated around X axis (roll) - roll = np.pi / 4 # 45 degrees - pose1 = Pose(1.0, 0.0, 0.0, np.sin(roll / 2), 0.0, 0.0, np.cos(roll / 2)) - - # Second pose: movement along Y and Z in local frame - pose2 = Pose(0.0, 1.0, 1.0) - - # Compose transformations - result = pose1 + pose2 - - # The Y and Z movement should be rotated around X - # After 45° rotation around X: - # - Local Y -> world Y * cos(45°) - Z * sin(45°) - # - Local Z -> world Y * sin(45°) + Z * cos(45°) - cos45 = np.cos(roll) - sin45 = np.sin(roll) - - assert np.isclose(result.position.x, 1.0, atol=1e-10) # X unchanged - assert np.isclose(result.position.y, cos45 - sin45, atol=1e-10) - assert np.isclose(result.position.z, sin45 + cos45, atol=1e-10) diff --git a/dimos/msgs/geometry_msgs/test_PoseStamped.py b/dimos/msgs/geometry_msgs/test_PoseStamped.py deleted file mode 100644 index 82250a9113..0000000000 --- a/dimos/msgs/geometry_msgs/test_PoseStamped.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pickle -import time - -from dimos.msgs.geometry_msgs import PoseStamped - - -def test_lcm_encode_decode() -> None: - """Test encoding and decoding of Pose to/from binary LCM format.""" - - pose_source = PoseStamped( - ts=time.time(), - position=(1.0, 2.0, 3.0), - orientation=(0.1, 0.2, 0.3, 0.9), - ) - binary_msg = pose_source.lcm_encode() - pose_dest = PoseStamped.lcm_decode(binary_msg) - - assert isinstance(pose_dest, PoseStamped) - assert pose_dest is not pose_source - - print(pose_source.position) - print(pose_source.orientation) - - print(pose_dest.position) - print(pose_dest.orientation) - assert pose_dest == pose_source - - -def test_pickle_encode_decode() -> None: - """Test encoding and decoding of PoseStamped to/from binary LCM format.""" - - pose_source = PoseStamped( - ts=time.time(), - position=(1.0, 2.0, 3.0), - orientation=(0.1, 0.2, 0.3, 0.9), - ) - binary_msg = pickle.dumps(pose_source) - pose_dest = pickle.loads(binary_msg) - assert isinstance(pose_dest, PoseStamped) - assert pose_dest is not pose_source - assert pose_dest == pose_source diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py b/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py deleted file mode 100644 index f6936db9f7..0000000000 --- a/dimos/msgs/geometry_msgs/test_PoseWithCovariance.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.geometry_msgs import PoseWithCovariance as LCMPoseWithCovariance -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.Pose import Pose -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance - - -def test_pose_with_covariance_default_init() -> None: - """Test that default initialization creates a pose at origin with zero covariance.""" - pose_cov = PoseWithCovariance() - - # Pose should be at origin with identity orientation - assert pose_cov.pose.position.x == 0.0 - assert pose_cov.pose.position.y == 0.0 - assert pose_cov.pose.position.z == 0.0 - assert pose_cov.pose.orientation.x == 0.0 - assert pose_cov.pose.orientation.y == 0.0 - assert pose_cov.pose.orientation.z == 0.0 - assert pose_cov.pose.orientation.w == 1.0 - - # Covariance should be all zeros - assert np.all(pose_cov.covariance == 0.0) - assert pose_cov.covariance.shape == (36,) - - -def test_pose_with_covariance_pose_init() -> None: - """Test initialization with a Pose object.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - pose_cov = PoseWithCovariance(pose) - - # Pose should match - assert pose_cov.pose.position.x == 1.0 - assert pose_cov.pose.position.y == 2.0 - assert pose_cov.pose.position.z == 3.0 - assert pose_cov.pose.orientation.x == 0.1 - assert pose_cov.pose.orientation.y == 0.2 - assert pose_cov.pose.orientation.z == 0.3 - assert pose_cov.pose.orientation.w == 0.9 - - # Covariance should be zeros by default - assert np.all(pose_cov.covariance == 0.0) - - -def test_pose_with_covariance_pose_and_covariance_init() -> None: - """Test initialization with pose and covariance.""" - pose = Pose(1.0, 2.0, 3.0) - covariance = np.arange(36, dtype=float) - pose_cov = PoseWithCovariance(pose, covariance) - - # Pose should match - assert pose_cov.pose.position.x == 1.0 - assert pose_cov.pose.position.y == 2.0 - assert pose_cov.pose.position.z == 3.0 - - # Covariance should match - assert np.array_equal(pose_cov.covariance, covariance) - - -def test_pose_with_covariance_list_covariance() -> None: - """Test initialization with covariance as a list.""" - pose = Pose(1.0, 2.0, 3.0) - covariance_list = list(range(36)) - pose_cov = PoseWithCovariance(pose, covariance_list) - - # Covariance should be converted to numpy array - assert isinstance(pose_cov.covariance, np.ndarray) - assert np.array_equal(pose_cov.covariance, np.array(covariance_list)) - - -def test_pose_with_covariance_copy_init() -> None: - """Test copy constructor.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - covariance = np.arange(36, dtype=float) - original = PoseWithCovariance(pose, covariance) - copy = PoseWithCovariance(original) - - # Should be equal but not the same object - assert copy == original - assert copy is not original - assert copy.pose is not original.pose - assert copy.covariance is not original.covariance - - # Modify original to ensure they're independent - original.covariance[0] = 999.0 - assert copy.covariance[0] != 999.0 - - -def test_pose_with_covariance_lcm_init() -> None: - """Test initialization from LCM message.""" - lcm_msg = LCMPoseWithCovariance() - lcm_msg.pose.position.x = 1.0 - lcm_msg.pose.position.y = 2.0 - lcm_msg.pose.position.z = 3.0 - lcm_msg.pose.orientation.x = 0.1 - lcm_msg.pose.orientation.y = 0.2 - lcm_msg.pose.orientation.z = 0.3 - lcm_msg.pose.orientation.w = 0.9 - lcm_msg.covariance = list(range(36)) - - pose_cov = PoseWithCovariance(lcm_msg) - - # Pose should match - assert pose_cov.pose.position.x == 1.0 - assert pose_cov.pose.position.y == 2.0 - assert pose_cov.pose.position.z == 3.0 - assert pose_cov.pose.orientation.x == 0.1 - assert pose_cov.pose.orientation.y == 0.2 - assert pose_cov.pose.orientation.z == 0.3 - assert pose_cov.pose.orientation.w == 0.9 - - # Covariance should match - assert np.array_equal(pose_cov.covariance, np.arange(36)) - - -def test_pose_with_covariance_dict_init() -> None: - """Test initialization from dictionary.""" - pose_dict = {"pose": Pose(1.0, 2.0, 3.0), "covariance": list(range(36))} - pose_cov = PoseWithCovariance(pose_dict) - - assert pose_cov.pose.position.x == 1.0 - assert pose_cov.pose.position.y == 2.0 - assert pose_cov.pose.position.z == 3.0 - assert np.array_equal(pose_cov.covariance, np.arange(36)) - - -def test_pose_with_covariance_dict_init_no_covariance() -> None: - """Test initialization from dictionary without covariance.""" - pose_dict = {"pose": Pose(1.0, 2.0, 3.0)} - pose_cov = PoseWithCovariance(pose_dict) - - assert pose_cov.pose.position.x == 1.0 - assert np.all(pose_cov.covariance == 0.0) - - -def test_pose_with_covariance_tuple_init() -> None: - """Test initialization from tuple.""" - pose = Pose(1.0, 2.0, 3.0) - covariance = np.arange(36, dtype=float) - pose_tuple = (pose, covariance) - pose_cov = PoseWithCovariance(pose_tuple) - - assert pose_cov.pose.position.x == 1.0 - assert pose_cov.pose.position.y == 2.0 - assert pose_cov.pose.position.z == 3.0 - assert np.array_equal(pose_cov.covariance, covariance) - - -def test_pose_with_covariance_properties() -> None: - """Test convenience properties.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - pose_cov = PoseWithCovariance(pose) - - # Position properties - assert pose_cov.x == 1.0 - assert pose_cov.y == 2.0 - assert pose_cov.z == 3.0 - assert pose_cov.position.x == 1.0 - assert pose_cov.position.y == 2.0 - assert pose_cov.position.z == 3.0 - - # Orientation properties - assert pose_cov.orientation.x == 0.1 - assert pose_cov.orientation.y == 0.2 - assert pose_cov.orientation.z == 0.3 - assert pose_cov.orientation.w == 0.9 - - # Euler angle properties - assert pose_cov.roll == pose.roll - assert pose_cov.pitch == pose.pitch - assert pose_cov.yaw == pose.yaw - - -def test_pose_with_covariance_matrix_property() -> None: - """Test covariance matrix property.""" - pose = Pose() - covariance_array = np.arange(36, dtype=float) - pose_cov = PoseWithCovariance(pose, covariance_array) - - # Get as matrix - cov_matrix = pose_cov.covariance_matrix - assert cov_matrix.shape == (6, 6) - assert cov_matrix[0, 0] == 0.0 - assert cov_matrix[5, 5] == 35.0 - - # Set from matrix - new_matrix = np.eye(6) * 2.0 - pose_cov.covariance_matrix = new_matrix - assert np.array_equal(pose_cov.covariance[:6], [2.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - - -def test_pose_with_covariance_repr() -> None: - """Test string representation.""" - pose = Pose(1.234, 2.567, 3.891) - pose_cov = PoseWithCovariance(pose) - - repr_str = repr(pose_cov) - assert "PoseWithCovariance" in repr_str - assert "pose=" in repr_str - assert "covariance=" in repr_str - assert "36 elements" in repr_str - - -def test_pose_with_covariance_str() -> None: - """Test string formatting.""" - pose = Pose(1.234, 2.567, 3.891) - covariance = np.eye(6).flatten() - pose_cov = PoseWithCovariance(pose, covariance) - - str_repr = str(pose_cov) - assert "PoseWithCovariance" in str_repr - assert "1.234" in str_repr - assert "2.567" in str_repr - assert "3.891" in str_repr - assert "cov_trace" in str_repr - assert "6.000" in str_repr # Trace of identity matrix is 6 - - -def test_pose_with_covariance_equality() -> None: - """Test equality comparison.""" - pose1 = Pose(1.0, 2.0, 3.0) - cov1 = np.arange(36, dtype=float) - pose_cov1 = PoseWithCovariance(pose1, cov1) - - pose2 = Pose(1.0, 2.0, 3.0) - cov2 = np.arange(36, dtype=float) - pose_cov2 = PoseWithCovariance(pose2, cov2) - - # Equal - assert pose_cov1 == pose_cov2 - - # Different pose - pose3 = Pose(1.1, 2.0, 3.0) - pose_cov3 = PoseWithCovariance(pose3, cov1) - assert pose_cov1 != pose_cov3 - - # Different covariance - cov3 = np.arange(36, dtype=float) + 1 - pose_cov4 = PoseWithCovariance(pose1, cov3) - assert pose_cov1 != pose_cov4 - - # Different type - assert pose_cov1 != "not a pose" - assert pose_cov1 is not None - - -def test_pose_with_covariance_lcm_encode_decode() -> None: - """Test LCM encoding and decoding.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - covariance = np.arange(36, dtype=float) - source = PoseWithCovariance(pose, covariance) - - # Encode and decode - binary_msg = source.lcm_encode() - decoded = PoseWithCovariance.lcm_decode(binary_msg) - - # Should be equal - assert decoded == source - assert isinstance(decoded, PoseWithCovariance) - assert isinstance(decoded.pose, Pose) - assert isinstance(decoded.covariance, np.ndarray) - - -def test_pose_with_covariance_zero_covariance() -> None: - """Test with zero covariance matrix.""" - pose = Pose(1.0, 2.0, 3.0) - pose_cov = PoseWithCovariance(pose) - - assert np.all(pose_cov.covariance == 0.0) - assert np.trace(pose_cov.covariance_matrix) == 0.0 - - -def test_pose_with_covariance_diagonal_covariance() -> None: - """Test with diagonal covariance matrix.""" - pose = Pose() - covariance = np.zeros(36) - # Set diagonal elements - for i in range(6): - covariance[i * 6 + i] = i + 1 - - pose_cov = PoseWithCovariance(pose, covariance) - - cov_matrix = pose_cov.covariance_matrix - assert np.trace(cov_matrix) == sum(range(1, 7)) # 1+2+3+4+5+6 = 21 - - # Check diagonal elements - for i in range(6): - assert cov_matrix[i, i] == i + 1 - - # Check off-diagonal elements are zero - for i in range(6): - for j in range(6): - if i != j: - assert cov_matrix[i, j] == 0.0 - - -@pytest.mark.parametrize( - "x,y,z", - [(0.0, 0.0, 0.0), (1.0, 2.0, 3.0), (-1.0, -2.0, -3.0), (100.0, -100.0, 0.0)], -) -def test_pose_with_covariance_parametrized_positions(x, y, z) -> None: - """Parametrized test for various position values.""" - pose = Pose(x, y, z) - pose_cov = PoseWithCovariance(pose) - - assert pose_cov.x == x - assert pose_cov.y == y - assert pose_cov.z == z diff --git a/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py deleted file mode 100644 index 1e910b53e7..0000000000 --- a/dimos/msgs/geometry_msgs/test_PoseWithCovarianceStamped.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np - -from dimos.msgs.geometry_msgs.Pose import Pose -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance -from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import PoseWithCovarianceStamped - - -def test_pose_with_covariance_stamped_default_init() -> None: - """Test default initialization.""" - pose_cov_stamped = PoseWithCovarianceStamped() - - # Should have current timestamp - assert pose_cov_stamped.ts > 0 - assert pose_cov_stamped.frame_id == "" - - # Pose should be at origin with identity orientation - assert pose_cov_stamped.pose.position.x == 0.0 - assert pose_cov_stamped.pose.position.y == 0.0 - assert pose_cov_stamped.pose.position.z == 0.0 - assert pose_cov_stamped.pose.orientation.w == 1.0 - - # Covariance should be all zeros - assert np.all(pose_cov_stamped.covariance == 0.0) - - -def test_pose_with_covariance_stamped_with_timestamp() -> None: - """Test initialization with specific timestamp.""" - ts = 1234567890.123456 - frame_id = "base_link" - pose_cov_stamped = PoseWithCovarianceStamped(ts=ts, frame_id=frame_id) - - assert pose_cov_stamped.ts == ts - assert pose_cov_stamped.frame_id == frame_id - - -def test_pose_with_covariance_stamped_with_pose() -> None: - """Test initialization with pose.""" - ts = 1234567890.123456 - frame_id = "map" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - covariance = np.arange(36, dtype=float) - - pose_cov_stamped = PoseWithCovarianceStamped( - ts=ts, frame_id=frame_id, pose=pose, covariance=covariance - ) - - assert pose_cov_stamped.ts == ts - assert pose_cov_stamped.frame_id == frame_id - assert pose_cov_stamped.pose.position.x == 1.0 - assert pose_cov_stamped.pose.position.y == 2.0 - assert pose_cov_stamped.pose.position.z == 3.0 - assert np.array_equal(pose_cov_stamped.covariance, covariance) - - -def test_pose_with_covariance_stamped_properties() -> None: - """Test convenience properties.""" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - covariance = np.eye(6).flatten() - pose_cov_stamped = PoseWithCovarianceStamped( - ts=1234567890.0, frame_id="odom", pose=pose, covariance=covariance - ) - - # Position properties - assert pose_cov_stamped.x == 1.0 - assert pose_cov_stamped.y == 2.0 - assert pose_cov_stamped.z == 3.0 - - # Orientation properties - assert pose_cov_stamped.orientation.x == 0.1 - assert pose_cov_stamped.orientation.y == 0.2 - assert pose_cov_stamped.orientation.z == 0.3 - assert pose_cov_stamped.orientation.w == 0.9 - - # Euler angles - assert pose_cov_stamped.roll == pose.roll - assert pose_cov_stamped.pitch == pose.pitch - assert pose_cov_stamped.yaw == pose.yaw - - # Covariance matrix - cov_matrix = pose_cov_stamped.covariance_matrix - assert cov_matrix.shape == (6, 6) - assert np.trace(cov_matrix) == 6.0 - - -def test_pose_with_covariance_stamped_str() -> None: - """Test string representation.""" - pose = Pose(1.234, 2.567, 3.891) - covariance = np.eye(6).flatten() * 2.0 - pose_cov_stamped = PoseWithCovarianceStamped( - ts=1234567890.0, frame_id="world", pose=pose, covariance=covariance - ) - - str_repr = str(pose_cov_stamped) - assert "PoseWithCovarianceStamped" in str_repr - assert "1.234" in str_repr - assert "2.567" in str_repr - assert "3.891" in str_repr - assert "cov_trace" in str_repr - assert "12.000" in str_repr # Trace of 2*identity is 12 - - -def test_pose_with_covariance_stamped_lcm_encode_decode() -> None: - """Test LCM encoding and decoding.""" - ts = 1234567890.123456 - frame_id = "camera_link" - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - covariance = np.arange(36, dtype=float) - - source = PoseWithCovarianceStamped(ts=ts, frame_id=frame_id, pose=pose, covariance=covariance) - - # Encode and decode - binary_msg = source.lcm_encode() - decoded = PoseWithCovarianceStamped.lcm_decode(binary_msg) - - # Check timestamp (may lose some precision) - assert abs(decoded.ts - ts) < 1e-6 - assert decoded.frame_id == frame_id - - # Check pose - assert decoded.pose.position.x == 1.0 - assert decoded.pose.position.y == 2.0 - assert decoded.pose.position.z == 3.0 - assert decoded.pose.orientation.x == 0.1 - assert decoded.pose.orientation.y == 0.2 - assert decoded.pose.orientation.z == 0.3 - assert decoded.pose.orientation.w == 0.9 - - # Check covariance - assert np.array_equal(decoded.covariance, covariance) - - -def test_pose_with_covariance_stamped_zero_timestamp() -> None: - """Test that zero timestamp gets replaced with current time.""" - pose_cov_stamped = PoseWithCovarianceStamped(ts=0.0) - - # Should have been replaced with current time - assert pose_cov_stamped.ts > 0 - assert pose_cov_stamped.ts <= time.time() - - -def test_pose_with_covariance_stamped_inheritance() -> None: - """Test that it properly inherits from PoseWithCovariance and Timestamped.""" - pose = Pose(1.0, 2.0, 3.0) - covariance = np.eye(6).flatten() - pose_cov_stamped = PoseWithCovarianceStamped( - ts=1234567890.0, frame_id="test", pose=pose, covariance=covariance - ) - - # Should be instance of parent classes - assert isinstance(pose_cov_stamped, PoseWithCovariance) - - # Should have Timestamped attributes - assert hasattr(pose_cov_stamped, "ts") - assert hasattr(pose_cov_stamped, "frame_id") - - # Should have PoseWithCovariance attributes - assert hasattr(pose_cov_stamped, "pose") - assert hasattr(pose_cov_stamped, "covariance") - - -def test_pose_with_covariance_stamped_sec_nsec() -> None: - """Test the sec_nsec helper function.""" - from dimos.msgs.geometry_msgs.PoseWithCovarianceStamped import sec_nsec - - # Test integer seconds - s, ns = sec_nsec(1234567890.0) - assert s == 1234567890 - assert ns == 0 - - # Test fractional seconds - s, ns = sec_nsec(1234567890.123456789) - assert s == 1234567890 - assert abs(ns - 123456789) < 100 # Allow small rounding error - - # Test small fractional seconds - s, ns = sec_nsec(0.000000001) - assert s == 0 - assert ns == 1 - - # Test large timestamp - s, ns = sec_nsec(9999999999.999999999) - # Due to floating point precision, this might round to 10000000000 - assert s in [9999999999, 10000000000] - if s == 9999999999: - assert abs(ns - 999999999) < 10 - else: - assert ns == 0 - - -def test_pose_with_covariance_stamped_different_covariances() -> None: - """Test with different covariance patterns.""" - pose = Pose(1.0, 2.0, 3.0) - - # Zero covariance - zero_cov = np.zeros(36) - pose_cov1 = PoseWithCovarianceStamped(pose=pose, covariance=zero_cov) - assert np.all(pose_cov1.covariance == 0.0) - - # Identity covariance - identity_cov = np.eye(6).flatten() - pose_cov2 = PoseWithCovarianceStamped(pose=pose, covariance=identity_cov) - assert np.trace(pose_cov2.covariance_matrix) == 6.0 - - # Full covariance - full_cov = np.random.rand(36) - pose_cov3 = PoseWithCovarianceStamped(pose=pose, covariance=full_cov) - assert np.array_equal(pose_cov3.covariance, full_cov) diff --git a/dimos/msgs/geometry_msgs/test_Quaternion.py b/dimos/msgs/geometry_msgs/test_Quaternion.py deleted file mode 100644 index 21c1e8caeb..0000000000 --- a/dimos/msgs/geometry_msgs/test_Quaternion.py +++ /dev/null @@ -1,387 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.geometry_msgs import Quaternion as LCMQuaternion -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.Quaternion import Quaternion - - -def test_quaternion_default_init() -> None: - """Test that default initialization creates an identity quaternion (w=1, x=y=z=0).""" - q = Quaternion() - assert q.x == 0.0 - assert q.y == 0.0 - assert q.z == 0.0 - assert q.w == 1.0 - assert q.to_tuple() == (0.0, 0.0, 0.0, 1.0) - - -def test_quaternion_component_init() -> None: - """Test initialization with four float components (x, y, z, w).""" - q = Quaternion(0.5, 0.5, 0.5, 0.5) - assert q.x == 0.5 - assert q.y == 0.5 - assert q.z == 0.5 - assert q.w == 0.5 - - # Test with different values - q2 = Quaternion(1.0, 2.0, 3.0, 4.0) - assert q2.x == 1.0 - assert q2.y == 2.0 - assert q2.z == 3.0 - assert q2.w == 4.0 - - # Test with negative values - q3 = Quaternion(-1.0, -2.0, -3.0, -4.0) - assert q3.x == -1.0 - assert q3.y == -2.0 - assert q3.z == -3.0 - assert q3.w == -4.0 - - # Test with integers (should convert to float) - q4 = Quaternion(1, 2, 3, 4) - assert q4.x == 1.0 - assert q4.y == 2.0 - assert q4.z == 3.0 - assert q4.w == 4.0 - assert isinstance(q4.x, float) - - -def test_quaternion_sequence_init() -> None: - """Test initialization from sequence (list, tuple) of 4 numbers.""" - # From list - q1 = Quaternion([0.1, 0.2, 0.3, 0.4]) - assert q1.x == 0.1 - assert q1.y == 0.2 - assert q1.z == 0.3 - assert q1.w == 0.4 - - # From tuple - q2 = Quaternion((0.5, 0.6, 0.7, 0.8)) - assert q2.x == 0.5 - assert q2.y == 0.6 - assert q2.z == 0.7 - assert q2.w == 0.8 - - # Test with integers in sequence - q3 = Quaternion([1, 2, 3, 4]) - assert q3.x == 1.0 - assert q3.y == 2.0 - assert q3.z == 3.0 - assert q3.w == 4.0 - - # Test error with wrong length - with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): - Quaternion([1, 2, 3]) # Only 3 components - - with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): - Quaternion([1, 2, 3, 4, 5]) # Too many components - - -def test_quaternion_numpy_init() -> None: - """Test initialization from numpy array.""" - # From numpy array - arr = np.array([0.1, 0.2, 0.3, 0.4]) - q1 = Quaternion(arr) - assert q1.x == 0.1 - assert q1.y == 0.2 - assert q1.z == 0.3 - assert q1.w == 0.4 - - # Test with different dtypes - arr_int = np.array([1, 2, 3, 4], dtype=int) - q2 = Quaternion(arr_int) - assert q2.x == 1.0 - assert q2.y == 2.0 - assert q2.z == 3.0 - assert q2.w == 4.0 - - # Test error with wrong size - with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): - Quaternion(np.array([1, 2, 3])) # Only 3 elements - - with pytest.raises(ValueError, match="Quaternion requires exactly 4 components"): - Quaternion(np.array([1, 2, 3, 4, 5])) # Too many elements - - -def test_quaternion_copy_init() -> None: - """Test initialization from another Quaternion (copy constructor).""" - original = Quaternion(0.1, 0.2, 0.3, 0.4) - copy = Quaternion(original) - - assert copy.x == 0.1 - assert copy.y == 0.2 - assert copy.z == 0.3 - assert copy.w == 0.4 - - # Verify it's a copy, not the same object - assert copy is not original - assert copy == original - - -def test_quaternion_lcm_init() -> None: - """Test initialization from LCM Quaternion.""" - lcm_quat = LCMQuaternion() - lcm_quat.x = 0.1 - lcm_quat.y = 0.2 - lcm_quat.z = 0.3 - lcm_quat.w = 0.4 - - q = Quaternion(lcm_quat) - assert q.x == 0.1 - assert q.y == 0.2 - assert q.z == 0.3 - assert q.w == 0.4 - - -def test_quaternion_properties() -> None: - """Test quaternion component properties.""" - q = Quaternion(1.0, 2.0, 3.0, 4.0) - - # Test property access - assert q.x == 1.0 - assert q.y == 2.0 - assert q.z == 3.0 - assert q.w == 4.0 - - # Test as_tuple property - assert q.to_tuple() == (1.0, 2.0, 3.0, 4.0) - - -def test_quaternion_indexing() -> None: - """Test quaternion indexing support.""" - q = Quaternion(1.0, 2.0, 3.0, 4.0) - - # Test indexing - assert q[0] == 1.0 - assert q[1] == 2.0 - assert q[2] == 3.0 - assert q[3] == 4.0 - - -def test_quaternion_euler() -> None: - """Test quaternion to Euler angles conversion.""" - - # Test identity quaternion (should give zero angles) - q_identity = Quaternion() - angles = q_identity.to_euler() - assert np.isclose(angles.x, 0.0, atol=1e-10) # roll - assert np.isclose(angles.y, 0.0, atol=1e-10) # pitch - assert np.isclose(angles.z, 0.0, atol=1e-10) # yaw - - # Test 90 degree rotation around Z-axis (yaw) - q_z90 = Quaternion(0, 0, np.sin(np.pi / 4), np.cos(np.pi / 4)) - angles_z90 = q_z90.to_euler() - assert np.isclose(angles_z90.roll, 0.0, atol=1e-10) # roll should be 0 - assert np.isclose(angles_z90.pitch, 0.0, atol=1e-10) # pitch should be 0 - assert np.isclose(angles_z90.yaw, np.pi / 2, atol=1e-10) # yaw should be π/2 (90 degrees) - - # Test 90 degree rotation around X-axis (roll) - q_x90 = Quaternion(np.sin(np.pi / 4), 0, 0, np.cos(np.pi / 4)) - angles_x90 = q_x90.to_euler() - assert np.isclose(angles_x90.x, np.pi / 2, atol=1e-10) # roll should be π/2 - assert np.isclose(angles_x90.y, 0.0, atol=1e-10) # pitch should be 0 - assert np.isclose(angles_x90.z, 0.0, atol=1e-10) # yaw should be 0 - - -def test_lcm_encode_decode() -> None: - """Test encoding and decoding of Quaternion to/from binary LCM format.""" - q_source = Quaternion(1.0, 2.0, 3.0, 4.0) - - binary_msg = q_source.lcm_encode() - - q_dest = Quaternion.lcm_decode(binary_msg) - - assert isinstance(q_dest, Quaternion) - assert q_dest is not q_source - assert q_dest == q_source - - -def test_quaternion_multiplication() -> None: - """Test quaternion multiplication (Hamilton product).""" - # Test identity multiplication - q1 = Quaternion(0.5, 0.5, 0.5, 0.5) - identity = Quaternion(0, 0, 0, 1) - - result = q1 * identity - assert np.allclose([result.x, result.y, result.z, result.w], [q1.x, q1.y, q1.z, q1.w]) - - # Test multiplication order matters (non-commutative) - q2 = Quaternion(0.1, 0.2, 0.3, 0.4) - q3 = Quaternion(0.4, 0.3, 0.2, 0.1) - - result1 = q2 * q3 - result2 = q3 * q2 - - # Results should be different - assert not np.allclose( - [result1.x, result1.y, result1.z, result1.w], [result2.x, result2.y, result2.z, result2.w] - ) - - # Test specific multiplication case - # 90 degree rotations around Z axis - angle = np.pi / 2 - q_90z = Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)) - - # Two 90 degree rotations should give 180 degrees - result = q_90z * q_90z - expected_angle = np.pi - assert np.isclose(result.x, 0, atol=1e-10) - assert np.isclose(result.y, 0, atol=1e-10) - assert np.isclose(result.z, np.sin(expected_angle / 2), atol=1e-10) - assert np.isclose(result.w, np.cos(expected_angle / 2), atol=1e-10) - - -def test_quaternion_conjugate() -> None: - """Test quaternion conjugate.""" - q = Quaternion(0.1, 0.2, 0.3, 0.4) - conj = q.conjugate() - - # Conjugate should negate x, y, z but keep w - assert conj.x == -q.x - assert conj.y == -q.y - assert conj.z == -q.z - assert conj.w == q.w - - # Test that q * q^* gives a real quaternion (x=y=z=0) - result = q * conj - assert np.isclose(result.x, 0, atol=1e-10) - assert np.isclose(result.y, 0, atol=1e-10) - assert np.isclose(result.z, 0, atol=1e-10) - # w should be the squared norm - expected_w = q.x**2 + q.y**2 + q.z**2 + q.w**2 - assert np.isclose(result.w, expected_w, atol=1e-10) - - -def test_quaternion_inverse() -> None: - """Test quaternion inverse.""" - # Test with unit quaternion - q_unit = Quaternion(0, 0, 0, 1).normalize() # Already normalized but being explicit - inv = q_unit.inverse() - - # For unit quaternion, inverse equals conjugate - conj = q_unit.conjugate() - assert np.allclose([inv.x, inv.y, inv.z, inv.w], [conj.x, conj.y, conj.z, conj.w]) - - # Test that q * q^-1 = identity - q = Quaternion(0.5, 0.5, 0.5, 0.5) - inv = q.inverse() - result = q * inv - - assert np.isclose(result.x, 0, atol=1e-10) - assert np.isclose(result.y, 0, atol=1e-10) - assert np.isclose(result.z, 0, atol=1e-10) - assert np.isclose(result.w, 1, atol=1e-10) - - # Test inverse of non-unit quaternion - q_non_unit = Quaternion(2, 0, 0, 0) # Non-unit quaternion - inv = q_non_unit.inverse() - result = q_non_unit * inv - - assert np.isclose(result.x, 0, atol=1e-10) - assert np.isclose(result.y, 0, atol=1e-10) - assert np.isclose(result.z, 0, atol=1e-10) - assert np.isclose(result.w, 1, atol=1e-10) - - -def test_quaternion_normalize() -> None: - """Test quaternion normalization.""" - # Test non-unit quaternion - q = Quaternion(1, 2, 3, 4) - q_norm = q.normalize() - - # Check that magnitude is 1 - magnitude = np.sqrt(q_norm.x**2 + q_norm.y**2 + q_norm.z**2 + q_norm.w**2) - assert np.isclose(magnitude, 1.0, atol=1e-10) - - # Check that direction is preserved - scale = np.sqrt(q.x**2 + q.y**2 + q.z**2 + q.w**2) - assert np.isclose(q_norm.x, q.x / scale, atol=1e-10) - assert np.isclose(q_norm.y, q.y / scale, atol=1e-10) - assert np.isclose(q_norm.z, q.z / scale, atol=1e-10) - assert np.isclose(q_norm.w, q.w / scale, atol=1e-10) - - -def test_quaternion_rotate_vector() -> None: - """Test rotating vectors with quaternions.""" - from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - # Test rotation of unit vectors - # 90 degree rotation around Z axis - angle = np.pi / 2 - q_rot = Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)) - - # Rotate X unit vector - v_x = Vector3(1, 0, 0) - v_rotated = q_rot.rotate_vector(v_x) - - # Should now point along Y axis - assert np.isclose(v_rotated.x, 0, atol=1e-10) - assert np.isclose(v_rotated.y, 1, atol=1e-10) - assert np.isclose(v_rotated.z, 0, atol=1e-10) - - # Rotate Y unit vector - v_y = Vector3(0, 1, 0) - v_rotated = q_rot.rotate_vector(v_y) - - # Should now point along negative X axis - assert np.isclose(v_rotated.x, -1, atol=1e-10) - assert np.isclose(v_rotated.y, 0, atol=1e-10) - assert np.isclose(v_rotated.z, 0, atol=1e-10) - - # Test that Z vector is unchanged (rotation axis) - v_z = Vector3(0, 0, 1) - v_rotated = q_rot.rotate_vector(v_z) - - assert np.isclose(v_rotated.x, 0, atol=1e-10) - assert np.isclose(v_rotated.y, 0, atol=1e-10) - assert np.isclose(v_rotated.z, 1, atol=1e-10) - - # Test identity rotation - q_identity = Quaternion(0, 0, 0, 1) - v = Vector3(1, 2, 3) - v_rotated = q_identity.rotate_vector(v) - - assert np.isclose(v_rotated.x, v.x, atol=1e-10) - assert np.isclose(v_rotated.y, v.y, atol=1e-10) - assert np.isclose(v_rotated.z, v.z, atol=1e-10) - - -def test_quaternion_inverse_zero() -> None: - """Test that inverting zero quaternion raises error.""" - q_zero = Quaternion(0, 0, 0, 0) - - with pytest.raises(ZeroDivisionError, match="Cannot invert zero quaternion"): - q_zero.inverse() - - -def test_quaternion_normalize_zero() -> None: - """Test that normalizing zero quaternion raises error.""" - q_zero = Quaternion(0, 0, 0, 0) - - with pytest.raises(ZeroDivisionError, match="Cannot normalize zero quaternion"): - q_zero.normalize() - - -def test_quaternion_multiplication_type_error() -> None: - """Test that multiplying quaternion with non-quaternion raises error.""" - q = Quaternion(1, 0, 0, 0) - - with pytest.raises(TypeError, match="Cannot multiply Quaternion with"): - q * 5.0 - - with pytest.raises(TypeError, match="Cannot multiply Quaternion with"): - q * [1, 2, 3, 4] diff --git a/dimos/msgs/geometry_msgs/test_Transform.py b/dimos/msgs/geometry_msgs/test_Transform.py deleted file mode 100644 index 0c15610b05..0000000000 --- a/dimos/msgs/geometry_msgs/test_Transform.py +++ /dev/null @@ -1,420 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -import time - -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Transform, Vector3 - - -def test_transform_initialization() -> None: - # Test default initialization (identity transform) - tf = Transform() - assert tf.translation.x == 0.0 - assert tf.translation.y == 0.0 - assert tf.translation.z == 0.0 - assert tf.rotation.x == 0.0 - assert tf.rotation.y == 0.0 - assert tf.rotation.z == 0.0 - assert tf.rotation.w == 1.0 - - # Test initialization with Vector3 and Quaternion - trans = Vector3(1.0, 2.0, 3.0) - rot = Quaternion(0.0, 0.0, 0.707107, 0.707107) # 90 degrees around Z - tf2 = Transform(translation=trans, rotation=rot) - assert tf2.translation == trans - assert tf2.rotation == rot - - # Test initialization with only translation - tf5 = Transform(translation=Vector3(7.0, 8.0, 9.0)) - assert tf5.translation.x == 7.0 - assert tf5.translation.y == 8.0 - assert tf5.translation.z == 9.0 - assert tf5.rotation.w == 1.0 # Identity rotation - - # Test initialization with only rotation - tf6 = Transform(rotation=Quaternion(0.0, 0.0, 0.0, 1.0)) - assert tf6.translation.is_zero() # Zero translation - assert tf6.rotation.w == 1.0 - - # Test keyword argument initialization - tf7 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion()) - assert tf7.translation == Vector3(1, 2, 3) - assert tf7.rotation == Quaternion() - - # Test keyword with only translation - tf8 = Transform(translation=Vector3(4, 5, 6)) - assert tf8.translation == Vector3(4, 5, 6) - assert tf8.rotation.w == 1.0 - - # Test keyword with only rotation - tf9 = Transform(rotation=Quaternion(0, 0, 1, 0)) - assert tf9.translation.is_zero() - assert tf9.rotation == Quaternion(0, 0, 1, 0) - - -def test_transform_identity() -> None: - # Test identity class method - tf = Transform.identity() - assert tf.translation.is_zero() - assert tf.rotation.x == 0.0 - assert tf.rotation.y == 0.0 - assert tf.rotation.z == 0.0 - assert tf.rotation.w == 1.0 - - # Identity should equal default constructor - assert tf == Transform() - - -def test_transform_equality() -> None: - tf1 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1)) - tf2 = Transform(translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1)) - tf3 = Transform(translation=Vector3(1, 2, 4), rotation=Quaternion(0, 0, 0, 1)) # Different z - tf4 = Transform( - translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 1, 0) - ) # Different rotation - - assert tf1 == tf2 - assert tf1 != tf3 - assert tf1 != tf4 - assert tf1 != "not a transform" - - -def test_transform_string_representations() -> None: - tf = Transform( - translation=Vector3(1.5, -2.0, 3.14), rotation=Quaternion(0, 0, 0.707107, 0.707107) - ) - - # Test repr - repr_str = repr(tf) - assert "Transform" in repr_str - assert "translation=" in repr_str - assert "rotation=" in repr_str - assert "1.5" in repr_str - - # Test str - str_str = str(tf) - assert "Translation:" in str_str - assert "Rotation:" in str_str - - -def test_pose_add_transform() -> None: - initial_pose = Pose(1.0, 0.0, 0.0) - - # 90 degree rotation around Z axis - angle = np.pi / 2 - transform = Transform( - translation=Vector3(2.0, 1.0, 0.0), - rotation=Quaternion(0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)), - ) - - transformed_pose = initial_pose @ transform - - # - Translation (2, 1, 0) is added directly to position (1, 0, 0) - # - Result position: (3, 1, 0) - assert np.isclose(transformed_pose.position.x, 3.0, atol=1e-10) - assert np.isclose(transformed_pose.position.y, 1.0, atol=1e-10) - assert np.isclose(transformed_pose.position.z, 0.0, atol=1e-10) - - # Rotation should be 90 degrees around Z - assert np.isclose(transformed_pose.orientation.x, 0.0, atol=1e-10) - assert np.isclose(transformed_pose.orientation.y, 0.0, atol=1e-10) - assert np.isclose(transformed_pose.orientation.z, np.sin(angle / 2), atol=1e-10) - assert np.isclose(transformed_pose.orientation.w, np.cos(angle / 2), atol=1e-10) - - initial_pose_stamped = PoseStamped( - position=initial_pose.position, orientation=initial_pose.orientation - ) - transformed_pose_stamped = PoseStamped( - position=transformed_pose.position, orientation=transformed_pose.orientation - ) - - found_tf = initial_pose_stamped.find_transform(transformed_pose_stamped) - - assert found_tf.translation == transform.translation - assert found_tf.rotation == transform.rotation - assert found_tf.translation.x == transform.translation.x - assert found_tf.translation.y == transform.translation.y - assert found_tf.translation.z == transform.translation.z - - assert found_tf.rotation.x == transform.rotation.x - assert found_tf.rotation.y == transform.rotation.y - assert found_tf.rotation.z == transform.rotation.z - assert found_tf.rotation.w == transform.rotation.w - - print(found_tf.rotation, found_tf.translation) - - -def test_pose_add_transform_with_rotation() -> None: - # Create a pose at (0, 0, 0) rotated 90 degrees around Z - angle = np.pi / 2 - initial_pose = Pose(0.0, 0.0, 0.0, 0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)) - - # Add 45 degree rotation to transform1 - rotation_angle = np.pi / 4 # 45 degrees - transform1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - rotation=Quaternion( - 0.0, 0.0, np.sin(rotation_angle / 2), np.cos(rotation_angle / 2) - ), # 45� around Z - ) - - transform2 = Transform( - translation=Vector3(0.0, 1.0, 1.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation - ) - - transformed_pose1 = initial_pose @ transform1 - transformed_pose2 = initial_pose @ transform1 @ transform2 - - # Test transformed_pose1: initial_pose + transform1 - # Since the pose is rotated 90� (facing +Y), moving forward (local X) - # means moving in the +Y direction in world frame - assert np.isclose(transformed_pose1.position.x, 0.0, atol=1e-10) - assert np.isclose(transformed_pose1.position.y, 1.0, atol=1e-10) - assert np.isclose(transformed_pose1.position.z, 0.0, atol=1e-10) - - # Orientation should be 90� + 45� = 135� around Z - total_angle1 = angle + rotation_angle # 135 degrees - assert np.isclose(transformed_pose1.orientation.x, 0.0, atol=1e-10) - assert np.isclose(transformed_pose1.orientation.y, 0.0, atol=1e-10) - assert np.isclose(transformed_pose1.orientation.z, np.sin(total_angle1 / 2), atol=1e-10) - assert np.isclose(transformed_pose1.orientation.w, np.cos(total_angle1 / 2), atol=1e-10) - - # Test transformed_pose2: initial_pose + transform1 + transform2 - # Starting from (0, 0, 0) facing 90�: - # - # - Apply transform1: move 1 forward (along +Y) � (0, 1, 0), now facing 135� - # - # - Apply transform2: move 1 in local Y and 1 up - # At 135�, local Y points at 225� (135� + 90�) - # - # x += cos(225�) = -2/2, y += sin(225�) = -2/2 - sqrt2_2 = np.sqrt(2) / 2 - expected_x = 0.0 - sqrt2_2 # 0 - 2/2 H -0.707 - expected_y = 1.0 - sqrt2_2 # 1 - 2/2 H 0.293 - expected_z = 1.0 # 0 + 1 - - assert np.isclose(transformed_pose2.position.x, expected_x, atol=1e-10) - assert np.isclose(transformed_pose2.position.y, expected_y, atol=1e-10) - assert np.isclose(transformed_pose2.position.z, expected_z, atol=1e-10) - - # Orientation should be 135� (only transform1 has rotation) - total_angle2 = total_angle1 # 135 degrees (transform2 has no rotation) - assert np.isclose(transformed_pose2.orientation.x, 0.0, atol=1e-10) - assert np.isclose(transformed_pose2.orientation.y, 0.0, atol=1e-10) - assert np.isclose(transformed_pose2.orientation.z, np.sin(total_angle2 / 2), atol=1e-10) - assert np.isclose(transformed_pose2.orientation.w, np.cos(total_angle2 / 2), atol=1e-10) - - -def test_lcm_encode_decode() -> None: - angle = np.pi / 2 - transform = Transform( - translation=Vector3(2.0, 1.0, 0.0), - rotation=Quaternion(0.0, 0.0, np.sin(angle / 2), np.cos(angle / 2)), - ) - - data = transform.lcm_encode() - - decoded_transform = Transform.lcm_decode(data) - - assert decoded_transform == transform - - -def test_transform_addition() -> None: - # Test 1: Simple translation addition (no rotation) - t1 = Transform( - translation=Vector3(1, 0, 0), - rotation=Quaternion(0, 0, 0, 1), # identity rotation - ) - t2 = Transform( - translation=Vector3(2, 0, 0), - rotation=Quaternion(0, 0, 0, 1), # identity rotation - ) - t3 = t1 + t2 - assert t3.translation == Vector3(3, 0, 0) - assert t3.rotation == Quaternion(0, 0, 0, 1) - - # Test 2: 90-degree rotation composition - # First transform: move 1 unit in X - t1 = Transform( - translation=Vector3(1, 0, 0), - rotation=Quaternion(0, 0, 0, 1), # identity - ) - # Second transform: move 1 unit in X with 90-degree rotation around Z - angle = np.pi / 2 - t2 = Transform( - translation=Vector3(1, 0, 0), - rotation=Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)), - ) - t3 = t1 + t2 - assert t3.translation == Vector3(2, 0, 0) - # Rotation should be 90 degrees around Z - assert np.isclose(t3.rotation.x, 0.0, atol=1e-10) - assert np.isclose(t3.rotation.y, 0.0, atol=1e-10) - assert np.isclose(t3.rotation.z, np.sin(angle / 2), atol=1e-10) - assert np.isclose(t3.rotation.w, np.cos(angle / 2), atol=1e-10) - - # Test 3: Rotation affects translation - # First transform: 90-degree rotation around Z - t1 = Transform( - translation=Vector3(0, 0, 0), - rotation=Quaternion(0, 0, np.sin(angle / 2), np.cos(angle / 2)), # 90° around Z - ) - # Second transform: move 1 unit in X - t2 = Transform( - translation=Vector3(1, 0, 0), - rotation=Quaternion(0, 0, 0, 1), # identity - ) - t3 = t1 + t2 - # X direction rotated 90° becomes Y direction - assert np.isclose(t3.translation.x, 0.0, atol=1e-10) - assert np.isclose(t3.translation.y, 1.0, atol=1e-10) - assert np.isclose(t3.translation.z, 0.0, atol=1e-10) - # Rotation remains 90° around Z - assert np.isclose(t3.rotation.z, np.sin(angle / 2), atol=1e-10) - assert np.isclose(t3.rotation.w, np.cos(angle / 2), atol=1e-10) - - # Test 4: Frame tracking - t1 = Transform( - translation=Vector3(1, 0, 0), - rotation=Quaternion(0, 0, 0, 1), - frame_id="world", - child_frame_id="robot", - ) - t2 = Transform( - translation=Vector3(2, 0, 0), - rotation=Quaternion(0, 0, 0, 1), - frame_id="robot", - child_frame_id="sensor", - ) - t3 = t1 + t2 - assert t3.frame_id == "world" - assert t3.child_frame_id == "sensor" - - # Test 5: Type error - with pytest.raises(TypeError): - t1 + "not a transform" - - -def test_transform_from_pose() -> None: - """Test converting Pose to Transform""" - # Create a Pose with position and orientation - pose = Pose( - position=Vector3(1.0, 2.0, 3.0), - orientation=Quaternion(0.0, 0.0, 0.707, 0.707), # 90 degrees around Z - ) - - # Convert to Transform - transform = Transform.from_pose("base_link", pose) - - # Check that translation and rotation match - assert transform.translation == pose.position - assert transform.rotation == pose.orientation - assert transform.frame_id == "world" # default frame_id - assert transform.child_frame_id == "base_link" # passed as first argument - - -# validating results from example @ -# https://foxglove.dev/blog/understanding-ros-transforms -def test_transform_from_ros() -> None: - """Test converting PoseStamped to Transform""" - test_time = time.time() - pose_stamped = PoseStamped( - ts=test_time, - frame_id="base_link", - position=Vector3(1, -1, 0), - orientation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), - ) - transform_base_link_to_arm = Transform.from_pose("arm_base_link", pose_stamped) - - transform_arm_to_end = Transform.from_pose( - "end", - PoseStamped( - ts=test_time, - frame_id="arm_base_link", - position=Vector3(1, 1, 0), - orientation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), - ), - ) - - print(transform_base_link_to_arm) - print(transform_arm_to_end) - - end_effector_global_pose = transform_base_link_to_arm + transform_arm_to_end - - assert end_effector_global_pose.translation.x == pytest.approx(1.366, abs=1e-3) - assert end_effector_global_pose.translation.y == pytest.approx(0.366, abs=1e-3) - - -def test_transform_from_pose_stamped() -> None: - """Test converting PoseStamped to Transform""" - # Create a PoseStamped with position, orientation, timestamp and frame - test_time = time.time() - pose_stamped = PoseStamped( - ts=test_time, - frame_id="map", - position=Vector3(4.0, 5.0, 6.0), - orientation=Quaternion(0.0, 0.707, 0.0, 0.707), # 90 degrees around Y - ) - - # Convert to Transform - transform = Transform.from_pose("robot_base", pose_stamped) - - # Check that all fields match - assert transform.translation == pose_stamped.position - assert transform.rotation == pose_stamped.orientation - assert transform.frame_id == pose_stamped.frame_id - assert transform.ts == pose_stamped.ts - assert transform.child_frame_id == "robot_base" # passed as first argument - - -def test_transform_from_pose_variants() -> None: - """Test from_pose with different Pose initialization methods""" - # Test with Pose created from x,y,z - pose1 = Pose(1.0, 2.0, 3.0) - transform1 = Transform.from_pose("base_link", pose1) - assert transform1.translation.x == 1.0 - assert transform1.translation.y == 2.0 - assert transform1.translation.z == 3.0 - assert transform1.rotation.w == 1.0 # Identity quaternion - - # Test with Pose created from tuple - pose2 = Pose(([7.0, 8.0, 9.0], [0.0, 0.0, 0.0, 1.0])) - transform2 = Transform.from_pose("base_link", pose2) - assert transform2.translation.x == 7.0 - assert transform2.translation.y == 8.0 - assert transform2.translation.z == 9.0 - - # Test with Pose created from dict - pose3 = Pose({"position": [10.0, 11.0, 12.0], "orientation": [0.0, 0.0, 0.0, 1.0]}) - transform3 = Transform.from_pose("base_link", pose3) - assert transform3.translation.x == 10.0 - assert transform3.translation.y == 11.0 - assert transform3.translation.z == 12.0 - - -def test_transform_from_pose_invalid_type() -> None: - """Test that from_pose raises TypeError for invalid types""" - with pytest.raises(TypeError): - Transform.from_pose("not a pose") - - with pytest.raises(TypeError): - Transform.from_pose(42) - - with pytest.raises(TypeError): - Transform.from_pose(None) diff --git a/dimos/msgs/geometry_msgs/test_Twist.py b/dimos/msgs/geometry_msgs/test_Twist.py deleted file mode 100644 index df4bd8b6a2..0000000000 --- a/dimos/msgs/geometry_msgs/test_Twist.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.geometry_msgs import Twist as LCMTwist -import numpy as np - -from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 - - -def test_twist_initialization() -> None: - # Test default initialization (zero twist) - tw = Twist() - assert tw.linear.x == 0.0 - assert tw.linear.y == 0.0 - assert tw.linear.z == 0.0 - assert tw.angular.x == 0.0 - assert tw.angular.y == 0.0 - assert tw.angular.z == 0.0 - - # Test initialization with Vector3 linear and angular - lin = Vector3(1.0, 2.0, 3.0) - ang = Vector3(0.1, 0.2, 0.3) - tw2 = Twist(lin, ang) - assert tw2.linear == lin - assert tw2.angular == ang - - # Test copy constructor - tw3 = Twist(tw2) - assert tw3.linear == tw2.linear - assert tw3.angular == tw2.angular - assert tw3 == tw2 - # Ensure it's a deep copy - tw3.linear.x = 10.0 - assert tw2.linear.x == 1.0 - - # Test initialization from LCM Twist - lcm_tw = LCMTwist() - lcm_tw.linear = Vector3(4.0, 5.0, 6.0) - lcm_tw.angular = Vector3(0.4, 0.5, 0.6) - tw4 = Twist(lcm_tw) - assert tw4.linear.x == 4.0 - assert tw4.linear.y == 5.0 - assert tw4.linear.z == 6.0 - assert tw4.angular.x == 0.4 - assert tw4.angular.y == 0.5 - assert tw4.angular.z == 0.6 - - # Test initialization with linear and angular as quaternion - quat = Quaternion(0, 0, 0.707107, 0.707107) # 90 degrees around Z - tw5 = Twist(Vector3(1.0, 2.0, 3.0), quat) - assert tw5.linear == Vector3(1.0, 2.0, 3.0) - # Quaternion should be converted to euler angles - euler = quat.to_euler() - assert np.allclose(tw5.angular.x, euler.x) - assert np.allclose(tw5.angular.y, euler.y) - assert np.allclose(tw5.angular.z, euler.z) - - # Test keyword argument initialization - tw7 = Twist(linear=Vector3(1, 2, 3), angular=Vector3(0.1, 0.2, 0.3)) - assert tw7.linear == Vector3(1, 2, 3) - assert tw7.angular == Vector3(0.1, 0.2, 0.3) - - # Test keyword with only linear - tw8 = Twist(linear=Vector3(4, 5, 6)) - assert tw8.linear == Vector3(4, 5, 6) - assert tw8.angular.is_zero() - - # Test keyword with only angular - tw9 = Twist(angular=Vector3(0.4, 0.5, 0.6)) - assert tw9.linear.is_zero() - assert tw9.angular == Vector3(0.4, 0.5, 0.6) - - # Test keyword with angular as quaternion - tw10 = Twist(angular=Quaternion(0, 0, 0.707107, 0.707107)) - assert tw10.linear.is_zero() - euler = Quaternion(0, 0, 0.707107, 0.707107).to_euler() - assert np.allclose(tw10.angular.x, euler.x) - assert np.allclose(tw10.angular.y, euler.y) - assert np.allclose(tw10.angular.z, euler.z) - - # Test keyword with linear and angular as quaternion - tw11 = Twist(linear=Vector3(1, 0, 0), angular=Quaternion(0, 0, 0, 1)) - assert tw11.linear == Vector3(1, 0, 0) - assert tw11.angular.is_zero() # Identity quaternion -> zero euler angles - - -def test_twist_zero() -> None: - # Test zero class method - tw = Twist.zero() - assert tw.linear.is_zero() - assert tw.angular.is_zero() - assert tw.is_zero() - - # Zero should equal default constructor - assert tw == Twist() - - -def test_twist_equality() -> None: - tw1 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) - tw2 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) - tw3 = Twist(Vector3(1, 2, 4), Vector3(0.1, 0.2, 0.3)) # Different linear z - tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.4)) # Different angular z - - assert tw1 == tw2 - assert tw1 != tw3 - assert tw1 != tw4 - assert tw1 != "not a twist" - - -def test_twist_string_representations() -> None: - tw = Twist(Vector3(1.5, -2.0, 3.14), Vector3(0.1, -0.2, 0.3)) - - # Test repr - repr_str = repr(tw) - assert "Twist" in repr_str - assert "linear=" in repr_str - assert "angular=" in repr_str - assert "1.5" in repr_str - assert "0.1" in repr_str - - # Test str - str_str = str(tw) - assert "Twist:" in str_str - assert "Linear:" in str_str - assert "Angular:" in str_str - - -def test_twist_is_zero() -> None: - # Test zero twist - tw1 = Twist() - assert tw1.is_zero() - - # Test non-zero linear - tw2 = Twist(linear=Vector3(0.1, 0, 0)) - assert not tw2.is_zero() - - # Test non-zero angular - tw3 = Twist(angular=Vector3(0, 0, 0.1)) - assert not tw3.is_zero() - - # Test both non-zero - tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) - assert not tw4.is_zero() - - -def test_twist_bool() -> None: - # Test zero twist is False - tw1 = Twist() - assert not tw1 - - # Test non-zero twist is True - tw2 = Twist(linear=Vector3(1, 0, 0)) - assert tw2 - - tw3 = Twist(angular=Vector3(0, 0, 0.1)) - assert tw3 - - tw4 = Twist(Vector3(1, 2, 3), Vector3(0.1, 0.2, 0.3)) - assert tw4 - - -def test_twist_lcm_encoding() -> None: - # Test encoding and decoding - tw = Twist(Vector3(1.5, 2.5, 3.5), Vector3(0.1, 0.2, 0.3)) - - # Encode - encoded = tw.lcm_encode() - assert isinstance(encoded, bytes) - - # Decode - decoded = Twist.lcm_decode(encoded) - assert decoded.linear == tw.linear - assert decoded.angular == tw.angular - - assert isinstance(decoded.linear, Vector3) - assert decoded == tw - - -def test_twist_with_lists() -> None: - # Test initialization with lists instead of Vector3 - tw1 = Twist(linear=[1, 2, 3], angular=[0.1, 0.2, 0.3]) - assert tw1.linear == Vector3(1, 2, 3) - assert tw1.angular == Vector3(0.1, 0.2, 0.3) - - # Test with numpy arrays - tw2 = Twist(linear=np.array([4, 5, 6]), angular=np.array([0.4, 0.5, 0.6])) - assert tw2.linear == Vector3(4, 5, 6) - assert tw2.angular == Vector3(0.4, 0.5, 0.6) diff --git a/dimos/msgs/geometry_msgs/test_TwistStamped.py b/dimos/msgs/geometry_msgs/test_TwistStamped.py deleted file mode 100644 index afb8489032..0000000000 --- a/dimos/msgs/geometry_msgs/test_TwistStamped.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pickle -import time - -from dimos.msgs.geometry_msgs.TwistStamped import TwistStamped - - -def test_lcm_encode_decode() -> None: - """Test encoding and decoding of TwistStamped to/from binary LCM format.""" - twist_source = TwistStamped( - ts=time.time(), - linear=(1.0, 2.0, 3.0), - angular=(0.1, 0.2, 0.3), - ) - binary_msg = twist_source.lcm_encode() - twist_dest = TwistStamped.lcm_decode(binary_msg) - - assert isinstance(twist_dest, TwistStamped) - assert twist_dest is not twist_source - - print(twist_source.linear) - print(twist_source.angular) - - print(twist_dest.linear) - print(twist_dest.angular) - assert twist_dest == twist_source - - -def test_pickle_encode_decode() -> None: - """Test encoding and decoding of TwistStamped to/from binary pickle format.""" - - twist_source = TwistStamped( - ts=time.time(), - linear=(1.0, 2.0, 3.0), - angular=(0.1, 0.2, 0.3), - ) - binary_msg = pickle.dumps(twist_source) - twist_dest = pickle.loads(binary_msg) - assert isinstance(twist_dest, TwistStamped) - assert twist_dest is not twist_source - assert twist_dest == twist_source - - -if __name__ == "__main__": - print("Running test_lcm_encode_decode...") - test_lcm_encode_decode() - print("test_lcm_encode_decode passed") - - print("Running test_pickle_encode_decode...") - test_pickle_encode_decode() - print("test_pickle_encode_decode passed") - - print("\nAll tests passed!") diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py b/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py deleted file mode 100644 index 1d8ae820ad..0000000000 --- a/dimos/msgs/geometry_msgs/test_TwistWithCovariance.py +++ /dev/null @@ -1,356 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.geometry_msgs import TwistWithCovariance as LCMTwistWithCovariance -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -def test_twist_with_covariance_default_init() -> None: - """Test that default initialization creates a zero twist with zero covariance.""" - twist_cov = TwistWithCovariance() - - # Twist should be zero - assert twist_cov.twist.linear.x == 0.0 - assert twist_cov.twist.linear.y == 0.0 - assert twist_cov.twist.linear.z == 0.0 - assert twist_cov.twist.angular.x == 0.0 - assert twist_cov.twist.angular.y == 0.0 - assert twist_cov.twist.angular.z == 0.0 - - # Covariance should be all zeros - assert np.all(twist_cov.covariance == 0.0) - assert twist_cov.covariance.shape == (36,) - - -def test_twist_with_covariance_twist_init() -> None: - """Test initialization with a Twist object.""" - linear = Vector3(1.0, 2.0, 3.0) - angular = Vector3(0.1, 0.2, 0.3) - twist = Twist(linear, angular) - twist_cov = TwistWithCovariance(twist) - - # Twist should match - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - assert twist_cov.twist.angular.x == 0.1 - assert twist_cov.twist.angular.y == 0.2 - assert twist_cov.twist.angular.z == 0.3 - - # Covariance should be zeros by default - assert np.all(twist_cov.covariance == 0.0) - - -def test_twist_with_covariance_twist_and_covariance_init() -> None: - """Test initialization with twist and covariance.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.arange(36, dtype=float) - twist_cov = TwistWithCovariance(twist, covariance) - - # Twist should match - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - - # Covariance should match - assert np.array_equal(twist_cov.covariance, covariance) - - -def test_twist_with_covariance_tuple_init() -> None: - """Test initialization with tuple of (linear, angular) velocities.""" - linear = [1.0, 2.0, 3.0] - angular = [0.1, 0.2, 0.3] - covariance = np.arange(36, dtype=float) - twist_cov = TwistWithCovariance((linear, angular), covariance) - - # Twist should match - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - assert twist_cov.twist.angular.x == 0.1 - assert twist_cov.twist.angular.y == 0.2 - assert twist_cov.twist.angular.z == 0.3 - - # Covariance should match - assert np.array_equal(twist_cov.covariance, covariance) - - -def test_twist_with_covariance_list_covariance() -> None: - """Test initialization with covariance as a list.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance_list = list(range(36)) - twist_cov = TwistWithCovariance(twist, covariance_list) - - # Covariance should be converted to numpy array - assert isinstance(twist_cov.covariance, np.ndarray) - assert np.array_equal(twist_cov.covariance, np.array(covariance_list)) - - -def test_twist_with_covariance_copy_init() -> None: - """Test copy constructor.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.arange(36, dtype=float) - original = TwistWithCovariance(twist, covariance) - copy = TwistWithCovariance(original) - - # Should be equal but not the same object - assert copy == original - assert copy is not original - assert copy.twist is not original.twist - assert copy.covariance is not original.covariance - - # Modify original to ensure they're independent - original.covariance[0] = 999.0 - assert copy.covariance[0] != 999.0 - - -def test_twist_with_covariance_lcm_init() -> None: - """Test initialization from LCM message.""" - lcm_msg = LCMTwistWithCovariance() - lcm_msg.twist.linear.x = 1.0 - lcm_msg.twist.linear.y = 2.0 - lcm_msg.twist.linear.z = 3.0 - lcm_msg.twist.angular.x = 0.1 - lcm_msg.twist.angular.y = 0.2 - lcm_msg.twist.angular.z = 0.3 - lcm_msg.covariance = list(range(36)) - - twist_cov = TwistWithCovariance(lcm_msg) - - # Twist should match - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - assert twist_cov.twist.angular.x == 0.1 - assert twist_cov.twist.angular.y == 0.2 - assert twist_cov.twist.angular.z == 0.3 - - # Covariance should match - assert np.array_equal(twist_cov.covariance, np.arange(36)) - - -def test_twist_with_covariance_dict_init() -> None: - """Test initialization from dictionary.""" - twist_dict = { - "twist": Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)), - "covariance": list(range(36)), - } - twist_cov = TwistWithCovariance(twist_dict) - - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - assert np.array_equal(twist_cov.covariance, np.arange(36)) - - -def test_twist_with_covariance_dict_init_no_covariance() -> None: - """Test initialization from dictionary without covariance.""" - twist_dict = {"twist": Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3))} - twist_cov = TwistWithCovariance(twist_dict) - - assert twist_cov.twist.linear.x == 1.0 - assert np.all(twist_cov.covariance == 0.0) - - -def test_twist_with_covariance_tuple_of_tuple_init() -> None: - """Test initialization from tuple of (twist_tuple, covariance).""" - twist_tuple = ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]) - covariance = np.arange(36, dtype=float) - twist_cov = TwistWithCovariance((twist_tuple, covariance)) - - assert twist_cov.twist.linear.x == 1.0 - assert twist_cov.twist.linear.y == 2.0 - assert twist_cov.twist.linear.z == 3.0 - assert twist_cov.twist.angular.x == 0.1 - assert twist_cov.twist.angular.y == 0.2 - assert twist_cov.twist.angular.z == 0.3 - assert np.array_equal(twist_cov.covariance, covariance) - - -def test_twist_with_covariance_properties() -> None: - """Test convenience properties.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - twist_cov = TwistWithCovariance(twist) - - # Linear and angular properties - assert twist_cov.linear.x == 1.0 - assert twist_cov.linear.y == 2.0 - assert twist_cov.linear.z == 3.0 - assert twist_cov.angular.x == 0.1 - assert twist_cov.angular.y == 0.2 - assert twist_cov.angular.z == 0.3 - - -def test_twist_with_covariance_matrix_property() -> None: - """Test covariance matrix property.""" - twist = Twist() - covariance_array = np.arange(36, dtype=float) - twist_cov = TwistWithCovariance(twist, covariance_array) - - # Get as matrix - cov_matrix = twist_cov.covariance_matrix - assert cov_matrix.shape == (6, 6) - assert cov_matrix[0, 0] == 0.0 - assert cov_matrix[5, 5] == 35.0 - - # Set from matrix - new_matrix = np.eye(6) * 2.0 - twist_cov.covariance_matrix = new_matrix - assert np.array_equal(twist_cov.covariance[:6], [2.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - - -def test_twist_with_covariance_repr() -> None: - """Test string representation.""" - twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.1, 0.2, 0.3)) - twist_cov = TwistWithCovariance(twist) - - repr_str = repr(twist_cov) - assert "TwistWithCovariance" in repr_str - assert "twist=" in repr_str - assert "covariance=" in repr_str - assert "36 elements" in repr_str - - -def test_twist_with_covariance_str() -> None: - """Test string formatting.""" - twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.1, 0.2, 0.3)) - covariance = np.eye(6).flatten() - twist_cov = TwistWithCovariance(twist, covariance) - - str_repr = str(twist_cov) - assert "TwistWithCovariance" in str_repr - assert "1.234" in str_repr - assert "2.567" in str_repr - assert "3.891" in str_repr - assert "cov_trace" in str_repr - assert "6.000" in str_repr # Trace of identity matrix is 6 - - -def test_twist_with_covariance_equality() -> None: - """Test equality comparison.""" - twist1 = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - cov1 = np.arange(36, dtype=float) - twist_cov1 = TwistWithCovariance(twist1, cov1) - - twist2 = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - cov2 = np.arange(36, dtype=float) - twist_cov2 = TwistWithCovariance(twist2, cov2) - - # Equal - assert twist_cov1 == twist_cov2 - - # Different twist - twist3 = Twist(Vector3(1.1, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - twist_cov3 = TwistWithCovariance(twist3, cov1) - assert twist_cov1 != twist_cov3 - - # Different covariance - cov3 = np.arange(36, dtype=float) + 1 - twist_cov4 = TwistWithCovariance(twist1, cov3) - assert twist_cov1 != twist_cov4 - - # Different type - assert twist_cov1 != "not a twist" - assert twist_cov1 is not None - - -def test_twist_with_covariance_is_zero() -> None: - """Test is_zero method.""" - # Zero twist - twist_cov1 = TwistWithCovariance() - assert twist_cov1.is_zero() - assert not twist_cov1 # Boolean conversion - - # Non-zero twist - twist = Twist(Vector3(1.0, 0.0, 0.0), Vector3(0.0, 0.0, 0.0)) - twist_cov2 = TwistWithCovariance(twist) - assert not twist_cov2.is_zero() - assert twist_cov2 # Boolean conversion - - -def test_twist_with_covariance_lcm_encode_decode() -> None: - """Test LCM encoding and decoding.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.arange(36, dtype=float) - source = TwistWithCovariance(twist, covariance) - - # Encode and decode - binary_msg = source.lcm_encode() - decoded = TwistWithCovariance.lcm_decode(binary_msg) - - # Should be equal - assert decoded == source - assert isinstance(decoded, TwistWithCovariance) - assert isinstance(decoded.twist, Twist) - assert isinstance(decoded.covariance, np.ndarray) - - -def test_twist_with_covariance_zero_covariance() -> None: - """Test with zero covariance matrix.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - twist_cov = TwistWithCovariance(twist) - - assert np.all(twist_cov.covariance == 0.0) - assert np.trace(twist_cov.covariance_matrix) == 0.0 - - -def test_twist_with_covariance_diagonal_covariance() -> None: - """Test with diagonal covariance matrix.""" - twist = Twist() - covariance = np.zeros(36) - # Set diagonal elements - for i in range(6): - covariance[i * 6 + i] = i + 1 - - twist_cov = TwistWithCovariance(twist, covariance) - - cov_matrix = twist_cov.covariance_matrix - assert np.trace(cov_matrix) == sum(range(1, 7)) # 1+2+3+4+5+6 = 21 - - # Check diagonal elements - for i in range(6): - assert cov_matrix[i, i] == i + 1 - - # Check off-diagonal elements are zero - for i in range(6): - for j in range(6): - if i != j: - assert cov_matrix[i, j] == 0.0 - - -@pytest.mark.parametrize( - "linear,angular", - [ - ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]), - ([1.0, 2.0, 3.0], [0.1, 0.2, 0.3]), - ([-1.0, -2.0, -3.0], [-0.1, -0.2, -0.3]), - ([100.0, -100.0, 0.0], [3.14, -3.14, 0.0]), - ], -) -def test_twist_with_covariance_parametrized_velocities(linear, angular) -> None: - """Parametrized test for various velocity values.""" - twist = Twist(linear, angular) - twist_cov = TwistWithCovariance(twist) - - assert twist_cov.linear.x == linear[0] - assert twist_cov.linear.y == linear[1] - assert twist_cov.linear.z == linear[2] - assert twist_cov.angular.x == angular[0] - assert twist_cov.angular.y == angular[1] - assert twist_cov.angular.z == angular[2] diff --git a/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py b/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py deleted file mode 100644 index 2be647bff1..0000000000 --- a/dimos/msgs/geometry_msgs/test_TwistWithCovarianceStamped.py +++ /dev/null @@ -1,252 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np - -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import TwistWithCovarianceStamped -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -def test_twist_with_covariance_stamped_default_init() -> None: - """Test default initialization.""" - twist_cov_stamped = TwistWithCovarianceStamped() - - # Should have current timestamp - assert twist_cov_stamped.ts > 0 - assert twist_cov_stamped.frame_id == "" - - # Twist should be zero - assert twist_cov_stamped.twist.linear.x == 0.0 - assert twist_cov_stamped.twist.linear.y == 0.0 - assert twist_cov_stamped.twist.linear.z == 0.0 - assert twist_cov_stamped.twist.angular.x == 0.0 - assert twist_cov_stamped.twist.angular.y == 0.0 - assert twist_cov_stamped.twist.angular.z == 0.0 - - # Covariance should be all zeros - assert np.all(twist_cov_stamped.covariance == 0.0) - - -def test_twist_with_covariance_stamped_with_timestamp() -> None: - """Test initialization with specific timestamp.""" - ts = 1234567890.123456 - frame_id = "base_link" - twist_cov_stamped = TwistWithCovarianceStamped(ts=ts, frame_id=frame_id) - - assert twist_cov_stamped.ts == ts - assert twist_cov_stamped.frame_id == frame_id - - -def test_twist_with_covariance_stamped_with_twist() -> None: - """Test initialization with twist.""" - ts = 1234567890.123456 - frame_id = "odom" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.arange(36, dtype=float) - - twist_cov_stamped = TwistWithCovarianceStamped( - ts=ts, frame_id=frame_id, twist=twist, covariance=covariance - ) - - assert twist_cov_stamped.ts == ts - assert twist_cov_stamped.frame_id == frame_id - assert twist_cov_stamped.twist.linear.x == 1.0 - assert twist_cov_stamped.twist.linear.y == 2.0 - assert twist_cov_stamped.twist.linear.z == 3.0 - assert np.array_equal(twist_cov_stamped.covariance, covariance) - - -def test_twist_with_covariance_stamped_with_tuple() -> None: - """Test initialization with tuple of velocities.""" - ts = 1234567890.123456 - frame_id = "robot_base" - linear = [1.0, 2.0, 3.0] - angular = [0.1, 0.2, 0.3] - covariance = np.arange(36, dtype=float) - - twist_cov_stamped = TwistWithCovarianceStamped( - ts=ts, frame_id=frame_id, twist=(linear, angular), covariance=covariance - ) - - assert twist_cov_stamped.ts == ts - assert twist_cov_stamped.frame_id == frame_id - assert twist_cov_stamped.twist.linear.x == 1.0 - assert twist_cov_stamped.twist.angular.x == 0.1 - assert np.array_equal(twist_cov_stamped.covariance, covariance) - - -def test_twist_with_covariance_stamped_properties() -> None: - """Test convenience properties.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.eye(6).flatten() - twist_cov_stamped = TwistWithCovarianceStamped( - ts=1234567890.0, frame_id="cmd_vel", twist=twist, covariance=covariance - ) - - # Linear and angular properties - assert twist_cov_stamped.linear.x == 1.0 - assert twist_cov_stamped.linear.y == 2.0 - assert twist_cov_stamped.linear.z == 3.0 - assert twist_cov_stamped.angular.x == 0.1 - assert twist_cov_stamped.angular.y == 0.2 - assert twist_cov_stamped.angular.z == 0.3 - - # Covariance matrix - cov_matrix = twist_cov_stamped.covariance_matrix - assert cov_matrix.shape == (6, 6) - assert np.trace(cov_matrix) == 6.0 - - -def test_twist_with_covariance_stamped_str() -> None: - """Test string representation.""" - twist = Twist(Vector3(1.234, 2.567, 3.891), Vector3(0.111, 0.222, 0.333)) - covariance = np.eye(6).flatten() * 2.0 - twist_cov_stamped = TwistWithCovarianceStamped( - ts=1234567890.0, frame_id="world", twist=twist, covariance=covariance - ) - - str_repr = str(twist_cov_stamped) - assert "TwistWithCovarianceStamped" in str_repr - assert "1.234" in str_repr - assert "2.567" in str_repr - assert "3.891" in str_repr - assert "cov_trace" in str_repr - assert "12.000" in str_repr # Trace of 2*identity is 12 - - -def test_twist_with_covariance_stamped_lcm_encode_decode() -> None: - """Test LCM encoding and decoding.""" - ts = 1234567890.123456 - frame_id = "camera_link" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.arange(36, dtype=float) - - source = TwistWithCovarianceStamped( - ts=ts, frame_id=frame_id, twist=twist, covariance=covariance - ) - - # Encode and decode - binary_msg = source.lcm_encode() - decoded = TwistWithCovarianceStamped.lcm_decode(binary_msg) - - # Check timestamp (may lose some precision) - assert abs(decoded.ts - ts) < 1e-6 - assert decoded.frame_id == frame_id - - # Check twist - assert decoded.twist.linear.x == 1.0 - assert decoded.twist.linear.y == 2.0 - assert decoded.twist.linear.z == 3.0 - assert decoded.twist.angular.x == 0.1 - assert decoded.twist.angular.y == 0.2 - assert decoded.twist.angular.z == 0.3 - - # Check covariance - assert np.array_equal(decoded.covariance, covariance) - - -def test_twist_with_covariance_stamped_zero_timestamp() -> None: - """Test that zero timestamp gets replaced with current time.""" - twist_cov_stamped = TwistWithCovarianceStamped(ts=0.0) - - # Should have been replaced with current time - assert twist_cov_stamped.ts > 0 - assert twist_cov_stamped.ts <= time.time() - - -def test_twist_with_covariance_stamped_inheritance() -> None: - """Test that it properly inherits from TwistWithCovariance and Timestamped.""" - twist = Twist(Vector3(1.0, 2.0, 3.0), Vector3(0.1, 0.2, 0.3)) - covariance = np.eye(6).flatten() - twist_cov_stamped = TwistWithCovarianceStamped( - ts=1234567890.0, frame_id="test", twist=twist, covariance=covariance - ) - - # Should be instance of parent classes - assert isinstance(twist_cov_stamped, TwistWithCovariance) - - # Should have Timestamped attributes - assert hasattr(twist_cov_stamped, "ts") - assert hasattr(twist_cov_stamped, "frame_id") - - # Should have TwistWithCovariance attributes - assert hasattr(twist_cov_stamped, "twist") - assert hasattr(twist_cov_stamped, "covariance") - - -def test_twist_with_covariance_stamped_is_zero() -> None: - """Test is_zero method inheritance.""" - # Zero twist - twist_cov_stamped1 = TwistWithCovarianceStamped() - assert twist_cov_stamped1.is_zero() - assert not twist_cov_stamped1 # Boolean conversion - - # Non-zero twist - twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.0)) - twist_cov_stamped2 = TwistWithCovarianceStamped(twist=twist) - assert not twist_cov_stamped2.is_zero() - assert twist_cov_stamped2 # Boolean conversion - - -def test_twist_with_covariance_stamped_sec_nsec() -> None: - """Test the sec_nsec helper function.""" - from dimos.msgs.geometry_msgs.TwistWithCovarianceStamped import sec_nsec - - # Test integer seconds - s, ns = sec_nsec(1234567890.0) - assert s == 1234567890 - assert ns == 0 - - # Test fractional seconds - s, ns = sec_nsec(1234567890.123456789) - assert s == 1234567890 - assert abs(ns - 123456789) < 100 # Allow small rounding error - - # Test small fractional seconds - s, ns = sec_nsec(0.000000001) - assert s == 0 - assert ns == 1 - - # Test large timestamp - s, ns = sec_nsec(9999999999.999999999) - # Due to floating point precision, this might round to 10000000000 - assert s in [9999999999, 10000000000] - if s == 9999999999: - assert abs(ns - 999999999) < 10 - else: - assert ns == 0 - - -def test_twist_with_covariance_stamped_different_covariances() -> None: - """Test with different covariance patterns.""" - twist = Twist(Vector3(1.0, 0.0, 0.0), Vector3(0.0, 0.0, 0.5)) - - # Zero covariance - zero_cov = np.zeros(36) - twist_cov1 = TwistWithCovarianceStamped(twist=twist, covariance=zero_cov) - assert np.all(twist_cov1.covariance == 0.0) - - # Identity covariance - identity_cov = np.eye(6).flatten() - twist_cov2 = TwistWithCovarianceStamped(twist=twist, covariance=identity_cov) - assert np.trace(twist_cov2.covariance_matrix) == 6.0 - - # Full covariance - full_cov = np.random.rand(36) - twist_cov3 = TwistWithCovarianceStamped(twist=twist, covariance=full_cov) - assert np.array_equal(twist_cov3.covariance, full_cov) diff --git a/dimos/msgs/geometry_msgs/test_Vector3.py b/dimos/msgs/geometry_msgs/test_Vector3.py deleted file mode 100644 index 099e35eb19..0000000000 --- a/dimos/msgs/geometry_msgs/test_Vector3.py +++ /dev/null @@ -1,462 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -def test_vector_default_init() -> None: - """Test that default initialization of Vector() has x,y,z components all zero.""" - v = Vector3() - assert v.x == 0.0 - assert v.y == 0.0 - assert v.z == 0.0 - assert len(v.data) == 3 - assert v.to_list() == [0.0, 0.0, 0.0] - assert v.is_zero() # Zero vector should be considered zero - - -def test_vector_specific_init() -> None: - """Test initialization with specific values and different input types.""" - - v1 = Vector3(1.0, 2.0) # 2D vector (now becomes 3D with z=0) - assert v1.x == 1.0 - assert v1.y == 2.0 - assert v1.z == 0.0 - - v2 = Vector3(3.0, 4.0, 5.0) # 3D vector - assert v2.x == 3.0 - assert v2.y == 4.0 - assert v2.z == 5.0 - - v3 = Vector3([6.0, 7.0, 8.0]) - assert v3.x == 6.0 - assert v3.y == 7.0 - assert v3.z == 8.0 - - v4 = Vector3((9.0, 10.0, 11.0)) - assert v4.x == 9.0 - assert v4.y == 10.0 - assert v4.z == 11.0 - - v5 = Vector3(np.array([12.0, 13.0, 14.0])) - assert v5.x == 12.0 - assert v5.y == 13.0 - assert v5.z == 14.0 - - original = Vector3([15.0, 16.0, 17.0]) - v6 = Vector3(original) - assert v6.x == 15.0 - assert v6.y == 16.0 - assert v6.z == 17.0 - - assert v6 is not original - assert v6 == original - - -def test_vector_addition() -> None: - """Test vector addition.""" - v1 = Vector3(1.0, 2.0, 3.0) - v2 = Vector3(4.0, 5.0, 6.0) - - v_add = v1 + v2 - assert v_add.x == 5.0 - assert v_add.y == 7.0 - assert v_add.z == 9.0 - - -def test_vector_subtraction() -> None: - """Test vector subtraction.""" - v1 = Vector3(1.0, 2.0, 3.0) - v2 = Vector3(4.0, 5.0, 6.0) - - v_sub = v2 - v1 - assert v_sub.x == 3.0 - assert v_sub.y == 3.0 - assert v_sub.z == 3.0 - - -def test_vector_scalar_multiplication() -> None: - """Test vector multiplication by a scalar.""" - v1 = Vector3(1.0, 2.0, 3.0) - - v_mul = v1 * 2.0 - assert v_mul.x == 2.0 - assert v_mul.y == 4.0 - assert v_mul.z == 6.0 - - # Test right multiplication - v_rmul = 2.0 * v1 - assert v_rmul.x == 2.0 - assert v_rmul.y == 4.0 - assert v_rmul.z == 6.0 - - -def test_vector_scalar_division() -> None: - """Test vector division by a scalar.""" - v2 = Vector3(4.0, 5.0, 6.0) - - v_div = v2 / 2.0 - assert v_div.x == 2.0 - assert v_div.y == 2.5 - assert v_div.z == 3.0 - - -def test_vector_dot_product() -> None: - """Test vector dot product.""" - v1 = Vector3(1.0, 2.0, 3.0) - v2 = Vector3(4.0, 5.0, 6.0) - - dot = v1.dot(v2) - assert dot == 32.0 - - -def test_vector_length() -> None: - """Test vector length calculation.""" - # 2D vector with length 5 (now 3D with z=0) - v1 = Vector3(3.0, 4.0) - assert v1.length() == 5.0 - - # 3D vector - v2 = Vector3(2.0, 3.0, 6.0) - assert v2.length() == pytest.approx(7.0, 0.001) - - # Test length_squared - assert v1.length_squared() == 25.0 - assert v2.length_squared() == 49.0 - - -def test_vector_normalize() -> None: - """Test vector normalization.""" - v = Vector3(2.0, 3.0, 6.0) - assert not v.is_zero() - - v_norm = v.normalize() - length = v.length() - expected_x = 2.0 / length - expected_y = 3.0 / length - expected_z = 6.0 / length - - assert np.isclose(v_norm.x, expected_x) - assert np.isclose(v_norm.y, expected_y) - assert np.isclose(v_norm.z, expected_z) - assert np.isclose(v_norm.length(), 1.0) - assert not v_norm.is_zero() - - # Test normalizing a zero vector - v_zero = Vector3(0.0, 0.0, 0.0) - assert v_zero.is_zero() - v_zero_norm = v_zero.normalize() - assert v_zero_norm.x == 0.0 - assert v_zero_norm.y == 0.0 - assert v_zero_norm.z == 0.0 - assert v_zero_norm.is_zero() - - -def test_vector_to_2d() -> None: - """Test conversion to 2D vector.""" - v = Vector3(2.0, 3.0, 6.0) - - v_2d = v.to_2d() - assert v_2d.x == 2.0 - assert v_2d.y == 3.0 - assert v_2d.z == 0.0 # z should be 0 for 2D conversion - - # Already 2D vector (z=0) - v2 = Vector3(4.0, 5.0) - v2_2d = v2.to_2d() - assert v2_2d.x == 4.0 - assert v2_2d.y == 5.0 - assert v2_2d.z == 0.0 - - -def test_vector_distance() -> None: - """Test distance calculations between vectors.""" - v1 = Vector3(1.0, 2.0, 3.0) - v2 = Vector3(4.0, 6.0, 8.0) - - # Distance - dist = v1.distance(v2) - expected_dist = np.sqrt(9.0 + 16.0 + 25.0) # sqrt((4-1)² + (6-2)² + (8-3)²) - assert dist == pytest.approx(expected_dist) - - # Distance squared - dist_sq = v1.distance_squared(v2) - assert dist_sq == 50.0 # 9 + 16 + 25 - - -def test_vector_cross_product() -> None: - """Test vector cross product.""" - v1 = Vector3(1.0, 0.0, 0.0) # Unit x vector - v2 = Vector3(0.0, 1.0, 0.0) # Unit y vector - - # v1 × v2 should be unit z vector - cross = v1.cross(v2) - assert cross.x == 0.0 - assert cross.y == 0.0 - assert cross.z == 1.0 - - # Test with more complex vectors - a = Vector3(2.0, 3.0, 4.0) - b = Vector3(5.0, 6.0, 7.0) - c = a.cross(b) - - # Cross product manually calculated: - # (3*7-4*6, 4*5-2*7, 2*6-3*5) - assert c.x == -3.0 - assert c.y == 6.0 - assert c.z == -3.0 - - # Test with vectors that have z=0 (still works as they're 3D) - v_2d1 = Vector3(1.0, 2.0) # (1, 2, 0) - v_2d2 = Vector3(3.0, 4.0) # (3, 4, 0) - cross_2d = v_2d1.cross(v_2d2) - # (2*0-0*4, 0*3-1*0, 1*4-2*3) = (0, 0, -2) - assert cross_2d.x == 0.0 - assert cross_2d.y == 0.0 - assert cross_2d.z == -2.0 - - -def test_vector_zeros() -> None: - """Test Vector3.zeros class method.""" - # 3D zero vector - v_zeros = Vector3.zeros() - assert v_zeros.x == 0.0 - assert v_zeros.y == 0.0 - assert v_zeros.z == 0.0 - assert v_zeros.is_zero() - - -def test_vector_ones() -> None: - """Test Vector3.ones class method.""" - # 3D ones vector - v_ones = Vector3.ones() - assert v_ones.x == 1.0 - assert v_ones.y == 1.0 - assert v_ones.z == 1.0 - - -def test_vector_conversion_methods() -> None: - """Test vector conversion methods (to_list, to_tuple, to_numpy).""" - v = Vector3(1.0, 2.0, 3.0) - - # to_list - assert v.to_list() == [1.0, 2.0, 3.0] - - # to_tuple - assert v.to_tuple() == (1.0, 2.0, 3.0) - - # to_numpy - np_array = v.to_numpy() - assert isinstance(np_array, np.ndarray) - assert np.array_equal(np_array, np.array([1.0, 2.0, 3.0])) - - -def test_vector_equality() -> None: - """Test vector equality.""" - v1 = Vector3(1, 2, 3) - v2 = Vector3(1, 2, 3) - v3 = Vector3(4, 5, 6) - - assert v1 == v2 - assert v1 != v3 - assert v1 != Vector3(1, 2) # Now (1, 2, 0) vs (1, 2, 3) - assert v1 != Vector3(1.1, 2, 3) # Different values - assert v1 != [1, 2, 3] - - -def test_vector_is_zero() -> None: - """Test is_zero method for vectors.""" - # Default zero vector - v0 = Vector3() - assert v0.is_zero() - - # Explicit zero vector - v1 = Vector3(0.0, 0.0, 0.0) - assert v1.is_zero() - - # Zero vector with different initialization (now always 3D) - v2 = Vector3(0.0, 0.0) # Becomes (0, 0, 0) - assert v2.is_zero() - - # Non-zero vectors - v3 = Vector3(1.0, 0.0, 0.0) - assert not v3.is_zero() - - v4 = Vector3(0.0, 2.0, 0.0) - assert not v4.is_zero() - - v5 = Vector3(0.0, 0.0, 3.0) - assert not v5.is_zero() - - # Almost zero (within tolerance) - v6 = Vector3(1e-10, 1e-10, 1e-10) - assert v6.is_zero() - - # Almost zero (outside tolerance) - v7 = Vector3(1e-6, 1e-6, 1e-6) - assert not v7.is_zero() - - -def test_vector_bool_conversion(): - """Test boolean conversion of vectors.""" - # Zero vectors should be False - v0 = Vector3() - assert not bool(v0) - - v1 = Vector3(0.0, 0.0, 0.0) - assert not bool(v1) - - # Almost zero vectors should be False - v2 = Vector3(1e-10, 1e-10, 1e-10) - assert not bool(v2) - - # Non-zero vectors should be True - v3 = Vector3(1.0, 0.0, 0.0) - assert bool(v3) - - v4 = Vector3(0.0, 2.0, 0.0) - assert bool(v4) - - v5 = Vector3(0.0, 0.0, 3.0) - assert bool(v5) - - # Direct use in if statements - if v0: - raise AssertionError("Zero vector should be False in boolean context") - else: - pass # Expected path - - if v3: - pass # Expected path - else: - raise AssertionError("Non-zero vector should be True in boolean context") - - -def test_vector_add() -> None: - """Test vector addition operator.""" - v1 = Vector3(1.0, 2.0, 3.0) - v2 = Vector3(4.0, 5.0, 6.0) - - # Using __add__ method - v_add = v1.__add__(v2) - assert v_add.x == 5.0 - assert v_add.y == 7.0 - assert v_add.z == 9.0 - - # Using + operator - v_add_op = v1 + v2 - assert v_add_op.x == 5.0 - assert v_add_op.y == 7.0 - assert v_add_op.z == 9.0 - - # Adding zero vector should return original vector - v_zero = Vector3.zeros() - assert (v1 + v_zero) == v1 - - -def test_vector_add_dim_mismatch() -> None: - """Test vector addition with different input dimensions (now all vectors are 3D).""" - v1 = Vector3(1.0, 2.0) # Becomes (1, 2, 0) - v2 = Vector3(4.0, 5.0, 6.0) # (4, 5, 6) - - # Using + operator - should work fine now since both are 3D - v_add_op = v1 + v2 - assert v_add_op.x == 5.0 # 1 + 4 - assert v_add_op.y == 7.0 # 2 + 5 - assert v_add_op.z == 6.0 # 0 + 6 - - -def test_yaw_pitch_roll_accessors() -> None: - """Test yaw, pitch, and roll accessor properties.""" - # Test with a 3D vector - v = Vector3(1.0, 2.0, 3.0) - - # According to standard convention: - # roll = rotation around x-axis = x component - # pitch = rotation around y-axis = y component - # yaw = rotation around z-axis = z component - assert v.roll == 1.0 # Should return x component - assert v.pitch == 2.0 # Should return y component - assert v.yaw == 3.0 # Should return z component - - # Test with a 2D vector (z should be 0.0) - v_2d = Vector3(4.0, 5.0) - assert v_2d.roll == 4.0 # Should return x component - assert v_2d.pitch == 5.0 # Should return y component - assert v_2d.yaw == 0.0 # Should return z component (defaults to 0 for 2D) - - # Test with empty vector (all should be 0.0) - v_empty = Vector3() - assert v_empty.roll == 0.0 - assert v_empty.pitch == 0.0 - assert v_empty.yaw == 0.0 - - # Test with negative values - v_neg = Vector3(-1.5, -2.5, -3.5) - assert v_neg.roll == -1.5 - assert v_neg.pitch == -2.5 - assert v_neg.yaw == -3.5 - - -def test_vector_to_quaternion() -> None: - """Test vector to quaternion conversion.""" - # Test with zero Euler angles (should produce identity quaternion) - v_zero = Vector3(0.0, 0.0, 0.0) - q_identity = v_zero.to_quaternion() - - # Identity quaternion should have w=1, x=y=z=0 - assert np.isclose(q_identity.x, 0.0, atol=1e-10) - assert np.isclose(q_identity.y, 0.0, atol=1e-10) - assert np.isclose(q_identity.z, 0.0, atol=1e-10) - assert np.isclose(q_identity.w, 1.0, atol=1e-10) - - # Test with small angles (to avoid gimbal lock issues) - v_small = Vector3(0.1, 0.2, 0.3) # Small roll, pitch, yaw - q_small = v_small.to_quaternion() - - # Quaternion should be normalized (magnitude = 1) - magnitude = np.sqrt(q_small.x**2 + q_small.y**2 + q_small.z**2 + q_small.w**2) - assert np.isclose(magnitude, 1.0, atol=1e-10) - - # Test conversion back to Euler (should be close to original) - v_back = q_small.to_euler() - assert np.isclose(v_back.x, 0.1, atol=1e-6) - assert np.isclose(v_back.y, 0.2, atol=1e-6) - assert np.isclose(v_back.z, 0.3, atol=1e-6) - - # Test with π/2 rotation around x-axis - v_x_90 = Vector3(np.pi / 2, 0.0, 0.0) - q_x_90 = v_x_90.to_quaternion() - - # Should be approximately (sin(π/4), 0, 0, cos(π/4)) = (√2/2, 0, 0, √2/2) - expected = np.sqrt(2) / 2 - assert np.isclose(q_x_90.x, expected, atol=1e-10) - assert np.isclose(q_x_90.y, 0.0, atol=1e-10) - assert np.isclose(q_x_90.z, 0.0, atol=1e-10) - assert np.isclose(q_x_90.w, expected, atol=1e-10) - - -def test_lcm_encode_decode() -> None: - v_source = Vector3(1.0, 2.0, 3.0) - - binary_msg = v_source.lcm_encode() - - v_dest = Vector3.lcm_decode(binary_msg) - - assert isinstance(v_dest, Vector3) - assert v_dest is not v_source - assert v_dest == v_source diff --git a/dimos/msgs/geometry_msgs/test_publish.py b/dimos/msgs/geometry_msgs/test_publish.py deleted file mode 100644 index b3d2324af0..0000000000 --- a/dimos/msgs/geometry_msgs/test_publish.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import lcm -import pytest - -from dimos.msgs.geometry_msgs import Vector3 - - -@pytest.mark.tool -def test_runpublish() -> None: - for i in range(10): - msg = Vector3(-5 + i, -5 + i, i) - lc = lcm.LCM() - lc.publish("thing1_vector3#geometry_msgs.Vector3", msg.encode()) - time.sleep(0.1) - print(f"Published: {msg}") - - -@pytest.mark.tool -def test_receive() -> None: - lc = lcm.LCM() - - def receive(bla, msg) -> None: - # print("receive", bla, msg) - print(Vector3.decode(msg)) - - lc.subscribe("thing1_vector3#geometry_msgs.Vector3", receive) - - def _loop() -> None: - while True: - """LCM message handling loop""" - try: - lc.handle() - # loop 10000 times - for _ in range(10000000): - 3 + 3 # noqa: B018 - except Exception as e: - print(f"Error in LCM handling: {e}") - - _loop() diff --git a/dimos/msgs/helpers.py b/dimos/msgs/helpers.py deleted file mode 100644 index 8464ec4ab1..0000000000 --- a/dimos/msgs/helpers.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from functools import lru_cache -import importlib -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.msgs import DimosMsg - - -@lru_cache(maxsize=256) -def resolve_msg_type(type_name: str) -> type[DimosMsg] | None: - """Resolve a message type name to its class. - - Args: - type_name: Type name in format "module.ClassName" (e.g., "geometry_msgs.Vector3") - - Returns: - The message class or None if not found. - """ - try: - module_name, class_name = type_name.rsplit(".", 1) - except ValueError: - return None - - # Try different import paths - import_paths = [ - f"dimos.msgs.{module_name}", - f"dimos_lcm.{module_name}", - ] - - for path in import_paths: - try: - module = importlib.import_module(path) - return getattr(module, class_name) # type: ignore[no-any-return] - except (ImportError, AttributeError): - continue - - return None diff --git a/dimos/msgs/nav_msgs/OccupancyGrid.py b/dimos/msgs/nav_msgs/OccupancyGrid.py deleted file mode 100644 index d45e1b6232..0000000000 --- a/dimos/msgs/nav_msgs/OccupancyGrid.py +++ /dev/null @@ -1,592 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from enum import IntEnum -from functools import lru_cache -import time -from typing import TYPE_CHECKING, BinaryIO - -from dimos_lcm.nav_msgs import ( - MapMetaData, - OccupancyGrid as LCMOccupancyGrid, -) -from dimos_lcm.std_msgs import Time as LCMTime # type: ignore[import-untyped] -import matplotlib.pyplot as plt -import numpy as np -from PIL import Image - -from dimos.msgs.geometry_msgs import Pose, Vector3, VectorLike -from dimos.types.timestamped import Timestamped - - -@lru_cache(maxsize=16) -def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] - """Get a matplotlib colormap by name (cached for performance).""" - return plt.get_cmap(name) - - -if TYPE_CHECKING: - from pathlib import Path - - from numpy.typing import NDArray - from rerun._baseclasses import Archetype - - -class CostValues(IntEnum): - """Standard cost values for occupancy grid cells. - - These values follow the ROS nav_msgs/OccupancyGrid convention: - - 0: Free space - - 1-99: Occupied space with varying cost levels - - 100: Lethal obstacle (definitely occupied) - - -1: Unknown space - """ - - UNKNOWN = -1 # Unknown space - FREE = 0 # Free space - OCCUPIED = 100 # Occupied/lethal space - - -class OccupancyGrid(Timestamped): - """ - Convenience wrapper for nav_msgs/OccupancyGrid with numpy array support. - """ - - msg_name = "nav_msgs.OccupancyGrid" - - # Attributes - ts: float - frame_id: str - info: MapMetaData - grid: NDArray[np.int8] - - def __init__( - self, - grid: NDArray[np.int8] | None = None, - width: int | None = None, - height: int | None = None, - resolution: float = 0.05, - origin: Pose | None = None, - frame_id: str = "world", - ts: float = 0.0, - ) -> None: - """Initialize OccupancyGrid. - - Args: - grid: 2D numpy array of int8 values (height x width) - width: Width in cells (used if grid is None) - height: Height in cells (used if grid is None) - resolution: Grid resolution in meters/cell - origin: Origin pose of the grid - frame_id: Reference frame - ts: Timestamp (defaults to current time if 0) - """ - - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - - if grid is not None: - # Initialize from numpy array - if grid.ndim != 2: - raise ValueError("Grid must be a 2D array") - height, width = grid.shape - self.info = MapMetaData( - map_load_time=self._to_lcm_time(), # type: ignore[no-untyped-call] - resolution=resolution, - width=width, - height=height, - origin=origin or Pose(), - ) - self.grid = grid.astype(np.int8) - elif width is not None and height is not None: - # Initialize with dimensions - self.info = MapMetaData( - map_load_time=self._to_lcm_time(), # type: ignore[no-untyped-call] - resolution=resolution, - width=width, - height=height, - origin=origin or Pose(), - ) - self.grid = np.full((height, width), -1, dtype=np.int8) - else: - # Initialize empty - self.info = MapMetaData(map_load_time=self._to_lcm_time()) # type: ignore[no-untyped-call] - self.grid = np.array([], dtype=np.int8) - - def _to_lcm_time(self): # type: ignore[no-untyped-def] - """Convert timestamp to LCM Time.""" - - s = int(self.ts) - return LCMTime(sec=s, nsec=int((self.ts - s) * 1_000_000_000)) - - @property - def width(self) -> int: - """Width of the grid in cells.""" - return self.info.width # type: ignore[no-any-return] - - @property - def height(self) -> int: - """Height of the grid in cells.""" - return self.info.height # type: ignore[no-any-return] - - @property - def resolution(self) -> float: - """Grid resolution in meters/cell.""" - return self.info.resolution # type: ignore[no-any-return] - - @property - def origin(self) -> Pose: - """Origin pose of the grid.""" - return self.info.origin # type: ignore[no-any-return] - - @property - def total_cells(self) -> int: - """Total number of cells in the grid.""" - return self.width * self.height - - @property - def occupied_cells(self) -> int: - """Number of occupied cells (value >= 1).""" - return int(np.sum(self.grid >= 1)) - - @property - def free_cells(self) -> int: - """Number of free cells (value == 0).""" - return int(np.sum(self.grid == 0)) - - @property - def unknown_cells(self) -> int: - """Number of unknown cells (value == -1).""" - return int(np.sum(self.grid == -1)) - - @property - def occupied_percent(self) -> float: - """Percentage of cells that are occupied.""" - return (self.occupied_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 - - @property - def free_percent(self) -> float: - """Percentage of cells that are free.""" - return (self.free_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 - - @property - def unknown_percent(self) -> float: - """Percentage of cells that are unknown.""" - return (self.unknown_cells / self.total_cells * 100) if self.total_cells > 0 else 0.0 - - @classmethod - def from_path(cls, path: Path) -> OccupancyGrid: - match path.suffix.lower(): - case ".npy": - return cls(grid=np.load(path)) - case ".png": - img = Image.open(path).convert("L") - return cls(grid=np.array(img).astype(np.int8)) - case _: - raise NotImplementedError(f"Unsupported file format: {path.suffix}") - - def world_to_grid(self, point: VectorLike) -> Vector3: - """Convert world coordinates to grid coordinates. - - Args: - point: A vector-like object containing X,Y coordinates - - Returns: - Vector3 with grid coordinates - """ - positionVector = Vector3(point) - # Get origin position - ox = self.origin.position.x - oy = self.origin.position.y - - # Convert to grid coordinates (simplified, assuming no rotation) - grid_x = (positionVector.x - ox) / self.resolution - grid_y = (positionVector.y - oy) / self.resolution - - return Vector3(grid_x, grid_y, 0.0) - - def grid_to_world(self, grid_point: VectorLike) -> Vector3: - """Convert grid coordinates to world coordinates. - - Args: - grid_point: Vector-like object containing grid coordinates - - Returns: - World position as Vector3 - """ - gridVector = Vector3(grid_point) - # Get origin position - ox = self.origin.position.x - oy = self.origin.position.y - - # Convert to world (simplified, no rotation) - x = ox + gridVector.x * self.resolution - y = oy + gridVector.y * self.resolution - - return Vector3(x, y, 0.0) - - def __str__(self) -> str: - """Create a concise string representation.""" - origin_pos = self.origin.position - - parts = [ - f"▦ OccupancyGrid[{self.frame_id}]", - f"{self.width}x{self.height}", - f"({self.width * self.resolution:.1f}x{self.height * self.resolution:.1f}m @", - f"{1 / self.resolution:.0f}cm res)", - f"Origin: ({origin_pos.x:.2f}, {origin_pos.y:.2f})", - f"▣ {self.occupied_percent:.1f}%", - f"□ {self.free_percent:.1f}%", - f"◌ {self.unknown_percent:.1f}%", - ] - - return " ".join(parts) - - def __repr__(self) -> str: - """Create a detailed representation.""" - return ( - f"OccupancyGrid(width={self.width}, height={self.height}, " - f"resolution={self.resolution}, frame_id='{self.frame_id}', " - f"occupied={self.occupied_cells}, free={self.free_cells}, " - f"unknown={self.unknown_cells})" - ) - - def lcm_encode(self) -> bytes: - """Encode OccupancyGrid to LCM bytes.""" - # Create LCM message - lcm_msg = LCMOccupancyGrid() - - # Build header on demand - s = int(self.ts) - lcm_msg.header.stamp.sec = s - lcm_msg.header.stamp.nsec = int((self.ts - s) * 1_000_000_000) - lcm_msg.header.frame_id = self.frame_id - - # Copy map metadata - lcm_msg.info = self.info - - # Convert numpy array to flat data list - if self.grid.size > 0: - flat_data = self.grid.flatten() - lcm_msg.data_length = len(flat_data) - lcm_msg.data = flat_data.tolist() - else: - lcm_msg.data_length = 0 - lcm_msg.data = [] - - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> OccupancyGrid: - """Decode LCM bytes to OccupancyGrid.""" - lcm_msg = LCMOccupancyGrid.lcm_decode(data) - - # Extract timestamp and frame_id from header - ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) - frame_id = lcm_msg.header.frame_id - - # Extract grid data - if lcm_msg.data and lcm_msg.info.width > 0 and lcm_msg.info.height > 0: - grid = np.array(lcm_msg.data, dtype=np.int8).reshape( - (lcm_msg.info.height, lcm_msg.info.width) - ) - else: - grid = np.array([], dtype=np.int8) - - # Create new instance - instance = cls( - grid=grid, - resolution=lcm_msg.info.resolution, - origin=lcm_msg.info.origin, - frame_id=frame_id, - ts=ts, - ) - instance.info = lcm_msg.info - return instance - - def filter_above(self, threshold: int) -> OccupancyGrid: - """Create a new OccupancyGrid with only values above threshold. - - Args: - threshold: Keep cells with values > threshold - - Returns: - New OccupancyGrid where: - - Cells > threshold: kept as-is - - Cells <= threshold: set to -1 (unknown) - - Unknown cells (-1): preserved - """ - new_grid = self.grid.copy() - - # Create mask for cells to filter (not unknown and <= threshold) - filter_mask = (new_grid != -1) & (new_grid <= threshold) - - # Set filtered cells to unknown - new_grid[filter_mask] = -1 - - # Create new OccupancyGrid - filtered = OccupancyGrid( - new_grid, - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) - - return filtered - - def filter_below(self, threshold: int) -> OccupancyGrid: - """Create a new OccupancyGrid with only values below threshold. - - Args: - threshold: Keep cells with values < threshold - - Returns: - New OccupancyGrid where: - - Cells < threshold: kept as-is - - Cells >= threshold: set to -1 (unknown) - - Unknown cells (-1): preserved - """ - new_grid = self.grid.copy() - - # Create mask for cells to filter (not unknown and >= threshold) - filter_mask = (new_grid != -1) & (new_grid >= threshold) - - # Set filtered cells to unknown - new_grid[filter_mask] = -1 - - # Create new OccupancyGrid - filtered = OccupancyGrid( - new_grid, - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) - - return filtered - - def max(self) -> OccupancyGrid: - """Create a new OccupancyGrid with all non-unknown cells set to maximum value. - - Returns: - New OccupancyGrid where: - - All non-unknown cells: set to CostValues.OCCUPIED (100) - - Unknown cells: preserved as CostValues.UNKNOWN (-1) - """ - new_grid = self.grid.copy() - - # Set all non-unknown cells to max - new_grid[new_grid != CostValues.UNKNOWN] = CostValues.OCCUPIED - - # Create new OccupancyGrid - maxed = OccupancyGrid( - new_grid, - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) - - return maxed - - def copy(self) -> OccupancyGrid: - """Create a deep copy of the OccupancyGrid. - - Returns: - A new OccupancyGrid instance with copied data. - """ - return OccupancyGrid( - grid=self.grid.copy(), - resolution=self.resolution, - origin=self.origin, - frame_id=self.frame_id, - ts=self.ts, - ) - - def cell_value(self, world_position: Vector3) -> int: - grid_position = self.world_to_grid(world_position) - x = int(grid_position.x) - y = int(grid_position.y) - - if not (0 <= x < self.width and 0 <= y < self.height): - return CostValues.UNKNOWN - - return int(self.grid[y, x]) - - def _generate_rgba_texture( - self, - colormap: str | None = None, - opacity: float = 1.0, - cost_range: tuple[int, int] | None = None, - background: str | None = None, - ) -> NDArray[np.uint8]: - """Generate RGBA texture for the occupancy grid. - - Args: - colormap: Optional matplotlib colormap name. - opacity: Blend factor (0.0 to 1.0). Blends towards background color. - cost_range: Optional (min, max) cost range. Cells outside range use background. - background: Hex color for background (e.g. "#484981"). Default is black. - - Returns: - RGBA numpy array of shape (height, width, 4). - Note: NOT flipped - caller handles orientation. - """ - # Parse background hex to RGB - if background is not None: - bg = background.lstrip("#") - bg_rgb = np.array([int(bg[i : i + 2], 16) for i in (0, 2, 4)], dtype=np.float32) - else: - bg_rgb = np.array([0, 0, 0], dtype=np.float32) - - # Determine which cells are in range (if cost_range specified) - if cost_range is not None: - in_range_mask = (self.grid >= cost_range[0]) & (self.grid <= cost_range[1]) - else: - in_range_mask = None - - if colormap is not None: - cmap = _get_matplotlib_cmap(colormap) - grid_float = self.grid.astype(np.float32) - - vis = np.zeros((self.height, self.width, 4), dtype=np.uint8) - - free_mask = self.grid == 0 - occupied_mask = self.grid > 0 - - if np.any(free_mask): - fg = np.array(cmap(0.0)[:3]) * 255 - blended = fg * opacity + bg_rgb * (1 - opacity) - vis[free_mask, :3] = blended.astype(np.uint8) - vis[free_mask, 3] = 255 - - if np.any(occupied_mask): - costs = grid_float[occupied_mask] - cost_norm = 0.5 + (costs / 100) * 0.5 - fg = cmap(cost_norm)[:, :3] * 255 - blended = fg * opacity + bg_rgb * (1 - opacity) - vis[occupied_mask, :3] = blended.astype(np.uint8) - vis[occupied_mask, 3] = 255 - - # Unknown cells: always black - unknown_mask = self.grid == -1 - vis[unknown_mask, :3] = 0 - vis[unknown_mask, 3] = 255 - - # Apply cost_range filter - set out-of-range cells to background - if in_range_mask is not None: - out_of_range = ~in_range_mask & (self.grid != -1) - vis[out_of_range, :3] = bg_rgb.astype(np.uint8) - vis[out_of_range, 3] = 255 - - return vis - - # Default: Foxglove-style coloring - vis = np.zeros((self.height, self.width, 4), dtype=np.uint8) - - free_mask = self.grid == 0 - occupied_mask = self.grid > 0 - - # Free space: blue-purple #484981, blended with background - fg_free = np.array([72, 73, 129], dtype=np.float32) - blended_free = fg_free * opacity + bg_rgb * (1 - opacity) - vis[free_mask, :3] = blended_free.astype(np.uint8) - vis[free_mask, 3] = 255 - - # Occupied: gradient from blue-purple to black, blended with background - if np.any(occupied_mask): - costs = self.grid[occupied_mask].astype(np.float32) - factor = (1 - costs / 100).clip(0, 1) - fg_occ = np.column_stack([72 * factor, 73 * factor, 129 * factor]) - blended_occ = fg_occ * opacity + bg_rgb * (1 - opacity) - vis[occupied_mask, :3] = blended_occ.astype(np.uint8) - vis[occupied_mask, 3] = 255 - - # Unknown cells: always black - unknown_mask = self.grid == -1 - vis[unknown_mask, :3] = 0 - vis[unknown_mask, 3] = 255 - - # Apply cost_range filter - set out-of-range cells to background - if in_range_mask is not None: - out_of_range = ~in_range_mask & (self.grid != -1) - vis[out_of_range, :3] = bg_rgb.astype(np.uint8) - vis[out_of_range, 3] = 255 - - return vis - - def to_rerun( - self, - colormap: str | None = None, - z_offset: float = 0.01, - opacity: float = 1.0, - cost_range: tuple[int, int] | None = None, - background: str | None = None, - ) -> Archetype: - """Convert to 3D textured mesh overlay on floor plane. - - Uses a single quad with the occupancy grid as a texture. - Much more efficient than per-cell quads (4 vertices vs n_cells*4). - """ - import rerun as rr - - if self.grid.size == 0: - return rr.Mesh3D(vertex_positions=[]) - - # Generate RGBA texture and flip to match world coordinates - # Grid row 0 is at world y=origin (bottom), but texture row 0 is at UV v=0 (top) - rgba = np.flipud(self._generate_rgba_texture(colormap, opacity, cost_range, background)) - - # Single quad covering entire grid - ox = self.origin.position.x - oy = self.origin.position.y - w = self.width * self.resolution - h = self.height * self.resolution - - vertices = np.array( - [ - [ox, oy, z_offset], # 0: bottom-left (world) - [ox + w, oy, z_offset], # 1: bottom-right - [ox + w, oy + h, z_offset], # 2: top-right - [ox, oy + h, z_offset], # 3: top-left - ], - dtype=np.float32, - ) - - indices = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.uint32) - - # UV coords: Rerun uses top-left origin for textures - # Grid row 0 is at world y=oy (bottom), row H-1 at y=oy+h (top) - # Texture row 0 = grid row 0, so: - # world bottom (v0,v1) -> texture v=1 (bottom of texture) - # world top (v2,v3) -> texture v=0 (top of texture) - texcoords = np.array( - [ - [0.0, 1.0], # v0: bottom-left world -> bottom-left tex - [1.0, 1.0], # v1: bottom-right world -> bottom-right tex - [1.0, 0.0], # v2: top-right world -> top-right tex - [0.0, 0.0], # v3: top-left world -> top-left tex - ], - dtype=np.float32, - ) - - return rr.Mesh3D( - vertex_positions=vertices, - triangle_indices=indices, - vertex_texcoords=texcoords, - albedo_texture=rgba, - ) diff --git a/dimos/msgs/nav_msgs/Odometry.py b/dimos/msgs/nav_msgs/Odometry.py deleted file mode 100644 index a958f8dba0..0000000000 --- a/dimos/msgs/nav_msgs/Odometry.py +++ /dev/null @@ -1,235 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from rerun._baseclasses import Archetype - -from dimos_lcm.nav_msgs import Odometry as LCMOdometry -import numpy as np - -from dimos.msgs.geometry_msgs.Pose import Pose -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.types.timestamped import Timestamped - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs.Quaternion import Quaternion - from dimos.msgs.geometry_msgs.Vector3 import Vector3 - - -class Odometry(Timestamped): - """Odometry message with pose, twist, and frame information.""" - - msg_name = "nav_msgs.Odometry" - - def __init__( - self, - ts: float = 0.0, - frame_id: str = "", - child_frame_id: str = "", - pose: PoseWithCovariance | Pose | None = None, - twist: TwistWithCovariance | Twist | None = None, - ) -> None: - self.ts = ts if ts != 0 else time.time() - self.frame_id = frame_id - self.child_frame_id = child_frame_id - - if pose is None: - self.pose = PoseWithCovariance() - elif isinstance(pose, Pose): - self.pose = PoseWithCovariance(pose) - else: - self.pose = pose - - if twist is None: - self.twist = TwistWithCovariance() - elif isinstance(twist, Twist): - self.twist = TwistWithCovariance(twist) - else: - self.twist = twist - - # -- Convenience properties -- - - @property - def position(self) -> Vector3: - return self.pose.position - - @property - def orientation(self) -> Quaternion: - return self.pose.orientation - - @property - def linear_velocity(self) -> Vector3: - return self.twist.linear - - @property - def angular_velocity(self) -> Vector3: - return self.twist.angular - - @property - def x(self) -> float: - return self.pose.x - - @property - def y(self) -> float: - return self.pose.y - - @property - def z(self) -> float: - return self.pose.z - - @property - def vx(self) -> float: - return self.twist.linear.x - - @property - def vy(self) -> float: - return self.twist.linear.y - - @property - def vz(self) -> float: - return self.twist.linear.z - - @property - def wx(self) -> float: - return self.twist.angular.x - - @property - def wy(self) -> float: - return self.twist.angular.y - - @property - def wz(self) -> float: - return self.twist.angular.z - - @property - def roll(self) -> float: - return self.pose.roll - - @property - def pitch(self) -> float: - return self.pose.pitch - - @property - def yaw(self) -> float: - return self.pose.yaw - - # -- Serialization -- - - def lcm_encode(self) -> bytes: - lcm_msg = LCMOdometry() - - lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec = self.ros_timestamp() - lcm_msg.header.frame_id = self.frame_id - lcm_msg.child_frame_id = self.child_frame_id - - lcm_msg.pose.pose = self.pose.pose - lcm_msg.pose.covariance = list(np.asarray(self.pose.covariance)) - - lcm_msg.twist.twist = self.twist.twist - lcm_msg.twist.covariance = list(np.asarray(self.twist.covariance)) - - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> Odometry: - lcm_msg = LCMOdometry.lcm_decode(data) - - ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) - - pose = Pose( - position=[ - lcm_msg.pose.pose.position.x, - lcm_msg.pose.pose.position.y, - lcm_msg.pose.pose.position.z, - ], - orientation=[ - lcm_msg.pose.pose.orientation.x, - lcm_msg.pose.pose.orientation.y, - lcm_msg.pose.pose.orientation.z, - lcm_msg.pose.pose.orientation.w, - ], - ) - twist = Twist( - linear=[ - lcm_msg.twist.twist.linear.x, - lcm_msg.twist.twist.linear.y, - lcm_msg.twist.twist.linear.z, - ], - angular=[ - lcm_msg.twist.twist.angular.x, - lcm_msg.twist.twist.angular.y, - lcm_msg.twist.twist.angular.z, - ], - ) - - return cls( - ts=ts, - frame_id=lcm_msg.header.frame_id, - child_frame_id=lcm_msg.child_frame_id, - pose=PoseWithCovariance(pose, lcm_msg.pose.covariance), - twist=TwistWithCovariance(twist, lcm_msg.twist.covariance), - ) - - # -- Comparison / display -- - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Odometry): - return False - return ( - abs(self.ts - other.ts) < 1e-6 - and self.frame_id == other.frame_id - and self.child_frame_id == other.child_frame_id - and self.pose == other.pose - and self.twist == other.twist - ) - - def __repr__(self) -> str: - return ( - f"Odometry(ts={self.ts:.6f}, frame_id='{self.frame_id}', " - f"child_frame_id='{self.child_frame_id}', pose={self.pose!r}, twist={self.twist!r})" - ) - - def __str__(self) -> str: - return ( - f"Odometry:\n" - f" Timestamp: {self.ts:.6f}\n" - f" Frame: {self.frame_id} -> {self.child_frame_id}\n" - f" Position: [{self.x:.3f}, {self.y:.3f}, {self.z:.3f}]\n" - f" Orientation: [roll={self.roll:.3f}, pitch={self.pitch:.3f}, yaw={self.yaw:.3f}]\n" - f" Linear Velocity: [{self.vx:.3f}, {self.vy:.3f}, {self.vz:.3f}]\n" - f" Angular Velocity: [{self.wx:.3f}, {self.wy:.3f}, {self.wz:.3f}]" - ) - - def to_rerun(self) -> Archetype: - """Convert to rerun Transform3D for visualizing the pose.""" - import rerun as rr - - return rr.Transform3D( - translation=[self.x, self.y, self.z], - rotation=rr.Quaternion( - xyzw=[ - self.orientation.x, - self.orientation.y, - self.orientation.z, - self.orientation.w, - ] - ), - ) diff --git a/dimos/msgs/nav_msgs/Path.py b/dimos/msgs/nav_msgs/Path.py deleted file mode 100644 index 1582c4b775..0000000000 --- a/dimos/msgs/nav_msgs/Path.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING, BinaryIO - -from dimos_lcm.geometry_msgs import ( - Point as LCMPoint, - Pose as LCMPose, - PoseStamped as LCMPoseStamped, - Quaternion as LCMQuaternion, -) -from dimos_lcm.nav_msgs import Path as LCMPath -from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime - -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.types.timestamped import Timestamped - -if TYPE_CHECKING: - from collections.abc import Iterator - - from rerun._baseclasses import Archetype - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class Path(Timestamped): - msg_name = "nav_msgs.Path" - ts: float - frame_id: str - poses: list[PoseStamped] - - def __init__( # type: ignore[no-untyped-def] - self, - ts: float = 0.0, - frame_id: str = "world", - poses: list[PoseStamped] | None = None, - **kwargs, - ) -> None: - self.frame_id = frame_id - self.ts = ts if ts != 0 else time.time() - self.poses = poses if poses is not None else [] - - def __len__(self) -> int: - """Return the number of poses in the path.""" - return len(self.poses) - - def __bool__(self) -> bool: - """Return True if path has poses.""" - return len(self.poses) > 0 - - def head(self) -> PoseStamped | None: - """Return the first pose in the path, or None if empty.""" - return self.poses[0] if self.poses else None - - def last(self) -> PoseStamped | None: - """Return the last pose in the path, or None if empty.""" - return self.poses[-1] if self.poses else None - - def tail(self) -> Path: - """Return a new Path with all poses except the first.""" - return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses[1:] if self.poses else []) - - def push(self, pose: PoseStamped) -> Path: - """Return a new Path with the pose appended (immutable).""" - return Path(ts=self.ts, frame_id=self.frame_id, poses=[*self.poses, pose]) - - def push_mut(self, pose: PoseStamped) -> None: - """Append a pose to this path (mutable).""" - self.poses.append(pose) - - def lcm_encode(self) -> bytes: - """Encode Path to LCM bytes.""" - lcm_msg = LCMPath() - - # Set poses - lcm_msg.poses_length = len(self.poses) - lcm_poses = [] # Build list separately to avoid LCM library reuse issues - for pose in self.poses: - lcm_pose = LCMPoseStamped() - # Create new pose objects to avoid LCM library reuse bug - lcm_pose.pose = LCMPose() - lcm_pose.pose.position = LCMPoint() - lcm_pose.pose.orientation = LCMQuaternion() - - # Set the pose geometry data - lcm_pose.pose.position.x = pose.x - lcm_pose.pose.position.y = pose.y - lcm_pose.pose.position.z = pose.z - lcm_pose.pose.orientation.x = pose.orientation.x - lcm_pose.pose.orientation.y = pose.orientation.y - lcm_pose.pose.orientation.z = pose.orientation.z - lcm_pose.pose.orientation.w = pose.orientation.w - - # Create new header to avoid reuse - lcm_pose.header = LCMHeader() - lcm_pose.header.stamp = LCMTime() - - # Set the header with pose timestamp but path's frame_id - [lcm_pose.header.stamp.sec, lcm_pose.header.stamp.nsec] = sec_nsec(pose.ts) # type: ignore[no-untyped-call] - lcm_pose.header.frame_id = self.frame_id # All poses use path's frame_id - lcm_poses.append(lcm_pose) - lcm_msg.poses = lcm_poses - - # Set header with path's own timestamp - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> Path: - """Decode LCM bytes to Path.""" - lcm_msg = LCMPath.lcm_decode(data) - - # Decode header - header_ts = lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000) - frame_id = lcm_msg.header.frame_id - - # Decode poses - all use the path's frame_id - poses = [] - for lcm_pose in lcm_msg.poses: - pose = PoseStamped( - ts=lcm_pose.header.stamp.sec + (lcm_pose.header.stamp.nsec / 1_000_000_000), - frame_id=frame_id, # Use path's frame_id for all poses - position=[ - lcm_pose.pose.position.x, - lcm_pose.pose.position.y, - lcm_pose.pose.position.z, - ], - orientation=[ - lcm_pose.pose.orientation.x, - lcm_pose.pose.orientation.y, - lcm_pose.pose.orientation.z, - lcm_pose.pose.orientation.w, - ], - ) - poses.append(pose) - - # Use header timestamp for the path - return cls(ts=header_ts, frame_id=frame_id, poses=poses) - - def __str__(self) -> str: - """String representation of Path.""" - return f"Path(frame_id='{self.frame_id}', poses={len(self.poses)})" - - def __getitem__(self, index: int | slice) -> PoseStamped | list[PoseStamped]: - """Allow indexing and slicing of poses.""" - return self.poses[index] - - def __iter__(self) -> Iterator: # type: ignore[type-arg] - """Allow iteration over poses.""" - return iter(self.poses) - - def slice(self, start: int, end: int | None = None) -> Path: - """Return a new Path with a slice of poses.""" - return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses[start:end]) - - def extend(self, other: Path) -> Path: - """Return a new Path with poses from both paths (immutable).""" - return Path(ts=self.ts, frame_id=self.frame_id, poses=self.poses + other.poses) - - def extend_mut(self, other: Path) -> None: - """Extend this path with poses from another path (mutable).""" - self.poses.extend(other.poses) - - def reverse(self) -> Path: - """Return a new Path with poses in reverse order.""" - return Path(ts=self.ts, frame_id=self.frame_id, poses=list(reversed(self.poses))) - - def clear(self) -> None: - """Clear all poses from this path (mutable).""" - self.poses.clear() - - def to_rerun( - self, - color: tuple[int, int, int] = (0, 255, 128), - z_offset: float = 0.5, - radii: float = 0.05, - ) -> Archetype: - """Convert to rerun LineStrips3D format. - - Args: - color: RGB color tuple for the path line - z_offset: Height above floor to render path (default 0.2m to avoid costmap occlusion) - radii: Thickness of the path line (default 0.05m = 5cm) - - Returns: - rr.LineStrips3D archetype for logging to rerun - """ - import rerun as rr - - if not self.poses: - return rr.LineStrips3D([]) - - # Lift path above floor so it's visible over costmap - points = [[p.x, p.y, p.z + z_offset] for p in self.poses] - return rr.LineStrips3D([points], colors=[color], radii=radii) diff --git a/dimos/msgs/nav_msgs/__init__.py b/dimos/msgs/nav_msgs/__init__.py deleted file mode 100644 index 9d099068ad..0000000000 --- a/dimos/msgs/nav_msgs/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from dimos.msgs.nav_msgs.OccupancyGrid import ( # type: ignore[attr-defined] - CostValues, - MapMetaData, - OccupancyGrid, -) -from dimos.msgs.nav_msgs.Odometry import Odometry -from dimos.msgs.nav_msgs.Path import Path - -__all__ = ["CostValues", "MapMetaData", "OccupancyGrid", "Odometry", "Path"] diff --git a/dimos/msgs/nav_msgs/test_OccupancyGrid.py b/dimos/msgs/nav_msgs/test_OccupancyGrid.py deleted file mode 100644 index 29ef196de8..0000000000 --- a/dimos/msgs/nav_msgs/test_OccupancyGrid.py +++ /dev/null @@ -1,470 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test the OccupancyGrid convenience class.""" - -import pickle - -import numpy as np -import pytest - -from dimos.mapping.occupancy.gradient import gradient -from dimos.mapping.occupancy.inflation import simple_inflate -from dimos.mapping.pointclouds.occupancy import general_occupancy -from dimos.msgs.geometry_msgs import Pose -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, Topic -from dimos.utils.data import get_data - - -def test_empty_grid() -> None: - """Test creating an empty grid.""" - grid = OccupancyGrid() - assert grid.width == 0 - assert grid.height == 0 - assert grid.grid.shape == (0,) - assert grid.total_cells == 0 - assert grid.frame_id == "world" - - -def test_grid_with_dimensions() -> None: - """Test creating a grid with specified dimensions.""" - grid = OccupancyGrid(width=10, height=10, resolution=0.1, frame_id="map") - assert grid.width == 10 - assert grid.height == 10 - assert grid.resolution == 0.1 - assert grid.frame_id == "map" - assert grid.grid.shape == (10, 10) - assert np.all(grid.grid == -1) # All unknown - assert grid.unknown_cells == 100 - assert grid.unknown_percent == 100.0 - - -def test_grid_from_numpy_array() -> None: - """Test creating a grid from a numpy array.""" - data = np.zeros((20, 30), dtype=np.int8) - data[5:10, 10:20] = 100 # Add some obstacles - data[15:18, 5:8] = -1 # Add unknown area - - origin = Pose(1.0, 2.0, 0.0) - grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") - - assert grid.width == 30 - assert grid.height == 20 - assert grid.resolution == 0.05 - assert grid.frame_id == "odom" - assert grid.origin.position.x == 1.0 - assert grid.origin.position.y == 2.0 - assert grid.grid.shape == (20, 30) - - # Check cell counts - assert grid.occupied_cells == 50 # 5x10 obstacle area - assert grid.free_cells == 541 # Total - occupied - unknown - assert grid.unknown_cells == 9 # 3x3 unknown area - - # Check percentages (approximately) - assert abs(grid.occupied_percent - 8.33) < 0.1 - assert abs(grid.free_percent - 90.17) < 0.1 - assert abs(grid.unknown_percent - 1.5) < 0.1 - - -def test_world_grid_coordinate_conversion() -> None: - """Test converting between world and grid coordinates.""" - data = np.zeros((20, 30), dtype=np.int8) - origin = Pose(1.0, 2.0, 0.0) - grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") - - # Test world to grid - grid_pos = grid.world_to_grid((2.5, 3.0)) - assert int(grid_pos.x) == 30 - assert int(grid_pos.y) == 20 - - # Test grid to world - world_pos = grid.grid_to_world((10, 5)) - assert world_pos.x == 1.5 - assert world_pos.y == 2.25 - - -def test_lcm_encode_decode() -> None: - """Test LCM encoding and decoding.""" - data = np.zeros((20, 30), dtype=np.int8) - data[5:10, 10:20] = 100 # Add some obstacles - data[15:18, 5:8] = -1 # Add unknown area - origin = Pose(1.0, 2.0, 0.0) - grid = OccupancyGrid(grid=data, resolution=0.05, origin=origin, frame_id="odom") - - # Set a specific value for testing - # Convert world coordinates to grid indices - grid_pos = grid.world_to_grid((1.5, 2.25)) - grid.grid[int(grid_pos.y), int(grid_pos.x)] = 50 - - # Encode - lcm_data = grid.lcm_encode() - assert isinstance(lcm_data, bytes) - assert len(lcm_data) > 0 - - # Decode - decoded = OccupancyGrid.lcm_decode(lcm_data) - - # Check that data matches exactly (grid arrays should be identical) - assert np.array_equal(grid.grid, decoded.grid) - assert grid.width == decoded.width - assert grid.height == decoded.height - assert abs(grid.resolution - decoded.resolution) < 1e-6 # Use approximate equality for floats - assert abs(grid.origin.position.x - decoded.origin.position.x) < 1e-6 - assert abs(grid.origin.position.y - decoded.origin.position.y) < 1e-6 - assert grid.frame_id == decoded.frame_id - - # Check that the actual grid data was preserved (don't rely on float conversions) - assert decoded.grid[5, 10] == 50 # Value we set should be preserved in grid - - -def test_string_representation() -> None: - """Test string representations.""" - grid = OccupancyGrid(width=10, height=10, resolution=0.1, frame_id="map") - - # Test __str__ - str_repr = str(grid) - assert "OccupancyGrid[map]" in str_repr - assert "10x10" in str_repr - assert "1.0x1.0m" in str_repr - assert "10cm res" in str_repr - - # Test __repr__ - repr_str = repr(grid) - assert "OccupancyGrid(" in repr_str - assert "width=10" in repr_str - assert "height=10" in repr_str - assert "resolution=0.1" in repr_str - - -def test_grid_property_sync() -> None: - """Test that the grid property works correctly.""" - grid = OccupancyGrid(width=5, height=5, resolution=0.1, frame_id="map") - - # Modify via numpy array - grid.grid[2, 3] = 100 - assert grid.grid[2, 3] == 100 - - # Check that we can access grid values - grid.grid[0, 0] = 50 - assert grid.grid[0, 0] == 50 - - -def test_invalid_grid_dimensions() -> None: - """Test handling of invalid grid dimensions.""" - # Test with non-2D array - with pytest.raises(ValueError, match="Grid must be a 2D array"): - OccupancyGrid(grid=np.zeros(10), resolution=0.1) - - -def test_from_pointcloud() -> None: - """Test creating OccupancyGrid from PointCloud2.""" - file_path = get_data("lcm_msgs") / "sensor_msgs/PointCloud2.pickle" - with open(file_path, "rb") as f: - lcm_msg = pickle.loads(f.read()) - - pointcloud = PointCloud2.lcm_decode(lcm_msg) - - # Convert pointcloud to occupancy grid - occupancygrid = general_occupancy(pointcloud, resolution=0.05, min_height=0.1, max_height=2.0) - # Apply inflation separately if needed - occupancygrid = simple_inflate(occupancygrid, 0.1) - - # Check that grid was created with reasonable properties - assert occupancygrid.width > 0 - assert occupancygrid.height > 0 - assert occupancygrid.resolution == 0.05 - assert occupancygrid.frame_id == pointcloud.frame_id - assert occupancygrid.occupied_cells > 0 # Should have some occupied cells - - -def test_gradient() -> None: - """Test converting occupancy grid to gradient field.""" - # Create a small test grid with an obstacle in the middle - data = np.zeros((10, 10), dtype=np.int8) - data[4:6, 4:6] = 100 # 2x2 obstacle in center - - grid = OccupancyGrid(grid=data, resolution=0.1) # 0.1m per cell - - # Convert to gradient - gradient_grid = gradient(grid, obstacle_threshold=50, max_distance=1.0) - - # Check that we get an OccupancyGrid back - assert isinstance(gradient_grid, OccupancyGrid) - assert gradient_grid.grid.shape == (10, 10) - assert gradient_grid.resolution == grid.resolution - assert gradient_grid.frame_id == grid.frame_id - - # Obstacle cells should have value 100 - assert gradient_grid.grid[4, 4] == 100 - assert gradient_grid.grid[5, 5] == 100 - - # Adjacent cells should have high values (near obstacles) - assert gradient_grid.grid[3, 4] > 85 # Very close to obstacle - assert gradient_grid.grid[4, 3] > 85 # Very close to obstacle - - # Cells at moderate distance should have moderate values - assert 30 < gradient_grid.grid[0, 0] < 60 # Corner is ~0.57m away - - # Check that gradient decreases with distance - assert gradient_grid.grid[3, 4] > gradient_grid.grid[2, 4] # Closer is higher - assert gradient_grid.grid[2, 4] > gradient_grid.grid[0, 4] # Further is lower - - # Test with unknown cells - data_with_unknown = data.copy() - data_with_unknown[0:2, 0:2] = -1 # Add unknown area (close to obstacle) - data_with_unknown[8:10, 8:10] = -1 # Add unknown area (far from obstacle) - - grid_with_unknown = OccupancyGrid(data_with_unknown, resolution=0.1) - gradient_with_unknown = gradient(grid_with_unknown, max_distance=1.0) # 1m max distance - - # Unknown cells should remain unknown (new behavior - unknowns are preserved) - assert gradient_with_unknown.grid[0, 0] == -1 # Should remain unknown - assert gradient_with_unknown.grid[1, 1] == -1 # Should remain unknown - assert gradient_with_unknown.grid[8, 8] == -1 # Should remain unknown - assert gradient_with_unknown.grid[9, 9] == -1 # Should remain unknown - - # Unknown cells count should be preserved - assert gradient_with_unknown.unknown_cells == 8 # All unknowns preserved - - -def test_filter_above() -> None: - """Test filtering cells above threshold.""" - # Create test grid with various values - data = np.array( - [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 - ) - - grid = OccupancyGrid(grid=data, resolution=0.1) - - # Filter to keep only values > 50 - filtered = grid.filter_above(50) - - # Check that values > 50 are preserved - assert filtered.grid[1, 2] == 60 - assert filtered.grid[1, 3] == 80 - assert filtered.grid[2, 1] == 70 - assert filtered.grid[2, 2] == 90 - assert filtered.grid[2, 3] == 100 - - # Check that values <= 50 are set to -1 (unknown) - assert filtered.grid[0, 1] == -1 # was 0 - assert filtered.grid[0, 2] == -1 # was 20 - assert filtered.grid[0, 3] == -1 # was 50 - assert filtered.grid[1, 0] == -1 # was 10 - assert filtered.grid[1, 1] == -1 # was 30 - assert filtered.grid[2, 0] == -1 # was 40 - - # Check that unknown cells are preserved - assert filtered.grid[0, 0] == -1 - assert filtered.grid[3, 0] == -1 - assert filtered.grid[3, 3] == -1 - - # Check dimensions and metadata preserved - assert filtered.width == grid.width - assert filtered.height == grid.height - assert filtered.resolution == grid.resolution - assert filtered.frame_id == grid.frame_id - - -def test_filter_below() -> None: - """Test filtering cells below threshold.""" - # Create test grid with various values - data = np.array( - [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 - ) - - grid = OccupancyGrid(grid=data, resolution=0.1) - - # Filter to keep only values < 50 - filtered = grid.filter_below(50) - - # Check that values < 50 are preserved - assert filtered.grid[0, 1] == 0 - assert filtered.grid[0, 2] == 20 - assert filtered.grid[1, 0] == 10 - assert filtered.grid[1, 1] == 30 - assert filtered.grid[2, 0] == 40 - assert filtered.grid[3, 1] == 15 - assert filtered.grid[3, 2] == 25 - - # Check that values >= 50 are set to -1 (unknown) - assert filtered.grid[0, 3] == -1 # was 50 - assert filtered.grid[1, 2] == -1 # was 60 - assert filtered.grid[1, 3] == -1 # was 80 - assert filtered.grid[2, 1] == -1 # was 70 - assert filtered.grid[2, 2] == -1 # was 90 - assert filtered.grid[2, 3] == -1 # was 100 - - # Check that unknown cells are preserved - assert filtered.grid[0, 0] == -1 - assert filtered.grid[3, 0] == -1 - assert filtered.grid[3, 3] == -1 - - # Check dimensions and metadata preserved - assert filtered.width == grid.width - assert filtered.height == grid.height - assert filtered.resolution == grid.resolution - assert filtered.frame_id == grid.frame_id - - -def test_max() -> None: - """Test setting all non-unknown cells to maximum.""" - # Create test grid with various values - data = np.array( - [[-1, 0, 20, 50], [10, 30, 60, 80], [40, 70, 90, 100], [-1, 15, 25, -1]], dtype=np.int8 - ) - - grid = OccupancyGrid(grid=data, resolution=0.1) - - # Apply max - maxed = grid.max() - - # Check that all non-unknown cells are set to 100 - assert maxed.grid[0, 1] == 100 # was 0 - assert maxed.grid[0, 2] == 100 # was 20 - assert maxed.grid[0, 3] == 100 # was 50 - assert maxed.grid[1, 0] == 100 # was 10 - assert maxed.grid[1, 1] == 100 # was 30 - assert maxed.grid[1, 2] == 100 # was 60 - assert maxed.grid[1, 3] == 100 # was 80 - assert maxed.grid[2, 0] == 100 # was 40 - assert maxed.grid[2, 1] == 100 # was 70 - assert maxed.grid[2, 2] == 100 # was 90 - assert maxed.grid[2, 3] == 100 # was 100 (already max) - assert maxed.grid[3, 1] == 100 # was 15 - assert maxed.grid[3, 2] == 100 # was 25 - - # Check that unknown cells are preserved - assert maxed.grid[0, 0] == -1 - assert maxed.grid[3, 0] == -1 - assert maxed.grid[3, 3] == -1 - - # Check dimensions and metadata preserved - assert maxed.width == grid.width - assert maxed.height == grid.height - assert maxed.resolution == grid.resolution - assert maxed.frame_id == grid.frame_id - - # Verify statistics - assert maxed.unknown_cells == 3 # Same as original - assert maxed.occupied_cells == 13 # All non-unknown cells - assert maxed.free_cells == 0 # No free cells - - -@pytest.mark.lcm -def test_lcm_broadcast() -> None: - """Test broadcasting OccupancyGrid and gradient over LCM.""" - file_path = get_data("lcm_msgs") / "sensor_msgs/PointCloud2.pickle" - with open(file_path, "rb") as f: - lcm_msg = pickle.loads(f.read()) - - pointcloud = PointCloud2.lcm_decode(lcm_msg) - - # Create occupancy grid from pointcloud - occupancygrid = general_occupancy(pointcloud, resolution=0.05, min_height=0.1, max_height=2.0) - # Apply inflation separately if needed - occupancygrid = simple_inflate(occupancygrid, 0.1) - - # Create gradient field with larger max_distance for better visualization - gradient_grid = gradient(occupancygrid, obstacle_threshold=70, max_distance=2.0) - - # Debug: Print actual values to see the difference - print("\n=== DEBUG: Comparing grids ===") - print(f"Original grid unique values: {np.unique(occupancygrid.grid)}") - print(f"Gradient grid unique values: {np.unique(gradient_grid.grid)}") - - # Find an area with occupied cells to show the difference - occupied_indices = np.argwhere(occupancygrid.grid == 100) - if len(occupied_indices) > 0: - # Pick a point near an occupied cell - idx = len(occupied_indices) // 2 # Middle occupied cell - sample_y, sample_x = occupied_indices[idx] - sample_size = 15 - - # Ensure we don't go out of bounds - y_start = max(0, sample_y - sample_size // 2) - y_end = min(occupancygrid.height, y_start + sample_size) - x_start = max(0, sample_x - sample_size // 2) - x_end = min(occupancygrid.width, x_start + sample_size) - - print(f"\nSample area around occupied cell ({sample_x}, {sample_y}):") - print("Original occupancy grid:") - print(occupancygrid.grid[y_start:y_end, x_start:x_end]) - print("\nGradient grid (same area):") - print(gradient_grid.grid[y_start:y_end, x_start:x_end]) - else: - print("\nNo occupied cells found for sampling") - - # Check statistics - print("\nOriginal grid stats:") - print(f" Occupied (100): {np.sum(occupancygrid.grid == 100)} cells") - print(f" Inflated (99): {np.sum(occupancygrid.grid == 99)} cells") - print(f" Free (0): {np.sum(occupancygrid.grid == 0)} cells") - print(f" Unknown (-1): {np.sum(occupancygrid.grid == -1)} cells") - - print("\nGradient grid stats:") - print(f" Max gradient (100): {np.sum(gradient_grid.grid == 100)} cells") - print( - f" High gradient (80-99): {np.sum((gradient_grid.grid >= 80) & (gradient_grid.grid < 100))} cells" - ) - print( - f" Medium gradient (40-79): {np.sum((gradient_grid.grid >= 40) & (gradient_grid.grid < 80))} cells" - ) - print( - f" Low gradient (1-39): {np.sum((gradient_grid.grid >= 1) & (gradient_grid.grid < 40))} cells" - ) - print(f" Zero gradient (0): {np.sum(gradient_grid.grid == 0)} cells") - print(f" Unknown (-1): {np.sum(gradient_grid.grid == -1)} cells") - - # # Save debug images - # import matplotlib.pyplot as plt - - # fig, axes = plt.subplots(1, 2, figsize=(12, 5)) - - # # Original - # ax = axes[0] - # im1 = ax.imshow(occupancygrid.grid, origin="lower", cmap="gray_r", vmin=-1, vmax=100) - # ax.set_title(f"Original Occupancy Grid\n{occupancygrid}") - # plt.colorbar(im1, ax=ax) - - # # Gradient - # ax = axes[1] - # im2 = ax.imshow(gradient_grid.grid, origin="lower", cmap="hot", vmin=-1, vmax=100) - # ax.set_title(f"Gradient Grid\n{gradient_grid}") - # plt.colorbar(im2, ax=ax) - - # plt.tight_layout() - # plt.savefig("lcm_debug_grids.png", dpi=150) - # print("\nSaved debug visualization to lcm_debug_grids.png") - # plt.close() - - # Broadcast all the data - lcm = LCM() - lcm.start() - lcm.publish(Topic("/global_map", PointCloud2), pointcloud) - lcm.publish(Topic("/global_costmap", OccupancyGrid), occupancygrid) - lcm.publish(Topic("/global_gradient", OccupancyGrid), gradient_grid) - - print("\nPublished to LCM:") - print(f" /global_map: PointCloud2 with {len(pointcloud)} points") - print(f" /global_costmap: {occupancygrid}") - print(f" /global_gradient: {gradient_grid}") - print("\nGradient info:") - print(" Values: 0 (free far from obstacles) -> 100 (at obstacles)") - print(f" Unknown cells: {gradient_grid.unknown_cells} (preserved as -1)") - print(" Max distance for gradient: 5.0 meters") diff --git a/dimos/msgs/nav_msgs/test_Odometry.py b/dimos/msgs/nav_msgs/test_Odometry.py deleted file mode 100644 index c532aed1ca..0000000000 --- a/dimos/msgs/nav_msgs/test_Odometry.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np - -from dimos.msgs.geometry_msgs.Pose import Pose -from dimos.msgs.geometry_msgs.PoseWithCovariance import PoseWithCovariance -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.TwistWithCovariance import TwistWithCovariance -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.nav_msgs.Odometry import Odometry - - -def test_odometry_default_init() -> None: - odom = Odometry() - - assert odom.ts > 0 - assert odom.frame_id == "" - assert odom.child_frame_id == "" - - assert odom.pose.position.x == 0.0 - assert odom.pose.position.y == 0.0 - assert odom.pose.position.z == 0.0 - assert odom.pose.orientation.w == 1.0 - - assert odom.twist.linear.x == 0.0 - assert odom.twist.angular.x == 0.0 - - assert np.all(odom.pose.covariance == 0.0) - assert np.all(odom.twist.covariance == 0.0) - - -def test_odometry_with_frames() -> None: - ts = 1234567890.123456 - odom = Odometry(ts=ts, frame_id="odom", child_frame_id="base_link") - - assert odom.ts == ts - assert odom.frame_id == "odom" - assert odom.child_frame_id == "base_link" - - -def test_odometry_with_pose_and_twist() -> None: - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)) - - odom = Odometry(ts=1000.0, frame_id="odom", child_frame_id="base_link", pose=pose, twist=twist) - - assert odom.pose.pose.position.x == 1.0 - assert odom.pose.pose.position.y == 2.0 - assert odom.pose.pose.position.z == 3.0 - assert odom.twist.twist.linear.x == 0.5 - assert odom.twist.twist.angular.z == 0.1 - - -def test_odometry_with_covariances() -> None: - pose = Pose(1.0, 2.0, 3.0) - pose_cov = np.arange(36, dtype=float) - pose_with_cov = PoseWithCovariance(pose, pose_cov) - - twist = Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)) - twist_cov = np.arange(36, 72, dtype=float) - twist_with_cov = TwistWithCovariance(twist, twist_cov) - - odom = Odometry( - ts=1000.0, - frame_id="odom", - child_frame_id="base_link", - pose=pose_with_cov, - twist=twist_with_cov, - ) - - assert odom.pose.position.x == 1.0 - assert np.array_equal(odom.pose.covariance, pose_cov) - assert odom.twist.linear.x == 0.5 - assert np.array_equal(odom.twist.covariance, twist_cov) - - -def test_odometry_properties() -> None: - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - twist = Twist(Vector3(0.5, 0.6, 0.7), Vector3(0.1, 0.2, 0.3)) - - odom = Odometry(ts=1000.0, frame_id="odom", child_frame_id="base_link", pose=pose, twist=twist) - - assert odom.x == 1.0 - assert odom.y == 2.0 - assert odom.z == 3.0 - assert odom.position.x == 1.0 - - assert odom.orientation.x == 0.1 - - assert odom.vx == 0.5 - assert odom.vy == 0.6 - assert odom.vz == 0.7 - assert odom.linear_velocity.x == 0.5 - - assert odom.wx == 0.1 - assert odom.wy == 0.2 - assert odom.wz == 0.3 - assert odom.angular_velocity.x == 0.1 - - assert odom.roll == pose.roll - assert odom.pitch == pose.pitch - assert odom.yaw == pose.yaw - - -def test_odometry_str_repr() -> None: - odom = Odometry( - ts=1234567890.123456, - frame_id="odom", - child_frame_id="base_link", - pose=Pose(1.234, 2.567, 3.891), - twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), - ) - - assert "Odometry" in repr(odom) - assert "1234567890.123456" in repr(odom) - - s = str(odom) - assert "odom -> base_link" in s - assert "1.234" in s - - -def test_odometry_equality() -> None: - kwargs = dict( - ts=1000.0, - frame_id="odom", - child_frame_id="base_link", - pose=Pose(1.0, 2.0, 3.0), - twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1)), - ) - - assert Odometry(**kwargs) == Odometry(**kwargs) - assert Odometry(**kwargs) != Odometry(**{**kwargs, "pose": Pose(1.1, 2.0, 3.0)}) - assert Odometry(**kwargs) != "not an odometry" - - -def test_odometry_lcm_roundtrip() -> None: - pose = Pose(1.0, 2.0, 3.0, 0.1, 0.2, 0.3, 0.9) - pose_cov = np.arange(36, dtype=float) - twist = Twist(Vector3(0.5, 0.6, 0.7), Vector3(0.1, 0.2, 0.3)) - twist_cov = np.arange(36, 72, dtype=float) - - source = Odometry( - ts=1234567890.123456, - frame_id="odom", - child_frame_id="base_link", - pose=PoseWithCovariance(pose, pose_cov), - twist=TwistWithCovariance(twist, twist_cov), - ) - - decoded = Odometry.lcm_decode(source.lcm_encode()) - - assert abs(decoded.ts - source.ts) < 1e-6 - assert decoded.frame_id == source.frame_id - assert decoded.child_frame_id == source.child_frame_id - assert decoded.pose == source.pose - assert decoded.twist == source.twist - - -def test_odometry_zero_timestamp() -> None: - odom = Odometry(ts=0.0) - assert odom.ts > 0 - assert odom.ts <= time.time() - - -def test_odometry_with_just_pose() -> None: - odom = Odometry(pose=Pose(1.0, 2.0, 3.0)) - - assert odom.pose.position.x == 1.0 - assert np.all(odom.pose.covariance == 0.0) - assert np.all(odom.twist.covariance == 0.0) - - -def test_odometry_with_just_twist() -> None: - odom = Odometry(twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.1))) - - assert odom.twist.linear.x == 0.5 - assert odom.twist.angular.z == 0.1 - assert np.all(odom.twist.covariance == 0.0) - - -def test_odometry_typical_robot_scenario() -> None: - odom = Odometry( - ts=1000.0, - frame_id="odom", - child_frame_id="base_footprint", - pose=Pose(10.0, 5.0, 0.0, 0.0, 0.0, np.sin(0.1), np.cos(0.1)), - twist=Twist(Vector3(0.5, 0.0, 0.0), Vector3(0.0, 0.0, 0.05)), - ) - - assert odom.x == 10.0 - assert odom.y == 5.0 - assert abs(odom.yaw - 0.2) < 0.01 - assert odom.vx == 0.5 - assert odom.wz == 0.05 diff --git a/dimos/msgs/nav_msgs/test_Path.py b/dimos/msgs/nav_msgs/test_Path.py deleted file mode 100644 index 9bd0cc92b6..0000000000 --- a/dimos/msgs/nav_msgs/test_Path.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.nav_msgs.Path import Path - - -def create_test_pose(x: float, y: float, z: float, frame_id: str = "map") -> PoseStamped: - """Helper to create a test PoseStamped.""" - return PoseStamped( - frame_id=frame_id, - position=[x, y, z], - orientation=Quaternion(0, 0, 0, 1), # Identity quaternion - ) - - -def test_init_empty() -> None: - """Test creating an empty path.""" - path = Path(frame_id="map") - assert path.frame_id == "map" - assert len(path) == 0 - assert not path # Should be falsy when empty - assert path.poses == [] - - -def test_init_with_poses() -> None: - """Test creating a path with initial poses.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(frame_id="map", poses=poses) - assert len(path) == 3 - assert bool(path) # Should be truthy when has poses - assert path.poses == poses - - -def test_head() -> None: - """Test getting the first pose.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - assert path.head() == poses[0] - - # Test empty path - empty_path = Path() - assert empty_path.head() is None - - -def test_last() -> None: - """Test getting the last pose.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - assert path.last() == poses[-1] - - # Test empty path - empty_path = Path() - assert empty_path.last() is None - - -def test_tail() -> None: - """Test getting all poses except the first.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - tail = path.tail() - assert len(tail) == 2 - assert tail.poses == poses[1:] - assert tail.frame_id == path.frame_id - - # Test single element path - single_path = Path(poses=[poses[0]]) - assert len(single_path.tail()) == 0 - - # Test empty path - empty_path = Path() - assert len(empty_path.tail()) == 0 - - -def test_push_immutable() -> None: - """Test immutable push operation.""" - path = Path(frame_id="map") - pose1 = create_test_pose(1, 1, 0) - pose2 = create_test_pose(2, 2, 0) - - # Push should return new path - path2 = path.push(pose1) - assert len(path) == 0 # Original unchanged - assert len(path2) == 1 - assert path2.poses[0] == pose1 - - # Chain pushes - path3 = path2.push(pose2) - assert len(path2) == 1 # Previous unchanged - assert len(path3) == 2 - assert path3.poses == [pose1, pose2] - - -def test_push_mutable() -> None: - """Test mutable push operation.""" - path = Path(frame_id="map") - pose1 = create_test_pose(1, 1, 0) - pose2 = create_test_pose(2, 2, 0) - - # Push should modify in place - path.push_mut(pose1) - assert len(path) == 1 - assert path.poses[0] == pose1 - - path.push_mut(pose2) - assert len(path) == 2 - assert path.poses == [pose1, pose2] - - -def test_indexing() -> None: - """Test indexing and slicing.""" - poses = [create_test_pose(i, i, 0) for i in range(5)] - path = Path(poses=poses) - - # Single index - assert path[0] == poses[0] - assert path[-1] == poses[-1] - - # Slicing - assert path[1:3] == poses[1:3] - assert path[:2] == poses[:2] - assert path[3:] == poses[3:] - - -def test_iteration() -> None: - """Test iterating over poses.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - - collected = [] - for pose in path: - collected.append(pose) - assert collected == poses - - -def test_slice_method() -> None: - """Test slice method.""" - poses = [create_test_pose(i, i, 0) for i in range(5)] - path = Path(frame_id="map", poses=poses) - - sliced = path.slice(1, 4) - assert len(sliced) == 3 - assert sliced.poses == poses[1:4] - assert sliced.frame_id == "map" - - # Test open-ended slice - sliced2 = path.slice(2) - assert sliced2.poses == poses[2:] - - -def test_extend_immutable() -> None: - """Test immutable extend operation.""" - poses1 = [create_test_pose(i, i, 0) for i in range(2)] - poses2 = [create_test_pose(i + 2, i + 2, 0) for i in range(2)] - - path1 = Path(frame_id="map", poses=poses1) - path2 = Path(frame_id="odom", poses=poses2) - - extended = path1.extend(path2) - assert len(path1) == 2 # Original unchanged - assert len(extended) == 4 - assert extended.poses == poses1 + poses2 - assert extended.frame_id == "map" # Keeps first path's frame - - -def test_extend_mutable() -> None: - """Test mutable extend operation.""" - poses1 = [create_test_pose(i, i, 0) for i in range(2)] - poses2 = [create_test_pose(i + 2, i + 2, 0) for i in range(2)] - - path1 = Path(frame_id="map", poses=poses1.copy()) # Use copy to avoid modifying original - path2 = Path(frame_id="odom", poses=poses2) - - path1.extend_mut(path2) - assert len(path1) == 4 - # Check poses are the same as concatenation - for _i, (p1, p2) in enumerate(zip(path1.poses, poses1 + poses2, strict=False)): - assert p1.x == p2.x - assert p1.y == p2.y - assert p1.z == p2.z - - -def test_reverse() -> None: - """Test reverse operation.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - - reversed_path = path.reverse() - assert len(path) == 3 # Original unchanged - assert reversed_path.poses == list(reversed(poses)) - - -def test_clear() -> None: - """Test clear operation.""" - poses = [create_test_pose(i, i, 0) for i in range(3)] - path = Path(poses=poses) - - path.clear() - assert len(path) == 0 - assert path.poses == [] - - -def test_lcm_encode_decode() -> None: - """Test encoding and decoding of Path to/from binary LCM format.""" - # Create path with poses - # Use timestamps that can be represented exactly in float64 - path_ts = 1234567890.5 - poses = [ - PoseStamped( - ts=1234567890.0 + i * 0.1, # Use simpler timestamps - frame_id=f"frame_{i}", - position=[i * 1.5, i * 2.5, i * 3.5], - orientation=(0.1 * i, 0.2 * i, 0.3 * i, 0.9), - ) - for i in range(3) - ] - - path_source = Path(ts=path_ts, frame_id="world", poses=poses) - - # Encode to binary - binary_msg = path_source.lcm_encode() - - # Decode from binary - path_dest = Path.lcm_decode(binary_msg) - - assert isinstance(path_dest, Path) - assert path_dest is not path_source - - # Check header - assert path_dest.frame_id == path_source.frame_id - # Path timestamp should be preserved - assert abs(path_dest.ts - path_source.ts) < 1e-6 # Microsecond precision - - # Check poses - assert len(path_dest.poses) == len(path_source.poses) - - for orig, decoded in zip(path_source.poses, path_dest.poses, strict=False): - # Check pose timestamps - assert abs(decoded.ts - orig.ts) < 1e-6 - # All poses should have the path's frame_id - assert decoded.frame_id == path_dest.frame_id - - # Check position - assert decoded.x == orig.x - assert decoded.y == orig.y - assert decoded.z == orig.z - - # Check orientation - assert decoded.orientation.x == orig.orientation.x - assert decoded.orientation.y == orig.orientation.y - assert decoded.orientation.z == orig.orientation.z - assert decoded.orientation.w == orig.orientation.w - - -def test_lcm_encode_decode_empty() -> None: - """Test encoding and decoding of empty Path.""" - path_source = Path(frame_id="base_link") - - binary_msg = path_source.lcm_encode() - path_dest = Path.lcm_decode(binary_msg) - - assert isinstance(path_dest, Path) - assert path_dest.frame_id == path_source.frame_id - assert len(path_dest.poses) == 0 - - -def test_str_representation() -> None: - """Test string representation.""" - path = Path(frame_id="map") - assert str(path) == "Path(frame_id='map', poses=0)" - - path.push_mut(create_test_pose(1, 1, 0)) - path.push_mut(create_test_pose(2, 2, 0)) - assert str(path) == "Path(frame_id='map', poses=2)" diff --git a/dimos/msgs/protocol.py b/dimos/msgs/protocol.py deleted file mode 100644 index 38d7ca57e2..0000000000 --- a/dimos/msgs/protocol.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol, runtime_checkable - - -@runtime_checkable -class DimosMsg(Protocol): - """Protocol for dimos message types (LCM-based messages from dimos.msgs).""" - - msg_name: str - - @classmethod - def lcm_decode(cls, data: bytes) -> "DimosMsg": - """Decode bytes into a message instance.""" - ... - - def lcm_encode(self) -> bytes: - """Encode this message instance into bytes.""" - ... diff --git a/dimos/msgs/sensor_msgs/CameraInfo.py b/dimos/msgs/sensor_msgs/CameraInfo.py deleted file mode 100644 index a371475675..0000000000 --- a/dimos/msgs/sensor_msgs/CameraInfo.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.visualization.rerun.bridge import RerunData, RerunMulti - -# Import LCM types -from dimos_lcm.sensor_msgs import CameraInfo as LCMCameraInfo -from dimos_lcm.std_msgs.Header import Header -import numpy as np - -from dimos.types.timestamped import Timestamped - - -class CameraInfo(Timestamped): - """Camera calibration information message.""" - - msg_name = "sensor_msgs.CameraInfo" - - def __init__( - self, - height: int = 0, - width: int = 0, - distortion_model: str = "", - D: list[float] | None = None, - K: list[float] | None = None, - R: list[float] | None = None, - P: list[float] | None = None, - binning_x: int = 0, - binning_y: int = 0, - frame_id: str = "", - ts: float | None = None, - ) -> None: - """Initialize CameraInfo. - - Args: - height: Image height - width: Image width - distortion_model: Name of distortion model (e.g., "plumb_bob") - D: Distortion coefficients - K: 3x3 intrinsic camera matrix - R: 3x3 rectification matrix - P: 3x4 projection matrix - binning_x: Horizontal binning - binning_y: Vertical binning - frame_id: Frame ID - ts: Timestamp - """ - self.ts = ts if ts is not None else time.time() - self.frame_id = frame_id - self.height = height - self.width = width - self.distortion_model = distortion_model - - # Initialize distortion coefficients - self.D = D if D is not None else [] - - # Initialize 3x3 intrinsic camera matrix (row-major) - self.K = K if K is not None else [0.0] * 9 - - # Initialize 3x3 rectification matrix (row-major) - self.R = R if R is not None else [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0] - - # Initialize 3x4 projection matrix (row-major) - self.P = P if P is not None else [0.0] * 12 - - self.binning_x = binning_x - self.binning_y = binning_y - - # Region of interest (not used in basic implementation) - self.roi_x_offset = 0 - self.roi_y_offset = 0 - self.roi_height = 0 - self.roi_width = 0 - self.roi_do_rectify = False - - def with_ts(self, ts: float) -> CameraInfo: - """Return a copy of this CameraInfo with the given timestamp. - - Args: - ts: New timestamp - - Returns: - New CameraInfo instance with updated timestamp - """ - return CameraInfo( - height=self.height, - width=self.width, - distortion_model=self.distortion_model, - D=self.D.copy(), - K=self.K.copy(), - R=self.R.copy(), - P=self.P.copy(), - binning_x=self.binning_x, - binning_y=self.binning_y, - frame_id=self.frame_id, - ts=ts, - ) - - @classmethod - def from_yaml(cls, yaml_file: str) -> CameraInfo: - """Create CameraInfo from YAML file. - - Args: - yaml_file: Path to YAML file containing camera calibration data - - Returns: - CameraInfo instance with loaded calibration data - """ - import yaml # type: ignore[import-untyped] - - with open(yaml_file) as f: - data = yaml.safe_load(f) - - # Extract basic parameters - width = data.get("image_width", 0) - height = data.get("image_height", 0) - distortion_model = data.get("distortion_model", "") - - # Extract matrices - camera_matrix = data.get("camera_matrix", {}) - K = camera_matrix.get("data", [0.0] * 9) - - distortion_coeffs = data.get("distortion_coefficients", {}) - D = distortion_coeffs.get("data", []) - - rect_matrix = data.get("rectification_matrix", {}) - R = rect_matrix.get("data", [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]) - - proj_matrix = data.get("projection_matrix", {}) - P = proj_matrix.get("data", [0.0] * 12) - - # Create CameraInfo instance - return cls( - height=height, - width=width, - distortion_model=distortion_model, - D=D, - K=K, - R=R, - P=P, - frame_id="camera_optical", - ) - - def get_K_matrix(self) -> np.ndarray: # type: ignore[type-arg] - """Get intrinsic matrix as numpy array.""" - return np.array(self.K, dtype=np.float64).reshape(3, 3) - - def get_P_matrix(self) -> np.ndarray: # type: ignore[type-arg] - """Get projection matrix as numpy array.""" - return np.array(self.P, dtype=np.float64).reshape(3, 4) - - def get_R_matrix(self) -> np.ndarray: # type: ignore[type-arg] - """Get rectification matrix as numpy array.""" - return np.array(self.R, dtype=np.float64).reshape(3, 3) - - def get_D_coeffs(self) -> np.ndarray: # type: ignore[type-arg] - """Get distortion coefficients as numpy array.""" - return np.array(self.D, dtype=np.float64) - - def set_K_matrix(self, K: np.ndarray): # type: ignore[no-untyped-def, type-arg] - """Set intrinsic matrix from numpy array.""" - if K.shape != (3, 3): - raise ValueError(f"K matrix must be 3x3, got {K.shape}") - self.K = K.flatten().tolist() - - def set_P_matrix(self, P: np.ndarray): # type: ignore[no-untyped-def, type-arg] - """Set projection matrix from numpy array.""" - if P.shape != (3, 4): - raise ValueError(f"P matrix must be 3x4, got {P.shape}") - self.P = P.flatten().tolist() - - def set_R_matrix(self, R: np.ndarray): # type: ignore[no-untyped-def, type-arg] - """Set rectification matrix from numpy array.""" - if R.shape != (3, 3): - raise ValueError(f"R matrix must be 3x3, got {R.shape}") - self.R = R.flatten().tolist() - - def set_D_coeffs(self, D: np.ndarray) -> None: # type: ignore[type-arg] - """Set distortion coefficients from numpy array.""" - self.D = D.flatten().tolist() - - def lcm_encode(self) -> bytes: - """Convert to LCM CameraInfo message.""" - msg = LCMCameraInfo() - - # Header - msg.header = Header() - msg.header.seq = 0 - msg.header.frame_id = self.frame_id - msg.header.stamp.sec = int(self.ts) - msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) - - # Image dimensions - msg.height = self.height - msg.width = self.width - - # Distortion model - msg.distortion_model = self.distortion_model - - # Distortion coefficients - msg.D_length = len(self.D) - msg.D = self.D - - # Camera matrices (all stored as row-major) - msg.K = self.K - msg.R = self.R - msg.P = self.P - - # Binning - msg.binning_x = self.binning_x - msg.binning_y = self.binning_y - - # ROI - msg.roi.x_offset = self.roi_x_offset - msg.roi.y_offset = self.roi_y_offset - msg.roi.height = self.roi_height - msg.roi.width = self.roi_width - msg.roi.do_rectify = self.roi_do_rectify - - return msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> CameraInfo: - """Decode from LCM CameraInfo bytes.""" - msg = LCMCameraInfo.lcm_decode(data) - - # Extract timestamp - ts = msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 if hasattr(msg, "header") else None - - camera_info = cls( - height=msg.height, - width=msg.width, - distortion_model=msg.distortion_model, - D=list(msg.D) if msg.D_length > 0 else [], - K=list(msg.K), - R=list(msg.R), - P=list(msg.P), - binning_x=msg.binning_x, - binning_y=msg.binning_y, - frame_id=msg.header.frame_id if hasattr(msg, "header") else "", - ts=ts, - ) - - # Set ROI if present - if hasattr(msg, "roi"): - camera_info.roi_x_offset = msg.roi.x_offset - camera_info.roi_y_offset = msg.roi.y_offset - camera_info.roi_height = msg.roi.height - camera_info.roi_width = msg.roi.width - camera_info.roi_do_rectify = msg.roi.do_rectify - - return camera_info - - def __repr__(self) -> str: - """String representation.""" - return ( - f"CameraInfo(height={self.height}, width={self.width}, " - f"distortion_model='{self.distortion_model}', " - f"frame_id='{self.frame_id}', ts={self.ts})" - ) - - def __str__(self) -> str: - """Human-readable string.""" - return ( - f"CameraInfo:\n" - f" Resolution: {self.width}x{self.height}\n" - f" Distortion model: {self.distortion_model}\n" - f" Frame ID: {self.frame_id}\n" - f" Binning: {self.binning_x}x{self.binning_y}" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two CameraInfo messages are equal.""" - if not isinstance(other, CameraInfo): - return False - - return ( - self.height == other.height - and self.width == other.width - and self.distortion_model == other.distortion_model - and self.D == other.D - and self.K == other.K - and self.R == other.R - and self.P == other.P - and self.binning_x == other.binning_x - and self.binning_y == other.binning_y - and self.frame_id == other.frame_id - ) - - def to_rerun( - self, - image_plane_distance: float = 1.0, - # These are defaults for a typical RGB camera with a known transform - # - # TODO this should be done by the actual emitting modules, - # they know the camera image topic, spatial relationships etc - # - # Poor CameraInfo class has no idea on this - # We just provide the parameters here for convenience in case your - # module doesn't implement this correctly - image_topic: str | None = None, - optical_frame: str | None = None, - ) -> RerunData: - """Convert to Rerun Pinhole archetype for camera frustum visualization. - - Args: - image_plane_distance: Distance to draw the image plane in the frustum - - Returns: - rr.Pinhole archetype for logging to Rerun - """ - import rerun as rr - - # Extract intrinsics from K matrix - # K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] - fx, fy = self.K[0], self.K[4] - cx, cy = self.K[2], self.K[5] - - pinhole = rr.Pinhole( - focal_length=[fx, fy], - principal_point=[cx, cy], - width=self.width, - height=self.height, - image_plane_distance=image_plane_distance, - ) - - # If no image topic is specified, We don't know which Image this CameraInfo refers to - # return just the pinhole - if not image_topic: - return pinhole - - ret: RerunMulti = [] - - # Add pinhole under world/image_topic (we know which Image this CameraInfo refers to) - # Note: parent_frame is supposed to work according to: - # https://rerun.io/docs/reference/types/archetypes/pinhole - # But it doesn't, so we add the transform separately below - ret.append( - ( - image_topic, - rr.Pinhole( - focal_length=[fx, fy], - principal_point=[cx, cy], - width=self.width, - height=self.height, - image_plane_distance=image_plane_distance, - ), - ) - ) - - if not optical_frame: - return ret - - # Add 3d transform from optical frame to world/image_topic (We know where the camera is) - ret.append( - ( - image_topic, - rr.Transform3D(parent_frame=f"tf#/{optical_frame}"), - ) - ) - - return ret - - -class CalibrationProvider: - """Provides lazy-loaded access to camera calibration YAML files in a directory.""" - - def __init__(self, calibration_dir) -> None: # type: ignore[no-untyped-def] - """Initialize with a directory containing calibration YAML files. - - Args: - calibration_dir: Path to directory containing .yaml calibration files - """ - from pathlib import Path - - self._calibration_dir = Path(calibration_dir) - self._cache = {} # type: ignore[var-annotated] - - def _to_snake_case(self, name: str) -> str: - """Convert PascalCase to snake_case.""" - import re - - # Insert underscore before capital letters (except first char) - s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name) - # Insert underscore before capital letter followed by lowercase - return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower() - - def _find_yaml_file(self, name: str): # type: ignore[no-untyped-def] - """Find YAML file matching the given name (tries both snake_case and exact match). - - Args: - name: Attribute name to look for - - Returns: - Path to YAML file if found, None otherwise - """ - # Try exact match first - yaml_file = self._calibration_dir / f"{name}.yaml" - if yaml_file.exists(): - return yaml_file - - # Try snake_case conversion for PascalCase names - snake_name = self._to_snake_case(name) - if snake_name != name: - yaml_file = self._calibration_dir / f"{snake_name}.yaml" - if yaml_file.exists(): - return yaml_file - - return None - - def __getattr__(self, name: str) -> CameraInfo: - """Load calibration YAML file on first access. - - Supports both snake_case and PascalCase attribute names. - For example, both 'single_webcam' and 'SingleWebcam' will load 'single_webcam.yaml'. - - Args: - name: Attribute name (can be PascalCase or snake_case) - - Returns: - CameraInfo object loaded from the YAML file - - Raises: - AttributeError: If no matching YAML file exists - """ - # Check cache first - if name in self._cache: - return self._cache[name] # type: ignore[no-any-return] - - # Also check if the snake_case version is cached (for PascalCase access) - snake_name = self._to_snake_case(name) - if snake_name != name and snake_name in self._cache: - return self._cache[snake_name] # type: ignore[no-any-return] - - # Find matching YAML file - yaml_file = self._find_yaml_file(name) - if not yaml_file: - raise AttributeError(f"No calibration file found for: {name}") - - # Load and cache the CameraInfo - camera_info = CameraInfo.from_yaml(str(yaml_file)) - - # Cache both the requested name and the snake_case version - self._cache[name] = camera_info - if snake_name != name: - self._cache[snake_name] = camera_info - - return camera_info - - def __dir__(self): # type: ignore[no-untyped-def] - """List available calibrations in both snake_case and PascalCase.""" - calibrations = [] - if self._calibration_dir.exists() and self._calibration_dir.is_dir(): - yaml_files = self._calibration_dir.glob("*.yaml") - for f in yaml_files: - stem = f.stem - calibrations.append(stem) - # Add PascalCase version - pascal = "".join(word.capitalize() for word in stem.split("_")) - if pascal != stem: - calibrations.append(pascal) - return calibrations diff --git a/dimos/msgs/sensor_msgs/Image.py b/dimos/msgs/sensor_msgs/Image.py deleted file mode 100644 index 66c2876b62..0000000000 --- a/dimos/msgs/sensor_msgs/Image.py +++ /dev/null @@ -1,659 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import base64 -from dataclasses import dataclass, field -from enum import Enum -import time -from typing import TYPE_CHECKING, Any, Literal, TypedDict - -import cv2 -from dimos_lcm.sensor_msgs.Image import Image as LCMImage -from dimos_lcm.std_msgs.Header import Header -import numpy as np -import reactivex as rx -from reactivex import operators as ops -import rerun as rr -from turbojpeg import TurboJPEG # type: ignore[import-untyped] - -from dimos.types.timestamped import Timestamped, TimestampedBufferCollection, to_human_readable -from dimos.utils.reactive import quality_barrier - -if TYPE_CHECKING: - from collections.abc import Callable - import os - - from reactivex.observable import Observable - - -class ImageFormat(Enum): - BGR = "BGR" - RGB = "RGB" - RGBA = "RGBA" - BGRA = "BGRA" - GRAY = "GRAY" - GRAY16 = "GRAY16" - DEPTH = "DEPTH" - DEPTH16 = "DEPTH16" - - -def _format_to_rerun(data: np.ndarray, fmt: ImageFormat) -> Any: # type: ignore[type-arg] - """Convert image data to Rerun archetype based on format.""" - match fmt: - case ImageFormat.RGB: - return rr.Image(data, color_model="RGB") - case ImageFormat.RGBA: - return rr.Image(data, color_model="RGBA") - case ImageFormat.BGR: - return rr.Image(data, color_model="BGR") - case ImageFormat.BGRA: - return rr.Image(data, color_model="BGRA") - case ImageFormat.GRAY: - return rr.Image(data, color_model="L") - case ImageFormat.GRAY16: - return rr.Image(data, color_model="L") - case ImageFormat.DEPTH: - return rr.DepthImage(data) - case ImageFormat.DEPTH16: - return rr.DepthImage(data) - case _: - raise ValueError(f"Unsupported format for Rerun: {fmt}") - - -class AgentImageMessage(TypedDict): - """Type definition for agent-compatible image representation.""" - - type: Literal["image"] - source_type: Literal["base64"] - mime_type: Literal["image/jpeg", "image/png"] - data: str # Base64 encoded image data - - -@dataclass -class Image(Timestamped): - """Simple NumPy-based image container.""" - - msg_name = "sensor_msgs.Image" - - data: np.ndarray[Any, np.dtype[Any]] = field( - default_factory=lambda: np.zeros((1, 1, 3), dtype=np.uint8) - ) - format: ImageFormat = field(default=ImageFormat.BGR) - frame_id: str = field(default="") - ts: float = field(default_factory=time.time) - - def __post_init__(self) -> None: - if not isinstance(self.data, np.ndarray): - self.data = np.asarray(self.data) - if self.data.ndim < 2: - raise ValueError("Image requires a 2D/3D NumPy array") - - def __str__(self) -> str: - return ( - f"Image(shape={self.shape}, format={self.format.value}, dtype={self.dtype}, " - f"ts={to_human_readable(self.ts)})" - ) - - def __repr__(self) -> str: - return f"Image(shape={self.shape}, format={self.format.value}, dtype={self.dtype}, frame_id='{self.frame_id}', ts={self.ts})" - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Image): - return False - return ( - np.array_equal(self.data, other.data) - and self.format == other.format - and self.frame_id == other.frame_id - and abs(self.ts - other.ts) < 1e-6 - ) - - def __len__(self) -> int: - return int(self.height * self.width) - - def __getstate__(self) -> dict[str, Any]: - return {"data": self.data, "format": self.format, "frame_id": self.frame_id, "ts": self.ts} - - def __setstate__(self, state: dict[str, Any]) -> None: - self.data = state.get("data", np.zeros((1, 1, 3), dtype=np.uint8)) - self.format = state.get("format", ImageFormat.BGR) - self.frame_id = state.get("frame_id", "") - self.ts = state.get("ts", time.time()) - - @property - def height(self) -> int: - return int(self.data.shape[0]) - - @property - def width(self) -> int: - return int(self.data.shape[1]) - - @property - def channels(self) -> int: - if self.data.ndim == 2: - return 1 - if self.data.ndim == 3: - return int(self.data.shape[2]) - raise ValueError("Invalid image dimensions") - - @property - def shape(self) -> tuple[int, ...]: - return tuple(self.data.shape) - - @property - def dtype(self) -> np.dtype[Any]: - return self.data.dtype - - def copy(self) -> Image: - return Image(data=self.data.copy(), format=self.format, frame_id=self.frame_id, ts=self.ts) - - @classmethod - def from_numpy( - cls, - np_image: np.ndarray, # type: ignore[type-arg] - format: ImageFormat = ImageFormat.BGR, - frame_id: str = "", - ts: float | None = None, - ) -> Image: - return cls( - data=np.asarray(np_image), - format=format, - frame_id=frame_id, - ts=ts if ts is not None else time.time(), - ) - - @classmethod - def from_file( - cls, - filepath: str | os.PathLike[str], - format: ImageFormat = ImageFormat.RGB, - ) -> Image: - arr = cv2.imread(str(filepath), cv2.IMREAD_UNCHANGED) - if arr is None: - raise ValueError(f"Could not load image from {filepath}") - if arr.ndim == 2: - detected = ImageFormat.GRAY16 if arr.dtype == np.uint16 else ImageFormat.GRAY - elif arr.shape[2] == 3: - detected = ImageFormat.BGR # OpenCV default - elif arr.shape[2] == 4: - detected = ImageFormat.BGRA # OpenCV default - else: - detected = format - return cls(data=arr, format=detected) - - @classmethod - def from_opencv( - cls, - cv_image: np.ndarray, # type: ignore[type-arg] - format: ImageFormat = ImageFormat.BGR, - frame_id: str = "", - ts: float | None = None, - ) -> Image: - """Construct from an OpenCV image (NumPy array).""" - return cls( - data=cv_image, - format=format, - frame_id=frame_id, - ts=ts if ts is not None else time.time(), - ) - - def to_opencv(self) -> np.ndarray: # type: ignore[type-arg] - """Convert to OpenCV BGR format.""" - arr = self.data - if self.format == ImageFormat.BGR: - return arr - if self.format == ImageFormat.RGB: - return cv2.cvtColor(arr, cv2.COLOR_RGB2BGR) - if self.format == ImageFormat.RGBA: - return cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR) - if self.format == ImageFormat.BGRA: - return cv2.cvtColor(arr, cv2.COLOR_BGRA2BGR) - if self.format in ( - ImageFormat.GRAY, - ImageFormat.GRAY16, - ImageFormat.DEPTH, - ImageFormat.DEPTH16, - ): - return arr - raise ValueError(f"Unsupported format: {self.format}") - - def as_numpy(self) -> np.ndarray: # type: ignore[type-arg] - """Get image data as numpy array.""" - return self.data - - def to_rgb(self) -> Image: - if self.format == ImageFormat.RGB: - return self.copy() - arr = self.data - if self.format == ImageFormat.BGR: - return Image( - data=cv2.cvtColor(arr, cv2.COLOR_BGR2RGB), - format=ImageFormat.RGB, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format == ImageFormat.RGBA: - return self.copy() # RGBA contains RGB + alpha - if self.format == ImageFormat.BGRA: - rgba = cv2.cvtColor(arr, cv2.COLOR_BGRA2RGBA) - return Image(data=rgba, format=ImageFormat.RGBA, frame_id=self.frame_id, ts=self.ts) - if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH16): - gray8 = (arr / 256).astype(np.uint8) if self.format != ImageFormat.GRAY else arr - rgb = cv2.cvtColor(gray8, cv2.COLOR_GRAY2RGB) - return Image(data=rgb, format=ImageFormat.RGB, frame_id=self.frame_id, ts=self.ts) - return self.copy() - - def to_bgr(self) -> Image: - if self.format == ImageFormat.BGR: - return self.copy() - arr = self.data - if self.format == ImageFormat.RGB: - return Image( - data=cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), - format=ImageFormat.BGR, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format == ImageFormat.RGBA: - return Image( - data=cv2.cvtColor(arr, cv2.COLOR_RGBA2BGR), - format=ImageFormat.BGR, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format == ImageFormat.BGRA: - return Image( - data=cv2.cvtColor(arr, cv2.COLOR_BGRA2BGR), - format=ImageFormat.BGR, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH16): - gray8 = (arr / 256).astype(np.uint8) if self.format != ImageFormat.GRAY else arr - return Image( - data=cv2.cvtColor(gray8, cv2.COLOR_GRAY2BGR), - format=ImageFormat.BGR, - frame_id=self.frame_id, - ts=self.ts, - ) - return self.copy() - - def to_grayscale(self) -> Image: - if self.format in (ImageFormat.GRAY, ImageFormat.GRAY16, ImageFormat.DEPTH): - return self.copy() - if self.format == ImageFormat.BGR: - return Image( - data=cv2.cvtColor(self.data, cv2.COLOR_BGR2GRAY), - format=ImageFormat.GRAY, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format == ImageFormat.RGB: - return Image( - data=cv2.cvtColor(self.data, cv2.COLOR_RGB2GRAY), - format=ImageFormat.GRAY, - frame_id=self.frame_id, - ts=self.ts, - ) - if self.format in (ImageFormat.RGBA, ImageFormat.BGRA): - code = cv2.COLOR_RGBA2GRAY if self.format == ImageFormat.RGBA else cv2.COLOR_BGRA2GRAY - return Image( - data=cv2.cvtColor(self.data, code), - format=ImageFormat.GRAY, - frame_id=self.frame_id, - ts=self.ts, - ) - raise ValueError(f"Unsupported format: {self.format}") - - def to_rerun(self) -> Any: - """Convert to rerun Image format.""" - return _format_to_rerun(self.data, self.format) - - def resize(self, width: int, height: int, interpolation: int = cv2.INTER_LINEAR) -> Image: - return Image( - data=cv2.resize(self.data, (width, height), interpolation=interpolation), - format=self.format, - frame_id=self.frame_id, - ts=self.ts, - ) - - def resize_to_fit( - self, max_width: int, max_height: int, interpolation: int = cv2.INTER_LINEAR - ) -> tuple[Image, float]: - """Resize image to fit within max dimensions while preserving aspect ratio. - - Only scales down if image exceeds max dimensions. Returns self if already fits. - - Returns: - Tuple of (resized_image, scale_factor). Scale factor is 1.0 if no resize needed. - """ - if self.width <= max_width and self.height <= max_height: - return self, 1.0 - - scale = min(max_width / self.width, max_height / self.height) - new_width = int(self.width * scale) - new_height = int(self.height * scale) - return self.resize(new_width, new_height, interpolation), scale - - def crop(self, x: int, y: int, width: int, height: int) -> Image: - """Crop the image to the specified region. - - Args: - x: Starting x coordinate (left edge) - y: Starting y coordinate (top edge) - width: Width of the cropped region - height: Height of the cropped region - - Returns: - A new Image containing the cropped region - """ - img_height, img_width = self.data.shape[:2] - - # Clamp the crop region to image bounds - x = max(0, min(x, img_width)) - y = max(0, min(y, img_height)) - x_end = min(x + width, img_width) - y_end = min(y + height, img_height) - - # Perform the crop using array slicing - if self.data.ndim == 2: - cropped_data = self.data[y:y_end, x:x_end] - else: - cropped_data = self.data[y:y_end, x:x_end, :] - - return Image(data=cropped_data, format=self.format, frame_id=self.frame_id, ts=self.ts) - - @property - def sharpness(self) -> float: - """Return sharpness score.""" - gray = self.to_grayscale() - sx = cv2.Sobel(gray.data, cv2.CV_32F, 1, 0, ksize=5) - sy = cv2.Sobel(gray.data, cv2.CV_32F, 0, 1, ksize=5) - magnitude = cv2.magnitude(sx, sy) - mean_mag = float(magnitude.mean()) - if mean_mag <= 0: - return 0.0 - return float(np.clip((np.log10(mean_mag + 1) - 1.7) / 2.0, 0.0, 1.0)) - - def save(self, filepath: str) -> bool: - arr = self.to_opencv() - return cv2.imwrite(filepath, arr) - - def to_base64( - self, - quality: int = 80, - *, - max_width: int | None = None, - max_height: int | None = None, - ) -> str: - """Encode the image as a base64 JPEG string. - - Args: - quality: JPEG quality (0-100). - max_width: Optional maximum width to constrain the encoded image. - max_height: Optional maximum height to constrain the encoded image. - - Returns: - Base64-encoded JPEG representation of the image. - """ - bgr_image = self.to_bgr().to_opencv() - height, width = bgr_image.shape[:2] - - scale = 1.0 - if max_width is not None and width > max_width: - scale = min(scale, max_width / width) - if max_height is not None and height > max_height: - scale = min(scale, max_height / height) - - if scale < 1.0: - new_width = max(1, round(width * scale)) - new_height = max(1, round(height * scale)) - bgr_image = cv2.resize(bgr_image, (new_width, new_height), interpolation=cv2.INTER_AREA) - - encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), int(np.clip(quality, 0, 100))] - success, buffer = cv2.imencode(".jpg", bgr_image, encode_param) - if not success: - raise ValueError("Failed to encode image as JPEG") - - return base64.b64encode(buffer.tobytes()).decode("utf-8") - - def agent_encode(self) -> AgentImageMessage: - return [ # type: ignore[return-value] - { - "type": "image_url", - "image_url": {"url": f"data:image/jpeg;base64,{self.to_base64()}"}, - } - ] - - # LCM encode/decode - def lcm_encode(self, frame_id: str | None = None) -> bytes: - """Convert to LCM Image message.""" - msg = LCMImage() - - # Header - msg.header = Header() - msg.header.seq = 0 - msg.header.frame_id = frame_id or self.frame_id - - # Set timestamp - if self.ts is not None: - msg.header.stamp.sec = int(self.ts) - msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) - else: - now = time.time() - msg.header.stamp.sec = int(now) - msg.header.stamp.nsec = int((now - int(now)) * 1e9) - - # Image properties - msg.height = self.height - msg.width = self.width - msg.encoding = _get_lcm_encoding(self.format, self.dtype) - msg.is_bigendian = False - - # Calculate step (bytes per row) - channels = 1 if self.data.ndim == 2 else self.data.shape[2] - msg.step = self.width * self.dtype.itemsize * channels - - view = memoryview(np.ascontiguousarray(self.data)).cast("B") # type: ignore[arg-type] - msg.data_length = len(view) - msg.data = view - - return msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes, **kwargs: Any) -> Image: - msg = LCMImage.lcm_decode(data) - fmt, dtype, channels = _parse_lcm_encoding(msg.encoding) - arr: np.ndarray[Any, Any] = np.frombuffer(msg.data, dtype=dtype) - if channels == 1: - arr = arr.reshape((msg.height, msg.width)) - else: - arr = arr.reshape((msg.height, msg.width, channels)) - return cls( - data=arr, - format=fmt, - frame_id=msg.header.frame_id if hasattr(msg, "header") else "", - ts=( - msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 - if hasattr(msg, "header") - and hasattr(msg.header, "stamp") - and msg.header.stamp.sec > 0 - else time.time() - ), - ) - - def lcm_jpeg_encode(self, quality: int = 75, frame_id: str | None = None) -> bytes: - """Convert to LCM Image message with JPEG-compressed data. - - Args: - quality: JPEG compression quality (0-100, default 75) - frame_id: Optional frame ID override - - Returns: - LCM-encoded bytes with JPEG-compressed image data - """ - jpeg = TurboJPEG() - msg = LCMImage() - - # Header - msg.header = Header() - msg.header.seq = 0 - msg.header.frame_id = frame_id or self.frame_id - - # Set timestamp - if self.ts is not None: - msg.header.stamp.sec = int(self.ts) - msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) - else: - now = time.time() - msg.header.stamp.sec = int(now) - msg.header.stamp.nsec = int((now - int(now)) * 1e9) - - # Get image in BGR format for JPEG encoding - bgr_image = self.to_bgr().to_opencv() - - # Encode as JPEG - jpeg_data = jpeg.encode(bgr_image, quality=quality) - - # Store JPEG data and metadata - msg.height = self.height - msg.width = self.width - msg.encoding = "jpeg" - msg.is_bigendian = False - msg.step = 0 # Not applicable for compressed format - - msg.data_length = len(jpeg_data) - msg.data = jpeg_data - - return msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_jpeg_decode(cls, data: bytes, **kwargs: Any) -> Image: - """Decode an LCM Image message with JPEG-compressed data. - - Args: - data: LCM-encoded bytes containing JPEG-compressed image - - Returns: - Image instance - """ - jpeg = TurboJPEG() - msg = LCMImage.lcm_decode(data) - - if msg.encoding != "jpeg": - raise ValueError(f"Expected JPEG encoding, got {msg.encoding}") - - # Decode JPEG data - bgr_array = jpeg.decode(msg.data) - - return cls( - data=bgr_array, - format=ImageFormat.BGR, - frame_id=msg.header.frame_id if hasattr(msg, "header") else "", - ts=( - msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 - if hasattr(msg, "header") - and hasattr(msg.header, "stamp") - and msg.header.stamp.sec > 0 - else time.time() - ), - ) - - -__all__ = [ - "Image", - "ImageFormat", - "sharpness_barrier", - "sharpness_window", -] - - -def sharpness_window(target_frequency: float, source: Observable[Image]) -> Observable[Image]: - """Emit the sharpest Image seen within each sliding time window.""" - from reactivex.scheduler import ThreadPoolScheduler - - if target_frequency <= 0: - raise ValueError("target_frequency must be positive") - - window = TimestampedBufferCollection(1.0 / target_frequency) # type: ignore[var-annotated] - source.subscribe(window.add) - - thread_scheduler = ThreadPoolScheduler(max_workers=1) - - def find_best(*_args: Any) -> Image | None: - if len(window) == 0: - return None - return max(window, key=lambda img: img.sharpness) # type: ignore[no-any-return] - - return rx.interval(1.0 / target_frequency).pipe( # type: ignore[misc] - ops.observe_on(thread_scheduler), - ops.map(find_best), - ops.filter(lambda img: img is not None), - ) - - -def sharpness_barrier(target_frequency: float) -> Callable[[Observable[Image]], Observable[Image]]: - """Select the sharpest Image within each time window.""" - if target_frequency <= 0: - raise ValueError("target_frequency must be positive") - return quality_barrier(lambda image: image.sharpness, target_frequency) # type: ignore[attr-defined] - - -def _get_lcm_encoding(fmt: ImageFormat, dtype: np.dtype) -> str: # type: ignore[type-arg] - if fmt == ImageFormat.GRAY: - if dtype == np.uint8: - return "mono8" - if dtype == np.uint16: - return "mono16" - if fmt == ImageFormat.GRAY16: - return "mono16" - if fmt == ImageFormat.RGB: - return "rgb8" - if fmt == ImageFormat.RGBA: - return "rgba8" - if fmt == ImageFormat.BGR: - return "bgr8" - if fmt == ImageFormat.BGRA: - return "bgra8" - if fmt == ImageFormat.DEPTH: - if dtype == np.float32: - return "32FC1" - if dtype == np.float64: - return "64FC1" - if fmt == ImageFormat.DEPTH16: - if dtype == np.uint16: - return "16UC1" - if dtype == np.int16: - return "16SC1" - raise ValueError(f"Unsupported LCM encoding for fmt={fmt}, dtype={dtype}") - - -def _parse_lcm_encoding(enc: str) -> tuple[ImageFormat, type, int]: - m = { - "mono8": (ImageFormat.GRAY, np.uint8, 1), - "mono16": (ImageFormat.GRAY16, np.uint16, 1), - "rgb8": (ImageFormat.RGB, np.uint8, 3), - "rgba8": (ImageFormat.RGBA, np.uint8, 4), - "bgr8": (ImageFormat.BGR, np.uint8, 3), - "bgra8": (ImageFormat.BGRA, np.uint8, 4), - "32FC1": (ImageFormat.DEPTH, np.float32, 1), - "32FC3": (ImageFormat.RGB, np.float32, 3), - "64FC1": (ImageFormat.DEPTH, np.float64, 1), - "16UC1": (ImageFormat.DEPTH16, np.uint16, 1), - "16SC1": (ImageFormat.DEPTH16, np.int16, 1), - } - if enc not in m: - raise ValueError(f"Unsupported encoding: {enc}") - return m[enc] diff --git a/dimos/msgs/sensor_msgs/Imu.py b/dimos/msgs/sensor_msgs/Imu.py deleted file mode 100644 index 7fe03ce03f..0000000000 --- a/dimos/msgs/sensor_msgs/Imu.py +++ /dev/null @@ -1,118 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time - -from dimos_lcm.sensor_msgs.Imu import Imu as LCMImu - -from dimos.msgs.geometry_msgs import Quaternion, Vector3 -from dimos.types.timestamped import Timestamped - - -class Imu(Timestamped): - """IMU sensor message mirroring ROS sensor_msgs/Imu. - - Contains orientation, angular velocity, and linear acceleration - with optional covariance matrices (3x3 row-major as flat 9-element lists). - """ - - msg_name = "sensor_msgs.Imu" - - def __init__( - self, - angular_velocity: Vector3 | None = None, - linear_acceleration: Vector3 | None = None, - orientation: Quaternion | None = None, - orientation_covariance: list[float] | None = None, - angular_velocity_covariance: list[float] | None = None, - linear_acceleration_covariance: list[float] | None = None, - frame_id: str = "imu_link", - ts: float | None = None, - ) -> None: - self.ts = ts if ts is not None else time.time() # type: ignore[assignment] - self.frame_id = frame_id - self.angular_velocity = angular_velocity or Vector3(0.0, 0.0, 0.0) - self.linear_acceleration = linear_acceleration or Vector3(0.0, 0.0, 0.0) - self.orientation = orientation or Quaternion(0.0, 0.0, 0.0, 1.0) - self.orientation_covariance = orientation_covariance or [0.0] * 9 - self.angular_velocity_covariance = angular_velocity_covariance or [0.0] * 9 - self.linear_acceleration_covariance = linear_acceleration_covariance or [0.0] * 9 - - def lcm_encode(self) -> bytes: - msg = LCMImu() - [msg.header.stamp.sec, msg.header.stamp.nsec] = self.ros_timestamp() - msg.header.frame_id = self.frame_id - - msg.orientation.x = self.orientation.x - msg.orientation.y = self.orientation.y - msg.orientation.z = self.orientation.z - msg.orientation.w = self.orientation.w - msg.orientation_covariance = self.orientation_covariance - - msg.angular_velocity.x = self.angular_velocity.x - msg.angular_velocity.y = self.angular_velocity.y - msg.angular_velocity.z = self.angular_velocity.z - msg.angular_velocity_covariance = self.angular_velocity_covariance - - msg.linear_acceleration.x = self.linear_acceleration.x - msg.linear_acceleration.y = self.linear_acceleration.y - msg.linear_acceleration.z = self.linear_acceleration.z - msg.linear_acceleration_covariance = self.linear_acceleration_covariance - - return msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> Imu: - msg = LCMImu.lcm_decode(data) - ts = msg.header.stamp.sec + (msg.header.stamp.nsec / 1_000_000_000) - return cls( - angular_velocity=Vector3( - msg.angular_velocity.x, - msg.angular_velocity.y, - msg.angular_velocity.z, - ), - linear_acceleration=Vector3( - msg.linear_acceleration.x, - msg.linear_acceleration.y, - msg.linear_acceleration.z, - ), - orientation=Quaternion( - msg.orientation.x, - msg.orientation.y, - msg.orientation.z, - msg.orientation.w, - ), - orientation_covariance=list(msg.orientation_covariance), - angular_velocity_covariance=list(msg.angular_velocity_covariance), - linear_acceleration_covariance=list(msg.linear_acceleration_covariance), - frame_id=msg.header.frame_id, - ts=ts, - ) - - def __str__(self) -> str: - return ( - f"Imu(frame_id='{self.frame_id}', " - f"gyro=({self.angular_velocity.x:.3f}, {self.angular_velocity.y:.3f}, {self.angular_velocity.z:.3f}), " - f"accel=({self.linear_acceleration.x:.3f}, {self.linear_acceleration.y:.3f}, {self.linear_acceleration.z:.3f}))" - ) - - def __repr__(self) -> str: - return ( - f"Imu(ts={self.ts}, frame_id='{self.frame_id}', " - f"angular_velocity={self.angular_velocity}, " - f"linear_acceleration={self.linear_acceleration}, " - f"orientation={self.orientation})" - ) diff --git a/dimos/msgs/sensor_msgs/JointCommand.py b/dimos/msgs/sensor_msgs/JointCommand.py deleted file mode 100644 index 78c541c50e..0000000000 --- a/dimos/msgs/sensor_msgs/JointCommand.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""LCM type definitions -This file automatically generated by lcm. -DO NOT MODIFY BY HAND!!!! -""" - -from io import BytesIO -import struct -import time - - -class JointCommand: - """ - Joint command message for robotic manipulators. - - Supports variable number of joints (DOF) with float64 values. - Can be used for position commands or velocity commands. - Includes timestamp for synchronization. - """ - - msg_name = "sensor_msgs.JointCommand" - - __slots__ = ["num_joints", "positions", "timestamp"] - - __typenames__ = ["double", "int32_t", "double"] - - __dimensions__ = [None, None, ["num_joints"]] - - def __init__( - self, positions: list[float] | None = None, timestamp: float | None = None - ) -> None: - """ - Initialize JointCommand. - - Args: - positions: List of joint values (positions or velocities) - timestamp: Unix timestamp (seconds since epoch). If None, uses current time. - """ - if positions is None: - positions = [] - - if timestamp is None: - timestamp = time.time() - - # LCM Type: double (timestamp) - self.timestamp = timestamp - # LCM Type: int32_t - self.num_joints = len(positions) - # LCM Type: double[num_joints] - self.positions = list(positions) - - def lcm_encode(self): # type: ignore[no-untyped-def] - """Encode for LCM transport (dimos uses lcm_encode method name).""" - return self.encode() # type: ignore[no-untyped-call] - - def encode(self): # type: ignore[no-untyped-def] - buf = BytesIO() - buf.write(JointCommand._get_packed_fingerprint()) # type: ignore[no-untyped-call] - self._encode_one(buf) - return buf.getvalue() - - def _encode_one(self, buf) -> None: # type: ignore[no-untyped-def] - # Encode timestamp - buf.write(struct.pack(">d", self.timestamp)) - - # Encode num_joints - buf.write(struct.pack(">i", self.num_joints)) - - # Encode positions array - for i in range(self.num_joints): - buf.write(struct.pack(">d", self.positions[i])) - - @classmethod - def lcm_decode(cls, data: bytes): # type: ignore[no-untyped-def] - """Decode from LCM transport (dimos uses lcm_decode method name).""" - return cls.decode(data) - - @classmethod - def decode(cls, data: bytes): # type: ignore[no-untyped-def] - if hasattr(data, "read"): - buf = data - else: - buf = BytesIO(data) # type: ignore[assignment] - if buf.read(8) != cls._get_packed_fingerprint(): # type: ignore[no-untyped-call] - raise ValueError("Decode error") - return cls._decode_one(buf) # type: ignore[no-untyped-call] - - @classmethod - def _decode_one(cls, buf): # type: ignore[no-untyped-def] - self = JointCommand.__new__(JointCommand) - - # Decode timestamp - self.timestamp = struct.unpack(">d", buf.read(8))[0] - - # Decode num_joints - self.num_joints = struct.unpack(">i", buf.read(4))[0] - - # Decode positions array - self.positions = [] - for _i in range(self.num_joints): - self.positions.append(struct.unpack(">d", buf.read(8))[0]) - - return self - - @classmethod - def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] - if cls in parents: - return 0 - # Hash for variable-length double array message - tmphash = (0x8A3D2E1C5F4B6A9D) & 0xFFFFFFFFFFFFFFFF - tmphash = (((tmphash << 1) & 0xFFFFFFFFFFFFFFFF) + (tmphash >> 63)) & 0xFFFFFFFFFFFFFFFF - return tmphash - - _packed_fingerprint = None - - @classmethod - def _get_packed_fingerprint(cls): # type: ignore[no-untyped-def] - if cls._packed_fingerprint is None: - cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] - return cls._packed_fingerprint - - def get_hash(self): # type: ignore[no-untyped-def] - """Get the LCM hash of the struct""" - return struct.unpack(">Q", JointCommand._get_packed_fingerprint())[0] # type: ignore[no-untyped-call] - - def __str__(self) -> str: - return f"JointCommand(timestamp={self.timestamp:.6f}, num_joints={self.num_joints}, positions={self.positions})" - - def __repr__(self) -> str: - return f"JointCommand(positions={self.positions}, timestamp={self.timestamp})" diff --git a/dimos/msgs/sensor_msgs/JointState.py b/dimos/msgs/sensor_msgs/JointState.py deleted file mode 100644 index 9faf0be42c..0000000000 --- a/dimos/msgs/sensor_msgs/JointState.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TypeAlias - -from dimos_lcm.sensor_msgs import JointState as LCMJointState -from plum import dispatch - -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from JointState -JointStateConvertable: TypeAlias = dict[str, list[str] | list[float]] | LCMJointState - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class JointState(Timestamped): - msg_name = "sensor_msgs.JointState" - ts: float - frame_id: str - name: list[str] - position: list[float] - velocity: list[float] - effort: list[float] - - @dispatch - def __init__( - self, - ts: float = 0.0, - frame_id: str = "", - name: list[str] | None = None, - position: list[float] | None = None, - velocity: list[float] | None = None, - effort: list[float] | None = None, - ) -> None: - """Initialize a JointState message. - - Args: - ts: Timestamp in seconds - frame_id: Frame ID for the message - name: List of joint names - position: List of joint positions (rad or m) - velocity: List of joint velocities (rad/s or m/s) - effort: List of joint efforts (Nm or N) - """ - self.ts = ts if ts != 0 else time.time() - self.frame_id = frame_id - self.name = name if name is not None else [] - self.position = position if position is not None else [] - self.velocity = velocity if velocity is not None else [] - self.effort = effort if effort is not None else [] - - @dispatch # type: ignore[no-redef] - def __init__(self, joint_dict: dict[str, list[str] | list[float]]) -> None: - """Initialize from a dictionary.""" - self.ts = joint_dict.get("ts", time.time()) - self.frame_id = joint_dict.get("frame_id", "") - self.name = list(joint_dict.get("name", [])) - self.position = list(joint_dict.get("position", [])) - self.velocity = list(joint_dict.get("velocity", [])) - self.effort = list(joint_dict.get("effort", [])) - - @dispatch # type: ignore[no-redef] - def __init__(self, joint: JointState) -> None: - """Initialize from another JointState (copy constructor).""" - self.ts = joint.ts - self.frame_id = joint.frame_id - self.name = list(joint.name) - self.position = list(joint.position) - self.velocity = list(joint.velocity) - self.effort = list(joint.effort) - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_joint: LCMJointState) -> None: - """Initialize from an LCM JointState message.""" - self.ts = lcm_joint.header.stamp.sec + (lcm_joint.header.stamp.nsec / 1_000_000_000) - self.frame_id = lcm_joint.header.frame_id - self.name = list(lcm_joint.name) if lcm_joint.name else [] - self.position = list(lcm_joint.position) if lcm_joint.position else [] - self.velocity = list(lcm_joint.velocity) if lcm_joint.velocity else [] - self.effort = list(lcm_joint.effort) if lcm_joint.effort else [] - - def lcm_encode(self) -> bytes: - lcm_msg = LCMJointState() - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - lcm_msg.name_length = len(self.name) - lcm_msg.name = self.name - lcm_msg.position_length = len(self.position) - lcm_msg.position = self.position - lcm_msg.velocity_length = len(self.velocity) - lcm_msg.velocity = self.velocity - lcm_msg.effort_length = len(self.effort) - lcm_msg.effort = self.effort - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> JointState: - lcm_msg = LCMJointState.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - name=list(lcm_msg.name) if lcm_msg.name else [], - position=list(lcm_msg.position) if lcm_msg.position else [], - velocity=list(lcm_msg.velocity) if lcm_msg.velocity else [], - effort=list(lcm_msg.effort) if lcm_msg.effort else [], - ) - - def __str__(self) -> str: - return f"JointState({len(self.name)} joints, frame_id='{self.frame_id}')" - - def __repr__(self) -> str: - return ( - f"JointState(ts={self.ts}, frame_id='{self.frame_id}', " - f"name={self.name}, position={self.position}, " - f"velocity={self.velocity}, effort={self.effort})" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two JointState messages are equal.""" - if not isinstance(other, JointState): - return False - return ( - self.name == other.name - and self.position == other.position - and self.velocity == other.velocity - and self.effort == other.effort - and self.frame_id == other.frame_id - ) diff --git a/dimos/msgs/sensor_msgs/Joy.py b/dimos/msgs/sensor_msgs/Joy.py deleted file mode 100644 index 3823f132b7..0000000000 --- a/dimos/msgs/sensor_msgs/Joy.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import time -from typing import TypeAlias - -from dimos_lcm.sensor_msgs import Joy as LCMJoy -from plum import dispatch - -from dimos.types.timestamped import Timestamped - -# Types that can be converted to/from Joy -JoyConvertable: TypeAlias = ( - tuple[list[float], list[int]] | dict[str, list[float] | list[int]] | LCMJoy -) - - -def sec_nsec(ts): # type: ignore[no-untyped-def] - s = int(ts) - return [s, int((ts - s) * 1_000_000_000)] - - -class Joy(Timestamped): - msg_name = "sensor_msgs.Joy" - ts: float - frame_id: str - axes: list[float] - buttons: list[int] - - @dispatch - def __init__( - self, - ts: float = 0.0, - frame_id: str = "", - axes: list[float] | None = None, - buttons: list[int] | None = None, - ) -> None: - """Initialize a Joy message. - - Args: - ts: Timestamp in seconds - frame_id: Frame ID for the message - axes: List of axis values (typically -1.0 to 1.0) - buttons: List of button states (0 or 1) - """ - self.ts = ts if ts != 0 else time.time() - self.frame_id = frame_id - self.axes = axes if axes is not None else [] - self.buttons = buttons if buttons is not None else [] - - @dispatch # type: ignore[no-redef] - def __init__(self, joy_tuple: tuple[list[float], list[int]]) -> None: - """Initialize from a tuple of (axes, buttons).""" - self.ts = time.time() - self.frame_id = "" - self.axes = list(joy_tuple[0]) - self.buttons = list(joy_tuple[1]) - - @dispatch # type: ignore[no-redef] - def __init__(self, joy_dict: dict[str, list[float] | list[int]]) -> None: - """Initialize from a dictionary with 'axes' and 'buttons' keys.""" - self.ts = joy_dict.get("ts", time.time()) - self.frame_id = joy_dict.get("frame_id", "") - self.axes = list(joy_dict.get("axes", [])) - self.buttons = list(joy_dict.get("buttons", [])) - - @dispatch # type: ignore[no-redef] - def __init__(self, joy: Joy) -> None: - """Initialize from another Joy (copy constructor).""" - self.ts = joy.ts - self.frame_id = joy.frame_id - self.axes = list(joy.axes) - self.buttons = list(joy.buttons) - - @dispatch # type: ignore[no-redef] - def __init__(self, lcm_joy: LCMJoy) -> None: - """Initialize from an LCM Joy message.""" - self.ts = lcm_joy.header.stamp.sec + (lcm_joy.header.stamp.nsec / 1_000_000_000) - self.frame_id = lcm_joy.header.frame_id - self.axes = list(lcm_joy.axes) - self.buttons = list(lcm_joy.buttons) - - def lcm_encode(self) -> bytes: - lcm_msg = LCMJoy() - [lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = sec_nsec(self.ts) # type: ignore[no-untyped-call] - lcm_msg.header.frame_id = self.frame_id - lcm_msg.axes_length = len(self.axes) - lcm_msg.axes = self.axes - lcm_msg.buttons_length = len(self.buttons) - lcm_msg.buttons = self.buttons - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> Joy: - lcm_msg = LCMJoy.lcm_decode(data) - return cls( - ts=lcm_msg.header.stamp.sec + (lcm_msg.header.stamp.nsec / 1_000_000_000), - frame_id=lcm_msg.header.frame_id, - axes=list(lcm_msg.axes) if lcm_msg.axes else [], - buttons=list(lcm_msg.buttons) if lcm_msg.buttons else [], - ) - - def __str__(self) -> str: - return ( - f"Joy(axes={len(self.axes)} values, buttons={len(self.buttons)} values, " - f"frame_id='{self.frame_id}')" - ) - - def __repr__(self) -> str: - return ( - f"Joy(ts={self.ts}, frame_id='{self.frame_id}', " - f"axes={self.axes}, buttons={self.buttons})" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two Joy messages are equal.""" - if not isinstance(other, Joy): - return False - return ( - self.axes == other.axes - and self.buttons == other.buttons - and self.frame_id == other.frame_id - ) diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py deleted file mode 100644 index 3f28edc680..0000000000 --- a/dimos/msgs/sensor_msgs/PointCloud2.py +++ /dev/null @@ -1,751 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import functools -import struct -from typing import TYPE_CHECKING, Any - -# Import LCM types -from dimos_lcm.sensor_msgs.PointCloud2 import ( - PointCloud2 as LCMPointCloud2, -) -from dimos_lcm.sensor_msgs.PointField import PointField # type: ignore[import-untyped] -from dimos_lcm.std_msgs.Header import Header # type: ignore[import-untyped] -import numpy as np -import open3d as o3d # type: ignore[import-untyped] -import open3d.core as o3c # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import Transform, Vector3 -from dimos.types.timestamped import Timestamped - -if TYPE_CHECKING: - from rerun._baseclasses import Archetype - - from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo - from dimos.msgs.sensor_msgs.Image import Image - - -@functools.lru_cache(maxsize=16) -def _get_matplotlib_cmap(name: str): # type: ignore[no-untyped-def] - """Get a matplotlib colormap by name (cached for performance).""" - import matplotlib.pyplot as plt - - return plt.get_cmap(name) - - -# TODO: encode/decode need to be updated to work with full spectrum of pointcloud2 fields -class PointCloud2(Timestamped): - msg_name = "sensor_msgs.PointCloud2" - - def __init__( - self, - pointcloud: o3d.geometry.PointCloud | o3d.t.geometry.PointCloud | None = None, - frame_id: str = "world", - ts: float | None = None, - ) -> None: - self.ts = ts # type: ignore[assignment] - self.frame_id = frame_id - - # Store internally as tensor pointcloud for speed - if pointcloud is None: - self._pcd_tensor: o3d.t.geometry.PointCloud = o3d.t.geometry.PointCloud() - elif isinstance(pointcloud, o3d.t.geometry.PointCloud): - self._pcd_tensor = pointcloud - else: - # Convert legacy to tensor - self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(pointcloud) - self._pcd_legacy_cache: o3d.geometry.PointCloud | None = None - - def _ensure_tensor_initialized(self) -> None: - """Ensure _pcd_tensor and _pcd_legacy_cache exist (handles unpickled old objects).""" - # Always ensure _pcd_legacy_cache exists - if not hasattr(self, "_pcd_legacy_cache"): - self._pcd_legacy_cache = None - - # Check for old pickled format: 'pointcloud' directly in __dict__ - # This takes priority even if _pcd_tensor exists (it might be empty) - old_pcd = self.__dict__.get("pointcloud") - if old_pcd is not None and isinstance(old_pcd, o3d.geometry.PointCloud): - self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(old_pcd) - self._pcd_legacy_cache = old_pcd # reuse it - del self.__dict__["pointcloud"] - return - - if not hasattr(self, "_pcd_tensor"): - self._pcd_tensor = o3d.t.geometry.PointCloud() - - def __getstate__(self) -> dict[str, object]: - """Serialize to numpy for pickling (tensors don't pickle well).""" - self._ensure_tensor_initialized() - state = self.__dict__.copy() - # Convert tensor to numpy for serialization - if "positions" in self._pcd_tensor.point: - state["_pcd_numpy"] = self._pcd_tensor.point["positions"].numpy() - else: - state["_pcd_numpy"] = np.zeros((0, 3), dtype=np.float32) - # Remove non-picklable objects - del state["_pcd_tensor"] - state["_pcd_legacy_cache"] = None - return state - - def __setstate__(self, state: dict[str, object]) -> None: - """Restore from pickled state.""" - points_obj = state.pop("_pcd_numpy", None) - points: np.ndarray[tuple[int, int], np.dtype[np.float32]] = ( - points_obj if isinstance(points_obj, np.ndarray) else np.zeros((0, 3), dtype=np.float32) - ) - self.__dict__.update(state) - # Recreate tensor from numpy - self._pcd_tensor = o3d.t.geometry.PointCloud() - if len(points) > 0: - self._pcd_tensor.point["positions"] = o3c.Tensor(points, dtype=o3c.float32) - - @property - def pointcloud(self) -> o3d.geometry.PointCloud: - """Legacy pointcloud property for backwards compatibility. Cached.""" - self._ensure_tensor_initialized() - if self._pcd_legacy_cache is None: - self._pcd_legacy_cache = self._pcd_tensor.to_legacy() - return self._pcd_legacy_cache - - @pointcloud.setter - def pointcloud(self, value: o3d.geometry.PointCloud | o3d.t.geometry.PointCloud) -> None: - if isinstance(value, o3d.t.geometry.PointCloud): - self._pcd_tensor = value - else: - self._pcd_tensor = o3d.t.geometry.PointCloud.from_legacy(value) - self._pcd_legacy_cache = None - - @property - def pointcloud_tensor(self) -> o3d.t.geometry.PointCloud: - """Direct access to tensor pointcloud (faster, no conversion).""" - self._ensure_tensor_initialized() - return self._pcd_tensor - - @classmethod - def from_numpy( - cls, - points: np.ndarray, # type: ignore[type-arg] - frame_id: str = "world", - timestamp: float | None = None, - ) -> PointCloud2: - """Create PointCloud2 from numpy array of shape (N, 3). - - Args: - points: Nx3 numpy array of 3D points - frame_id: Frame ID for the point cloud - timestamp: Timestamp for the point cloud (defaults to current time) - - Returns: - PointCloud2 instance - """ - pcd_t = o3d.t.geometry.PointCloud() - pcd_t.point["positions"] = o3c.Tensor(points.astype(np.float32), dtype=o3c.float32) - return cls(pointcloud=pcd_t, ts=timestamp, frame_id=frame_id) - - @classmethod - def from_rgbd( - cls, - color_image: Image, - depth_image: Image, - camera_info: CameraInfo, - depth_scale: float = 1.0, - depth_trunc: float = 5.0, - ) -> PointCloud2: - """Create PointCloud2 from RGB and depth Image messages. - - Uses frame_id and timestamp from the depth image. - - Args: - color_image: RGB/BGR color Image message - depth_image: Depth Image message (float32 meters or uint16 mm) - camera_info: CameraInfo message with intrinsics - depth_scale: Scale factor to convert depth to meters (default 1.0 for float32) - depth_trunc: Maximum depth in meters to include - - Returns: - PointCloud2 instance with colored points - """ - # Get color as RGB numpy array - color_data = color_image.to_rgb().data - if hasattr(color_data, "get"): # CuPy array - color_data = color_data.get() - color_data = np.ascontiguousarray(color_data) - - # Get depth numpy array - depth_data = depth_image.data - if hasattr(depth_data, "get"): # CuPy array - depth_data = depth_data.get() - - # Convert depth to float32 meters if needed - if depth_data.dtype == np.uint16: - depth_data = depth_data.astype(np.float32) * depth_scale - elif depth_data.dtype != np.float32: - depth_data = depth_data.astype(np.float32) - depth_data = np.ascontiguousarray(depth_data) - - # Verify dimensions match - color_h, color_w = color_data.shape[:2] - depth_h, depth_w = depth_data.shape[:2] - if (color_h, color_w) != (depth_h, depth_w): - raise ValueError( - f"Color {color_w}x{color_h} and depth {depth_w}x{depth_h} dimensions don't match" - ) - - # Get intrinsics from camera_info - intrinsic = camera_info.get_K_matrix() - fx, fy = intrinsic[0, 0], intrinsic[1, 1] - cx, cy = intrinsic[0, 2], intrinsic[1, 2] - - # Verify intrinsics match image dimensions - if camera_info.width != color_w or camera_info.height != color_h: - # Scale intrinsics if resolution differs - scale_x = color_w / camera_info.width - scale_y = color_h / camera_info.height - fx *= scale_x - fy *= scale_y - cx *= scale_x - cy *= scale_y - - # Create Open3D images - color_o3d = o3d.geometry.Image(color_data.astype(np.uint8)) - - # Filter invalid depth values - depth_filtered = depth_data.copy() - valid_mask = np.isfinite(depth_filtered) & (depth_filtered > 0) - depth_filtered[~valid_mask] = 0.0 - depth_o3d = o3d.geometry.Image(depth_filtered.astype(np.float32)) - - o3d_intrinsic = o3d.camera.PinholeCameraIntrinsic( - width=color_w, - height=color_h, - fx=fx, - fy=fy, - cx=cx, - cy=cy, - ) - - # Create RGBD image and point cloud - rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( - color_o3d, - depth_o3d, - depth_scale=1.0, # Already scaled - depth_trunc=depth_trunc, - convert_rgb_to_intensity=False, - ) - - pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, o3d_intrinsic) - - return cls( - pointcloud=pcd, - frame_id=depth_image.frame_id, - ts=depth_image.ts, - ) - - def __str__(self) -> str: - return f"PointCloud2(frame_id='{self.frame_id}', num_points={len(self)})" - - @functools.cached_property - def center(self) -> Vector3: - """Calculate the center of the pointcloud in world frame.""" - center = np.asarray(self.pointcloud.points).mean(axis=0) - return Vector3(*center) - - def points(self): # type: ignore[no-untyped-def] - """Get points (returns tensor positions, use as_numpy() for numpy array).""" - self._ensure_tensor_initialized() - if "positions" not in self._pcd_tensor.point: - return o3c.Tensor(np.zeros((0, 3), dtype=np.float32)) - return self._pcd_tensor.point["positions"] - - def __add__(self, other: PointCloud2) -> PointCloud2: - """Combine two PointCloud2 instances into one. - - The resulting point cloud contains points from both inputs. - The frame_id and timestamp are taken from the first point cloud. - - Args: - other: Another PointCloud2 instance to combine with - - Returns: - New PointCloud2 instance containing combined points - """ - if not isinstance(other, PointCloud2): - raise ValueError("Can only add PointCloud2 to another PointCloud2") - - return PointCloud2( - pointcloud=self.pointcloud + other.pointcloud, - frame_id=self.frame_id, - ts=max(self.ts, other.ts), - ) - - def transform(self, tf: Transform) -> PointCloud2: - """Transform the pointcloud using a Transform object. - - Applies the rotation and translation from the transform to all points, - converting them into the transform's frame_id. - - Args: - tf: Transform object containing rotation and translation - - Returns: - New PointCloud2 instance with transformed points in the new frame - """ - points, _ = self.as_numpy() - - if len(points) == 0: - return PointCloud2( - pointcloud=o3d.geometry.PointCloud(), - frame_id=tf.frame_id, - ts=self.ts, - ) - - # Build 4x4 transformation matrix from Transform - transform_matrix = tf.to_matrix() - - # Convert points to homogeneous coordinates (N, 4) - ones = np.ones((len(points), 1)) - points_homogeneous = np.hstack([points, ones]) - - # Apply transformation: (4, 4) @ (4, N) -> (4, N) -> transpose to (N, 4) - transformed_points = (transform_matrix @ points_homogeneous.T).T - - # Extract xyz coordinates (drop homogeneous coordinate) - transformed_xyz = transformed_points[:, :3].astype(np.float64) - - # Create new Open3D point cloud - new_pcd = o3d.geometry.PointCloud() - new_pcd.points = o3d.utility.Vector3dVector(transformed_xyz) - - # Copy colors if available - if self.pointcloud.has_colors(): - new_pcd.colors = self.pointcloud.colors - - return PointCloud2( - pointcloud=new_pcd, - frame_id=tf.frame_id, - ts=self.ts, - ) - - def voxel_downsample(self, voxel_size: float = 0.025) -> PointCloud2: - """Downsample the pointcloud with a voxel grid.""" - if voxel_size <= 0: - return self - if len(self.pointcloud.points) < 20: - return self - downsampled = self._pcd_tensor.voxel_down_sample(voxel_size) - return PointCloud2(pointcloud=downsampled, frame_id=self.frame_id, ts=self.ts) - - def as_numpy( - self, - ) -> tuple[np.ndarray[Any, Any], np.ndarray[Any, Any] | None]: - """Get points and colors as numpy arrays. - - Returns: - Tuple of (points, colors) where: - - points: Nx3 numpy array of 3D points - - colors: Nx3 array in [0, 1] range, or None if no colors - """ - points = np.asarray(self.pointcloud.points) - colors = np.asarray(self.pointcloud.colors) if self.pointcloud.has_colors() else None - return points, colors - - @functools.cache - def get_axis_aligned_bounding_box(self) -> o3d.geometry.AxisAlignedBoundingBox: - """Get axis-aligned bounding box of the point cloud.""" - return self.pointcloud.get_axis_aligned_bounding_box() - - @functools.cache - def get_oriented_bounding_box(self) -> o3d.geometry.OrientedBoundingBox: - """Get oriented bounding box of the point cloud.""" - return self.pointcloud.get_oriented_bounding_box() - - @functools.cache - def get_bounding_box_dimensions(self) -> tuple[float, float, float]: - """Get dimensions (width, height, depth) of axis-aligned bounding box.""" - bbox = self.get_axis_aligned_bounding_box() - extent = bbox.get_extent() - return tuple(extent) - - def bounding_box_intersects(self, other: PointCloud2) -> bool: - # Get axis-aligned bounding boxes - bbox1 = self.get_axis_aligned_bounding_box() - bbox2 = other.get_axis_aligned_bounding_box() - - # Get min and max bounds - min1 = bbox1.get_min_bound() - max1 = bbox1.get_max_bound() - min2 = bbox2.get_min_bound() - max2 = bbox2.get_max_bound() - - # Check overlap in all three dimensions - # Boxes intersect if they overlap in ALL dimensions - return ( # type: ignore[no-any-return] - min1[0] <= max2[0] - and max1[0] >= min2[0] - and min1[1] <= max2[1] - and max1[1] >= min2[1] - and min1[2] <= max2[2] - and max1[2] >= min2[2] - ) - - def lcm_encode(self, frame_id: str | None = None) -> bytes: - """Convert to LCM PointCloud2 message with optional RGB colors.""" - msg = LCMPointCloud2() - - # Header - msg.header = Header() - msg.header.seq = 0 - msg.header.frame_id = frame_id or self.frame_id - - msg.header.stamp.sec = int(self.ts) - msg.header.stamp.nsec = int((self.ts - int(self.ts)) * 1e9) - - points, _ = self.as_numpy() - - # Check if pointcloud has colors - self._ensure_tensor_initialized() - has_colors = "colors" in self._pcd_tensor.point - - if len(points) == 0: - msg.height = 0 - msg.width = 0 - msg.point_step = 16 - msg.row_step = 0 - msg.data_length = 0 - msg.data = b"" - msg.is_dense = True - msg.is_bigendian = False - msg.fields_length = 4 - msg.fields = self._create_xyzrgb_fields() if has_colors else self._create_xyz_fields() - return msg.lcm_encode() # type: ignore[no-any-return] - - msg.height = 1 - msg.width = len(points) - - if has_colors: - # Get colors (0-1 range) and convert to uint8 - colors = self._pcd_tensor.point["colors"].numpy() - if colors.max() <= 1.0: - colors = (colors * 255).astype(np.uint8) - else: - colors = colors.astype(np.uint8) - - # Pack RGB into float32 (ROS convention: bytes are [padding, r, g, b]) - rgb_packed = np.zeros(len(points), dtype=np.float32) - rgb_uint32 = ( - (colors[:, 0].astype(np.uint32) << 16) - | (colors[:, 1].astype(np.uint32) << 8) - | colors[:, 2].astype(np.uint32) - ) - rgb_packed = rgb_uint32.view(np.float32) - - msg.fields = self._create_xyzrgb_fields() - msg.fields_length = 4 - msg.point_step = 16 # x, y, z, rgb (4 floats) - - point_data = np.column_stack([points, rgb_packed]).astype(np.float32) - else: - msg.fields = self._create_xyz_fields() - msg.fields_length = 4 - msg.point_step = 16 # x, y, z, intensity - - point_data = np.column_stack( - [ - points, - np.zeros(len(points), dtype=np.float32), - ] - ).astype(np.float32) - - msg.row_step = msg.point_step * msg.width - data_bytes = point_data.tobytes() - msg.data_length = len(data_bytes) - msg.data = data_bytes - - msg.is_dense = True - msg.is_bigendian = False - - return msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes) -> PointCloud2: - msg = LCMPointCloud2.lcm_decode(data) - - if msg.width == 0 or msg.height == 0: - pc = o3d.geometry.PointCloud() - return cls( - pointcloud=pc, - frame_id=msg.header.frame_id if hasattr(msg, "header") else "", - ts=msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 - if hasattr(msg, "header") and msg.header.stamp.sec > 0 - else None, - ) - - # Parse field offsets - x_offset = y_offset = z_offset = rgb_offset = None - for msgfield in msg.fields: - if msgfield.name == "x": - x_offset = msgfield.offset - elif msgfield.name == "y": - y_offset = msgfield.offset - elif msgfield.name == "z": - z_offset = msgfield.offset - elif msgfield.name == "rgb": - rgb_offset = msgfield.offset - - if any(offset is None for offset in [x_offset, y_offset, z_offset]): - raise ValueError("PointCloud2 message missing X, Y, or Z msgfields") - - num_points = msg.width * msg.height - raw_data = msg.data - point_step = msg.point_step - - # Fast path for standard layout - if x_offset == 0 and y_offset == 4 and z_offset == 8 and point_step >= 12: - if point_step == 12: - points = np.frombuffer(raw_data, dtype=np.float32).reshape(-1, 3) - else: - dt = np.dtype( - [("x", "> 16) & 0xFF).astype(np.float32) / 255.0 - g = ((rgb_packed >> 8) & 0xFF).astype(np.float32) / 255.0 - b = (rgb_packed & 0xFF).astype(np.float32) / 255.0 - colors = np.column_stack([r, g, b]) - pcd_t.point["colors"] = o3c.Tensor(colors, dtype=o3c.float32) - - return cls( - pointcloud=pcd_t, - frame_id=msg.header.frame_id if hasattr(msg, "header") else "", - ts=msg.header.stamp.sec + msg.header.stamp.nsec / 1e9 - if hasattr(msg, "header") and msg.header.stamp.sec > 0 - else None, - ) - - def _create_xyz_fields(self) -> list: # type: ignore[type-arg] - """Create X, Y, Z, intensity field definitions.""" - fields = [] - for i, name in enumerate(["x", "y", "z", "intensity"]): - field = PointField() - field.name = name - field.offset = i * 4 - field.datatype = 7 # FLOAT32 - field.count = 1 - fields.append(field) - return fields - - def _create_xyzrgb_fields(self) -> list: # type: ignore[type-arg] - """Create X, Y, Z, RGB field definitions for colored pointclouds.""" - fields = [] - for i, name in enumerate(["x", "y", "z"]): - field = PointField() - field.name = name - field.offset = i * 4 - field.datatype = 7 # FLOAT32 - field.count = 1 - fields.append(field) - - # RGB field (packed as float32, ROS convention) - rgb_field = PointField() - rgb_field.name = "rgb" - rgb_field.offset = 12 - rgb_field.datatype = 7 # FLOAT32 (contains packed RGB) - rgb_field.count = 1 - fields.append(rgb_field) - - return fields - - def __len__(self) -> int: - """Return number of points.""" - self._ensure_tensor_initialized() - if "positions" not in self._pcd_tensor.point: - return 0 - return int(self._pcd_tensor.point["positions"].shape[0]) - - def to_rerun( - self, - voxel_size: float = 0.05, - colormap: str | None = None, - colors: list[int] | None = None, - mode: str = "points", - size: float | None = None, - fill_mode: str = "solid", - **kwargs: object, - ) -> Archetype: - """Convert to Rerun archetype for visualization. - - Args: - voxel_size: size for visualization - colormap: Optional colormap name (e.g., "turbo", "viridis") to color by height - colors: Optional RGB color [r, g, b] for all points (0-255) - mode: "points" for raw points, "boxes" for cubes (default), or "spheres" for sized spheres - size: Box size for mode="boxes" (e.g., voxel_size). Defaults to radii*2. - fill_mode: Fill mode for boxes - "solid", "majorwireframe", or "densewireframe" - **kwargs: Additional args (ignored for compatibility) - - Returns: - rr.Points3D or rr.Boxes3D archetype for logging to Rerun - """ - import rerun as rr - - points, _ = self.as_numpy() - if len(points) == 0: - return rr.Points3D([]) if mode != "boxes" else rr.Boxes3D(centers=[]) - - if colors is None and colormap is None: - colormap = "turbo" # Default colormap if no colors provided - # Determine colors - point_colors = None - if colormap is not None: - z = points[:, 2] - z_norm = (z - z.min()) / (z.max() - z.min() + 1e-8) - cmap = _get_matplotlib_cmap(colormap) - point_colors = (cmap(z_norm)[:, :3] * 255).astype(np.uint8) - elif colors is not None: - point_colors = colors - - if mode == "points": - return rr.Points3D( - positions=points, - colors=point_colors, - ) - elif mode == "boxes": - box_size = size if size is not None else voxel_size - half = box_size / 2 - # Snap points to voxel grid centers so boxes tile properly - points = np.floor(points / box_size) * box_size + half - points, unique_idx = np.unique(points, axis=0, return_index=True) - if point_colors is not None and isinstance(point_colors, np.ndarray): - point_colors = point_colors[unique_idx] - return rr.Boxes3D( - centers=points, - half_sizes=[half, half, half], - colors=point_colors, - fill_mode=fill_mode, # type: ignore[arg-type] - ) - else: - return rr.Points3D( - positions=points, - radii=voxel_size / 2, - colors=point_colors, - ) - - def filter_by_height( - self, - min_height: float | None = None, - max_height: float | None = None, - ) -> PointCloud2: - """Filter points based on their height (z-coordinate). - - This method creates a new PointCloud2 containing only points within the specified - height range. All metadata (frame_id, timestamp) is preserved. - - Args: - min_height: Optional minimum height threshold. Points with z < min_height are filtered out. - If None, no lower limit is applied. - max_height: Optional maximum height threshold. Points with z > max_height are filtered out. - If None, no upper limit is applied. - - Returns: - New PointCloud2 instance containing only the filtered points. - - Raises: - ValueError: If both min_height and max_height are None (no filtering would occur). - - Example: - # Remove ground points below 0.1m height - filtered_pc = pointcloud.filter_by_height(min_height=0.1) - - # Keep only points between ground level and 2m height - filtered_pc = pointcloud.filter_by_height(min_height=0.0, max_height=2.0) - - # Remove points above 1.5m (e.g., ceiling) - filtered_pc = pointcloud.filter_by_height(max_height=1.5) - """ - # Validate that at least one threshold is provided - if min_height is None and max_height is None: - raise ValueError("At least one of min_height or max_height must be specified") - - # Get points as numpy array - points, _ = self.as_numpy() - - if len(points) == 0: - # Empty pointcloud - return a copy - return PointCloud2( - pointcloud=o3d.geometry.PointCloud(), - frame_id=self.frame_id, - ts=self.ts, - ) - - # Extract z-coordinates (height values) - column index 2 - heights = points[:, 2] - - # Create boolean mask for filtering based on height thresholds - # Start with all True values - mask = np.ones(len(points), dtype=bool) - - # Apply minimum height filter if specified - if min_height is not None: - mask &= heights >= min_height - - # Apply maximum height filter if specified - if max_height is not None: - mask &= heights <= max_height - - # Apply mask to filter points - filtered_points = points[mask] - - # Create new PointCloud2 with filtered points - return PointCloud2.from_numpy( - points=filtered_points, - frame_id=self.frame_id, - timestamp=self.ts, - ) - - def __repr__(self) -> str: - """String representation.""" - return f"PointCloud(points={len(self)}, frame_id='{self.frame_id}', ts={self.ts})" diff --git a/dimos/msgs/sensor_msgs/RobotState.py b/dimos/msgs/sensor_msgs/RobotState.py deleted file mode 100644 index 20e41e7d24..0000000000 --- a/dimos/msgs/sensor_msgs/RobotState.py +++ /dev/null @@ -1,188 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""LCM type definitions -This file automatically generated by lcm. -DO NOT MODIFY BY HAND!!!! -""" - -from io import BytesIO -import struct - - -class RobotState: - msg_name = "sensor_msgs.RobotState" - - __slots__ = [ - "cmdnum", - "error_code", - "joints", - "mode", - "mt_able", - "mt_brake", - "state", - "tcp_offset", - "tcp_pose", - "warn_code", - ] - - __typenames__ = [ - "int32_t", - "int32_t", - "int32_t", - "int32_t", - "int32_t", - "int32_t", - "int32_t", - "float", - "float", - "float", - ] - - __dimensions__ = [None, None, None, None, None, None, None, None, None, None] - - def __init__( # type: ignore[no-untyped-def] - self, - state: int = 0, - mode: int = 0, - error_code: int = 0, - warn_code: int = 0, - cmdnum: int = 0, - mt_brake: int = 0, - mt_able: int = 0, - tcp_pose=None, - tcp_offset=None, - joints=None, - ) -> None: - # LCM Type: int32_t - self.state = state - # LCM Type: int32_t - self.mode = mode - # LCM Type: int32_t - self.error_code = error_code - # LCM Type: int32_t - self.warn_code = warn_code - # LCM Type: int32_t - self.cmdnum = cmdnum - # LCM Type: int32_t - self.mt_brake = mt_brake - # LCM Type: int32_t - self.mt_able = mt_able - # LCM Type: float[] - TCP pose [x, y, z, roll, pitch, yaw] - self.tcp_pose = tcp_pose if tcp_pose is not None else [] - # LCM Type: float[] - TCP offset [x, y, z, roll, pitch, yaw] - self.tcp_offset = tcp_offset if tcp_offset is not None else [] - # LCM Type: float[] - Joint positions (variable length based on robot DOF) - self.joints = joints if joints is not None else [] - - def lcm_encode(self): # type: ignore[no-untyped-def] - """Encode for LCM transport (dimos uses lcm_encode method name).""" - return self.encode() # type: ignore[no-untyped-call] - - def encode(self): # type: ignore[no-untyped-def] - buf = BytesIO() - buf.write(RobotState._get_packed_fingerprint()) # type: ignore[no-untyped-call] - self._encode_one(buf) - return buf.getvalue() - - def _encode_one(self, buf) -> None: # type: ignore[no-untyped-def] - buf.write( - struct.pack( - ">iiiiiii", - self.state, - self.mode, - self.error_code, - self.warn_code, - self.cmdnum, - self.mt_brake, - self.mt_able, - ) - ) - # Encode tcp_pose array - buf.write(struct.pack(">i", len(self.tcp_pose))) - for val in self.tcp_pose: - buf.write(struct.pack(">f", val)) - # Encode tcp_offset array - buf.write(struct.pack(">i", len(self.tcp_offset))) - for val in self.tcp_offset: - buf.write(struct.pack(">f", val)) - # Encode joints array - buf.write(struct.pack(">i", len(self.joints))) - for val in self.joints: - buf.write(struct.pack(">f", val)) - - @classmethod - def lcm_decode(cls, data: bytes): # type: ignore[no-untyped-def] - """Decode from LCM transport (dimos uses lcm_decode method name).""" - return cls.decode(data) - - @classmethod - def decode(cls, data: bytes): # type: ignore[no-untyped-def] - if hasattr(data, "read"): - buf = data - else: - buf = BytesIO(data) # type: ignore[assignment] - if buf.read(8) != cls._get_packed_fingerprint(): # type: ignore[no-untyped-call] - raise ValueError("Decode error") - return cls._decode_one(buf) # type: ignore[no-untyped-call] - - @classmethod - def _decode_one(cls, buf): # type: ignore[no-untyped-def] - self = RobotState() - ( - self.state, - self.mode, - self.error_code, - self.warn_code, - self.cmdnum, - self.mt_brake, - self.mt_able, - ) = struct.unpack(">iiiiiii", buf.read(28)) - # Decode tcp_pose array - tcp_pose_len = struct.unpack(">i", buf.read(4))[0] - self.tcp_pose = [] - for _ in range(tcp_pose_len): - self.tcp_pose.append(struct.unpack(">f", buf.read(4))[0]) - # Decode tcp_offset array - tcp_offset_len = struct.unpack(">i", buf.read(4))[0] - self.tcp_offset = [] - for _ in range(tcp_offset_len): - self.tcp_offset.append(struct.unpack(">f", buf.read(4))[0]) - # Decode joints array - joints_len = struct.unpack(">i", buf.read(4))[0] - self.joints = [] - for _ in range(joints_len): - self.joints.append(struct.unpack(">f", buf.read(4))[0]) - return self - - @classmethod - def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] - if cls in parents: - return 0 - # Updated hash to reflect new fields: tcp_pose, tcp_offset, joints - tmphash = (0x8C3B9A1FE7D24E6A) & 0xFFFFFFFFFFFFFFFF - tmphash = (((tmphash << 1) & 0xFFFFFFFFFFFFFFFF) + (tmphash >> 63)) & 0xFFFFFFFFFFFFFFFF - return tmphash - - _packed_fingerprint = None - - @classmethod - def _get_packed_fingerprint(cls): # type: ignore[no-untyped-def] - if cls._packed_fingerprint is None: - cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] - return cls._packed_fingerprint - - def get_hash(self): # type: ignore[no-untyped-def] - """Get the LCM hash of the struct""" - return struct.unpack(">Q", RobotState._get_packed_fingerprint())[0] # type: ignore[no-untyped-call] diff --git a/dimos/msgs/sensor_msgs/__init__.py b/dimos/msgs/sensor_msgs/__init__.py deleted file mode 100644 index 7fec2d2793..0000000000 --- a/dimos/msgs/sensor_msgs/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat -from dimos.msgs.sensor_msgs.Imu import Imu -from dimos.msgs.sensor_msgs.JointCommand import JointCommand -from dimos.msgs.sensor_msgs.JointState import JointState -from dimos.msgs.sensor_msgs.Joy import Joy -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.msgs.sensor_msgs.RobotState import RobotState - -__all__ = [ - "CameraInfo", - "Image", - "ImageFormat", - "Imu", - "JointCommand", - "JointState", - "Joy", - "PointCloud2", - "RobotState", -] diff --git a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py b/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py deleted file mode 100644 index 80a1bb7cec..0000000000 --- a/dimos/msgs/sensor_msgs/image_impls/AbstractImage.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Backwards compatibility stub for unpickling old data. -# AbstractImage and ImageFormat were moved to Image. -from dimos.msgs.sensor_msgs.Image import Image as AbstractImage, ImageFormat - -__all__ = ["AbstractImage", "ImageFormat"] diff --git a/dimos/msgs/sensor_msgs/test_CameraInfo.py b/dimos/msgs/sensor_msgs/test_CameraInfo.py deleted file mode 100644 index 0cf51d15c7..0000000000 --- a/dimos/msgs/sensor_msgs/test_CameraInfo.py +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np - -from dimos.msgs.sensor_msgs.CameraInfo import CalibrationProvider, CameraInfo -from dimos.utils.path_utils import get_project_root - - -def test_lcm_encode_decode() -> None: - """Test LCM encode/decode preserves CameraInfo data.""" - print("Testing CameraInfo LCM encode/decode...") - - # Create test camera info with sample calibration data - original = CameraInfo( - height=480, - width=640, - distortion_model="plumb_bob", - D=[-0.1, 0.05, 0.001, -0.002, 0.0], # 5 distortion coefficients - K=[ - 500.0, - 0.0, - 320.0, # fx, 0, cx - 0.0, - 500.0, - 240.0, # 0, fy, cy - 0.0, - 0.0, - 1.0, - ], # 0, 0, 1 - R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], - P=[ - 500.0, - 0.0, - 320.0, - 0.0, # fx, 0, cx, Tx - 0.0, - 500.0, - 240.0, - 0.0, # 0, fy, cy, Ty - 0.0, - 0.0, - 1.0, - 0.0, - ], # 0, 0, 1, 0 - binning_x=2, - binning_y=2, - frame_id="camera_optical_frame", - ts=1234567890.123456, - ) - - # Set ROI - original.roi_x_offset = 100 - original.roi_y_offset = 50 - original.roi_height = 200 - original.roi_width = 300 - original.roi_do_rectify = True - - # Encode and decode - binary_msg = original.lcm_encode() - decoded = CameraInfo.lcm_decode(binary_msg) - - # Check basic properties - assert original.height == decoded.height, ( - f"Height mismatch: {original.height} vs {decoded.height}" - ) - assert original.width == decoded.width, f"Width mismatch: {original.width} vs {decoded.width}" - print(f"✓ Image dimensions preserved: {decoded.width}x{decoded.height}") - - assert original.distortion_model == decoded.distortion_model, ( - f"Distortion model mismatch: '{original.distortion_model}' vs '{decoded.distortion_model}'" - ) - print(f"✓ Distortion model preserved: '{decoded.distortion_model}'") - - # Check distortion coefficients - assert len(original.D) == len(decoded.D), ( - f"D length mismatch: {len(original.D)} vs {len(decoded.D)}" - ) - np.testing.assert_allclose( - original.D, decoded.D, rtol=1e-9, atol=1e-9, err_msg="Distortion coefficients don't match" - ) - print(f"✓ Distortion coefficients preserved: {len(decoded.D)} coefficients") - - # Check camera matrices - np.testing.assert_allclose( - original.K, decoded.K, rtol=1e-9, atol=1e-9, err_msg="K matrix doesn't match" - ) - print("✓ Intrinsic matrix K preserved") - - np.testing.assert_allclose( - original.R, decoded.R, rtol=1e-9, atol=1e-9, err_msg="R matrix doesn't match" - ) - print("✓ Rectification matrix R preserved") - - np.testing.assert_allclose( - original.P, decoded.P, rtol=1e-9, atol=1e-9, err_msg="P matrix doesn't match" - ) - print("✓ Projection matrix P preserved") - - # Check binning - assert original.binning_x == decoded.binning_x, ( - f"Binning X mismatch: {original.binning_x} vs {decoded.binning_x}" - ) - assert original.binning_y == decoded.binning_y, ( - f"Binning Y mismatch: {original.binning_y} vs {decoded.binning_y}" - ) - print(f"✓ Binning preserved: {decoded.binning_x}x{decoded.binning_y}") - - # Check ROI - assert original.roi_x_offset == decoded.roi_x_offset, "ROI x_offset mismatch" - assert original.roi_y_offset == decoded.roi_y_offset, "ROI y_offset mismatch" - assert original.roi_height == decoded.roi_height, "ROI height mismatch" - assert original.roi_width == decoded.roi_width, "ROI width mismatch" - assert original.roi_do_rectify == decoded.roi_do_rectify, "ROI do_rectify mismatch" - print("✓ ROI preserved") - - # Check metadata - assert original.frame_id == decoded.frame_id, ( - f"Frame ID mismatch: '{original.frame_id}' vs '{decoded.frame_id}'" - ) - print(f"✓ Frame ID preserved: '{decoded.frame_id}'") - - assert abs(original.ts - decoded.ts) < 1e-6, ( - f"Timestamp mismatch: {original.ts} vs {decoded.ts}" - ) - print(f"✓ Timestamp preserved: {decoded.ts}") - - print("✓ LCM encode/decode test passed - all properties preserved!") - - -def test_numpy_matrix_operations() -> None: - """Test numpy matrix getter/setter operations.""" - print("\nTesting numpy matrix operations...") - - camera_info = CameraInfo() - - # Test K matrix - K = np.array([[525.0, 0.0, 319.5], [0.0, 525.0, 239.5], [0.0, 0.0, 1.0]]) - camera_info.set_K_matrix(K) - K_retrieved = camera_info.get_K_matrix() - np.testing.assert_allclose(K, K_retrieved, rtol=1e-9, atol=1e-9) - print("✓ K matrix setter/getter works") - - # Test P matrix - P = np.array([[525.0, 0.0, 319.5, 0.0], [0.0, 525.0, 239.5, 0.0], [0.0, 0.0, 1.0, 0.0]]) - camera_info.set_P_matrix(P) - P_retrieved = camera_info.get_P_matrix() - np.testing.assert_allclose(P, P_retrieved, rtol=1e-9, atol=1e-9) - print("✓ P matrix setter/getter works") - - # Test R matrix - R = np.eye(3) - camera_info.set_R_matrix(R) - R_retrieved = camera_info.get_R_matrix() - np.testing.assert_allclose(R, R_retrieved, rtol=1e-9, atol=1e-9) - print("✓ R matrix setter/getter works") - - # Test D coefficients - D = np.array([-0.2, 0.1, 0.001, -0.002, 0.05]) - camera_info.set_D_coeffs(D) - D_retrieved = camera_info.get_D_coeffs() - np.testing.assert_allclose(D, D_retrieved, rtol=1e-9, atol=1e-9) - print("✓ D coefficients setter/getter works") - - print("✓ All numpy matrix operations passed!") - - -def test_equality() -> None: - """Test CameraInfo equality comparison.""" - print("\nTesting CameraInfo equality...") - - info1 = CameraInfo( - height=480, - width=640, - distortion_model="plumb_bob", - D=[-0.1, 0.05, 0.0, 0.0, 0.0], - frame_id="camera1", - ) - - info2 = CameraInfo( - height=480, - width=640, - distortion_model="plumb_bob", - D=[-0.1, 0.05, 0.0, 0.0, 0.0], - frame_id="camera1", - ) - - info3 = CameraInfo( - height=720, - width=1280, # Different resolution - distortion_model="plumb_bob", - D=[-0.1, 0.05, 0.0, 0.0, 0.0], - frame_id="camera1", - ) - - assert info1 == info2, "Identical CameraInfo objects should be equal" - assert info1 != info3, "Different CameraInfo objects should not be equal" - assert info1 != "not_camera_info", "CameraInfo should not equal non-CameraInfo object" - - print("✓ Equality comparison works correctly") - - -def test_camera_info_from_yaml() -> None: - """Test loading CameraInfo from YAML file.""" - - # Get path to the single webcam YAML file - yaml_path = ( - get_project_root() - / "dimos" - / "hardware" - / "sensors" - / "camera" - / "zed" - / "single_webcam.yaml" - ) - - # Load CameraInfo from YAML - camera_info = CameraInfo.from_yaml(str(yaml_path)) - - # Verify loaded values - assert camera_info.width == 640 - assert camera_info.height == 376 - assert camera_info.distortion_model == "plumb_bob" - assert camera_info.frame_id == "camera_optical" - - # Check camera matrix K - K = camera_info.get_K_matrix() - assert K.shape == (3, 3) - assert np.isclose(K[0, 0], 379.45267) # fx - assert np.isclose(K[1, 1], 380.67871) # fy - assert np.isclose(K[0, 2], 302.43516) # cx - assert np.isclose(K[1, 2], 228.00954) # cy - - # Check distortion coefficients - D = camera_info.get_D_coeffs() - assert len(D) == 5 - assert np.isclose(D[0], -0.309435) - - # Check projection matrix P - P = camera_info.get_P_matrix() - assert P.shape == (3, 4) - assert np.isclose(P[0, 0], 291.12888) - - print("✓ CameraInfo loaded successfully from YAML file") - - -def test_calibration_provider() -> None: - """Test CalibrationProvider lazy loading of YAML files.""" - # Get the directory containing calibration files (not the file itself) - calibration_dir = get_project_root() / "dimos" / "hardware" / "sensors" / "camera" / "zed" - - # Create CalibrationProvider instance - Calibrations = CalibrationProvider(calibration_dir) - - # Test lazy loading of single_webcam.yaml using snake_case - camera_info = Calibrations.single_webcam - assert isinstance(camera_info, CameraInfo) - assert camera_info.width == 640 - assert camera_info.height == 376 - - # Test PascalCase access to same calibration - camera_info2 = Calibrations.SingleWebcam - assert isinstance(camera_info2, CameraInfo) - assert camera_info2.width == 640 - assert camera_info2.height == 376 - - # Test caching - both access methods should return same object - assert camera_info is camera_info2 # Same object reference - - # Test __dir__ lists available calibrations in both cases - available = dir(Calibrations) - assert "single_webcam" in available - assert "SingleWebcam" in available - - print("✓ CalibrationProvider test passed with both naming conventions!") diff --git a/dimos/msgs/sensor_msgs/test_Joy.py b/dimos/msgs/sensor_msgs/test_Joy.py deleted file mode 100644 index 499c2bb860..0000000000 --- a/dimos/msgs/sensor_msgs/test_Joy.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.msgs.sensor_msgs.Joy import Joy - - -def test_lcm_encode_decode() -> None: - """Test LCM encode/decode preserves Joy data.""" - print("Testing Joy LCM encode/decode...") - - # Create test joy message with sample gamepad data - original = Joy( - ts=1234567890.123456789, - frame_id="gamepad", - axes=[0.5, -0.25, 1.0, -1.0, 0.0, 0.75], # 6 axes (e.g., left/right sticks + triggers) - buttons=[1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0], # 12 buttons - ) - - # Encode to LCM bytes - encoded = original.lcm_encode() - assert isinstance(encoded, bytes) - assert len(encoded) > 0 - - # Decode back - decoded = Joy.lcm_decode(encoded) - - # Verify all fields match - assert abs(decoded.ts - original.ts) < 1e-9 - assert decoded.frame_id == original.frame_id - assert decoded.axes == original.axes - assert decoded.buttons == original.buttons - - print("✓ Joy LCM encode/decode test passed") - - -def test_initialization_methods() -> None: - """Test various initialization methods for Joy.""" - print("Testing Joy initialization methods...") - - # Test default initialization - joy1 = Joy() - assert joy1.axes == [] - assert joy1.buttons == [] - assert joy1.frame_id == "" - assert joy1.ts > 0 # Should have current time - - # Test full initialization - joy2 = Joy(ts=1234567890.0, frame_id="xbox_controller", axes=[0.1, 0.2, 0.3], buttons=[1, 0, 1]) - assert joy2.ts == 1234567890.0 - assert joy2.frame_id == "xbox_controller" - assert joy2.axes == [0.1, 0.2, 0.3] - assert joy2.buttons == [1, 0, 1] - - # Test tuple initialization - joy3 = Joy(([0.5, -0.5], [1, 1, 0])) - assert joy3.axes == [0.5, -0.5] - assert joy3.buttons == [1, 1, 0] - - # Test dict initialization - joy4 = Joy({"axes": [0.7, 0.8], "buttons": [0, 1], "frame_id": "ps4_controller"}) - assert joy4.axes == [0.7, 0.8] - assert joy4.buttons == [0, 1] - assert joy4.frame_id == "ps4_controller" - - # Test copy constructor - joy5 = Joy(joy2) - assert joy5.ts == joy2.ts - assert joy5.frame_id == joy2.frame_id - assert joy5.axes == joy2.axes - assert joy5.buttons == joy2.buttons - assert joy5 is not joy2 # Different objects - - print("✓ Joy initialization methods test passed") - - -def test_equality() -> None: - """Test Joy equality comparison.""" - print("Testing Joy equality...") - - joy1 = Joy(ts=1000.0, frame_id="controller1", axes=[0.5, -0.5], buttons=[1, 0, 1]) - - joy2 = Joy(ts=1000.0, frame_id="controller1", axes=[0.5, -0.5], buttons=[1, 0, 1]) - - joy3 = Joy( - ts=1000.0, - frame_id="controller2", # Different frame_id - axes=[0.5, -0.5], - buttons=[1, 0, 1], - ) - - joy4 = Joy( - ts=1000.0, - frame_id="controller1", - axes=[0.6, -0.5], # Different axes - buttons=[1, 0, 1], - ) - - # Same content should be equal - assert joy1 == joy2 - - # Different frame_id should not be equal - assert joy1 != joy3 - - # Different axes should not be equal - assert joy1 != joy4 - - # Different type should not be equal - assert joy1 != "not a joy" - assert joy1 != 42 - - print("✓ Joy equality test passed") - - -def test_string_representation() -> None: - """Test Joy string representations.""" - print("Testing Joy string representations...") - - joy = Joy( - ts=1234567890.123, - frame_id="test_controller", - axes=[0.1, -0.2, 0.3, 0.4], - buttons=[1, 0, 1, 0, 0, 1], - ) - - # Test __str__ - str_repr = str(joy) - assert "Joy" in str_repr - assert "axes=4 values" in str_repr - assert "buttons=6 values" in str_repr - assert "test_controller" in str_repr - - # Test __repr__ - repr_str = repr(joy) - assert "Joy" in repr_str - assert "1234567890.123" in repr_str - assert "test_controller" in repr_str - assert "[0.1, -0.2, 0.3, 0.4]" in repr_str - assert "[1, 0, 1, 0, 0, 1]" in repr_str - - print("✓ Joy string representation test passed") - - -def test_edge_cases() -> None: - """Test Joy with edge cases.""" - print("Testing Joy edge cases...") - - # Empty axes and buttons - joy1 = Joy(axes=[], buttons=[]) - assert joy1.axes == [] - assert joy1.buttons == [] - encoded = joy1.lcm_encode() - decoded = Joy.lcm_decode(encoded) - assert decoded.axes == [] - assert decoded.buttons == [] - - # Large number of axes and buttons - many_axes = [float(i) / 100.0 for i in range(20)] - many_buttons = [i % 2 for i in range(32)] - joy2 = Joy(axes=many_axes, buttons=many_buttons) - assert len(joy2.axes) == 20 - assert len(joy2.buttons) == 32 - encoded = joy2.lcm_encode() - decoded = Joy.lcm_decode(encoded) - # Check axes with floating point tolerance - assert len(decoded.axes) == len(many_axes) - for i, (a, b) in enumerate(zip(decoded.axes, many_axes, strict=False)): - assert abs(a - b) < 1e-6, f"Axis {i}: {a} != {b}" - assert decoded.buttons == many_buttons - - # Extreme axis values - extreme_axes = [-1.0, 1.0, 0.0, -0.999999, 0.999999] - joy3 = Joy(axes=extreme_axes) - assert joy3.axes == extreme_axes - - print("✓ Joy edge cases test passed") diff --git a/dimos/msgs/sensor_msgs/test_PointCloud2.py b/dimos/msgs/sensor_msgs/test_PointCloud2.py deleted file mode 100644 index 501a4cd441..0000000000 --- a/dimos/msgs/sensor_msgs/test_PointCloud2.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import numpy as np - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.lidar import pointcloud2_from_webrtc_lidar -from dimos.utils.testing import SensorReplay - - -def test_lcm_encode_decode() -> None: - """Test LCM encode/decode preserves pointcloud data.""" - replay = SensorReplay("office_lidar", autocast=pointcloud2_from_webrtc_lidar) - lidar_msg: PointCloud2 = replay.load_one("lidar_data_021") - - binary_msg = lidar_msg.lcm_encode() - decoded = PointCloud2.lcm_decode(binary_msg) - - # 1. Check number of points - original_points, _ = lidar_msg.as_numpy() - decoded_points, _ = decoded.as_numpy() - - print(f"Original points: {len(original_points)}") - print(f"Decoded points: {len(decoded_points)}") - assert len(original_points) == len(decoded_points), ( - f"Point count mismatch: {len(original_points)} vs {len(decoded_points)}" - ) - - # 2. Check point coordinates are preserved (within floating point tolerance) - if len(original_points) > 0: - np.testing.assert_allclose( - original_points, - decoded_points, - rtol=1e-6, - atol=1e-6, - err_msg="Point coordinates don't match between original and decoded", - ) - print(f"✓ All {len(original_points)} point coordinates match within tolerance") - - # 3. Check frame_id is preserved - assert lidar_msg.frame_id == decoded.frame_id, ( - f"Frame ID mismatch: '{lidar_msg.frame_id}' vs '{decoded.frame_id}'" - ) - print(f"✓ Frame ID preserved: '{decoded.frame_id}'") - - # 4. Check timestamp is preserved (within reasonable tolerance for float precision) - if lidar_msg.ts is not None and decoded.ts is not None: - assert abs(lidar_msg.ts - decoded.ts) < 1e-6, ( - f"Timestamp mismatch: {lidar_msg.ts} vs {decoded.ts}" - ) - print(f"✓ Timestamp preserved: {decoded.ts}") - - # 5. Check pointcloud properties - assert len(lidar_msg.pointcloud.points) == len(decoded.pointcloud.points), ( - "Open3D pointcloud size mismatch" - ) - - # 6. Additional detailed checks - print("✓ Original pointcloud summary:") - print(f" - Points: {len(original_points)}") - print(f" - Bounds: {original_points.min(axis=0)} to {original_points.max(axis=0)}") - print(f" - Mean: {original_points.mean(axis=0)}") - - print("✓ Decoded pointcloud summary:") - print(f" - Points: {len(decoded_points)}") - print(f" - Bounds: {decoded_points.min(axis=0)} to {decoded_points.max(axis=0)}") - print(f" - Mean: {decoded_points.mean(axis=0)}") - - print("✓ LCM encode/decode test passed - all properties preserved!") - - -def test_bounding_box_intersects() -> None: - """Test bounding_box_intersects method with various scenarios.""" - # Test 1: Overlapping boxes - pc1 = PointCloud2.from_numpy(np.array([[0, 0, 0], [2, 2, 2]])) - pc2 = PointCloud2.from_numpy(np.array([[1, 1, 1], [3, 3, 3]])) - assert pc1.bounding_box_intersects(pc2) - assert pc2.bounding_box_intersects(pc1) # Should be symmetric - - # Test 2: Non-overlapping boxes - pc3 = PointCloud2.from_numpy(np.array([[0, 0, 0], [1, 1, 1]])) - pc4 = PointCloud2.from_numpy(np.array([[2, 2, 2], [3, 3, 3]])) - assert not pc3.bounding_box_intersects(pc4) - assert not pc4.bounding_box_intersects(pc3) - - # Test 3: Touching boxes (edge case - should be True) - pc5 = PointCloud2.from_numpy(np.array([[0, 0, 0], [1, 1, 1]])) - pc6 = PointCloud2.from_numpy(np.array([[1, 1, 1], [2, 2, 2]])) - assert pc5.bounding_box_intersects(pc6) - assert pc6.bounding_box_intersects(pc5) - - # Test 4: One box completely inside another - pc7 = PointCloud2.from_numpy(np.array([[0, 0, 0], [3, 3, 3]])) - pc8 = PointCloud2.from_numpy(np.array([[1, 1, 1], [2, 2, 2]])) - assert pc7.bounding_box_intersects(pc8) - assert pc8.bounding_box_intersects(pc7) - - # Test 5: Boxes overlapping only in 2 dimensions (not all 3) - pc9 = PointCloud2.from_numpy(np.array([[0, 0, 0], [2, 2, 1]])) - pc10 = PointCloud2.from_numpy(np.array([[1, 1, 2], [3, 3, 3]])) - assert not pc9.bounding_box_intersects(pc10) - assert not pc10.bounding_box_intersects(pc9) - - # Test 6: Real-world detection scenario with floating point coordinates - detection1_points = np.array( - [[-3.5, -0.3, 0.1], [-3.3, -0.2, 0.1], [-3.5, -0.3, 0.3], [-3.3, -0.2, 0.3]] - ) - pc_det1 = PointCloud2.from_numpy(detection1_points) - - detection2_points = np.array( - [[-3.4, -0.25, 0.15], [-3.2, -0.15, 0.15], [-3.4, -0.25, 0.35], [-3.2, -0.15, 0.35]] - ) - pc_det2 = PointCloud2.from_numpy(detection2_points) - - assert pc_det1.bounding_box_intersects(pc_det2) - - # Test 7: Single point clouds - pc_single1 = PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) - pc_single2 = PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) - pc_single3 = PointCloud2.from_numpy(np.array([[2.0, 2.0, 2.0]])) - - # Same point should intersect - assert pc_single1.bounding_box_intersects(pc_single2) - # Different points should not intersect - assert not pc_single1.bounding_box_intersects(pc_single3) - - # Test 8: Empty point clouds - pc_empty1 = PointCloud2.from_numpy(np.array([]).reshape(0, 3)) - pc_empty2 = PointCloud2.from_numpy(np.array([]).reshape(0, 3)) - PointCloud2.from_numpy(np.array([[1.0, 1.0, 1.0]])) - - # Empty clouds should handle gracefully (Open3D returns inf bounds) - # This might raise an exception or return False - we should handle gracefully - try: - result = pc_empty1.bounding_box_intersects(pc_empty2) - # If no exception, verify behavior is consistent - assert isinstance(result, bool) - except: - # If it raises an exception, that's also acceptable for empty clouds - pass - - print("✓ All bounding box intersection tests passed!") - - -if __name__ == "__main__": - test_lcm_encode_decode() - test_bounding_box_intersects() diff --git a/dimos/msgs/sensor_msgs/test_image.py b/dimos/msgs/sensor_msgs/test_image.py deleted file mode 100644 index 24375139b3..0000000000 --- a/dimos/msgs/sensor_msgs/test_image.py +++ /dev/null @@ -1,148 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest -from reactivex import operators as ops - -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat, sharpness_barrier -from dimos.utils.data import get_data -from dimos.utils.testing import TimedSensorReplay - - -@pytest.fixture -def img(): - image_file_path = get_data("cafe.jpg") - return Image.from_file(str(image_file_path)) - - -def test_file_load(img: Image) -> None: - assert isinstance(img.data, np.ndarray) - assert img.width == 1024 - assert img.height == 771 - assert img.channels == 3 - assert img.shape == (771, 1024, 3) - assert img.data.dtype == np.uint8 - assert img.format == ImageFormat.BGR - assert img.frame_id == "" - assert isinstance(img.ts, float) - assert img.ts > 0 - assert img.data.flags["C_CONTIGUOUS"] - - -def test_lcm_encode_decode(img: Image) -> None: - binary_msg = img.lcm_encode() - decoded_img = Image.lcm_decode(binary_msg) - - assert isinstance(decoded_img, Image) - assert decoded_img is not img - assert decoded_img == img - - -def test_rgb_bgr_conversion(img: Image) -> None: - rgb = img.to_rgb() - assert not rgb == img - assert rgb.to_bgr() == img - - -def test_opencv_conversion(img: Image) -> None: - ocv = img.to_opencv() - decoded_img = Image.from_opencv(ocv) - - # artificially patch timestamp - decoded_img.ts = img.ts - assert decoded_img == img - - -@pytest.mark.tool -def test_sharpness_stream() -> None: - get_data("unitree_office_walk") # Preload data for testing - video_store = TimedSensorReplay( - "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() - ) - - cnt = 0 - for image in video_store.iterate(): - cnt = cnt + 1 - print(image.sharpness) - if cnt > 30: - return - - -def test_sharpness_barrier() -> None: - import time - from unittest.mock import MagicMock - - # Create mock images with known sharpness values - # This avoids loading real data from disk - mock_images = [] - sharpness_values = [0.3711, 0.3241, 0.3067, 0.2583, 0.3665] # Just 5 images for 1 window - - for i, sharp in enumerate(sharpness_values): - img = MagicMock() - img.sharpness = sharp - img.ts = 1758912038.208 + i * 0.01 # Simulate timestamps - mock_images.append(img) - - # Track what goes into windows and what comes out - start_wall_time = None - window_contents = [] # List of (wall_time, image) - emitted_images = [] - - def track_input(img): - """Track all images going into sharpness_barrier with wall-clock time""" - nonlocal start_wall_time - wall_time = time.time() - if start_wall_time is None: - start_wall_time = wall_time - relative_time = wall_time - start_wall_time - window_contents.append((relative_time, img)) - return img - - def track_output(img) -> None: - """Track what sharpness_barrier emits""" - emitted_images.append(img) - - # Use 20Hz frequency (0.05s windows) for faster test - # Emit images at 100Hz to get ~5 per window - from reactivex import from_iterable, interval - - source = from_iterable(mock_images).pipe( - ops.zip(interval(0.01)), # 100Hz emission rate - ops.map(lambda x: x[0]), # Extract just the image - ) - - source.pipe( - ops.do_action(track_input), # Track inputs - sharpness_barrier(20), # 20Hz = 0.05s windows - ops.do_action(track_output), # Track outputs - ).run() - - # Only need 0.08s for 1 full window at 20Hz plus buffer - time.sleep(0.08) - - # Verify we got correct emissions (items span across 2 windows due to timing) - # Items 1-4 arrive in first window (0-50ms), item 5 arrives in second window (50-100ms) - assert len(emitted_images) == 2, ( - f"Expected exactly 2 emissions (one per window), got {len(emitted_images)}" - ) - - # Group inputs by wall-clock windows and verify we got the sharpest - - # Verify each window emitted the sharpest image from that window - # First window (0-50ms): items 1-4 - assert emitted_images[0].sharpness == 0.3711 # Highest among first 4 items - - # Second window (50-100ms): only item 5 - assert emitted_images[1].sharpness == 0.3665 # Only item in second window diff --git a/dimos/msgs/std_msgs/Bool.py b/dimos/msgs/std_msgs/Bool.py deleted file mode 100644 index 4421447edf..0000000000 --- a/dimos/msgs/std_msgs/Bool.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Bool message type.""" - -from dimos_lcm.std_msgs import Bool as LCMBool - - -class Bool(LCMBool): # type: ignore[misc] - """Bool message.""" - - msg_name = "std_msgs.Bool" - - def __init__(self, data: bool = False) -> None: - """Initialize Bool with data value.""" - self.data = data diff --git a/dimos/msgs/std_msgs/Header.py b/dimos/msgs/std_msgs/Header.py deleted file mode 100644 index 5c54200497..0000000000 --- a/dimos/msgs/std_msgs/Header.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from datetime import datetime -import time - -from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime -from plum import dispatch - -# Import the actual LCM header type that's returned from decoding -try: - from lcm_msgs.std_msgs.Header import ( # type: ignore[import-not-found] - Header as DecodedLCMHeader, - ) -except ImportError: - DecodedLCMHeader = None - - -class Header(LCMHeader): # type: ignore[misc] - msg_name = "std_msgs.Header" - ts: float - - @dispatch - def __init__(self) -> None: - """Initialize a Header with current time and empty frame_id.""" - self.ts = time.time() - sec = int(self.ts) - nsec = int((self.ts - sec) * 1_000_000_000) - super().__init__(seq=0, stamp=LCMTime(sec=sec, nsec=nsec), frame_id="") - - @dispatch # type: ignore[no-redef] - def __init__(self, frame_id: str) -> None: - """Initialize a Header with current time and specified frame_id.""" - self.ts = time.time() - sec = int(self.ts) - nsec = int((self.ts - sec) * 1_000_000_000) - super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - - @dispatch # type: ignore[no-redef] - def __init__(self, timestamp: float, frame_id: str = "", seq: int = 1) -> None: - """Initialize a Header with Unix timestamp, frame_id, and optional seq.""" - sec = int(timestamp) - nsec = int((timestamp - sec) * 1_000_000_000) - super().__init__(seq=seq, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - - @dispatch # type: ignore[no-redef] - def __init__(self, timestamp: datetime, frame_id: str = "") -> None: - """Initialize a Header with datetime object and frame_id.""" - self.ts = timestamp.timestamp() - sec = int(self.ts) - nsec = int((self.ts - sec) * 1_000_000_000) - super().__init__(seq=1, stamp=LCMTime(sec=sec, nsec=nsec), frame_id=frame_id) - - @dispatch # type: ignore[no-redef] - def __init__(self, seq: int, stamp: LCMTime, frame_id: str) -> None: - """Initialize with explicit seq, stamp, and frame_id (LCM compatibility).""" - super().__init__(seq=seq, stamp=stamp, frame_id=frame_id) - - @dispatch # type: ignore[no-redef] - def __init__(self, header: LCMHeader) -> None: - """Initialize from another Header (copy constructor).""" - super().__init__(seq=header.seq, stamp=header.stamp, frame_id=header.frame_id) - - @dispatch # type: ignore[no-redef] - def __init__(self, header: object) -> None: - """Initialize from a decoded LCM header object.""" - # Handle the case where we get an lcm_msgs.std_msgs.Header.Header object - if hasattr(header, "seq") and hasattr(header, "stamp") and hasattr(header, "frame_id"): - super().__init__(seq=header.seq, stamp=header.stamp, frame_id=header.frame_id) - else: - raise ValueError(f"Cannot create Header from {type(header)}") - - @classmethod - def now(cls, frame_id: str = "", seq: int = 1) -> Header: - """Create a Header with current timestamp.""" - ts = time.time() - return cls(ts, frame_id, seq) - - @property - def timestamp(self) -> float: - """Get timestamp as Unix time (float).""" - return self.stamp.sec + (self.stamp.nsec / 1_000_000_000) # type: ignore[no-any-return] - - @property - def datetime(self) -> datetime: - """Get timestamp as datetime object.""" - return datetime.fromtimestamp(self.timestamp) - - def __str__(self) -> str: - return f"Header(seq={self.seq}, time={self.timestamp:.6f}, frame_id='{self.frame_id}')" - - def __repr__(self) -> str: - return f"Header(seq={self.seq}, stamp=Time(sec={self.stamp.sec}, nsec={self.stamp.nsec}), frame_id='{self.frame_id}')" diff --git a/dimos/msgs/std_msgs/Int32.py b/dimos/msgs/std_msgs/Int32.py deleted file mode 100644 index ba4906f485..0000000000 --- a/dimos/msgs/std_msgs/Int32.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Int32 message type.""" - -from typing import ClassVar - -from dimos_lcm.std_msgs import Int32 as LCMInt32 - - -class Int32(LCMInt32): # type: ignore[misc] - """ROS-compatible Int32 message.""" - - msg_name: ClassVar[str] = "std_msgs.Int32" - - def __init__(self, data: int = 0) -> None: - """Initialize Int32 with data value.""" - self.data = data diff --git a/dimos/msgs/std_msgs/Int8.py b/dimos/msgs/std_msgs/Int8.py deleted file mode 100644 index ca87140353..0000000000 --- a/dimos/msgs/std_msgs/Int8.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Int32 message type.""" - -from typing import ClassVar - -from dimos_lcm.std_msgs import Int8 as LCMInt8 - - -class Int8(LCMInt8): # type: ignore[misc] - """Int8 message.""" - - msg_name: ClassVar[str] = "std_msgs.Int8" - - def __init__(self, data: int = 0) -> None: - """Initialize Int8 with data value.""" - self.data = data diff --git a/dimos/msgs/std_msgs/UInt32.py b/dimos/msgs/std_msgs/UInt32.py deleted file mode 100644 index e617c782fe..0000000000 --- a/dimos/msgs/std_msgs/UInt32.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""UInt32 message type.""" - -from typing import ClassVar - -from dimos_lcm.std_msgs import UInt32 as LCMUInt32 - - -class UInt32(LCMUInt32): # type: ignore[misc] - """ROS-compatible UInt32 message.""" - - msg_name: ClassVar[str] = "std_msgs.UInt32" - - def __init__(self, data: int = 0) -> None: - """Initialize UInt32 with data value.""" - self.data = data diff --git a/dimos/msgs/std_msgs/__init__.py b/dimos/msgs/std_msgs/__init__.py deleted file mode 100644 index ae8e3dd8f6..0000000000 --- a/dimos/msgs/std_msgs/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .Bool import Bool -from .Header import Header -from .Int8 import Int8 -from .Int32 import Int32 -from .UInt32 import UInt32 - -__all__ = ["Bool", "Header", "Int8", "Int32", "UInt32"] diff --git a/dimos/msgs/std_msgs/test_header.py b/dimos/msgs/std_msgs/test_header.py deleted file mode 100644 index 93f20da283..0000000000 --- a/dimos/msgs/std_msgs/test_header.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime -import time - -from dimos.msgs.std_msgs import Header - - -def test_header_initialization_methods() -> None: - """Test various ways to initialize a Header.""" - - # Method 1: With timestamp and frame_id - header1 = Header(123.456, "world") - assert header1.seq == 1 - assert header1.stamp.sec == 123 - assert header1.stamp.nsec == 456000000 - assert header1.frame_id == "world" - - # Method 2: With just frame_id (uses current time) - header2 = Header("base_link") - assert header2.seq == 1 - assert header2.frame_id == "base_link" - # Timestamp should be close to current time - assert abs(header2.timestamp - time.time()) < 0.1 - - # Method 3: Empty header (current time, empty frame_id) - header3 = Header() - assert header3.seq == 0 - assert header3.frame_id == "" - - # Method 4: With datetime object - dt = datetime(2025, 1, 18, 12, 30, 45, 500000) # 500ms - header4 = Header(dt, "sensor") - assert header4.seq == 1 - assert header4.frame_id == "sensor" - expected_timestamp = dt.timestamp() - assert abs(header4.timestamp - expected_timestamp) < 1e-6 - - # Method 5: With custom seq number - header5 = Header(999.123, "custom", seq=42) - assert header5.seq == 42 - assert header5.stamp.sec == 999 - assert header5.stamp.nsec == 123000000 - assert header5.frame_id == "custom" - - # Method 6: Using now() class method - header6 = Header.now("camera") - assert header6.seq == 1 - assert header6.frame_id == "camera" - assert abs(header6.timestamp - time.time()) < 0.1 - - # Method 7: now() with custom seq - header7 = Header.now("lidar", seq=99) - assert header7.seq == 99 - assert header7.frame_id == "lidar" - - -def test_header_properties() -> None: - """Test Header property accessors.""" - header = Header(1234567890.123456789, "test") - - # Test timestamp property - assert abs(header.timestamp - 1234567890.123456789) < 1e-6 - - # Test datetime property - dt = header.datetime - assert isinstance(dt, datetime) - assert abs(dt.timestamp() - 1234567890.123456789) < 1e-6 - - -def test_header_string_representation() -> None: - """Test Header string representations.""" - header = Header(100.5, "map", seq=10) - - # Test __str__ - str_repr = str(header) - assert "seq=10" in str_repr - assert "time=100.5" in str_repr - assert "frame_id='map'" in str_repr - - # Test __repr__ - repr_str = repr(header) - assert "Header(" in repr_str - assert "seq=10" in repr_str - assert "Time(sec=100, nsec=500000000)" in repr_str - assert "frame_id='map'" in repr_str diff --git a/dimos/msgs/tf2_msgs/TFMessage.py b/dimos/msgs/tf2_msgs/TFMessage.py deleted file mode 100644 index 7a47a96e6d..0000000000 --- a/dimos/msgs/tf2_msgs/TFMessage.py +++ /dev/null @@ -1,146 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License.# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TYPE_CHECKING, BinaryIO - -from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage - -from dimos.msgs.geometry_msgs.Quaternion import Quaternion -from dimos.msgs.geometry_msgs.Transform import Transform -from dimos.msgs.geometry_msgs.Vector3 import Vector3 - -if TYPE_CHECKING: - from collections.abc import Iterator - - from dimos.visualization.rerun.bridge import RerunMulti - - -class TFMessage: - """TFMessage that accepts Transform objects and encodes to LCM format.""" - - transforms: list[Transform] - msg_name = "tf2_msgs.TFMessage" - - def __init__(self, *transforms: Transform) -> None: - self.transforms = list(transforms) - - def add_transform(self, transform: Transform, child_frame_id: str = "base_link") -> None: - """Add a transform to the message.""" - self.transforms.append(transform) - self.transforms_length = len(self.transforms) - - def lcm_encode(self) -> bytes: - """Encode as LCM TFMessage. - - Args: - child_frame_ids: Optional list of child frame IDs for each transform. - If not provided, defaults to "base_link" for all. - """ - - res = list(map(lambda t: t.lcm_transform(), self.transforms)) - - lcm_msg = LCMTFMessage( - transforms_length=len(self.transforms), - transforms=res, - ) - - return lcm_msg.lcm_encode() # type: ignore[no-any-return] - - @classmethod - def lcm_decode(cls, data: bytes | BinaryIO) -> TFMessage: - """Decode from LCM TFMessage bytes.""" - lcm_msg = LCMTFMessage.lcm_decode(data) - - # Convert LCM TransformStamped objects to Transform objects - transforms = [] - for lcm_transform_stamped in lcm_msg.transforms: - # Extract timestamp - ts = lcm_transform_stamped.header.stamp.sec + ( - lcm_transform_stamped.header.stamp.nsec / 1_000_000_000 - ) - - # Create Transform with our custom types - lcm_trans = lcm_transform_stamped.transform.translation - lcm_rot = lcm_transform_stamped.transform.rotation - - transform = Transform( - translation=Vector3(lcm_trans.x, lcm_trans.y, lcm_trans.z), - rotation=Quaternion(lcm_rot.x, lcm_rot.y, lcm_rot.z, lcm_rot.w), - frame_id=lcm_transform_stamped.header.frame_id, - child_frame_id=lcm_transform_stamped.child_frame_id, - ts=ts, - ) - transforms.append(transform) - - return cls(*transforms) - - def __len__(self) -> int: - """Return number of transforms.""" - return len(self.transforms) - - def __getitem__(self, index: int) -> Transform: - """Get transform by index.""" - return self.transforms[index] - - def __iter__(self) -> Iterator: # type: ignore[type-arg] - """Iterate over transforms.""" - return iter(self.transforms) - - def __repr__(self) -> str: - return f"TFMessage({len(self.transforms)} transforms)" - - def __str__(self) -> str: - lines = [f"TFMessage with {len(self.transforms)} transforms:"] - for i, transform in enumerate(self.transforms): - lines.append( - f" [{i}] {transform.frame_id} → {transform.child_frame_id} @ {transform.ts:.3f}" - ) - return "\n".join(lines) - - def to_rerun(self) -> RerunMulti: - """Convert to a list of rerun Transform3D archetypes. - - Returns a list of tuples (entity_path, Transform3D) for each transform - in the message. The entity_path is derived from the child_frame_id and - logged under `world/tf/...` so it is visible under the default `world` - origin while keeping TF visualization isolated from semantic entities - like `world/robot/...`. - - Returns: - List of (entity_path, rr.Transform3D) tuples - - Example: - for path, transform in tf_msg.to_rerun(): - rr.log(path, transform) - """ - results: RerunMulti = [] - for transform in self.transforms: - entity_path = f"world/tf/{transform.child_frame_id}" - results.append((entity_path, transform.to_rerun())) - return results diff --git a/dimos/msgs/tf2_msgs/__init__.py b/dimos/msgs/tf2_msgs/__init__.py deleted file mode 100644 index 69d4e0137e..0000000000 --- a/dimos/msgs/tf2_msgs/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.msgs.tf2_msgs.TFMessage import TFMessage - -__all__ = ["TFMessage"] diff --git a/dimos/msgs/tf2_msgs/test_TFMessage.py b/dimos/msgs/tf2_msgs/test_TFMessage.py deleted file mode 100644 index 8567de9988..0000000000 --- a/dimos/msgs/tf2_msgs/test_TFMessage.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage - -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.tf2_msgs import TFMessage - - -def test_tfmessage_initialization() -> None: - """Test TFMessage initialization with Transform objects.""" - # Create some transforms - tf1 = Transform( - translation=Vector3(1, 2, 3), rotation=Quaternion(0, 0, 0, 1), frame_id="world", ts=100.0 - ) - tf2 = Transform( - translation=Vector3(4, 5, 6), - rotation=Quaternion(0, 0, 0.707, 0.707), - frame_id="map", - ts=101.0, - ) - - # Create TFMessage with transforms - msg = TFMessage(tf1, tf2) - - assert len(msg) == 2 - assert msg[0] == tf1 - assert msg[1] == tf2 - - # Test iteration - transforms = list(msg) - assert transforms == [tf1, tf2] - - -def test_tfmessage_empty() -> None: - """Test empty TFMessage.""" - msg = TFMessage() - assert len(msg) == 0 - assert list(msg) == [] - - -def test_tfmessage_add_transform() -> None: - """Test adding transforms to TFMessage.""" - msg = TFMessage() - - tf = Transform(translation=Vector3(1, 2, 3), frame_id="base", ts=200.0) - - msg.add_transform(tf) - assert len(msg) == 1 - assert msg[0] == tf - - -def test_tfmessage_tree() -> None: - """Test adding transforms to TFMessage.""" - msg = TFMessage() - - msg.add_transform( - Transform( - translation=Vector3(1, 2, 0.5), frame_id="world", child_frame_id="robot", ts=200.0 - ) - ) - msg.add_transform( - Transform( - translation=Vector3(0.1, 0, 1.0), frame_id="robot", child_frame_id="camera", ts=200.0 - ) - ) - msg.add_transform( - Transform( - translation=Vector3(0.2, 0, 0.2), frame_id="robot", child_frame_id="lidar", ts=200.0 - ) - ) - msg.add_transform( - Transform( - translation=Vector3(0.05, 0, 0.0), - frame_id="lidar", - child_frame_id="lidar_scanner", - ts=200.0, - ) - ) - - assert len(msg) == 4 - print(msg) - - -def test_tfmessage_lcm_encode_decode() -> None: - """Test encoding TFMessage to LCM bytes.""" - # Create transforms - tf1 = Transform( - translation=Vector3(1.0, 2.0, 3.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - child_frame_id="robot", - frame_id="world", - ts=123.456, - ) - tf2 = Transform( - translation=Vector3(4.0, 5.0, 6.0), - rotation=Quaternion(0.0, 0.0, 0.707, 0.707), - frame_id="robot", - child_frame_id="target", - ts=124.567, - ) - - # Create TFMessage - msg = TFMessage(tf1, tf2) - - # Encode with custom child_frame_ids - encoded = msg.lcm_encode() - - # Decode using LCM to verify - lcm_msg = LCMTFMessage.lcm_decode(encoded) - - assert lcm_msg.transforms_length == 2 - - # Check first transform - ts1 = lcm_msg.transforms[0] - assert ts1.header.frame_id == "world" - assert ts1.child_frame_id == "robot" - assert ts1.header.stamp.sec == 123 - assert ts1.header.stamp.nsec == 456000000 - assert ts1.transform.translation.x == 1.0 - assert ts1.transform.translation.y == 2.0 - assert ts1.transform.translation.z == 3.0 - - # Check second transform - ts2 = lcm_msg.transforms[1] - assert ts2.header.frame_id == "robot" - assert ts2.child_frame_id == "target" - assert ts2.transform.rotation.z == 0.707 - assert ts2.transform.rotation.w == 0.707 diff --git a/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py b/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py deleted file mode 100644 index 396d796193..0000000000 --- a/dimos/msgs/tf2_msgs/test_TFMessage_lcmpub.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 -from dimos.msgs.tf2_msgs import TFMessage -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, Topic - - -# Publishes a series of transforms representing a robot kinematic chain -# to actual LCM messages, foxglove running in parallel should render this -@pytest.mark.skip -def test_publish_transforms() -> None: - from dimos_lcm.tf2_msgs import TFMessage as LCMTFMessage - - lcm = LCM(autoconf=True) - lcm.start() - - topic = Topic(topic="/tf", lcm_type=LCMTFMessage) - - # Create a robot kinematic chain using our new types - current_time = time.time() - - # 1. World to base_link transform (robot at position) - world_to_base = Transform( - translation=Vector3(4.0, 3.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.382683, 0.923880), # 45 degrees around Z - frame_id="world", - child_frame_id="base_link", - ts=current_time, - ) - - # 2. Base to arm transform (arm lifted up) - base_to_arm = Transform( - translation=Vector3(0.2, 0.0, 1.5), - rotation=Quaternion(0.0, 0.258819, 0.0, 0.965926), # 30 degrees around Y - frame_id="base_link", - child_frame_id="arm_link", - ts=current_time, - ) - - lcm.publish(topic, TFMessage(world_to_base, base_to_arm)) - - time.sleep(0.05) - # 3. Arm to gripper transform (gripper extended) - arm_to_gripper = Transform( - translation=Vector3(0.5, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation - frame_id="arm_link", - child_frame_id="gripper_link", - ts=current_time, - ) - - lcm.publish(topic, TFMessage(world_to_base, arm_to_gripper)) diff --git a/dimos/msgs/trajectory_msgs/JointTrajectory.py b/dimos/msgs/trajectory_msgs/JointTrajectory.py deleted file mode 100644 index ae2ad55fd1..0000000000 --- a/dimos/msgs/trajectory_msgs/JointTrajectory.py +++ /dev/null @@ -1,211 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -JointTrajectory message type. - -A sequence of joint trajectory points representing a full trajectory. -Similar to ROS trajectory_msgs/JointTrajectory. -""" - -from io import BytesIO -import struct -import time - -from dimos.msgs.trajectory_msgs.TrajectoryPoint import TrajectoryPoint - - -class JointTrajectory: - """ - A joint-space trajectory consisting of timestamped waypoints. - - Attributes: - timestamp: When trajectory was created (seconds since epoch) - joint_names: Names of joints (optional) - points: Sequence of TrajectoryPoints - duration: Total trajectory duration (seconds) - """ - - msg_name = "trajectory_msgs.JointTrajectory" - - __slots__ = ["duration", "joint_names", "num_joints", "num_points", "points", "timestamp"] - - def __init__( - self, - points: list[TrajectoryPoint] | None = None, - joint_names: list[str] | None = None, - timestamp: float | None = None, - ) -> None: - """ - Initialize JointTrajectory. - - Args: - points: List of TrajectoryPoints - joint_names: Names of joints (optional) - timestamp: Creation timestamp (defaults to now) - """ - self.timestamp = timestamp if timestamp is not None else time.time() - self.points = list(points) if points else [] - self.num_points = len(self.points) - self.joint_names = list(joint_names) if joint_names else [] - self.num_joints = ( - len(self.joint_names) - if self.joint_names - else (self.points[0].num_joints if self.points else 0) - ) - - # Compute duration from last point - if self.points: - self.duration = max(p.time_from_start for p in self.points) - else: - self.duration = 0.0 - - def sample(self, t: float) -> tuple[list[float], list[float]]: - """ - Sample the trajectory at time t using linear interpolation. - - Args: - t: Time from trajectory start (seconds) - - Returns: - Tuple of (positions, velocities) at time t - """ - if not self.points: - return [], [] - - # Clamp t to valid range - t = max(0.0, min(t, self.duration)) - - # Find bracketing points - if t <= self.points[0].time_from_start: - return list(self.points[0].positions), list(self.points[0].velocities) - - if t >= self.points[-1].time_from_start: - return list(self.points[-1].positions), list(self.points[-1].velocities) - - # Find interval - for i in range(len(self.points) - 1): - t0 = self.points[i].time_from_start - t1 = self.points[i + 1].time_from_start - - if t0 <= t <= t1: - # Linear interpolation - alpha = (t - t0) / (t1 - t0) if t1 > t0 else 0.0 - p0 = self.points[i] - p1 = self.points[i + 1] - - positions = [ - p0.positions[j] + alpha * (p1.positions[j] - p0.positions[j]) - for j in range(len(p0.positions)) - ] - velocities = [ - p0.velocities[j] + alpha * (p1.velocities[j] - p0.velocities[j]) - for j in range(len(p0.velocities)) - ] - return positions, velocities - - # Fallback - return list(self.points[-1].positions), list(self.points[-1].velocities) - - def lcm_encode(self) -> bytes: - """Encode for LCM transport.""" - return self.encode() - - def encode(self) -> bytes: - buf = BytesIO() - buf.write(JointTrajectory._get_packed_fingerprint()) - self._encode_one(buf) - return buf.getvalue() - - def _encode_one(self, buf: BytesIO) -> None: - # timestamp (double) - buf.write(struct.pack(">d", self.timestamp)) - # duration (double) - buf.write(struct.pack(">d", self.duration)) - # num_joint_names (int32) - actual count of joint names - buf.write(struct.pack(">i", len(self.joint_names))) - # joint_names (string[num_joint_names]) - for name in self.joint_names: - name_bytes = name.encode("utf-8") - buf.write(struct.pack(">i", len(name_bytes))) - buf.write(name_bytes) - # num_points (int32) - buf.write(struct.pack(">i", self.num_points)) - # points (TrajectoryPoint[num_points]) - for point in self.points: - point._encode_one(buf) - - @classmethod - def lcm_decode(cls, data: bytes) -> "JointTrajectory": - """Decode from LCM transport.""" - return cls.decode(data) - - @classmethod - def decode(cls, data: bytes) -> "JointTrajectory": - buf = BytesIO(data) if not hasattr(data, "read") else data - if buf.read(8) != cls._get_packed_fingerprint(): - raise ValueError("Decode error: fingerprint mismatch") - return cls._decode_one(buf) # type: ignore[arg-type] - - @classmethod - def _decode_one(cls, buf: BytesIO) -> "JointTrajectory": - self = cls.__new__(cls) - self.timestamp = struct.unpack(">d", buf.read(8))[0] - self.duration = struct.unpack(">d", buf.read(8))[0] - - # Read joint names - num_joint_names = struct.unpack(">i", buf.read(4))[0] - self.joint_names = [] - for _ in range(num_joint_names): - name_len = struct.unpack(">i", buf.read(4))[0] - self.joint_names.append(buf.read(name_len).decode("utf-8")) - - # Read points - self.num_points = struct.unpack(">i", buf.read(4))[0] - self.points = [TrajectoryPoint._decode_one(buf) for _ in range(self.num_points)] - - # Set num_joints from joint_names or points - self.num_joints = ( - len(self.joint_names) - if self.joint_names - else (self.points[0].num_joints if self.points else 0) - ) - - return self - - _packed_fingerprint = None - - @classmethod - def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] - if cls in parents: - return 0 - return 0x2B3C4D5E6F708192 & 0xFFFFFFFFFFFFFFFF - - @classmethod - def _get_packed_fingerprint(cls) -> bytes: - if cls._packed_fingerprint is None: - cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] - return cls._packed_fingerprint - - def __str__(self) -> str: - return f"JointTrajectory({self.num_points} points, duration={self.duration:.3f}s)" - - def __repr__(self) -> str: - return ( - f"JointTrajectory(points={self.points}, joint_names={self.joint_names}, " - f"timestamp={self.timestamp})" - ) - - def __len__(self) -> int: - return self.num_points diff --git a/dimos/msgs/trajectory_msgs/TrajectoryPoint.py b/dimos/msgs/trajectory_msgs/TrajectoryPoint.py deleted file mode 100644 index b2b9ab8406..0000000000 --- a/dimos/msgs/trajectory_msgs/TrajectoryPoint.py +++ /dev/null @@ -1,136 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -TrajectoryPoint message type. - -A single point in a joint trajectory with positions, velocities, and time. -Similar to ROS trajectory_msgs/JointTrajectoryPoint. -""" - -from io import BytesIO -import struct - - -class TrajectoryPoint: - """ - A single point in a joint trajectory. - - Attributes: - time_from_start: Time from trajectory start (seconds) - positions: Joint positions (radians) - velocities: Joint velocities (rad/s) - """ - - msg_name = "trajectory_msgs.TrajectoryPoint" - - __slots__ = ["num_joints", "positions", "time_from_start", "velocities"] - - def __init__( - self, - time_from_start: float = 0.0, - positions: list[float] | None = None, - velocities: list[float] | None = None, - ) -> None: - """ - Initialize TrajectoryPoint. - - Args: - time_from_start: Time from trajectory start (seconds) - positions: Joint positions (radians) - velocities: Joint velocities (rad/s), defaults to zeros if None - """ - self.time_from_start = time_from_start - self.positions = list(positions) if positions else [] - self.num_joints = len(self.positions) - - if velocities is not None: - self.velocities = list(velocities) - else: - self.velocities = [0.0] * self.num_joints - - def lcm_encode(self) -> bytes: - """Encode for LCM transport.""" - return self.encode() - - def encode(self) -> bytes: - buf = BytesIO() - buf.write(TrajectoryPoint._get_packed_fingerprint()) - self._encode_one(buf) - return buf.getvalue() - - def _encode_one(self, buf: BytesIO) -> None: - # time_from_start (double) - buf.write(struct.pack(">d", self.time_from_start)) - # num_joints (int32) - buf.write(struct.pack(">i", self.num_joints)) - # positions (double[num_joints]) - for p in self.positions: - buf.write(struct.pack(">d", p)) - # velocities (double[num_joints]) - for v in self.velocities: - buf.write(struct.pack(">d", v)) - - @classmethod - def lcm_decode(cls, data: bytes) -> "TrajectoryPoint": - """Decode from LCM transport.""" - return cls.decode(data) - - @classmethod - def decode(cls, data: bytes) -> "TrajectoryPoint": - buf = BytesIO(data) if not hasattr(data, "read") else data - if buf.read(8) != cls._get_packed_fingerprint(): - raise ValueError("Decode error: fingerprint mismatch") - return cls._decode_one(buf) # type: ignore[arg-type] - - @classmethod - def _decode_one(cls, buf: BytesIO) -> "TrajectoryPoint": - self = cls.__new__(cls) - self.time_from_start = struct.unpack(">d", buf.read(8))[0] - self.num_joints = struct.unpack(">i", buf.read(4))[0] - self.positions = [struct.unpack(">d", buf.read(8))[0] for _ in range(self.num_joints)] - self.velocities = [struct.unpack(">d", buf.read(8))[0] for _ in range(self.num_joints)] - return self - - _packed_fingerprint = None - - @classmethod - def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] - if cls in parents: - return 0 - return 0x1A2B3C4D5E6F7081 & 0xFFFFFFFFFFFFFFFF - - @classmethod - def _get_packed_fingerprint(cls) -> bytes: - if cls._packed_fingerprint is None: - cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] - return cls._packed_fingerprint - - def __str__(self) -> str: - return f"TrajectoryPoint(t={self.time_from_start:.3f}s, {self.num_joints} joints)" - - def __repr__(self) -> str: - return ( - f"TrajectoryPoint(time_from_start={self.time_from_start}, " - f"positions={self.positions}, velocities={self.velocities})" - ) - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - if not isinstance(other, TrajectoryPoint): - return False - return ( - self.time_from_start == other.time_from_start - and self.positions == other.positions - and self.velocities == other.velocities - ) diff --git a/dimos/msgs/trajectory_msgs/TrajectoryStatus.py b/dimos/msgs/trajectory_msgs/TrajectoryStatus.py deleted file mode 100644 index 0a3c117e68..0000000000 --- a/dimos/msgs/trajectory_msgs/TrajectoryStatus.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -TrajectoryStatus message type. - -Status feedback for trajectory execution. -""" - -from enum import IntEnum -from io import BytesIO -import struct -import time - - -class TrajectoryState(IntEnum): - """States for trajectory execution.""" - - IDLE = 0 # No trajectory, ready to accept - EXECUTING = 1 # Currently executing trajectory - COMPLETED = 2 # Trajectory finished successfully - ABORTED = 3 # Trajectory was cancelled - FAULT = 4 # Error occurred, requires reset() - - -class TrajectoryStatus: - """ - Status of trajectory execution. - - Attributes: - timestamp: When status was generated - state: Current TrajectoryState - progress: Progress 0.0 to 1.0 - time_elapsed: Seconds since trajectory start - time_remaining: Estimated seconds remaining - error: Error message if FAULT state (empty string otherwise) - """ - - msg_name = "trajectory_msgs.TrajectoryStatus" - - __slots__ = ["error", "progress", "state", "time_elapsed", "time_remaining", "timestamp"] - - def __init__( - self, - state: TrajectoryState = TrajectoryState.IDLE, - progress: float = 0.0, - time_elapsed: float = 0.0, - time_remaining: float = 0.0, - error: str = "", - timestamp: float | None = None, - ) -> None: - """ - Initialize TrajectoryStatus. - - Args: - state: Current execution state - progress: Progress through trajectory (0.0 to 1.0) - time_elapsed: Time since trajectory start (seconds) - time_remaining: Estimated time remaining (seconds) - error: Error message if in FAULT state - timestamp: When status was generated (defaults to now) - """ - self.timestamp = timestamp if timestamp is not None else time.time() - self.state = state - self.progress = progress - self.time_elapsed = time_elapsed - self.time_remaining = time_remaining - self.error = error - - @property - def state_name(self) -> str: - """Get human-readable state name.""" - return self.state.name - - def is_done(self) -> bool: - """Check if trajectory execution is finished (completed, aborted, or fault).""" - return self.state in ( - TrajectoryState.COMPLETED, - TrajectoryState.ABORTED, - TrajectoryState.FAULT, - ) - - def is_active(self) -> bool: - """Check if trajectory is currently executing.""" - return self.state == TrajectoryState.EXECUTING - - def lcm_encode(self) -> bytes: - """Encode for LCM transport.""" - return self.encode() - - def encode(self) -> bytes: - buf = BytesIO() - buf.write(TrajectoryStatus._get_packed_fingerprint()) - self._encode_one(buf) - return buf.getvalue() - - def _encode_one(self, buf: BytesIO) -> None: - # timestamp (double) - buf.write(struct.pack(">d", self.timestamp)) - # state (int32) - buf.write(struct.pack(">i", int(self.state))) - # progress (double) - buf.write(struct.pack(">d", self.progress)) - # time_elapsed (double) - buf.write(struct.pack(">d", self.time_elapsed)) - # time_remaining (double) - buf.write(struct.pack(">d", self.time_remaining)) - # error (string) - error_bytes = self.error.encode("utf-8") - buf.write(struct.pack(">i", len(error_bytes))) - buf.write(error_bytes) - - @classmethod - def lcm_decode(cls, data: bytes) -> "TrajectoryStatus": - """Decode from LCM transport.""" - return cls.decode(data) - - @classmethod - def decode(cls, data: bytes) -> "TrajectoryStatus": - buf = BytesIO(data) if not hasattr(data, "read") else data - if buf.read(8) != cls._get_packed_fingerprint(): - raise ValueError("Decode error: fingerprint mismatch") - return cls._decode_one(buf) # type: ignore[arg-type] - - @classmethod - def _decode_one(cls, buf: BytesIO) -> "TrajectoryStatus": - self = cls.__new__(cls) - self.timestamp = struct.unpack(">d", buf.read(8))[0] - self.state = TrajectoryState(struct.unpack(">i", buf.read(4))[0]) - self.progress = struct.unpack(">d", buf.read(8))[0] - self.time_elapsed = struct.unpack(">d", buf.read(8))[0] - self.time_remaining = struct.unpack(">d", buf.read(8))[0] - error_len = struct.unpack(">i", buf.read(4))[0] - self.error = buf.read(error_len).decode("utf-8") - return self - - _packed_fingerprint = None - - @classmethod - def _get_hash_recursive(cls, parents): # type: ignore[no-untyped-def] - if cls in parents: - return 0 - return 0x3C4D5E6F708192A3 & 0xFFFFFFFFFFFFFFFF - - @classmethod - def _get_packed_fingerprint(cls) -> bytes: - if cls._packed_fingerprint is None: - cls._packed_fingerprint = struct.pack(">Q", cls._get_hash_recursive([])) # type: ignore[no-untyped-call] - return cls._packed_fingerprint - - def __str__(self) -> str: - return f"TrajectoryStatus({self.state_name}, progress={self.progress:.1%})" - - def __repr__(self) -> str: - return ( - f"TrajectoryStatus(state={self.state_name}, progress={self.progress}, " - f"time_elapsed={self.time_elapsed}, time_remaining={self.time_remaining}, " - f"error='{self.error}')" - ) diff --git a/dimos/msgs/trajectory_msgs/__init__.py b/dimos/msgs/trajectory_msgs/__init__.py deleted file mode 100644 index 44039e594e..0000000000 --- a/dimos/msgs/trajectory_msgs/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Trajectory message types. - -Similar to ROS trajectory_msgs package. -""" - -from dimos.msgs.trajectory_msgs.JointTrajectory import JointTrajectory -from dimos.msgs.trajectory_msgs.TrajectoryPoint import TrajectoryPoint -from dimos.msgs.trajectory_msgs.TrajectoryStatus import TrajectoryState, TrajectoryStatus - -__all__ = [ - "JointTrajectory", - "TrajectoryPoint", - "TrajectoryState", - "TrajectoryStatus", -] diff --git a/dimos/msgs/vision_msgs/BoundingBox2DArray.py b/dimos/msgs/vision_msgs/BoundingBox2DArray.py deleted file mode 100644 index f376de6372..0000000000 --- a/dimos/msgs/vision_msgs/BoundingBox2DArray.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.vision_msgs.BoundingBox2DArray import ( - BoundingBox2DArray as LCMBoundingBox2DArray, -) - - -class BoundingBox2DArray(LCMBoundingBox2DArray): # type: ignore[misc] - msg_name = "vision_msgs.BoundingBox2DArray" diff --git a/dimos/msgs/vision_msgs/BoundingBox3DArray.py b/dimos/msgs/vision_msgs/BoundingBox3DArray.py deleted file mode 100644 index d8d7775f91..0000000000 --- a/dimos/msgs/vision_msgs/BoundingBox3DArray.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.vision_msgs.BoundingBox3DArray import ( - BoundingBox3DArray as LCMBoundingBox3DArray, -) - - -class BoundingBox3DArray(LCMBoundingBox3DArray): # type: ignore[misc] - msg_name = "vision_msgs.BoundingBox3DArray" diff --git a/dimos/msgs/vision_msgs/Detection2D.py b/dimos/msgs/vision_msgs/Detection2D.py deleted file mode 100644 index aa957f8061..0000000000 --- a/dimos/msgs/vision_msgs/Detection2D.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dimos_lcm.vision_msgs.Detection2D import Detection2D as LCMDetection2D - -from dimos.types.timestamped import to_timestamp - - -class Detection2D(LCMDetection2D): # type: ignore[misc] - msg_name = "vision_msgs.Detection2D" - - # for _get_field_type() to work when decoding in _decode_one() - __annotations__ = LCMDetection2D.__annotations__ - - @property - def ts(self) -> float: - return to_timestamp(self.header.stamp) diff --git a/dimos/msgs/vision_msgs/Detection2DArray.py b/dimos/msgs/vision_msgs/Detection2DArray.py deleted file mode 100644 index f33cc4cc2a..0000000000 --- a/dimos/msgs/vision_msgs/Detection2DArray.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dimos_lcm.vision_msgs.Detection2DArray import ( - Detection2DArray as LCMDetection2DArray, -) - -from dimos.types.timestamped import to_timestamp - - -class Detection2DArray(LCMDetection2DArray): # type: ignore[misc] - msg_name = "vision_msgs.Detection2DArray" - - # for _get_field_type() to work when decoding in _decode_one() - __annotations__ = LCMDetection2DArray.__annotations__ - - @property - def ts(self) -> float: - return to_timestamp(self.header.stamp) diff --git a/dimos/msgs/vision_msgs/Detection3D.py b/dimos/msgs/vision_msgs/Detection3D.py deleted file mode 100644 index e074ecb0b1..0000000000 --- a/dimos/msgs/vision_msgs/Detection3D.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dimos_lcm.vision_msgs.Detection3D import Detection3D as LCMDetection3D - -from dimos.types.timestamped import to_timestamp - - -class Detection3D(LCMDetection3D): # type: ignore[misc] - msg_name = "vision_msgs.Detection3D" - - # for _get_field_type() to work when decoding in _decode_one() - __annotations__ = LCMDetection3D.__annotations__ - - @property - def ts(self) -> float: - return to_timestamp(self.header.stamp) diff --git a/dimos/msgs/vision_msgs/Detection3DArray.py b/dimos/msgs/vision_msgs/Detection3DArray.py deleted file mode 100644 index 2eba82204d..0000000000 --- a/dimos/msgs/vision_msgs/Detection3DArray.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from dimos_lcm.vision_msgs.Detection3DArray import Detection3DArray as LCMDetection3DArray - -from dimos.types.timestamped import to_timestamp - - -class Detection3DArray(LCMDetection3DArray): # type: ignore[misc] - msg_name = "vision_msgs.Detection3DArray" - - # for _get_field_type() to work when decoding in _decode_one() - __annotations__ = LCMDetection3DArray.__annotations__ - - @property - def ts(self) -> float: - return to_timestamp(self.header.stamp) diff --git a/dimos/msgs/vision_msgs/__init__.py b/dimos/msgs/vision_msgs/__init__.py deleted file mode 100644 index 0f1c9c8dc1..0000000000 --- a/dimos/msgs/vision_msgs/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -from .BoundingBox2DArray import BoundingBox2DArray -from .BoundingBox3DArray import BoundingBox3DArray -from .Detection2D import Detection2D -from .Detection2DArray import Detection2DArray -from .Detection3D import Detection3D -from .Detection3DArray import Detection3DArray - -__all__ = [ - "BoundingBox2DArray", - "BoundingBox3DArray", - "Detection2D", - "Detection2DArray", - "Detection3D", - "Detection3DArray", -] diff --git a/dimos/navigation/base.py b/dimos/navigation/base.py deleted file mode 100644 index 347c4ad124..0000000000 --- a/dimos/navigation/base.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from enum import Enum - -from dimos.msgs.geometry_msgs import PoseStamped - - -class NavigationState(Enum): - IDLE = "idle" - FOLLOWING_PATH = "following_path" - RECOVERY = "recovery" - - -class NavigationInterface(ABC): - @abstractmethod - def set_goal(self, goal: PoseStamped) -> bool: - """ - Set a new navigation goal (non-blocking). - - Args: - goal: Target pose to navigate to - - Returns: - True if goal was accepted, False otherwise - """ - pass - - @abstractmethod - def get_state(self) -> NavigationState: - """ - Get the current state of the navigator. - - Returns: - Current navigation state - """ - pass - - @abstractmethod - def is_goal_reached(self) -> bool: - """ - Check if the current goal has been reached. - - Returns: - True if goal was reached, False otherwise - """ - pass - - @abstractmethod - def cancel_goal(self) -> bool: - """ - Cancel the current navigation goal. - - Returns: - True if goal was cancelled, False if no goal was active - """ - pass - - -__all__ = ["NavigationInterface", "NavigationState"] diff --git a/dimos/navigation/bbox_navigation.py b/dimos/navigation/bbox_navigation.py deleted file mode 100644 index 4f4aff3d16..0000000000 --- a/dimos/navigation/bbox_navigation.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from dimos_lcm.sensor_msgs import CameraInfo -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.DEBUG) - - -class BBoxNavigationModule(Module): - """Minimal module that converts 2D bbox center to navigation goals.""" - - detection2d: In[Detection2DArray] - camera_info: In[CameraInfo] - goal_request: Out[PoseStamped] - - def __init__(self, goal_distance: float = 1.0) -> None: - super().__init__() - self.goal_distance = goal_distance - self.camera_intrinsics = None - - @rpc - def start(self) -> None: - unsub = self.camera_info.subscribe( - lambda msg: setattr(self, "camera_intrinsics", [msg.K[0], msg.K[4], msg.K[2], msg.K[5]]) - ) - self._disposables.add(Disposable(unsub)) - - unsub = self.detection2d.subscribe(self._on_detection) - self._disposables.add(Disposable(unsub)) - - @rpc - def stop(self) -> None: - super().stop() - - def _on_detection(self, det: Detection2DArray) -> None: - if det.detections_length == 0 or not self.camera_intrinsics: - return - fx, fy, cx, cy = self.camera_intrinsics - center_x, center_y = ( - det.detections[0].bbox.center.position.x, - det.detections[0].bbox.center.position.y, - ) - x, y, z = ( - (center_x - cx) / fx * self.goal_distance, - (center_y - cy) / fy * self.goal_distance, - self.goal_distance, - ) - goal = PoseStamped( - position=Vector3(z, -x, -y), - orientation=Quaternion(0, 0, 0, 1), - frame_id=det.header.frame_id, - ) - logger.debug( - f"BBox center: ({center_x:.1f}, {center_y:.1f}) → " - f"Goal pose: ({z:.2f}, {-x:.2f}, {-y:.2f}) in frame '{det.header.frame_id}'" - ) - self.goal_request.publish(goal) diff --git a/dimos/navigation/demo_ros_navigation.py b/dimos/navigation/demo_ros_navigation.py deleted file mode 100644 index 4919ab0efd..0000000000 --- a/dimos/navigation/demo_ros_navigation.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -from dimos import core -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.navigation import rosnav -from dimos.protocol import pubsub -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def main() -> None: - pubsub.lcm.autoconf() # type: ignore[attr-defined] - dimos = core.start(2) - - ros_nav = rosnav.deploy(dimos) - - logger.info("\nTesting navigation in 2 seconds...") - time.sleep(2) - - test_pose = PoseStamped( - ts=time.time(), - frame_id="map", - position=Vector3(10.0, 10.0, 0.0), - orientation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - - logger.info("Sending navigation goal to: (10.0, 10.0, 0.0)") - ros_nav.set_goal(test_pose) - time.sleep(5) - - logger.info("Cancelling goal after 5 seconds...") - cancelled = ros_nav.cancel_goal() - logger.info(f"Goal cancelled: {cancelled}") - - try: - logger.info("\nNavBot running. Press Ctrl+C to stop.") - while True: - time.sleep(1) - except KeyboardInterrupt: - logger.info("\nShutting down...") - ros_nav.stop() - - -if __name__ == "__main__": - main() diff --git a/dimos/navigation/frontier_exploration/__init__.py b/dimos/navigation/frontier_exploration/__init__.py deleted file mode 100644 index 24ce957ccf..0000000000 --- a/dimos/navigation/frontier_exploration/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .wavefront_frontier_goal_selector import WavefrontFrontierExplorer, wavefront_frontier_explorer - -__all__ = ["WavefrontFrontierExplorer", "wavefront_frontier_explorer"] diff --git a/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py deleted file mode 100644 index 1c8082b414..0000000000 --- a/dimos/navigation/frontier_exploration/test_wavefront_frontier_goal_selector.py +++ /dev/null @@ -1,368 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs import Vector3 -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid -from dimos.navigation.frontier_exploration.wavefront_frontier_goal_selector import ( - WavefrontFrontierExplorer, -) - - -@pytest.fixture -def explorer(): - """Create a WavefrontFrontierExplorer instance for testing.""" - explorer = WavefrontFrontierExplorer( - min_frontier_perimeter=0.3, # Smaller for faster tests - safe_distance=0.5, # Smaller for faster distance calculations - info_gain_threshold=0.02, - ) - yield explorer - # Cleanup after test - try: - explorer.stop() - except: - pass - - -@pytest.fixture -def quick_costmap(): - """Create a very small costmap for quick tests.""" - width, height = 20, 20 - grid = np.full((height, width), CostValues.UNKNOWN, dtype=np.int8) - - # Simple free space in center - grid[8:12, 8:12] = CostValues.FREE - - # Small extensions - grid[9:11, 6:8] = CostValues.FREE # Left - grid[9:11, 12:14] = CostValues.FREE # Right - - # One obstacle - grid[9:10, 9:10] = CostValues.OCCUPIED - - from dimos.msgs.geometry_msgs import Pose - - origin = Pose() - origin.position.x = -1.0 - origin.position.y = -1.0 - origin.position.z = 0.0 - origin.orientation.w = 1.0 - - occupancy_grid = OccupancyGrid( - grid=grid, resolution=0.1, origin=origin, frame_id="map", ts=time.time() - ) - - class MockLidar: - def __init__(self) -> None: - self.origin = Vector3(0.0, 0.0, 0.0) - - return occupancy_grid, MockLidar() - - -def create_test_costmap(width: int = 40, height: int = 40, resolution: float = 0.1): - """Create a simple test costmap with free, occupied, and unknown regions. - - Default size reduced from 100x100 to 40x40 for faster tests. - """ - grid = np.full((height, width), CostValues.UNKNOWN, dtype=np.int8) - - # Create a smaller free space region with simple shape - # Central room - grid[15:25, 15:25] = CostValues.FREE - - # Small corridors extending from central room - grid[18:22, 10:15] = CostValues.FREE # Left corridor - grid[18:22, 25:30] = CostValues.FREE # Right corridor - grid[10:15, 18:22] = CostValues.FREE # Top corridor - grid[25:30, 18:22] = CostValues.FREE # Bottom corridor - - # Add fewer obstacles for faster processing - grid[19:21, 19:21] = CostValues.OCCUPIED # Central obstacle - grid[13:14, 18:22] = CostValues.OCCUPIED # Top corridor obstacle - - # Create origin at bottom-left, adjusted for map size - from dimos.msgs.geometry_msgs import Pose - - origin = Pose() - # Center the map around (0, 0) in world coordinates - origin.position.x = -(width * resolution) / 2.0 - origin.position.y = -(height * resolution) / 2.0 - origin.position.z = 0.0 - origin.orientation.w = 1.0 - - occupancy_grid = OccupancyGrid( - grid=grid, resolution=resolution, origin=origin, frame_id="map", ts=time.time() - ) - - # Create a mock lidar message with origin - class MockLidar: - def __init__(self) -> None: - self.origin = Vector3(0.0, 0.0, 0.0) - - return occupancy_grid, MockLidar() - - -def test_frontier_detection_with_office_lidar(explorer, quick_costmap) -> None: - """Test frontier detection using a test costmap.""" - # Get test costmap - costmap, first_lidar = quick_costmap - - # Verify we have a valid costmap - assert costmap is not None, "Costmap should not be None" - assert costmap.width > 0 and costmap.height > 0, "Costmap should have valid dimensions" - - print(f"Costmap dimensions: {costmap.width}x{costmap.height}") - print(f"Costmap resolution: {costmap.resolution}") - print(f"Unknown percent: {costmap.unknown_percent:.1f}%") - print(f"Free percent: {costmap.free_percent:.1f}%") - print(f"Occupied percent: {costmap.occupied_percent:.1f}%") - - # Set robot pose near the center of free space in the costmap - # We'll use the lidar origin as a reasonable robot position - robot_pose = first_lidar.origin - print(f"Robot pose: {robot_pose}") - - # Detect frontiers - frontiers = explorer.detect_frontiers(robot_pose, costmap) - - # Verify frontier detection results - assert isinstance(frontiers, list), "Frontiers should be returned as a list" - print(f"Detected {len(frontiers)} frontiers") - - # Test that we get some frontiers (office environment should have unexplored areas) - if len(frontiers) > 0: - print("Frontier detection successful - found unexplored areas") - - # Verify frontiers are Vector objects with valid coordinates - for i, frontier in enumerate(frontiers[:5]): # Check first 5 - assert isinstance(frontier, Vector3), f"Frontier {i} should be a Vector3" - assert hasattr(frontier, "x") and hasattr(frontier, "y"), ( - f"Frontier {i} should have x,y coordinates" - ) - print(f" Frontier {i}: ({frontier.x:.2f}, {frontier.y:.2f})") - else: - print("No frontiers detected - map may be fully explored or parameters too restrictive") - - explorer.stop() # TODO: this should be a in try-finally - - -def test_exploration_goal_selection(explorer) -> None: - """Test the complete exploration goal selection pipeline.""" - # Get test costmap - use regular size for more realistic test - costmap, first_lidar = create_test_costmap() - - # Use lidar origin as robot position - robot_pose = first_lidar.origin - - # Get exploration goal - goal = explorer.get_exploration_goal(robot_pose, costmap) - - if goal is not None: - assert isinstance(goal, Vector3), "Goal should be a Vector3" - print(f"Selected exploration goal: ({goal.x:.2f}, {goal.y:.2f})") - - # Test that goal gets marked as explored - assert len(explorer.explored_goals) == 1, "Goal should be marked as explored" - assert explorer.explored_goals[0] == goal, "Explored goal should match selected goal" - - # Test that goal is within costmap bounds - grid_pos = costmap.world_to_grid(goal) - assert 0 <= grid_pos.x < costmap.width, "Goal x should be within costmap bounds" - assert 0 <= grid_pos.y < costmap.height, "Goal y should be within costmap bounds" - - # Test that goal is at a reasonable distance from robot - distance = np.sqrt((goal.x - robot_pose.x) ** 2 + (goal.y - robot_pose.y) ** 2) - assert 0.1 < distance < 20.0, f"Goal distance {distance:.2f}m should be reasonable" - - else: - print("No exploration goal selected - map may be fully explored") - - explorer.stop() # TODO: this should be a in try-finally - - -def test_exploration_session_reset(explorer) -> None: - """Test exploration session reset functionality.""" - # Get test costmap - costmap, first_lidar = create_test_costmap() - - # Use lidar origin as robot position - robot_pose = first_lidar.origin - - # Select a goal to populate exploration state - goal = explorer.get_exploration_goal(robot_pose, costmap) - - # Verify state is populated (skip if no goals available) - if goal: - initial_explored_count = len(explorer.explored_goals) - assert initial_explored_count > 0, "Should have at least one explored goal" - - # Reset exploration session - explorer.reset_exploration_session() - - # Verify state is cleared - assert len(explorer.explored_goals) == 0, "Explored goals should be cleared after reset" - assert explorer.exploration_direction.x == 0.0 and explorer.exploration_direction.y == 0.0, ( - "Exploration direction should be reset" - ) - assert explorer.last_costmap is None, "Last costmap should be cleared" - assert explorer.no_gain_counter == 0, "No-gain counter should be reset" - - print("Exploration session reset successfully") - explorer.stop() # TODO: this should be a in try-finally - - -def test_frontier_ranking(explorer) -> None: - """Test frontier ranking and scoring logic.""" - # Get test costmap - costmap, first_lidar = create_test_costmap() - - robot_pose = first_lidar.origin - - # Get first set of frontiers - frontiers1 = explorer.detect_frontiers(robot_pose, costmap) - goal1 = explorer.get_exploration_goal(robot_pose, costmap) - - if goal1: - # Verify the selected goal is the first in the ranked list - assert frontiers1[0].x == goal1.x and frontiers1[0].y == goal1.y, ( - "Selected goal should be the highest ranked frontier" - ) - - # Test that goals are being marked as explored - assert len(explorer.explored_goals) == 1, "Goal should be marked as explored" - assert ( - explorer.explored_goals[0].x == goal1.x and explorer.explored_goals[0].y == goal1.y - ), "Explored goal should match selected goal" - - # Get another goal - goal2 = explorer.get_exploration_goal(robot_pose, costmap) - if goal2: - assert len(explorer.explored_goals) == 2, ( - "Second goal should also be marked as explored" - ) - - # Test distance to obstacles - obstacle_dist = explorer._compute_distance_to_obstacles(goal1, costmap) - # Note: Goals might be closer than safe_distance if that's the best available frontier - # The safe_distance is used for scoring, not as a hard constraint - print( - f"Distance to obstacles: {obstacle_dist:.2f}m (safe distance: {explorer.safe_distance}m)" - ) - - print(f"Frontier ranking test passed - selected goal at ({goal1.x:.2f}, {goal1.y:.2f})") - print(f"Total frontiers detected: {len(frontiers1)}") - else: - print("No frontiers found for ranking test") - - explorer.stop() # TODO: this should be a in try-finally - - -def test_exploration_with_no_gain_detection() -> None: - """Test information gain detection and exploration termination.""" - # Get initial costmap - costmap1, first_lidar = create_test_costmap() - - # Initialize explorer with low no-gain threshold for testing - explorer = WavefrontFrontierExplorer(info_gain_threshold=0.01, num_no_gain_attempts=2) - - try: - robot_pose = first_lidar.origin - - # Select multiple goals to populate history - for i in range(6): - goal = explorer.get_exploration_goal(robot_pose, costmap1) - if goal: - print(f"Goal {i + 1}: ({goal.x:.2f}, {goal.y:.2f})") - - # Now use same costmap repeatedly to trigger no-gain detection - initial_counter = explorer.no_gain_counter - - # This should increment no-gain counter - goal = explorer.get_exploration_goal(robot_pose, costmap1) - assert explorer.no_gain_counter > initial_counter, "No-gain counter should increment" - - # Continue until exploration stops - for _ in range(3): - goal = explorer.get_exploration_goal(robot_pose, costmap1) - if goal is None: - break - - # Should have stopped due to no information gain - assert goal is None, "Exploration should stop after no-gain threshold" - assert explorer.no_gain_counter == 0, "Counter should reset after stopping" - finally: - explorer.stop() - - -def test_performance_timing() -> None: - """Test performance by timing frontier detection operations.""" - import time - - # Test with different costmap sizes - sizes = [(20, 20), (40, 40), (60, 60)] - results = [] - - for width, height in sizes: - # Create costmap of specified size - costmap, lidar = create_test_costmap(width, height) - - # Create explorer with optimized parameters - explorer = WavefrontFrontierExplorer( - min_frontier_perimeter=0.3, - safe_distance=0.5, - info_gain_threshold=0.02, - ) - - try: - robot_pose = lidar.origin - - # Time frontier detection - start = time.time() - frontiers = explorer.detect_frontiers(robot_pose, costmap) - detect_time = time.time() - start - - # Time goal selection - start = time.time() - explorer.get_exploration_goal(robot_pose, costmap) - goal_time = time.time() - start - - results.append( - { - "size": f"{width}x{height}", - "cells": width * height, - "detect_time": detect_time, - "goal_time": goal_time, - "frontiers": len(frontiers), - } - ) - - print(f"\nSize {width}x{height}:") - print(f" Cells: {width * height}") - print(f" Frontier detection: {detect_time:.4f}s") - print(f" Goal selection: {goal_time:.4f}s") - print(f" Frontiers found: {len(frontiers)}") - finally: - explorer.stop() - - # Check that larger maps take more time (expected behavior) - for result in results: - assert result["detect_time"] < 3.0, f"Detection too slow: {result['detect_time']}s" - assert result["goal_time"] < 1.5, f"Goal selection too slow: {result['goal_time']}s" - - print("\nPerformance test passed - all operations completed within time limits") diff --git a/dimos/navigation/frontier_exploration/utils.py b/dimos/navigation/frontier_exploration/utils.py deleted file mode 100644 index 28644cdd41..0000000000 --- a/dimos/navigation/frontier_exploration/utils.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Utility functions for frontier exploration visualization and testing. -""" - -import numpy as np -from PIL import Image, ImageDraw - -from dimos.msgs.geometry_msgs import Vector3 -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid - - -def costmap_to_pil_image(costmap: OccupancyGrid, scale_factor: int = 2) -> Image.Image: - """ - Convert costmap to PIL Image with ROS-style coloring and optional scaling. - - Args: - costmap: Costmap to convert - scale_factor: Factor to scale up the image for better visibility - - Returns: - PIL Image with ROS-style colors - """ - # Create image array (height, width, 3 for RGB) - img_array = np.zeros((costmap.height, costmap.width, 3), dtype=np.uint8) - - # Apply ROS-style coloring based on costmap values - for i in range(costmap.height): - for j in range(costmap.width): - value = costmap.grid[i, j] - if value == CostValues.FREE: # Free space = light grey - img_array[i, j] = [205, 205, 205] - elif value == CostValues.UNKNOWN: # Unknown = dark gray - img_array[i, j] = [128, 128, 128] - elif value >= CostValues.OCCUPIED: # Occupied/obstacles = black - img_array[i, j] = [0, 0, 0] - else: # Any other values (low cost) = light grey - img_array[i, j] = [205, 205, 205] - - # Flip vertically to match ROS convention (origin at bottom-left) - img_array = np.flipud(img_array) # type: ignore[assignment] - - # Create PIL image - img = Image.fromarray(img_array, "RGB") - - # Scale up if requested - if scale_factor > 1: - new_size = (img.width * scale_factor, img.height * scale_factor) - img = img.resize(new_size, Image.NEAREST) # type: ignore[attr-defined] # Use NEAREST to keep sharp pixels - - return img - - -def draw_frontiers_on_image( - image: Image.Image, - costmap: OccupancyGrid, - frontiers: list[Vector3], - scale_factor: int = 2, - unfiltered_frontiers: list[Vector3] | None = None, -) -> Image.Image: - """ - Draw frontier points on the costmap image. - - Args: - image: PIL Image to draw on - costmap: Original costmap for coordinate conversion - frontiers: List of frontier centroids (top 5) - scale_factor: Scaling factor used for the image - unfiltered_frontiers: All unfiltered frontier results (light green) - - Returns: - PIL Image with frontiers drawn - """ - img_copy = image.copy() - draw = ImageDraw.Draw(img_copy) - - def world_to_image_coords(world_pos: Vector3) -> tuple[int, int]: - """Convert world coordinates to image pixel coordinates.""" - grid_pos = costmap.world_to_grid(world_pos) - # Flip Y coordinate and apply scaling - img_x = int(grid_pos.x * scale_factor) - img_y = int((costmap.height - grid_pos.y) * scale_factor) # Flip Y - return img_x, img_y - - # Draw all unfiltered frontiers as light green circles - if unfiltered_frontiers: - for frontier in unfiltered_frontiers: - x, y = world_to_image_coords(frontier) - radius = 3 * scale_factor - draw.ellipse( - [x - radius, y - radius, x + radius, y + radius], - fill=(144, 238, 144), - outline=(144, 238, 144), - ) # Light green - - # Draw top 5 frontiers as green circles - for i, frontier in enumerate(frontiers[1:]): # Skip the best one for now - x, y = world_to_image_coords(frontier) - radius = 4 * scale_factor - draw.ellipse( - [x - radius, y - radius, x + radius, y + radius], - fill=(0, 255, 0), - outline=(0, 128, 0), - width=2, - ) # Green - - # Add number label - draw.text((x + radius + 2, y - radius), str(i + 2), fill=(0, 255, 0)) - - # Draw best frontier as red circle - if frontiers: - best_frontier = frontiers[0] - x, y = world_to_image_coords(best_frontier) - radius = 6 * scale_factor - draw.ellipse( - [x - radius, y - radius, x + radius, y + radius], - fill=(255, 0, 0), - outline=(128, 0, 0), - width=3, - ) # Red - - # Add "BEST" label - draw.text((x + radius + 2, y - radius), "BEST", fill=(255, 0, 0)) - - return img_copy diff --git a/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py b/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py deleted file mode 100644 index 3adfc1c598..0000000000 --- a/dimos/navigation/frontier_exploration/wavefront_frontier_goal_selector.py +++ /dev/null @@ -1,848 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple wavefront frontier exploration algorithm implementation using dimos types. - -This module provides frontier detection and exploration goal selection -for autonomous navigation using the dimos Costmap and Vector types. -""" - -from collections import deque -from dataclasses import dataclass -from enum import IntFlag -import threading - -from dimos_lcm.std_msgs import Bool -import numpy as np -from reactivex.disposable import Disposable - -from dimos.agents.annotation import skill -from dimos.core import In, Module, Out, rpc -from dimos.mapping.occupancy.inflation import simple_inflate -from dimos.msgs.geometry_msgs import PoseStamped, Vector3 -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import get_distance - -logger = setup_logger() - - -class PointClassification(IntFlag): - """Point classification flags for frontier detection algorithm.""" - - NoInformation = 0 - MapOpen = 1 - MapClosed = 2 - FrontierOpen = 4 - FrontierClosed = 8 - - -@dataclass -class GridPoint: - """Represents a point in the grid map with classification.""" - - x: int - y: int - classification: int = PointClassification.NoInformation - - -class FrontierCache: - """Cache for grid points to avoid duplicate point creation.""" - - def __init__(self) -> None: - self.points = {} # type: ignore[var-annotated] - - def get_point(self, x: int, y: int) -> GridPoint: - """Get or create a grid point at the given coordinates.""" - key = (x, y) - if key not in self.points: - self.points[key] = GridPoint(x, y) - return self.points[key] # type: ignore[no-any-return] - - def clear(self) -> None: - """Clear the point cache.""" - self.points.clear() - - -class WavefrontFrontierExplorer(Module): - """ - Wavefront frontier exploration algorithm implementation. - - This class encapsulates the frontier detection and exploration goal selection - functionality using the wavefront algorithm with BFS exploration. - - Inputs: - - costmap: Current costmap for frontier detection - - odometry: Current robot pose - - Outputs: - - goal_request: Exploration goals sent to the navigator - """ - - # LCM inputs - global_costmap: In[OccupancyGrid] - odom: In[PoseStamped] - goal_reached: In[Bool] - explore_cmd: In[Bool] - stop_explore_cmd: In[Bool] - - # LCM outputs - goal_request: Out[PoseStamped] - - def __init__( - self, - min_frontier_perimeter: float = 0.5, - occupancy_threshold: int = 99, - safe_distance: float = 3.0, - lookahead_distance: float = 5.0, - max_explored_distance: float = 10.0, - info_gain_threshold: float = 0.03, - num_no_gain_attempts: int = 2, - goal_timeout: float = 15.0, - ) -> None: - """ - Initialize the frontier explorer. - - Args: - min_frontier_perimeter: Minimum perimeter in meters to consider a valid frontier - occupancy_threshold: Cost threshold above which a cell is considered occupied (0-255) - safe_distance: Safe distance from obstacles for scoring (meters) - info_gain_threshold: Minimum percentage increase in costmap information required to continue exploration (0.05 = 5%) - num_no_gain_attempts: Maximum number of consecutive attempts with no information gain - """ - super().__init__() - self.min_frontier_perimeter = min_frontier_perimeter - self.occupancy_threshold = occupancy_threshold - self.safe_distance = safe_distance - self.max_explored_distance = max_explored_distance - self.lookahead_distance = lookahead_distance - self.info_gain_threshold = info_gain_threshold - self.num_no_gain_attempts = num_no_gain_attempts - self._cache = FrontierCache() - self.explored_goals = [] # type: ignore[var-annotated] # list of explored goals - self.exploration_direction = Vector3(0.0, 0.0, 0.0) # current exploration direction - self.last_costmap = None # store last costmap for information comparison - self.no_gain_counter = 0 # track consecutive no-gain attempts - self.goal_timeout = goal_timeout - - # Latest data - self.latest_costmap: OccupancyGrid | None = None - self.latest_odometry: PoseStamped | None = None - - # Goal reached event - self.goal_reached_event = threading.Event() - - # Exploration state - self.exploration_active = False - self.exploration_thread: threading.Thread | None = None - self.stop_event = threading.Event() - - @rpc - def start(self) -> None: - super().start() - - unsub = self.global_costmap.subscribe(self._on_costmap) - self._disposables.add(Disposable(unsub)) - - unsub = self.odom.subscribe(self._on_odometry) - self._disposables.add(Disposable(unsub)) - - if self.goal_reached.transport is not None: - unsub = self.goal_reached.subscribe(self._on_goal_reached) - self._disposables.add(Disposable(unsub)) - - if self.explore_cmd.transport is not None: - unsub = self.explore_cmd.subscribe(self._on_explore_cmd) - self._disposables.add(Disposable(unsub)) - - if self.stop_explore_cmd.transport is not None: - unsub = self.stop_explore_cmd.subscribe(self._on_stop_explore_cmd) - self._disposables.add(Disposable(unsub)) - - @rpc - def stop(self) -> None: - self.stop_exploration() - super().stop() - - def _on_costmap(self, msg: OccupancyGrid) -> None: - """Handle incoming costmap messages.""" - self.latest_costmap = msg - - def _on_odometry(self, msg: PoseStamped) -> None: - """Handle incoming odometry messages.""" - self.latest_odometry = msg - - def _on_goal_reached(self, msg: Bool) -> None: - """Handle goal reached messages.""" - if msg.data: - self.goal_reached_event.set() - - def _on_explore_cmd(self, msg: Bool) -> None: - """Handle exploration command messages.""" - if msg.data: - logger.info("Received exploration start command via LCM") - self.explore() - - def _on_stop_explore_cmd(self, msg: Bool) -> None: - """Handle stop exploration command messages.""" - if msg.data: - logger.info("Received exploration stop command via LCM") - self.stop_exploration() - - def _count_costmap_information(self, costmap: OccupancyGrid) -> int: - """ - Count the amount of information in a costmap (free space + obstacles). - - Args: - costmap: Costmap to analyze - - Returns: - Number of cells that are free space or obstacles (not unknown) - """ - free_count = np.sum(costmap.grid == CostValues.FREE) - obstacle_count = np.sum(costmap.grid >= self.occupancy_threshold) - return int(free_count + obstacle_count) - - def _get_neighbors(self, point: GridPoint, costmap: OccupancyGrid) -> list[GridPoint]: - """Get valid neighboring points for a given grid point.""" - neighbors = [] - - # 8-connected neighbors - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - if dx == 0 and dy == 0: - continue - - nx, ny = point.x + dx, point.y + dy - - # Check bounds - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - neighbors.append(self._cache.get_point(nx, ny)) - - return neighbors - - def _is_frontier_point(self, point: GridPoint, costmap: OccupancyGrid) -> bool: - """ - Check if a point is a frontier point. - A frontier point is an unknown cell adjacent to at least one free cell - and not adjacent to any occupied cells. - """ - # Point must be unknown - cost = costmap.grid[point.y, point.x] - if cost != CostValues.UNKNOWN: - return False - - has_free = False - - for neighbor in self._get_neighbors(point, costmap): - neighbor_cost = costmap.grid[neighbor.y, neighbor.x] - - # If adjacent to occupied space, not a frontier - if neighbor_cost > self.occupancy_threshold: - return False - - # Check if adjacent to free space - if neighbor_cost == CostValues.FREE: - has_free = True - - return has_free - - def _find_free_space( - self, start_x: int, start_y: int, costmap: OccupancyGrid - ) -> tuple[int, int]: - """ - Find the nearest free space point using BFS from the starting position. - """ - queue = deque([self._cache.get_point(start_x, start_y)]) - visited = set() - - while queue: - point = queue.popleft() - - if (point.x, point.y) in visited: - continue - visited.add((point.x, point.y)) - - # Check if this point is free space - if costmap.grid[point.y, point.x] == CostValues.FREE: - return (point.x, point.y) - - # Add neighbors to search - for neighbor in self._get_neighbors(point, costmap): - if (neighbor.x, neighbor.y) not in visited: - queue.append(neighbor) - - # If no free space found, return original position - return (start_x, start_y) - - def _compute_centroid(self, frontier_points: list[Vector3]) -> Vector3: - """Compute the centroid of a list of frontier points.""" - if not frontier_points: - return Vector3(0.0, 0.0, 0.0) - - # Vectorized approach using numpy - points_array = np.array([[point.x, point.y] for point in frontier_points]) - centroid = np.mean(points_array, axis=0) - - return Vector3(centroid[0], centroid[1], 0.0) - - def detect_frontiers(self, robot_pose: Vector3, costmap: OccupancyGrid) -> list[Vector3]: - """ - Main frontier detection algorithm using wavefront exploration. - - Args: - robot_pose: Current robot position in world coordinates - costmap: Costmap for frontier detection - - Returns: - List of frontier centroids in world coordinates - """ - self._cache.clear() - - # Convert robot pose to grid coordinates - grid_pos = costmap.world_to_grid(robot_pose) - grid_x, grid_y = int(grid_pos.x), int(grid_pos.y) - - # Find nearest free space to start exploration - free_x, free_y = self._find_free_space(grid_x, grid_y, costmap) - start_point = self._cache.get_point(free_x, free_y) - start_point.classification = PointClassification.MapOpen - - # Main exploration queue - explore ALL reachable free space - map_queue = deque([start_point]) - frontiers = [] - frontier_sizes = [] - - points_checked = 0 - frontier_candidates = 0 - - while map_queue: - current_point = map_queue.popleft() - points_checked += 1 - - # Skip if already processed - if current_point.classification & PointClassification.MapClosed: - continue - - # Mark as processed - current_point.classification |= PointClassification.MapClosed - - # Check if this point starts a new frontier - if self._is_frontier_point(current_point, costmap): - frontier_candidates += 1 - current_point.classification |= PointClassification.FrontierOpen - frontier_queue = deque([current_point]) - new_frontier = [] - - # Explore this frontier region using BFS - while frontier_queue: - frontier_point = frontier_queue.popleft() - - # Skip if already processed - if frontier_point.classification & PointClassification.FrontierClosed: - continue - - # If this is still a frontier point, add to current frontier - if self._is_frontier_point(frontier_point, costmap): - new_frontier.append(frontier_point) - - # Add neighbors to frontier queue - for neighbor in self._get_neighbors(frontier_point, costmap): - if not ( - neighbor.classification - & ( - PointClassification.FrontierOpen - | PointClassification.FrontierClosed - ) - ): - neighbor.classification |= PointClassification.FrontierOpen - frontier_queue.append(neighbor) - - frontier_point.classification |= PointClassification.FrontierClosed - - # Check if we found a large enough frontier - # Convert minimum perimeter to minimum number of cells based on resolution - min_cells = int(self.min_frontier_perimeter / costmap.resolution) - if len(new_frontier) >= min_cells: - world_points = [] - for point in new_frontier: - world_pos = costmap.grid_to_world( - Vector3(float(point.x), float(point.y), 0.0) - ) - world_points.append(world_pos) - - # Compute centroid in world coordinates (already correctly scaled) - centroid = self._compute_centroid(world_points) - frontiers.append(centroid) # Store centroid - frontier_sizes.append(len(new_frontier)) # Store frontier size - - # Add ALL neighbors to main exploration queue to explore entire free space - for neighbor in self._get_neighbors(current_point, costmap): - if not ( - neighbor.classification - & (PointClassification.MapOpen | PointClassification.MapClosed) - ): - # Check if neighbor is free space or unknown (explorable) - neighbor_cost = costmap.grid[neighbor.y, neighbor.x] - - # Add free space and unknown space to exploration queue - if neighbor_cost == CostValues.FREE or neighbor_cost == CostValues.UNKNOWN: - neighbor.classification |= PointClassification.MapOpen - map_queue.append(neighbor) - - # Extract just the centroids for ranking - frontier_centroids = frontiers - - if not frontier_centroids: - return [] - - # Rank frontiers using original costmap for proper filtering - ranked_frontiers = self._rank_frontiers( - frontier_centroids, frontier_sizes, robot_pose, costmap - ) - - return ranked_frontiers - - def _update_exploration_direction( - self, robot_pose: Vector3, goal_pose: Vector3 | None = None - ) -> None: - """Update the current exploration direction based on robot movement or selected goal.""" - if goal_pose is not None: - # Calculate direction from robot to goal - direction = Vector3(goal_pose.x - robot_pose.x, goal_pose.y - robot_pose.y, 0.0) - magnitude = np.sqrt(direction.x**2 + direction.y**2) - if magnitude > 0.1: # Avoid division by zero for very close goals - self.exploration_direction = Vector3( - direction.x / magnitude, direction.y / magnitude, 0.0 - ) - - def _compute_direction_momentum_score(self, frontier: Vector3, robot_pose: Vector3) -> float: - """Compute direction momentum score for a frontier.""" - if self.exploration_direction.x == 0 and self.exploration_direction.y == 0: - return 0.0 # No momentum if no previous direction - - # Calculate direction from robot to frontier - frontier_direction = Vector3(frontier.x - robot_pose.x, frontier.y - robot_pose.y, 0.0) - magnitude = np.sqrt(frontier_direction.x**2 + frontier_direction.y**2) - - if magnitude < 0.1: - return 0.0 # Too close to calculate meaningful direction - - # Normalize frontier direction - frontier_direction = Vector3( - frontier_direction.x / magnitude, frontier_direction.y / magnitude, 0.0 - ) - - # Calculate dot product for directional alignment - dot_product = ( - self.exploration_direction.x * frontier_direction.x - + self.exploration_direction.y * frontier_direction.y - ) - - # Return momentum score (higher for same direction, lower for opposite) - return max(0.0, dot_product) # Only positive momentum, no penalty for different directions - - def _compute_distance_to_explored_goals(self, frontier: Vector3) -> float: - """Compute distance from frontier to the nearest explored goal.""" - if not self.explored_goals: - return 5.0 # Default consistent value when no explored goals - # Calculate distance to nearest explored goal - min_distance = float("inf") - for goal in self.explored_goals: - distance = np.sqrt((frontier.x - goal.x) ** 2 + (frontier.y - goal.y) ** 2) - min_distance = min(min_distance, distance) - - return min_distance - - def _compute_distance_to_obstacles(self, frontier: Vector3, costmap: OccupancyGrid) -> float: - """ - Compute the minimum distance from a frontier point to the nearest obstacle. - - Args: - frontier: Frontier point in world coordinates - costmap: Costmap to check for obstacles - - Returns: - Minimum distance to nearest obstacle in meters - """ - # Convert frontier to grid coordinates - grid_pos = costmap.world_to_grid(frontier) - grid_x, grid_y = int(grid_pos.x), int(grid_pos.y) - - # Check if frontier is within costmap bounds - if grid_x < 0 or grid_x >= costmap.width or grid_y < 0 or grid_y >= costmap.height: - return 0.0 # Consider out-of-bounds as obstacle - - min_distance = float("inf") - search_radius = ( - int(self.safe_distance / costmap.resolution) + 5 - ) # Search a bit beyond minimum - - # Search in a square around the frontier point - for dy in range(-search_radius, search_radius + 1): - for dx in range(-search_radius, search_radius + 1): - check_x = grid_x + dx - check_y = grid_y + dy - - # Skip if out of bounds - if ( - check_x < 0 - or check_x >= costmap.width - or check_y < 0 - or check_y >= costmap.height - ): - continue - - # Check if this cell is an obstacle - if costmap.grid[check_y, check_x] >= self.occupancy_threshold: - # Calculate distance in meters - distance = np.sqrt(dx**2 + dy**2) * costmap.resolution - min_distance = min(min_distance, distance) - - # If no obstacles found within search radius, return the safe distance - # This indicates the frontier is safely away from obstacles - return min_distance if min_distance != float("inf") else self.safe_distance - - def _compute_comprehensive_frontier_score( - self, frontier: Vector3, frontier_size: int, robot_pose: Vector3, costmap: OccupancyGrid - ) -> float: - """Compute comprehensive score considering multiple criteria.""" - - # 1. Distance from robot (preference for moderate distances) - robot_distance = get_distance(frontier, robot_pose) - - # Distance score: prefer moderate distances (not too close, not too far) - # Normalized to 0-1 range - distance_score = 1.0 / (1.0 + abs(robot_distance - self.lookahead_distance)) - - # 2. Information gain (frontier size) - # Normalize by a reasonable max frontier size - max_expected_frontier_size = self.min_frontier_perimeter / costmap.resolution * 10 - info_gain_score = min(frontier_size / max_expected_frontier_size, 1.0) - - # 3. Distance to explored goals (bonus for being far from explored areas) - # Normalize by a reasonable max distance (e.g., 10 meters) - explored_goals_distance = self._compute_distance_to_explored_goals(frontier) - explored_goals_score = min(explored_goals_distance / self.max_explored_distance, 1.0) - - # 4. Distance to obstacles (score based on safety) - # 0 = too close to obstacles, 1 = at or beyond safe distance - obstacles_distance = self._compute_distance_to_obstacles(frontier, costmap) - if obstacles_distance >= self.safe_distance: - obstacles_score = 1.0 # Fully safe - else: - obstacles_score = obstacles_distance / self.safe_distance # Linear penalty - - # 5. Direction momentum (already in 0-1 range from dot product) - momentum_score = self._compute_direction_momentum_score(frontier, robot_pose) - - logger.info( - f"Distance score: {distance_score:.2f}, Info gain: {info_gain_score:.2f}, Explored goals: {explored_goals_score:.2f}, Obstacles: {obstacles_score:.2f}, Momentum: {momentum_score:.2f}" - ) - - # Combine scores with consistent scaling - total_score = ( - 0.3 * info_gain_score # 30% information gain - + 0.3 * explored_goals_score # 30% distance from explored goals - + 0.2 * distance_score # 20% distance optimization - + 0.15 * obstacles_score # 15% distance from obstacles - + 0.05 * momentum_score # 5% direction momentum - ) - - return total_score - - def _rank_frontiers( - self, - frontier_centroids: list[Vector3], - frontier_sizes: list[int], - robot_pose: Vector3, - costmap: OccupancyGrid, - ) -> list[Vector3]: - """ - Find the single best frontier using comprehensive scoring and filtering. - - Args: - frontier_centroids: List of frontier centroids - frontier_sizes: List of frontier sizes - robot_pose: Current robot position - costmap: Costmap for additional analysis - - Returns: - List containing single best frontier, or empty list if none suitable - """ - if not frontier_centroids: - return [] - - valid_frontiers = [] - - for i, frontier in enumerate(frontier_centroids): - # Compute comprehensive score - frontier_size = frontier_sizes[i] if i < len(frontier_sizes) else 1 - score = self._compute_comprehensive_frontier_score( - frontier, frontier_size, robot_pose, costmap - ) - - valid_frontiers.append((frontier, score)) - - logger.info(f"Valid frontiers: {len(valid_frontiers)}") - - if not valid_frontiers: - return [] - - # Sort by score and return all valid frontiers (highest scores first) - valid_frontiers.sort(key=lambda x: x[1], reverse=True) - - # Extract just the frontiers (remove scores) and return as list - return [frontier for frontier, _ in valid_frontiers] - - def get_exploration_goal(self, robot_pose: Vector3, costmap: OccupancyGrid) -> Vector3 | None: - """ - Get the single best exploration goal using comprehensive frontier scoring. - - Args: - robot_pose: Current robot position in world coordinates - costmap: Costmap for additional analysis - - Returns: - Single best frontier goal in world coordinates, or None if no suitable frontiers found - """ - # Check if we should compare costmaps for information gain - if len(self.explored_goals) > 5 and self.last_costmap is not None: - current_info = self._count_costmap_information(costmap) - last_info = self._count_costmap_information(self.last_costmap) - - # Check if information increase meets minimum percentage threshold - if last_info > 0: # Avoid division by zero - info_increase_percent = (current_info - last_info) / last_info - if info_increase_percent < self.info_gain_threshold: - logger.info( - f"Information increase ({info_increase_percent:.2f}) below threshold ({self.info_gain_threshold:.2f})" - ) - logger.info( - f"Current information: {current_info}, Last information: {last_info}" - ) - self.no_gain_counter += 1 - if self.no_gain_counter >= self.num_no_gain_attempts: - logger.info( - f"No information gain for {self.no_gain_counter} consecutive attempts" - ) - self.no_gain_counter = 0 # Reset counter when stopping due to no gain - self.stop_exploration() - return None - else: - self.no_gain_counter = 0 - - # Always detect new frontiers to get most up-to-date information - # The new algorithm filters out explored areas and returns only the best frontier - frontiers = self.detect_frontiers(robot_pose, costmap) - - if not frontiers: - # Store current costmap before returning - self.last_costmap = costmap # type: ignore[assignment] - self.reset_exploration_session() - return None - - # Update exploration direction based on best goal selection - if frontiers: - self._update_exploration_direction(robot_pose, frontiers[0]) - - # Store the selected goal as explored - selected_goal = frontiers[0] - self.mark_explored_goal(selected_goal) - - # Store current costmap for next comparison - self.last_costmap = costmap # type: ignore[assignment] - - return selected_goal - - # Store current costmap before returning - self.last_costmap = costmap # type: ignore[assignment] - return None - - def mark_explored_goal(self, goal: Vector3) -> None: - """Mark a goal as explored.""" - self.explored_goals.append(goal) - - def reset_exploration_session(self) -> None: - """ - Reset all exploration state variables for a new exploration session. - - Call this method when starting a new exploration or when the robot - needs to forget its previous exploration history. - """ - self.explored_goals.clear() # Clear all previously explored goals - self.exploration_direction = Vector3(0.0, 0.0, 0.0) # Reset exploration direction - self.last_costmap = None # Clear last costmap comparison - self.no_gain_counter = 0 # Reset no-gain attempt counter - self._cache.clear() # Clear frontier point cache - - logger.info("Exploration session reset - all state variables cleared") - - @rpc - def explore(self) -> bool: - """ - Start autonomous frontier exploration. - - Returns: - bool: True if exploration started, False if already exploring - """ - if self.exploration_active: - logger.warning("Exploration already active") - return False - - self.exploration_active = True - self.stop_event.clear() - - # Start exploration thread - self.exploration_thread = threading.Thread(target=self._exploration_loop, daemon=True) - self.exploration_thread.start() - - logger.info("Started autonomous frontier exploration") - return True - - @rpc - def stop_exploration(self) -> bool: - """ - Stop autonomous frontier exploration. - - Returns: - bool: True if exploration was stopped, False if not exploring - """ - if not self.exploration_active: - return False - - self.exploration_active = False - self.no_gain_counter = 0 # Reset counter when exploration stops - self.stop_event.set() - - # Only join if we're NOT being called from the exploration thread itself - if ( - self.exploration_thread - and self.exploration_thread.is_alive() - and threading.current_thread() != self.exploration_thread - ): - self.exploration_thread.join(timeout=2.0) - - # Publish current location as goal to stop the robot. - if self.latest_odometry is not None: - goal = PoseStamped( - position=self.latest_odometry.position, - orientation=self.latest_odometry.orientation, - frame_id="world", - ts=self.latest_odometry.ts, - ) - self.goal_request.publish(goal) - - logger.info("Stopped autonomous frontier exploration") - return True - - @rpc - def is_exploration_active(self) -> bool: - return self.exploration_active - - def _exploration_loop(self) -> None: - """Main exploration loop running in separate thread.""" - # Track number of goals published - goals_published = 0 - consecutive_failures = 0 - max_consecutive_failures = 10 # Allow more attempts before giving up - - while self.exploration_active and not self.stop_event.is_set(): - # Check if we have required data - if self.latest_costmap is None or self.latest_odometry is None: - threading.Event().wait(0.5) - continue - - # Get robot pose from odometry - robot_pose = Vector3( - self.latest_odometry.position.x, self.latest_odometry.position.y, 0.0 - ) - - # Get exploration goal - costmap = simple_inflate(self.latest_costmap, 0.25) - goal = self.get_exploration_goal(robot_pose, costmap) - - if goal: - # Publish goal to navigator - goal_msg = PoseStamped() - goal_msg.position.x = goal.x - goal_msg.position.y = goal.y - goal_msg.position.z = 0.0 - goal_msg.orientation.w = 1.0 # No rotation - goal_msg.frame_id = "world" - goal_msg.ts = self.latest_costmap.ts - - self.goal_request.publish(goal_msg) - logger.info(f"Published frontier goal: ({goal.x:.2f}, {goal.y:.2f})") - - goals_published += 1 - consecutive_failures = 0 # Reset failure counter on success - - # Clear the goal reached event for next iteration - self.goal_reached_event.clear() - - # Wait for goal to be reached or timeout - logger.info("Waiting for goal to be reached...") - goal_reached = self.goal_reached_event.wait(timeout=self.goal_timeout) - - if goal_reached: - logger.info("Goal reached, finding next frontier") - else: - logger.warning("Goal timeout after 30 seconds, finding next frontier anyway") - else: - consecutive_failures += 1 - - # Only give up if we've published at least 2 goals AND had many consecutive failures - if goals_published >= 2 and consecutive_failures >= max_consecutive_failures: - logger.info( - f"Exploration complete after {goals_published} goals and {consecutive_failures} consecutive failures finding new frontiers" - ) - self.exploration_active = False - break - elif goals_published < 2: - logger.info( - f"No frontier found, but only {goals_published} goals published so far. Retrying in 2 seconds..." - ) - threading.Event().wait(2.0) - else: - logger.info( - f"No frontier found (attempt {consecutive_failures}/{max_consecutive_failures}). Retrying in 2 seconds..." - ) - threading.Event().wait(2.0) - - @skill - def begin_exploration(self) -> str: - """Command the robot to move around and explore the area. Cancelled with end_exploration.""" - started = self.explore() - if not started: - return "Exploration skill is already active. Use end_exploration to stop before starting again." - return ( - "Started exploration skill. The robot is now moving. Use end_exploration " - "to stop. You also need to cancel before starting a new movement tool." - ) - - @skill - def end_exploration(self) -> str: - """Cancel the exploration. The robot will stop moving and remain where it is.""" - stopped = self.stop_exploration() - if stopped: - return "Stopped exploration. The robot has stopped moving." - else: - return "Exploration skill was not active, so nothing was stopped." - - -wavefront_frontier_explorer = WavefrontFrontierExplorer.blueprint - -__all__ = ["WavefrontFrontierExplorer", "wavefront_frontier_explorer"] diff --git a/dimos/navigation/replanning_a_star/controllers.py b/dimos/navigation/replanning_a_star/controllers.py deleted file mode 100644 index 865aafb8be..0000000000 --- a/dimos/navigation/replanning_a_star/controllers.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from typing import Protocol - -import numpy as np -from numpy.typing import NDArray - -from dimos.core.global_config import GlobalConfig -from dimos.msgs.geometry_msgs import Twist, Vector3 -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.utils.trigonometry import angle_diff - - -class Controller(Protocol): - def advance(self, lookahead_point: NDArray[np.float64], current_odom: PoseStamped) -> Twist: ... - - def rotate(self, yaw_error: float) -> Twist: ... - - def reset_errors(self) -> None: ... - - def reset_yaw_error(self, value: float) -> None: ... - - -class PController: - _global_config: GlobalConfig - _speed: float - _control_frequency: float - - _min_linear_velocity: float = 0.2 - _min_angular_velocity: float = 0.2 - _k_angular: float = 0.5 - _max_angular_accel: float = 2.0 - _rotation_threshold: float = 90 * (math.pi / 180) - - def __init__(self, global_config: GlobalConfig, speed: float, control_frequency: float): - self._global_config = global_config - self._speed = speed - self._control_frequency = control_frequency - - def advance(self, lookahead_point: NDArray[np.float64], current_odom: PoseStamped) -> Twist: - current_pos = np.array([current_odom.position.x, current_odom.position.y]) - direction = lookahead_point - current_pos - distance = np.linalg.norm(direction) - - if distance < 1e-6: - # Robot is coincidentally at the lookahead point; skip this cycle. - return Twist() - - robot_yaw = current_odom.orientation.euler[2] - desired_yaw = np.arctan2(direction[1], direction[0]) - yaw_error = angle_diff(desired_yaw, robot_yaw) - - angular_velocity = self._compute_angular_velocity(yaw_error) - - # Rotate-then-drive: if heading error is large, rotate in place first - if abs(yaw_error) > self._rotation_threshold: - return self._angular_twist(angular_velocity) - - # When aligned, drive forward with proportional angular correction - linear_velocity = self._speed * (1.0 - abs(yaw_error) / self._rotation_threshold) - linear_velocity = self._apply_min_velocity(linear_velocity, self._min_linear_velocity) - - return Twist( - linear=Vector3(linear_velocity, 0.0, 0.0), - angular=Vector3(0.0, 0.0, angular_velocity), - ) - - def rotate(self, yaw_error: float) -> Twist: - angular_velocity = self._compute_angular_velocity(yaw_error) - return self._angular_twist(angular_velocity) - - def _compute_angular_velocity(self, yaw_error: float) -> float: - angular_velocity = self._k_angular * yaw_error - angular_velocity = np.clip(angular_velocity, -self._speed, self._speed) - angular_velocity = self._apply_min_velocity(angular_velocity, self._min_angular_velocity) - return float(angular_velocity) - - def reset_errors(self) -> None: - pass - - def reset_yaw_error(self, value: float) -> None: - pass - - def _apply_min_velocity(self, velocity: float, min_velocity: float) -> float: - """Apply minimum velocity threshold, preserving sign. Returns 0 if velocity is 0.""" - if velocity == 0.0: - return 0.0 - if abs(velocity) < min_velocity: - return min_velocity if velocity > 0 else -min_velocity - return velocity - - def _angular_twist(self, angular_velocity: float) -> Twist: - # In simulation, add a small forward velocity to help the locomotion - # policy execute rotation (some policies don't handle pure in-place rotation). - linear_x = 0.18 if self._global_config.simulation else 0.0 - - return Twist( - linear=Vector3(linear_x, 0.0, 0.0), - angular=Vector3(0.0, 0.0, angular_velocity), - ) - - -class PdController(PController): - _k_derivative: float = 0.15 - - _prev_yaw_error: float - _prev_angular_velocity: float - - def __init__(self, global_config: GlobalConfig, speed: float, control_frequency: float): - super().__init__(global_config, speed, control_frequency) - - self._prev_yaw_error = 0.0 - self._prev_angular_velocity = 0.0 - - def reset_errors(self) -> None: - self._prev_yaw_error = 0.0 - self._prev_angular_velocity = 0.0 - - def reset_yaw_error(self, value: float) -> None: - self._prev_yaw_error = value - - def _compute_angular_velocity(self, yaw_error: float) -> float: - dt = 1.0 / self._control_frequency - - # PD control: proportional + derivative damping - yaw_error_derivative = (yaw_error - self._prev_yaw_error) / dt - angular_velocity = self._k_angular * yaw_error - self._k_derivative * yaw_error_derivative - - # Rate limiting: limit angular acceleration to prevent jerky corrections - max_delta = self._max_angular_accel * dt - angular_velocity = np.clip( - angular_velocity, - self._prev_angular_velocity - max_delta, - self._prev_angular_velocity + max_delta, - ) - - angular_velocity = np.clip(angular_velocity, -self._speed, self._speed) - angular_velocity = self._apply_min_velocity(angular_velocity, self._min_angular_velocity) - - self._prev_yaw_error = yaw_error - self._prev_angular_velocity = angular_velocity - - return float(angular_velocity) diff --git a/dimos/navigation/replanning_a_star/global_planner.py b/dimos/navigation/replanning_a_star/global_planner.py deleted file mode 100644 index df2680a4a7..0000000000 --- a/dimos/navigation/replanning_a_star/global_planner.py +++ /dev/null @@ -1,348 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from threading import Event, RLock, Thread, current_thread -import time - -from dimos_lcm.std_msgs import Bool -from reactivex import Subject -from reactivex.disposable import CompositeDisposable - -from dimos.core.global_config import GlobalConfig -from dimos.core.resource import Resource -from dimos.mapping.occupancy.path_resampling import smooth_resample_path -from dimos.msgs.geometry_msgs import Twist -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid -from dimos.msgs.nav_msgs.Path import Path -from dimos.navigation.base import NavigationState -from dimos.navigation.replanning_a_star.goal_validator import find_safe_goal -from dimos.navigation.replanning_a_star.local_planner import LocalPlanner, StopMessage -from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar -from dimos.navigation.replanning_a_star.navigation_map import NavigationMap -from dimos.navigation.replanning_a_star.position_tracker import PositionTracker -from dimos.navigation.replanning_a_star.replan_limiter import ReplanLimiter -from dimos.utils.logging_config import setup_logger -from dimos.utils.trigonometry import angle_diff - -logger = setup_logger() - - -class GlobalPlanner(Resource): - path: Subject[Path] - goal_reached: Subject[Bool] - - _current_odom: PoseStamped | None = None - _current_goal: PoseStamped | None = None - _goal_reached: bool = False - _thread: Thread | None = None - - _global_config: GlobalConfig - _navigation_map: NavigationMap - _local_planner: LocalPlanner - _position_tracker: PositionTracker - _replan_limiter: ReplanLimiter - _disposables: CompositeDisposable - _stop_planner: Event - _replan_event: Event - _replan_reason: StopMessage | None - _lock: RLock - - _safe_goal_tolerance: float = 4.0 - _goal_tolerance: float = 0.2 - _rotation_tolerance: float = math.radians(15) - _replan_goal_tolerance: float = 0.5 - _max_replan_attempts: int = 10 - _stuck_time_window: float = 8.0 - _max_path_deviation: float = 0.9 - - def __init__(self, global_config: GlobalConfig) -> None: - self.path = Subject() - self.goal_reached = Subject() - - self._global_config = global_config - self._navigation_map = NavigationMap(self._global_config) - self._local_planner = LocalPlanner( - self._global_config, self._navigation_map, self._goal_tolerance - ) - self._position_tracker = PositionTracker(self._stuck_time_window) - self._replan_limiter = ReplanLimiter() - self._disposables = CompositeDisposable() - self._stop_planner = Event() - self._replan_event = Event() - self._replan_reason = None - self._lock = RLock() - - def start(self) -> None: - self._local_planner.start() - self._disposables.add( - self._local_planner.stopped_navigating.subscribe(self._on_stopped_navigating) - ) - self._stop_planner.clear() - self._thread = Thread(target=self._thread_entrypoint, daemon=True) - self._thread.start() - - def stop(self) -> None: - self.cancel_goal() - self._local_planner.stop() - self._disposables.dispose() - self._stop_planner.set() - self._replan_event.set() - - if self._thread is not None and self._thread is not current_thread(): - self._thread.join(2) - if self._thread.is_alive(): - logger.error("GlobalPlanner thread did not stop in time.") - self._thread = None - - def handle_odom(self, msg: PoseStamped) -> None: - with self._lock: - self._current_odom = msg - - self._local_planner.handle_odom(msg) - self._position_tracker.add_position(msg) - - def handle_global_costmap(self, msg: OccupancyGrid) -> None: - self._navigation_map.update(msg) - - def handle_goal_request(self, goal: PoseStamped) -> None: - logger.info("Got new goal", goal=str(goal)) - with self._lock: - self._current_goal = goal - self._goal_reached = False - self._replan_limiter.reset() - self._plan_path() - - def cancel_goal(self, *, but_will_try_again: bool = False, arrived: bool = False) -> None: - logger.info("Cancelling goal.", but_will_try_again=but_will_try_again, arrived=arrived) - - with self._lock: - self._position_tracker.reset_data() - - if not but_will_try_again: - self._current_goal = None - self._goal_reached = arrived - self._replan_limiter.reset() - - self.path.on_next(Path()) - self._local_planner.stop_planning() - - if not but_will_try_again: - self.goal_reached.on_next(Bool(arrived)) - - def get_state(self) -> NavigationState: - return self._local_planner.get_state() - - def is_goal_reached(self) -> bool: - with self._lock: - return self._goal_reached - - @property - def cmd_vel(self) -> Subject[Twist]: - return self._local_planner.cmd_vel - - @property - def navigation_costmap(self) -> Subject[OccupancyGrid]: - return self._local_planner.navigation_costmap - - def _thread_entrypoint(self) -> None: - """Monitor if the robot is stuck, veers off track, or stopped navigating.""" - - last_id = -1 - last_stuck_check = time.perf_counter() - - while not self._stop_planner.is_set(): - # Wait for either timeout or replan signal from local planner. - replanning_wanted = self._replan_event.wait(timeout=0.1) - - if self._stop_planner.is_set(): - break - - # Handle stop message from local planner (priority) - if replanning_wanted: - self._replan_event.clear() - with self._lock: - reason = self._replan_reason - self._replan_reason = None - - if reason is not None: - self._handle_stop_message(reason) - last_stuck_check = time.perf_counter() - continue - - with self._lock: - current_goal = self._current_goal - current_odom = self._current_odom - - if not current_goal or not current_odom: - continue - - if ( - current_goal.position.distance(current_odom.position) < self._goal_tolerance - and abs( - angle_diff(current_goal.orientation.euler[2], current_odom.orientation.euler[2]) - ) - < self._rotation_tolerance - ): - logger.info("Close enough to goal. Accepting as arrived.") - self.cancel_goal(arrived=True) - continue - - # Check if robot has veered too far off the path - deviation = self._local_planner.get_distance_to_path() - if deviation is not None and deviation > self._max_path_deviation: - logger.info( - "Robot veered off track. Replanning.", - deviation=round(deviation, 2), - threshold=self._max_path_deviation, - ) - self._replan_path() - last_stuck_check = time.perf_counter() - continue - - _, new_id = self._local_planner.get_unique_state() - - if new_id != last_id: - last_id = new_id - last_stuck_check = time.perf_counter() - continue - - if ( - time.perf_counter() - last_stuck_check > self._stuck_time_window - and self._position_tracker.is_stuck() - ): - logger.info("Robot is stuck. Replanning.") - self._replan_path() - last_stuck_check = time.perf_counter() - - def _on_stopped_navigating(self, stop_message: StopMessage) -> None: - with self._lock: - self._replan_reason = stop_message - # Signal the monitoring thread to do the replanning. This is so we don't have two - # threads which could be replanning at the same time. - self._replan_event.set() - - def _handle_stop_message(self, stop_message: StopMessage) -> None: - # Note, this runs in the monitoring thread. - - self.path.on_next(Path()) - - if stop_message == "arrived": - logger.info("Arrived at goal.") - self.cancel_goal(arrived=True) - elif stop_message == "obstacle_found": - logger.info("Replanning path due to obstacle found.") - self._replan_path() - elif stop_message == "error": - logger.info("Failure in navigation.") - self._replan_path() - else: - logger.error(f"No code to handle '{stop_message}'.") - self.cancel_goal() - - def _replan_path(self) -> None: - with self._lock: - current_odom = self._current_odom - current_goal = self._current_goal - - logger.info("Replanning.", attempt=self._replan_limiter.get_attempt()) - - assert current_odom is not None - assert current_goal is not None - - if current_goal.position.distance(current_odom.position) < self._replan_goal_tolerance: - self.cancel_goal(arrived=True) - return - - if not self._replan_limiter.can_retry(current_odom.position): - self.cancel_goal() - return - - self._replan_limiter.will_retry() - - self._plan_path() - - def _plan_path(self) -> None: - self.cancel_goal(but_will_try_again=True) - - with self._lock: - current_odom = self._current_odom - current_goal = self._current_goal - - assert current_goal is not None - - if current_odom is None: - logger.warning("Cannot handle goal request: missing odometry.") - return - - safe_goal = self._find_safe_goal(current_goal.position) - - if not safe_goal: - return - - path = self._find_wide_path(safe_goal, current_odom.position) - - if not path: - logger.warning( - "No path found to the goal.", x=round(safe_goal.x, 3), y=round(safe_goal.y, 3) - ) - return - - resampled_path = smooth_resample_path(path, current_goal, 0.1) - - self.path.on_next(resampled_path) - - self._local_planner.start_planning(resampled_path) - - def _find_wide_path(self, goal: Vector3, robot_pos: Vector3) -> Path | None: - # sizes_to_try: list[float] = [2.2, 1.7, 1.3, 1] - sizes_to_try: list[float] = [1.1] - - for size in sizes_to_try: - costmap = self._navigation_map.make_gradient_costmap(size) - path = min_cost_astar(costmap, goal, robot_pos) - if path and path.poses: - logger.info(f"Found path {size}x robot width.") - return path - - return None - - def _find_safe_goal(self, goal: Vector3) -> Vector3 | None: - costmap = self._navigation_map.binary_costmap - - if costmap.cell_value(goal) == CostValues.UNKNOWN: - return goal - - safe_goal = find_safe_goal( - costmap, - goal, - algorithm="bfs_contiguous", - cost_threshold=CostValues.OCCUPIED, - min_clearance=self._global_config.robot_rotation_diameter / 2, - max_search_distance=self._safe_goal_tolerance, - ) - - if safe_goal is None: - logger.warning("No safe goal found near requested target.") - return None - - goals_distance = safe_goal.distance(goal) - if goals_distance > 0.2: - logger.warning(f"Travelling to goal {goals_distance}m away from requested goal.") - - logger.info("Found safe goal.", x=round(safe_goal.x, 2), y=round(safe_goal.y, 2)) - - return safe_goal diff --git a/dimos/navigation/replanning_a_star/goal_validator.py b/dimos/navigation/replanning_a_star/goal_validator.py deleted file mode 100644 index 5cd093e955..0000000000 --- a/dimos/navigation/replanning_a_star/goal_validator.py +++ /dev/null @@ -1,264 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import deque - -import numpy as np - -from dimos.msgs.geometry_msgs import Vector3, VectorLike -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid - - -def find_safe_goal( - costmap: OccupancyGrid, - goal: VectorLike, - algorithm: str = "bfs", - cost_threshold: int = 50, - min_clearance: float = 0.3, - max_search_distance: float = 5.0, - connectivity_check_radius: int = 3, -) -> Vector3 | None: - """ - Find a safe goal position when the original goal is in collision or too close to obstacles. - - Args: - costmap: The occupancy grid/costmap - goal: Original goal position in world coordinates - algorithm: Algorithm to use ("bfs", "spiral", "voronoi", "gradient_descent") - cost_threshold: Maximum acceptable cost for a safe position (default: 50) - min_clearance: Minimum clearance from obstacles in meters (default: 0.3m) - max_search_distance: Maximum distance to search from original goal in meters (default: 5.0m) - connectivity_check_radius: Radius in cells to check for connectivity (default: 3) - - Returns: - Safe goal position in world coordinates, or None if no safe position found - """ - - if algorithm == "bfs": - return _find_safe_goal_bfs( - costmap, - goal, - cost_threshold, - min_clearance, - max_search_distance, - connectivity_check_radius, - ) - elif algorithm == "bfs_contiguous": - return _find_safe_goal_bfs_contiguous( - costmap, - goal, - cost_threshold, - min_clearance, - max_search_distance, - connectivity_check_radius, - ) - else: - raise ValueError(f"Unknown algorithm: {algorithm}") - - -def _find_safe_goal_bfs( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, - connectivity_check_radius: int, -) -> Vector3 | None: - """ - BFS-based search for nearest safe goal position. - This guarantees finding the closest valid position. - - Pros: - - Guarantees finding the closest safe position - - Can check connectivity to avoid isolated spots - - Efficient for small to medium search areas - - Cons: - - Can be slower for large search areas - - Memory usage scales with search area - """ - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - gx, gy = int(goal_grid.x), int(goal_grid.y) - - # Convert distances to grid cells - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) - - # BFS queue and visited set - queue = deque([(gx, gy, 0)]) - visited = set([(gx, gy)]) - - # 8-connected neighbors - neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] - - while queue: - x, y, dist = queue.popleft() - - # Check if we've exceeded max search distance - if dist > max_search_cells: - break - - # Check if position is valid - if _is_position_safe( - costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius - ): - # Convert back to world coordinates - return costmap.grid_to_world((x, y)) - - # Add neighbors to queue - for dx, dy in neighbors: - nx, ny = x + dx, y + dy - - # Check bounds - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - if (nx, ny) not in visited: - visited.add((nx, ny)) - queue.append((nx, ny, dist + 1)) - - return None - - -def _find_safe_goal_bfs_contiguous( - costmap: OccupancyGrid, - goal: VectorLike, - cost_threshold: int, - min_clearance: float, - max_search_distance: float, - connectivity_check_radius: int, -) -> Vector3 | None: - """ - BFS-based search for nearest safe goal position, only following passable cells. - Unlike regular BFS, this only expands through cells with occupancy < 100, - ensuring the path doesn't cross through impassable obstacles. - - Pros: - - Guarantees finding the closest safe position reachable without crossing obstacles - - Ensures connectivity to the goal through passable space - - Good for finding safe positions in the same "room" or connected area - - Cons: - - May not find nearby safe spots if they're on the other side of a wall - - Slightly slower than regular BFS due to additional checks - """ - - # Convert goal to grid coordinates - goal_grid = costmap.world_to_grid(goal) - gx, gy = int(goal_grid.x), int(goal_grid.y) - - # Convert distances to grid cells - clearance_cells = int(np.ceil(min_clearance / costmap.resolution)) - max_search_cells = int(np.ceil(max_search_distance / costmap.resolution)) - - # BFS queue and visited set - queue = deque([(gx, gy, 0)]) - visited = set([(gx, gy)]) - - # 8-connected neighbors - neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (1, -1), (-1, 1), (-1, -1)] - - while queue: - x, y, dist = queue.popleft() - - # Check if we've exceeded max search distance - if dist > max_search_cells: - break - - # Check if position is valid - if _is_position_safe( - costmap, x, y, cost_threshold, clearance_cells, connectivity_check_radius - ): - # Convert back to world coordinates - return costmap.grid_to_world((x, y)) - - # Add neighbors to queue - for dx, dy in neighbors: - nx, ny = x + dx, y + dy - - # Check bounds - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - if (nx, ny) not in visited: - # Only expand through passable cells (occupancy < 100) - if costmap.grid[ny, nx] < 100: - visited.add((nx, ny)) - queue.append((nx, ny, dist + 1)) - - return None - - -def _is_position_safe( - costmap: OccupancyGrid, - x: int, - y: int, - cost_threshold: int, - clearance_cells: int, - connectivity_check_radius: int, -) -> bool: - """ - Check if a position is safe based on multiple criteria. - - Args: - costmap: The occupancy grid - x, y: Grid coordinates to check - cost_threshold: Maximum acceptable cost - clearance_cells: Minimum clearance in cells - connectivity_check_radius: Radius to check for connectivity - - Returns: - True if position is safe, False otherwise - """ - - # Check bounds first - if not (0 <= x < costmap.width and 0 <= y < costmap.height): - return False - - # Check if position itself is free - if costmap.grid[y, x] >= cost_threshold or costmap.grid[y, x] == CostValues.UNKNOWN: - return False - - # Check clearance around position - for dy in range(-clearance_cells, clearance_cells + 1): - for dx in range(-clearance_cells, clearance_cells + 1): - nx, ny = x + dx, y + dy - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - # Check if within circular clearance - if dx * dx + dy * dy <= clearance_cells * clearance_cells: - if costmap.grid[ny, nx] >= cost_threshold: - return False - - # Check connectivity (not surrounded by obstacles) - # Count free neighbors in a larger radius - free_count = 0 - total_count = 0 - - for dy in range(-connectivity_check_radius, connectivity_check_radius + 1): - for dx in range(-connectivity_check_radius, connectivity_check_radius + 1): - if dx == 0 and dy == 0: - continue - - nx, ny = x + dx, y + dy - if 0 <= nx < costmap.width and 0 <= ny < costmap.height: - total_count += 1 - if ( - costmap.grid[ny, nx] < cost_threshold - and costmap.grid[ny, nx] != CostValues.UNKNOWN - ): - free_count += 1 - - # Require at least 50% of neighbors to be free (not surrounded) - if total_count > 0 and free_count < total_count * 0.5: - return False - - return True diff --git a/dimos/navigation/replanning_a_star/local_planner.py b/dimos/navigation/replanning_a_star/local_planner.py deleted file mode 100644 index a5f8d9e457..0000000000 --- a/dimos/navigation/replanning_a_star/local_planner.py +++ /dev/null @@ -1,324 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from threading import Event, RLock, Thread -import time -import traceback -from typing import Literal, TypeAlias - -import numpy as np -from reactivex import Subject - -from dimos.core.global_config import GlobalConfig -from dimos.core.resource import Resource -from dimos.msgs.geometry_msgs import Twist -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.navigation.base import NavigationState -from dimos.navigation.replanning_a_star.controllers import Controller, PController -from dimos.navigation.replanning_a_star.navigation_map import NavigationMap -from dimos.navigation.replanning_a_star.path_clearance import PathClearance -from dimos.navigation.replanning_a_star.path_distancer import PathDistancer -from dimos.utils.logging_config import setup_logger -from dimos.utils.trigonometry import angle_diff - -PlannerState: TypeAlias = Literal[ - "idle", "initial_rotation", "path_following", "final_rotation", "arrived" -] -StopMessage: TypeAlias = Literal["arrived", "obstacle_found", "error"] - -logger = setup_logger() - - -class LocalPlanner(Resource): - cmd_vel: Subject[Twist] - stopped_navigating: Subject[StopMessage] - navigation_costmap: Subject[OccupancyGrid] - - _thread: Thread | None = None - _path: Path | None = None - _path_clearance: PathClearance | None = None - _path_distancer: PathDistancer | None = None - _current_odom: PoseStamped | None = None - - _pose_index: int - _lock: RLock - _stop_planning_event: Event - _state: PlannerState - _state_unique_id: int - _global_config: GlobalConfig - _navigation_map: NavigationMap - _goal_tolerance: float - _controller: Controller - - _speed: float = 0.55 - _control_frequency: float = 10 - _orientation_tolerance: float = 0.35 - _navigation_costmap_interval: float = 1.0 - _navigation_costmap_last: float = 0.0 - - def __init__( - self, global_config: GlobalConfig, navigation_map: NavigationMap, goal_tolerance: float - ) -> None: - self.cmd_vel = Subject() - self.stopped_navigating = Subject() - self.navigation_costmap = Subject() - - self._pose_index = 0 - self._lock = RLock() - self._stop_planning_event = Event() - self._state = "idle" - self._state_unique_id = 0 - self._global_config = global_config - self._navigation_map = navigation_map - self._goal_tolerance = goal_tolerance - - self._controller = PController( - self._global_config, - self._speed, - self._control_frequency, - ) - - def start(self) -> None: - pass - - def stop(self) -> None: - self.stop_planning() - - def handle_odom(self, msg: PoseStamped) -> None: - with self._lock: - self._current_odom = msg - - def start_planning(self, path: Path) -> None: - self.stop_planning() - - self._stop_planning_event = Event() - - with self._lock: - self._path = path - self._path_clearance = PathClearance(self._global_config, self._path) - self._path_distancer = PathDistancer(self._path) - self._pose_index = 0 - self._thread = Thread(target=self._thread_entrypoint, daemon=True) - self._thread.start() - - def stop_planning(self) -> None: - self.cmd_vel.on_next(Twist()) - self._stop_planning_event.set() - - with self._lock: - self._thread = None - - self._reset_state() - - def get_state(self) -> NavigationState: - with self._lock: - state = self._state - - match state: - case "idle" | "arrived": - return NavigationState.IDLE - case "initial_rotation" | "path_following" | "final_rotation": - return NavigationState.FOLLOWING_PATH - case _: - raise ValueError(f"Unknown planner state: {state}") - - def get_unique_state(self) -> tuple[PlannerState, int]: - with self._lock: - return (self._state, self._state_unique_id) - - def _thread_entrypoint(self) -> None: - try: - self._loop() - except Exception as e: - traceback.print_exc() - logger.exception("Error in local planning", exc_info=e) - self.stopped_navigating.on_next("error") - finally: - self._reset_state() - self.cmd_vel.on_next(Twist()) - - def _change_state(self, new_state: PlannerState) -> None: - self._state = new_state - self._state_unique_id += 1 - logger.info("changed state", state=new_state) - - def _loop(self) -> None: - stop_event = self._stop_planning_event - - with self._lock: - path = self._path - path_clearance = self._path_clearance - current_odom = self._current_odom - - if path is None or path_clearance is None: - raise RuntimeError("No path set for local planner.") - - # Determine initial state: skip initial_rotation if already aligned. - new_state: PlannerState = "initial_rotation" - if current_odom is not None and len(path.poses) > 0: - first_yaw = path.poses[0].orientation.euler[2] - robot_yaw = current_odom.orientation.euler[2] - initial_yaw_error = angle_diff(first_yaw, robot_yaw) - self._controller.reset_yaw_error(initial_yaw_error) - angle_in_tolerance = abs(initial_yaw_error) < self._orientation_tolerance - if angle_in_tolerance: - position_in_tolerance = ( - path.poses[0].position.distance(current_odom.position) < 0.01 - ) - if position_in_tolerance: - new_state = "final_rotation" - else: - new_state = "path_following" - - with self._lock: - self._change_state(new_state) - - while not stop_event.is_set(): - start_time = time.perf_counter() - - with self._lock: - path_clearance.update_costmap(self._navigation_map.binary_costmap) - path_clearance.update_pose_index(self._pose_index) - - self._send_navigation_costmap(path, path_clearance) - - if path_clearance.is_obstacle_ahead(): - logger.info("Obstacle detected ahead, stopping local planner.") - self.stopped_navigating.on_next("obstacle_found") - break - - with self._lock: - state: PlannerState = self._state - - if state == "initial_rotation": - cmd_vel = self._compute_initial_rotation() - elif state == "path_following": - cmd_vel = self._compute_path_following() - elif state == "final_rotation": - cmd_vel = self._compute_final_rotation() - elif state == "arrived": - self.stopped_navigating.on_next("arrived") - break - elif state == "idle": - cmd_vel = None - - if cmd_vel is not None: - self.cmd_vel.on_next(cmd_vel) - - elapsed = time.perf_counter() - start_time - sleep_time = max(0.0, (1.0 / self._control_frequency) - elapsed) - stop_event.wait(sleep_time) - - if stop_event.is_set(): - logger.info("Local planner loop exited due to stop event.") - - def _compute_initial_rotation(self) -> Twist: - with self._lock: - path = self._path - current_odom = self._current_odom - - assert path is not None - assert current_odom is not None - - first_pose = path.poses[0] - first_yaw = first_pose.orientation.euler[2] - robot_yaw = current_odom.orientation.euler[2] - yaw_error = angle_diff(first_yaw, robot_yaw) - - if abs(yaw_error) < self._orientation_tolerance: - with self._lock: - self._change_state("path_following") - return self._compute_path_following() - - return self._controller.rotate(yaw_error) - - def get_distance_to_path(self) -> float | None: - with self._lock: - path_distancer = self._path_distancer - current_odom = self._current_odom - - if path_distancer is None or current_odom is None: - return None - - current_pos = np.array([current_odom.position.x, current_odom.position.y]) - - return path_distancer.get_distance_to_path(current_pos) - - def _compute_path_following(self) -> Twist: - with self._lock: - path_distancer = self._path_distancer - current_odom = self._current_odom - - assert path_distancer is not None - assert current_odom is not None - - current_pos = np.array([current_odom.position.x, current_odom.position.y]) - - if path_distancer.distance_to_goal(current_pos) < self._goal_tolerance: - logger.info("Reached goal position, starting final rotation") - with self._lock: - self._change_state("final_rotation") - return self._compute_final_rotation() - - closest_index = path_distancer.find_closest_point_index(current_pos) - - with self._lock: - self._pose_index = closest_index - - lookahead_point = path_distancer.find_lookahead_point(closest_index) - - return self._controller.advance(lookahead_point, current_odom) - - def _compute_final_rotation(self) -> Twist: - with self._lock: - path = self._path - current_odom = self._current_odom - - assert path is not None - assert current_odom is not None - - goal_yaw = path.poses[-1].orientation.euler[2] - robot_yaw = current_odom.orientation.euler[2] - yaw_error = angle_diff(goal_yaw, robot_yaw) - - if abs(yaw_error) < self._orientation_tolerance: - logger.info("Final rotation complete, goal reached") - with self._lock: - self._change_state("arrived") - return Twist() - - return self._controller.rotate(yaw_error) - - def _reset_state(self) -> None: - with self._lock: - self._change_state("idle") - self._path = None - self._path_clearance = None - self._path_distancer = None - self._pose_index = 0 - self._controller.reset_errors() - - def _send_navigation_costmap(self, path: Path, path_clearance: PathClearance) -> None: - if "DEBUG_NAVIGATION" not in os.environ: - return - - now = time.time() - if now - self._navigation_costmap_last < self._navigation_costmap_interval: - return - - self._navigation_costmap_last = now - - self.navigation_costmap.on_next(self._navigation_map.gradient_costmap) diff --git a/dimos/navigation/replanning_a_star/min_cost_astar.py b/dimos/navigation/replanning_a_star/min_cost_astar.py deleted file mode 100644 index c3430e64d9..0000000000 --- a/dimos/navigation/replanning_a_star/min_cost_astar.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import heapq - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, VectorLike -from dimos.msgs.nav_msgs import CostValues, OccupancyGrid, Path -from dimos.utils.logging_config import setup_logger - -# Try to import C++ extension for faster pathfinding -try: - from dimos.navigation.replanning_a_star.min_cost_astar_ext import ( - min_cost_astar_cpp as _astar_cpp, - ) - - _USE_CPP = True -except ImportError: - _USE_CPP = False - -logger = setup_logger() - -# Define possible movements (8-connected grid with diagonal movements) -_directions = [ - (0, 1), - (1, 0), - (0, -1), - (-1, 0), - (1, 1), - (1, -1), - (-1, 1), - (-1, -1), -] - -# Cost for each movement (straight vs diagonal) -_sc = 1.0 # Straight cost -_dc = 1.42 # Diagonal cost (approximately sqrt(2)) -_movement_costs = [_sc, _sc, _sc, _sc, _dc, _dc, _dc, _dc] - - -# Heuristic function (Octile distance for 8-connected grid) -def _heuristic(x1: int, y1: int, x2: int, y2: int) -> float: - dx = abs(x2 - x1) - dy = abs(y2 - y1) - # Octile distance: optimal for 8-connected grids with diagonal movement - return (dx + dy) + (_dc - 2 * _sc) * min(dx, dy) - - -def _reconstruct_path( - parents: dict[tuple[int, int], tuple[int, int]], - current: tuple[int, int], - costmap: OccupancyGrid, - start_tuple: tuple[int, int], - goal_tuple: tuple[int, int], -) -> Path: - waypoints: list[PoseStamped] = [] - while current in parents: - world_point = costmap.grid_to_world(current) - pose = PoseStamped( - frame_id="world", - position=[world_point.x, world_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), # Identity quaternion - ) - waypoints.append(pose) - current = parents[current] - - start_world_point = costmap.grid_to_world(start_tuple) - start_pose = PoseStamped( - frame_id="world", - position=[start_world_point.x, start_world_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - waypoints.append(start_pose) - - waypoints.reverse() - - # Add the goal position if it's not already included - goal_point = costmap.grid_to_world(goal_tuple) - - if ( - not waypoints - or (waypoints[-1].x - goal_point.x) ** 2 + (waypoints[-1].y - goal_point.y) ** 2 > 1e-10 - ): - goal_pose = PoseStamped( - frame_id="world", - position=[goal_point.x, goal_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - waypoints.append(goal_pose) - - return Path(frame_id="world", poses=waypoints) - - -def _reconstruct_path_from_coords( - path_coords: list[tuple[int, int]], - costmap: OccupancyGrid, -) -> Path: - waypoints: list[PoseStamped] = [] - - for gx, gy in path_coords: - world_point = costmap.grid_to_world((gx, gy)) - pose = PoseStamped( - frame_id="world", - position=[world_point.x, world_point.y, 0.0], - orientation=Quaternion(0, 0, 0, 1), - ) - waypoints.append(pose) - - return Path(frame_id="world", poses=waypoints) - - -def min_cost_astar( - costmap: OccupancyGrid, - goal: VectorLike, - start: VectorLike = (0.0, 0.0), - cost_threshold: int = 100, - unknown_penalty: float = 0.8, - use_cpp: bool = True, -) -> Path | None: - start_vector = costmap.world_to_grid(start) - goal_vector = costmap.world_to_grid(goal) - - start_tuple = (int(start_vector.x), int(start_vector.y)) - goal_tuple = (int(goal_vector.x), int(goal_vector.y)) - - if not (0 <= goal_tuple[0] < costmap.width and 0 <= goal_tuple[1] < costmap.height): - return None - - if use_cpp: - if _USE_CPP: - path_coords = _astar_cpp( - costmap.grid, - start_tuple[0], - start_tuple[1], - goal_tuple[0], - goal_tuple[1], - cost_threshold, - unknown_penalty, - ) - if not path_coords: - return None - return _reconstruct_path_from_coords(path_coords, costmap) - else: - logger.warning("C++ A* module could not be imported. Using Python.") - - open_set: list[tuple[float, float, tuple[int, int]]] = [] # Priority queue for nodes to explore - closed_set: set[tuple[int, int]] = set() # Set of explored nodes - - # Dictionary to store cost and distance from start, and parents for each node - # Track cumulative cell cost and path length separately - cost_score: dict[tuple[int, int], float] = {start_tuple: 0.0} # Cumulative cell cost - dist_score: dict[tuple[int, int], float] = {start_tuple: 0.0} # Cumulative path length - parents: dict[tuple[int, int], tuple[int, int]] = {} - - # Start with the starting node - # Priority: (total_cost + heuristic_cost, total_distance + heuristic_distance, node) - h_dist = _heuristic(start_tuple[0], start_tuple[1], goal_tuple[0], goal_tuple[1]) - heapq.heappush(open_set, (0.0, h_dist, start_tuple)) - - while open_set: - _, _, current = heapq.heappop(open_set) - current_x, current_y = current - - if current in closed_set: - continue - - if current == goal_tuple: - return _reconstruct_path(parents, current, costmap, start_tuple, goal_tuple) - - closed_set.add(current) - - for i, (dx, dy) in enumerate(_directions): - neighbor_x, neighbor_y = current_x + dx, current_y + dy - neighbor = (neighbor_x, neighbor_y) - - if not (0 <= neighbor_x < costmap.width and 0 <= neighbor_y < costmap.height): - continue - - if neighbor in closed_set: - continue - - neighbor_val = costmap.grid[neighbor_y, neighbor_x] - - if neighbor_val >= cost_threshold: - continue - - if neighbor_val == CostValues.UNKNOWN: - # Unknown cells have a moderate traversal cost - cell_cost = cost_threshold * unknown_penalty - elif neighbor_val == CostValues.FREE: - cell_cost = 0.0 - else: - cell_cost = neighbor_val - - tentative_cost = cost_score[current] + cell_cost - tentative_dist = dist_score[current] + _movement_costs[i] - - # Get the current scores for the neighbor or set to infinity if not yet explored - neighbor_cost = cost_score.get(neighbor, float("inf")) - neighbor_dist = dist_score.get(neighbor, float("inf")) - - # If this path to the neighbor is better (prioritize cost, then distance) - if (tentative_cost, tentative_dist) < (neighbor_cost, neighbor_dist): - # Update the neighbor's scores and parent - parents[neighbor] = current - cost_score[neighbor] = tentative_cost - dist_score[neighbor] = tentative_dist - - # Calculate priority: cost first, then distance (both with heuristic) - h_dist = _heuristic(neighbor_x, neighbor_y, goal_tuple[0], goal_tuple[1]) - priority_cost = tentative_cost - priority_dist = tentative_dist + h_dist - - # Add the neighbor to the open set with its priority - heapq.heappush(open_set, (priority_cost, priority_dist, neighbor)) - - return None diff --git a/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp b/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp deleted file mode 100644 index f19b3bf826..0000000000 --- a/dimos/navigation/replanning_a_star/min_cost_astar_cpp.cpp +++ /dev/null @@ -1,265 +0,0 @@ -// Copyright 2025 Dimensional Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace py = pybind11; - -// Movement directions (8-connected grid) -// Order: right, down, left, up, down-right, down-left, up-right, up-left -constexpr int DX[8] = {0, 1, 0, -1, 1, 1, -1, -1}; -constexpr int DY[8] = {1, 0, -1, 0, 1, -1, 1, -1}; - -// Movement costs: straight = 1.0, diagonal = sqrt(2) ≈ 1.42 -constexpr double STRAIGHT_COST = 1.0; -constexpr double DIAGONAL_COST = 1.42; -constexpr double MOVE_COSTS[8] = { - STRAIGHT_COST, STRAIGHT_COST, STRAIGHT_COST, STRAIGHT_COST, - DIAGONAL_COST, DIAGONAL_COST, DIAGONAL_COST, DIAGONAL_COST -}; - -constexpr int8_t COST_UNKNOWN = -1; -constexpr int8_t COST_FREE = 0; - -// Pack coordinates into a single 64-bit key for fast hashing -inline uint64_t pack_coords(int x, int y) { - return (static_cast(static_cast(x)) << 32) | - static_cast(static_cast(y)); -} - -// Unpack coordinates from 64-bit key -inline std::pair unpack_coords(uint64_t key) { - return {static_cast(key >> 32), static_cast(key & 0xFFFFFFFF)}; -} - -// Octile distance heuristic - optimal for 8-connected grids with diagonal movement -inline double heuristic(int x1, int y1, int x2, int y2) { - int dx = std::abs(x2 - x1); - int dy = std::abs(y2 - y1); - // Octile distance: straight moves + diagonal adjustment - return (dx + dy) + (DIAGONAL_COST - 2 * STRAIGHT_COST) * std::min(dx, dy); -} - -// Reconstruct path from goal to start using parent map -inline std::vector> reconstruct_path( - const std::unordered_map& parents, - uint64_t goal_key, - int start_x, - int start_y -) { - std::vector> path; - uint64_t node = goal_key; - - while (parents.count(node)) { - auto [x, y] = unpack_coords(node); - path.emplace_back(x, y); - node = parents.at(node); - } - - path.emplace_back(start_x, start_y); - std::reverse(path.begin(), path.end()); - return path; -} - -// Priority queue node: (priority_cost, priority_dist, x, y) -struct Node { - double cost; - double dist; - int x; - int y; - - // Min-heap comparison: lower values have higher priority - bool operator>(const Node& other) const { - if (cost != other.cost) return cost > other.cost; - return dist > other.dist; - } -}; - -/** - * A* pathfinding algorithm optimized for costmap grids. - * - * @param grid 2D numpy array of int8 values (height x width) - * @param start_x Starting X coordinate in grid cells - * @param start_y Starting Y coordinate in grid cells - * @param goal_x Goal X coordinate in grid cells - * @param goal_y Goal Y coordinate in grid cells - * @param cost_threshold Cells with value >= this are obstacles (default: 100) - * @param unknown_penalty Cost multiplier for unknown cells (default: 0.8) - * @return Vector of (x, y) grid coordinates from start to goal, empty if no path - */ -std::vector> min_cost_astar_cpp( - py::array_t grid, - int start_x, - int start_y, - int goal_x, - int goal_y, - int cost_threshold = 100, - double unknown_penalty = 0.8 -) { - // Get buffer info for direct array access - auto buf = grid.unchecked<2>(); - const int height = static_cast(buf.shape(0)); - const int width = static_cast(buf.shape(1)); - - // Bounds check for goal - if (goal_x < 0 || goal_x >= width || goal_y < 0 || goal_y >= height) { - return {}; - } - - // Bounds check for start - if (start_x < 0 || start_x >= width || start_y < 0 || start_y >= height) { - return {}; - } - - const uint64_t start_key = pack_coords(start_x, start_y); - const uint64_t goal_key = pack_coords(goal_x, goal_y); - - std::priority_queue, std::greater> open_set; - - std::unordered_set closed_set; - closed_set.reserve(width * height / 4); // Pre-allocate - - // Parent tracking for path reconstruction - std::unordered_map parents; - parents.reserve(width * height / 4); - - // Score tracking (cost and distance) - std::unordered_map cost_score; - std::unordered_map dist_score; - cost_score.reserve(width * height / 4); - dist_score.reserve(width * height / 4); - - // Initialize start node - cost_score[start_key] = 0.0; - dist_score[start_key] = 0.0; - double h = heuristic(start_x, start_y, goal_x, goal_y); - open_set.push({0.0, h, start_x, start_y}); - - while (!open_set.empty()) { - Node current = open_set.top(); - open_set.pop(); - - const int cx = current.x; - const int cy = current.y; - const uint64_t current_key = pack_coords(cx, cy); - - if (closed_set.count(current_key)) { - continue; - } - - if (current_key == goal_key) { - return reconstruct_path(parents, current_key, start_x, start_y); - } - - closed_set.insert(current_key); - - const double current_cost = cost_score[current_key]; - const double current_dist = dist_score[current_key]; - - // Explore all 8 neighbors - for (int i = 0; i < 8; ++i) { - const int nx = cx + DX[i]; - const int ny = cy + DY[i]; - - if (nx < 0 || nx >= width || ny < 0 || ny >= height) { - continue; - } - - const uint64_t neighbor_key = pack_coords(nx, ny); - - if (closed_set.count(neighbor_key)) { - continue; - } - - // Get cell value (note: grid is [y, x] in row-major order) - const int8_t val = buf(ny, nx); - - if (val >= cost_threshold) { - continue; - } - - double cell_cost; - if (val == COST_UNKNOWN) { - // Unknown cells have a moderate traversal cost - cell_cost = cost_threshold * unknown_penalty; - } else if (val == COST_FREE) { - cell_cost = 0.0; - } else { - cell_cost = static_cast(val); - } - - const double tentative_cost = current_cost + cell_cost; - const double tentative_dist = current_dist + MOVE_COSTS[i]; - - // Get existing scores (infinity if not yet visited) - auto cost_it = cost_score.find(neighbor_key); - auto dist_it = dist_score.find(neighbor_key); - const double n_cost = (cost_it != cost_score.end()) ? cost_it->second : INFINITY; - const double n_dist = (dist_it != dist_score.end()) ? dist_it->second : INFINITY; - - // Check if this path is better (prioritize cost, then distance) - if (tentative_cost < n_cost || - (tentative_cost == n_cost && tentative_dist < n_dist)) { - - // Update parent and scores - parents[neighbor_key] = current_key; - cost_score[neighbor_key] = tentative_cost; - dist_score[neighbor_key] = tentative_dist; - - // Calculate priority with heuristic - const double h_dist = heuristic(nx, ny, goal_x, goal_y); - const double priority_cost = tentative_cost; - const double priority_dist = tentative_dist + h_dist; - - open_set.push({priority_cost, priority_dist, nx, ny}); - } - } - } - - return {}; -} - -PYBIND11_MODULE(min_cost_astar_ext, m) { - m.doc() = "C++ implementation of A* pathfinding for costmap grids"; - - m.def("min_cost_astar_cpp", &min_cost_astar_cpp, - "A* pathfinding on a costmap grid.\n\n" - "Args:\n" - " grid: 2D numpy array of int8 values (height x width)\n" - " start_x: Starting X coordinate in grid cells\n" - " start_y: Starting Y coordinate in grid cells\n" - " goal_x: Goal X coordinate in grid cells\n" - " goal_y: Goal Y coordinate in grid cells\n" - " cost_threshold: Cells >= this value are obstacles (default: 100)\n" - " unknown_penalty: Cost multiplier for unknown cells (default: 0.8)\n\n" - "Returns:\n" - " List of (x, y) grid coordinates from start to goal, or empty list if no path", - py::arg("grid"), - py::arg("start_x"), - py::arg("start_y"), - py::arg("goal_x"), - py::arg("goal_y"), - py::arg("cost_threshold") = 100, - py::arg("unknown_penalty") = 0.8); -} diff --git a/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi b/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi deleted file mode 100644 index 558b010ce5..0000000000 --- a/dimos/navigation/replanning_a_star/min_cost_astar_ext.pyi +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -from numpy.typing import NDArray - -def min_cost_astar_cpp( - grid: NDArray[np.int8], - start_x: int, - start_y: int, - goal_x: int, - goal_y: int, - cost_threshold: int, - unknown_penalty: float, -) -> list[tuple[int, int]]: ... diff --git a/dimos/navigation/replanning_a_star/module.py b/dimos/navigation/replanning_a_star/module.py deleted file mode 100644 index d1d87cbbf6..0000000000 --- a/dimos/navigation/replanning_a_star/module.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos_lcm.std_msgs import Bool, String -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.msgs.geometry_msgs import PoseStamped, Twist -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.navigation.base import NavigationInterface, NavigationState -from dimos.navigation.replanning_a_star.global_planner import GlobalPlanner - - -class ReplanningAStarPlanner(Module, NavigationInterface): - odom: In[PoseStamped] # TODO: Use TF. - global_costmap: In[OccupancyGrid] - goal_request: In[PoseStamped] - target: In[PoseStamped] - - goal_reached: Out[Bool] - navigation_state: Out[String] # TODO: set it - cmd_vel: Out[Twist] - path: Out[Path] - navigation_costmap: Out[OccupancyGrid] - - _planner: GlobalPlanner - _global_config: GlobalConfig - - def __init__(self, cfg: GlobalConfig = global_config) -> None: - super().__init__() - self._global_config = cfg - self._planner = GlobalPlanner(self._global_config) - - @rpc - def start(self) -> None: - super().start() - - self._disposables.add(Disposable(self.odom.subscribe(self._planner.handle_odom))) - self._disposables.add( - Disposable(self.global_costmap.subscribe(self._planner.handle_global_costmap)) - ) - self._disposables.add( - Disposable(self.goal_request.subscribe(self._planner.handle_goal_request)) - ) - self._disposables.add(Disposable(self.target.subscribe(self._planner.handle_goal_request))) - - self._disposables.add(self._planner.path.subscribe(self.path.publish)) - - self._disposables.add(self._planner.cmd_vel.subscribe(self.cmd_vel.publish)) - - self._disposables.add(self._planner.goal_reached.subscribe(self.goal_reached.publish)) - - if "DEBUG_NAVIGATION" in os.environ: - self._disposables.add( - self._planner.navigation_costmap.subscribe(self.navigation_costmap.publish) - ) - - self._planner.start() - - @rpc - def stop(self) -> None: - self.cancel_goal() - self._planner.stop() - - super().stop() - - @rpc - def set_goal(self, goal: PoseStamped) -> bool: - self._planner.handle_goal_request(goal) - return True - - @rpc - def get_state(self) -> NavigationState: - return self._planner.get_state() - - @rpc - def is_goal_reached(self) -> bool: - return self._planner.is_goal_reached() - - @rpc - def cancel_goal(self) -> bool: - self._planner.cancel_goal() - return True - - -replanning_a_star_planner = ReplanningAStarPlanner.blueprint - -__all__ = ["ReplanningAStarPlanner", "replanning_a_star_planner"] diff --git a/dimos/navigation/replanning_a_star/navigation_map.py b/dimos/navigation/replanning_a_star/navigation_map.py deleted file mode 100644 index f1c149ded6..0000000000 --- a/dimos/navigation/replanning_a_star/navigation_map.py +++ /dev/null @@ -1,66 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import RLock - -from dimos.core.global_config import GlobalConfig -from dimos.mapping.occupancy.path_map import make_navigation_map -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid - - -class NavigationMap: - _global_config: GlobalConfig - _binary: OccupancyGrid | None = None - _lock: RLock - - def __init__(self, global_config: GlobalConfig) -> None: - self._global_config = global_config - self._lock = RLock() - - def update(self, occupancy_grid: OccupancyGrid) -> None: - with self._lock: - self._binary = occupancy_grid - - @property - def binary_costmap(self) -> OccupancyGrid: - """ - Get the latest binary costmap received from the global costmap source. - """ - - with self._lock: - if self._binary is None: - raise ValueError("No current global costmap available") - - return self._binary - - @property - def gradient_costmap(self) -> OccupancyGrid: - return self.make_gradient_costmap() - - def make_gradient_costmap(self, robot_increase: float = 1.0) -> OccupancyGrid: - """ - Get the latest navigation map created from inflating and applying a - gradient to the binary costmap. - """ - - with self._lock: - binary = self._binary - if binary is None: - raise ValueError("No current global costmap available") - - return make_navigation_map( - binary, - self._global_config.robot_width * robot_increase, - strategy=self._global_config.planner_strategy, - ) diff --git a/dimos/navigation/replanning_a_star/path_clearance.py b/dimos/navigation/replanning_a_star/path_clearance.py deleted file mode 100644 index e99fba26c3..0000000000 --- a/dimos/navigation/replanning_a_star/path_clearance.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import RLock - -import numpy as np -from numpy.typing import NDArray - -from dimos.core.global_config import GlobalConfig -from dimos.mapping.occupancy.path_mask import make_path_mask -from dimos.msgs.nav_msgs import Path -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid - - -class PathClearance: - _costmap: OccupancyGrid | None = None - _last_costmap: OccupancyGrid | None = None - _path_lookup_distance: float = 3.0 - _max_distance_cache: float = 1.0 - _last_used_shape: tuple[int, ...] | None = None - _last_mask: NDArray[np.bool_] | None = None - _last_used_pose: int | None = None - _global_config: GlobalConfig - _lock: RLock - _path: Path - _pose_index: int - - def __init__(self, global_config: GlobalConfig, path: Path) -> None: - self._global_config = global_config - self._path = path - self._pose_index = 0 - self._lock = RLock() - - def update_costmap(self, costmap: OccupancyGrid) -> None: - with self._lock: - self._costmap = costmap - - def update_pose_index(self, index: int) -> None: - with self._lock: - self._pose_index = index - - @property - def mask(self) -> NDArray[np.bool_]: - with self._lock: - costmap = self._costmap - pose_index = self._pose_index - - assert costmap is not None - - if ( - self._last_mask is not None - and self._last_used_pose is not None - and costmap.grid.shape == self._last_used_shape - and self._pose_distance(self._last_used_pose, pose_index) < self._max_distance_cache - ): - return self._last_mask - - self._last_mask = make_path_mask( - occupancy_grid=costmap, - path=self._path, - robot_width=self._global_config.robot_width, - pose_index=pose_index, - max_length=self._path_lookup_distance, - ) - - self._last_used_shape = costmap.grid.shape - self._last_used_pose = pose_index - - return self._last_mask - - def is_obstacle_ahead(self) -> bool: - with self._lock: - costmap = self._costmap - - if costmap is None: - return True - - return bool(np.any(costmap.grid[self.mask] == CostValues.OCCUPIED)) - - def _pose_distance(self, index1: int, index2: int) -> float: - p1 = self._path.poses[index1].position - p2 = self._path.poses[index2].position - return p1.distance(p2) diff --git a/dimos/navigation/replanning_a_star/path_distancer.py b/dimos/navigation/replanning_a_star/path_distancer.py deleted file mode 100644 index 04d844267f..0000000000 --- a/dimos/navigation/replanning_a_star/path_distancer.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import cast - -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.nav_msgs import Path - - -class PathDistancer: - _lookahead_dist: float = 0.5 - _path: NDArray[np.float64] - _cumulative_dists: NDArray[np.float64] - - def __init__(self, path: Path) -> None: - self._path = np.array([[p.position.x, p.position.y] for p in path.poses]) - self._cumulative_dists = _make_cumulative_distance_array(self._path) - - def find_lookahead_point(self, start_idx: int) -> NDArray[np.float64]: - """ - Given a path, and a precomputed array of cumulative distances, find the - point which is `lookahead_dist` ahead of the current point. - """ - - if start_idx >= len(self._path) - 1: - return cast("NDArray[np.float64]", self._path[-1]) - - # Distance from path[0] to path[start_idx]. - base_dist = self._cumulative_dists[start_idx - 1] if start_idx > 0 else 0.0 - target_dist = base_dist + self._lookahead_dist - - # Binary search: cumulative_dists[i] = distance from path[0] to path[i+1] - idx = int(np.searchsorted(self._cumulative_dists, target_dist)) - - if idx >= len(self._cumulative_dists): - return cast("NDArray[np.float64]", self._path[-1]) - - # Interpolate within segment from path[idx] to path[idx+1]. - prev_cum_dist = self._cumulative_dists[idx - 1] if idx > 0 else 0.0 - segment_dist = self._cumulative_dists[idx] - prev_cum_dist - remaining_dist = target_dist - prev_cum_dist - - if segment_dist > 0: - t = remaining_dist / segment_dist - return cast( - "NDArray[np.float64]", - self._path[idx] + t * (self._path[idx + 1] - self._path[idx]), - ) - - return cast("NDArray[np.float64]", self._path[idx]) - - def distance_to_goal(self, current_pos: NDArray[np.float64]) -> float: - return float(np.linalg.norm(self._path[-1] - current_pos)) - - def get_distance_to_path(self, pos: NDArray[np.float64]) -> float: - index = self.find_closest_point_index(pos) - return float(np.linalg.norm(self._path[index] - pos)) - - def find_closest_point_index(self, pos: NDArray[np.float64]) -> int: - """Find the index of the closest point on the path.""" - distances = np.linalg.norm(self._path - pos, axis=1) - return int(np.argmin(distances)) - - -def _make_cumulative_distance_array(array: NDArray[np.float64]) -> NDArray[np.float64]: - """ - For an array representing 2D points, create an array of all the distances - between the points. - """ - - if len(array) < 2: - return np.array([0.0]) - - segments = array[1:] - array[:-1] - segment_dists = np.linalg.norm(segments, axis=1) - return np.cumsum(segment_dists) diff --git a/dimos/navigation/replanning_a_star/position_tracker.py b/dimos/navigation/replanning_a_star/position_tracker.py deleted file mode 100644 index 77b4df0dd0..0000000000 --- a/dimos/navigation/replanning_a_star/position_tracker.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import RLock -import time -from typing import cast - -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped - -_max_points_per_second = 1000 - - -class PositionTracker: - _lock: RLock - _time_window: float - _max_points: int - _threshold: float - _timestamps: NDArray[np.float32] - _positions: NDArray[np.float32] - _index: int - _size: int - - def __init__(self, time_window: float) -> None: - self._lock = RLock() - self._time_window = time_window - self._threshold = 0.4 - self._max_points = int(_max_points_per_second * self._time_window) - self.reset_data() - - def reset_data(self) -> None: - with self._lock: - self._timestamps = np.zeros(self._max_points, dtype=np.float32) - self._positions = np.zeros((self._max_points, 2), dtype=np.float32) - self._index = 0 - self._size = 0 - - def add_position(self, pose: PoseStamped) -> None: - with self._lock: - self._timestamps[self._index] = time.time() - self._positions[self._index] = (pose.position.x, pose.position.y) - self._index = (self._index + 1) % self._max_points - self._size = min(self._size + 1, self._max_points) - - def _get_recent_positions(self) -> NDArray[np.float32]: - cutoff = time.time() - self._time_window - - if self._size == 0: - return np.empty((0, 2), dtype=np.float32) - - if self._size < self._max_points: - mask = self._timestamps[: self._size] >= cutoff - return self._positions[: self._size][mask] - - ts = np.concatenate([self._timestamps[self._index :], self._timestamps[: self._index]]) - pos = np.concatenate([self._positions[self._index :], self._positions[: self._index]]) - mask = ts >= cutoff - return cast("NDArray[np.float32]", pos[mask]) - - def is_stuck(self) -> bool: - with self._lock: - recent = self._get_recent_positions() - - if len(recent) == 0: - return False - - centroid = recent.mean(axis=0) - distances = np.linalg.norm(recent - centroid, axis=1) - - return bool(np.all(distances < self._threshold)) diff --git a/dimos/navigation/replanning_a_star/replan_limiter.py b/dimos/navigation/replanning_a_star/replan_limiter.py deleted file mode 100644 index 8cc630f3df..0000000000 --- a/dimos/navigation/replanning_a_star/replan_limiter.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import RLock - -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class ReplanLimiter: - """ - This class limits replanning too many times in the same area. But if we exit - the area, the number of attempts is reset. - """ - - _max_attempts: int = 6 - _reset_distance: float = 2.0 - _attempt_pos: Vector3 | None = None - _lock: RLock - - _attempt: int - - def __init__(self) -> None: - self._lock = RLock() - self._attempt = 0 - - def can_retry(self, position: Vector3) -> bool: - with self._lock: - if self._attempt == 0: - self._attempt_pos = position - - if self._attempt >= 1 and self._attempt_pos: - distance = self._attempt_pos.distance(position) - if distance >= self._reset_distance: - logger.info( - "Traveled enough to reset attempts", - attempts=self._attempt, - distance=distance, - ) - self._attempt = 0 - self._attempt_pos = position - - return self._attempt + 1 <= self._max_attempts - - def will_retry(self) -> None: - with self._lock: - self._attempt += 1 - - def reset(self) -> None: - with self._lock: - self._attempt = 0 - - def get_attempt(self) -> int: - with self._lock: - return self._attempt diff --git a/dimos/navigation/replanning_a_star/test_goal_validator.py b/dimos/navigation/replanning_a_star/test_goal_validator.py deleted file mode 100644 index 4cda9de863..0000000000 --- a/dimos/navigation/replanning_a_star/test_goal_validator.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs import Vector3 -from dimos.msgs.nav_msgs.OccupancyGrid import CostValues, OccupancyGrid -from dimos.navigation.replanning_a_star.goal_validator import find_safe_goal -from dimos.utils.data import get_data - - -@pytest.fixture -def costmap() -> OccupancyGrid: - return OccupancyGrid(np.load(get_data("occupancy_simple.npy"))) - - -@pytest.mark.parametrize( - "input_pos,expected_pos", - [ - # Identical. - ((6.15, 10.0), (6.15, 10.0)), - # Very slightly off. - ((6.0, 10.0), (6.05, 10.0)), - # Don't pick a spot that's the closest, but is actually on the other side of the wall. - ((5.0, 9.0), (5.85, 9.6)), - ], -) -def test_find_safe_goal(costmap, input_pos, expected_pos) -> None: - goal = Vector3(input_pos[0], input_pos[1], 0.0) - - safe_goal = find_safe_goal( - costmap, - goal, - algorithm="bfs_contiguous", - cost_threshold=CostValues.OCCUPIED, - min_clearance=0.3, - max_search_distance=5.0, - connectivity_check_radius=0, - ) - - assert safe_goal == Vector3(expected_pos[0], expected_pos[1], 0.0) diff --git a/dimos/navigation/replanning_a_star/test_min_cost_astar.py b/dimos/navigation/replanning_a_star/test_min_cost_astar.py deleted file mode 100644 index 9cc0cad29a..0000000000 --- a/dimos/navigation/replanning_a_star/test_min_cost_astar.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import numpy as np -from open3d.geometry import PointCloud -import pytest - -from dimos.mapping.occupancy.gradient import gradient, voronoi_gradient -from dimos.mapping.occupancy.visualizations import visualize_occupancy_grid -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.msgs.sensor_msgs.Image import Image -from dimos.navigation.replanning_a_star.min_cost_astar import min_cost_astar -from dimos.utils.data import get_data - - -@pytest.fixture -def costmap() -> PointCloud: - return gradient(OccupancyGrid(np.load(get_data("occupancy_simple.npy"))), max_distance=1.5) - - -@pytest.fixture -def costmap_three_paths() -> PointCloud: - return voronoi_gradient(OccupancyGrid(np.load(get_data("three_paths.npy"))), max_distance=1.5) - - -def test_astar(costmap) -> None: - start = Vector3(4.0, 2.0) - goal = Vector3(6.15, 10.0) - expected = Image.from_file(get_data("astar_min_cost.png")) - - path = min_cost_astar(costmap, goal, start, use_cpp=False) - actual = visualize_occupancy_grid(costmap, "rainbow", path) - - np.testing.assert_array_equal(actual.data, expected.data) - - -def test_astar_corner(costmap_three_paths) -> None: - start = Vector3(2.8, 3.35) - goal = Vector3(6.35, 4.25) - expected = Image.from_file(get_data("astar_corner_min_cost.png")) - - path = min_cost_astar(costmap_three_paths, goal, start, use_cpp=False) - actual = visualize_occupancy_grid(costmap_three_paths, "rainbow", path) - - np.testing.assert_array_equal(actual.data, expected.data) - - -def test_astar_python_and_cpp(costmap) -> None: - start = Vector3(4.0, 2.0, 0) - goal = Vector3(6.15, 10.0) - - start_time = time.perf_counter() - path_python = min_cost_astar(costmap, goal, start, use_cpp=False) - elapsed_time_python = time.perf_counter() - start_time - print(f"\nastar Python took {elapsed_time_python:.6f} seconds") - assert path_python is not None - assert len(path_python.poses) > 0 - - start_time = time.perf_counter() - path_cpp = min_cost_astar(costmap, goal, start, use_cpp=True) - elapsed_time_cpp = time.perf_counter() - start_time - print(f"astar C++ took {elapsed_time_cpp:.6f} seconds") - assert path_cpp is not None - assert len(path_cpp.poses) > 0 - - times_better = elapsed_time_python / elapsed_time_cpp - print(f"astar C++ is {times_better:.2f} times faster than Python") - - # Assert that both implementations return almost identical points. - np.testing.assert_allclose( - [(p.position.x, p.position.y) for p in path_python.poses], - [(p.position.x, p.position.y) for p in path_cpp.poses], - atol=0.05001, - ) diff --git a/dimos/navigation/rosnav.py b/dimos/navigation/rosnav.py deleted file mode 100644 index 8efabaebee..0000000000 --- a/dimos/navigation/rosnav.py +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -NavBot class for navigation-related functionality. -Encapsulates ROS transport and topic remapping for Unitree robots. -""" - -from dataclasses import dataclass, field -import logging -import threading -import time - -from reactivex import operators as ops -from reactivex.subject import Subject - -from dimos import spec -from dimos.agents.annotation import skill -from dimos.core import DimosCluster, In, LCMTransport, Module, Out, rpc -from dimos.core._dask_exports import DimosCluster -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import In, Out -from dimos.core.transport import LCMTransport, ROSTransport -from dimos.msgs.geometry_msgs import ( - PoseStamped, - Quaternion, - Transform, - Twist, - TwistStamped, - Vector3, -) -from dimos.msgs.nav_msgs import Path -from dimos.msgs.sensor_msgs import Joy, PointCloud2 -from dimos.msgs.std_msgs import Bool, Int8 -from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.navigation.base import NavigationInterface, NavigationState -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import euler_to_quaternion - -logger = setup_logger(level=logging.INFO) - - -@dataclass -class Config(ModuleConfig): - local_pointcloud_freq: float = 2.0 - global_map_freq: float = 1.0 - sensor_to_base_link_transform: Transform = field( - default_factory=lambda: Transform(frame_id="sensor", child_frame_id="base_link") - ) - - -class ROSNav( - Module, NavigationInterface, spec.Nav, spec.GlobalPointcloud, spec.Pointcloud, spec.LocalPlanner -): - config: Config - default_config = Config - - # Existing ports (default LCM/pSHM transport) - goal_req: In[PoseStamped] - - pointcloud: Out[PointCloud2] - global_map: Out[PointCloud2] - - goal_active: Out[PoseStamped] - path_active: Out[Path] - cmd_vel: Out[Twist] - - # ROS In ports (receiving from ROS topics via ROSTransport) - ros_goal_reached: In[Bool] - ros_cmd_vel: In[TwistStamped] - ros_way_point: In[PoseStamped] - ros_registered_scan: In[PointCloud2] - ros_global_map: In[PointCloud2] - ros_path: In[Path] - ros_tf: In[TFMessage] - - # ROS Out ports (publishing to ROS topics via ROSTransport) - ros_goal_pose: Out[PoseStamped] - ros_cancel_goal: Out[Bool] - ros_soft_stop: Out[Int8] - ros_joy: Out[Joy] - - # Using RxPY Subjects for reactive data flow instead of storing state - _local_pointcloud_subject: Subject # type: ignore[type-arg] - _global_map_subject: Subject # type: ignore[type-arg] - - _current_position_running: bool = False - _goal_reach: bool | None = None - - # Navigation state tracking for NavigationInterface - _navigation_state: NavigationState = NavigationState.IDLE - _state_lock: threading.Lock - _navigation_thread: threading.Thread | None = None - _current_goal: PoseStamped | None = None - _goal_reached: bool = False - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - - # Initialize RxPY Subjects for streaming data - self._local_pointcloud_subject = Subject() - self._global_map_subject = Subject() - - # Initialize state tracking - self._state_lock = threading.Lock() - self._navigation_state = NavigationState.IDLE - self._goal_reached = False - - logger.info("NavigationModule initialized") - - @rpc - def start(self) -> None: - self._running = True - - self._disposables.add( - self._local_pointcloud_subject.pipe( - ops.sample(1.0 / self.config.local_pointcloud_freq), - ).subscribe( - on_next=self.pointcloud.publish, - on_error=lambda e: logger.error(f"Lidar stream error: {e}"), - ) - ) - - self._disposables.add( - self._global_map_subject.pipe( - ops.sample(1.0 / self.config.global_map_freq), - ).subscribe( - on_next=self.global_map.publish, - on_error=lambda e: logger.error(f"Map stream error: {e}"), - ) - ) - - # Subscribe to ROS In ports - self.ros_goal_reached.subscribe(self._on_ros_goal_reached) - self.ros_cmd_vel.subscribe(self._on_ros_cmd_vel) - self.ros_way_point.subscribe(self._on_ros_goal_waypoint) - self.ros_registered_scan.subscribe(self._on_ros_registered_scan) - self.ros_global_map.subscribe(self._on_ros_global_map) - self.ros_path.subscribe(self._on_ros_path) - self.ros_tf.subscribe(self._on_ros_tf) - - self.goal_req.subscribe(self._on_goal_pose) - logger.info("NavigationModule started with ROS transport and RxPY streams") - - def _on_ros_goal_reached(self, msg: Bool) -> None: - self._goal_reach = msg.data - if msg.data: - with self._state_lock: - self._goal_reached = True - self._navigation_state = NavigationState.IDLE - - def _on_ros_goal_waypoint(self, msg: PoseStamped) -> None: - self.goal_active.publish(msg) - - def _on_ros_cmd_vel(self, msg: TwistStamped) -> None: - self.cmd_vel.publish(Twist(linear=msg.linear, angular=msg.angular)) - - def _on_ros_registered_scan(self, msg: PointCloud2) -> None: - self._local_pointcloud_subject.on_next(msg) - - def _on_ros_global_map(self, msg: PointCloud2) -> None: - self._global_map_subject.on_next(msg) - - def _on_ros_path(self, msg: Path) -> None: - msg.frame_id = "base_link" - self.path_active.publish(msg) - - def _on_ros_tf(self, msg: TFMessage) -> None: - map_to_world_tf = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=euler_to_quaternion(Vector3(0.0, 0.0, 0.0)), - frame_id="map", - child_frame_id="world", - ts=time.time(), - ) - - self.tf.publish( - self.config.sensor_to_base_link_transform.now(), - map_to_world_tf, - *msg.transforms, - ) - - def _on_goal_pose(self, msg: PoseStamped) -> None: - self.navigate_to(msg) - - def _on_cancel_goal(self, msg: Bool) -> None: - if msg.data: - self.stop() - - def _set_autonomy_mode(self) -> None: - joy_msg = Joy( - axes=[0.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0], - buttons=[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], - ) - self.ros_joy.publish(joy_msg) - logger.info("Setting autonomy mode via Joy message") - - @skill - def goto(self, x: float, y: float) -> str: - """ - move the robot in relative coordinates - x is forward, y is left - - goto(1, 0) will move the robot forward by 1 meter - """ - pose_to = PoseStamped( - position=Vector3(x, y, 0), - orientation=Quaternion(0.0, 0.0, 0.0, 0.0), - frame_id="base_link", - ts=time.time(), - ) - - self.navigate_to(pose_to) - return "arrived" - - @skill - def goto_global(self, x: float, y: float) -> str: - """ - go to coordinates x,y in the map frame - 0,0 is your starting position - """ - target = PoseStamped( - ts=time.time(), - frame_id="map", - position=Vector3(x, y, 0.0), - orientation=Quaternion(0.0, 0.0, 0.0, 0.0), - ) - - self.navigate_to(target) - - return f"arrived to {x:.2f}, {y:.2f}" - - @rpc - def navigate_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: - """ - Navigate to a target pose by publishing to ROS topics. - - Args: - pose: Target pose to navigate to - timeout: Maximum time to wait for goal (seconds) - - Returns: - True if navigation was successful - """ - logger.info( - f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f} @ {pose.frame_id})" - ) - - self._goal_reach = None - self._set_autonomy_mode() - - # Enable soft stop (0 = enable) - self.ros_soft_stop.publish(Int8(data=0)) - self.ros_goal_pose.publish(pose) - - # Wait for goal to be reached - start_time = time.time() - while time.time() - start_time < timeout: - if self._goal_reach is not None: - self.ros_soft_stop.publish(Int8(data=2)) - return self._goal_reach - time.sleep(0.1) - - self.stop_navigation() - logger.warning(f"Navigation timed out after {timeout} seconds") - return False - - @rpc - def stop_navigation(self) -> bool: - """ - Stop current navigation by publishing to ROS topics. - - Returns: - True if stop command was sent successfully - """ - logger.info("Stopping navigation") - - self.ros_cancel_goal.publish(Bool(data=True)) - self.ros_soft_stop.publish(Int8(data=2)) - - with self._state_lock: - self._navigation_state = NavigationState.IDLE - self._current_goal = None - self._goal_reached = False - - return True - - @rpc - def set_goal(self, goal: PoseStamped) -> bool: - """Set a new navigation goal (non-blocking).""" - with self._state_lock: - self._current_goal = goal - self._goal_reached = False - self._navigation_state = NavigationState.FOLLOWING_PATH - - # Start navigation in a separate thread to make it non-blocking - if self._navigation_thread and self._navigation_thread.is_alive(): - logger.warning("Previous navigation still running, cancelling") - self.stop_navigation() - self._navigation_thread.join(timeout=1.0) - - self._navigation_thread = threading.Thread( - target=self._navigate_to_goal_async, - args=(goal,), - daemon=True, - name="ROSNavNavigationThread", - ) - self._navigation_thread.start() - - return True - - def _navigate_to_goal_async(self, goal: PoseStamped) -> None: - """Internal method to handle navigation in a separate thread.""" - try: - result = self.navigate_to(goal, timeout=60.0) - with self._state_lock: - self._goal_reached = result - self._navigation_state = NavigationState.IDLE - except Exception as e: - logger.error(f"Navigation failed: {e}") - with self._state_lock: - self._goal_reached = False - self._navigation_state = NavigationState.IDLE - - @rpc - def get_state(self) -> NavigationState: - """Get the current state of the navigator.""" - with self._state_lock: - return self._navigation_state - - @rpc - def is_goal_reached(self) -> bool: - """Check if the current goal has been reached.""" - with self._state_lock: - return self._goal_reached - - @rpc - def cancel_goal(self) -> bool: - """Cancel the current navigation goal.""" - - with self._state_lock: - had_goal = self._current_goal is not None - - if had_goal: - self.stop_navigation() - - return had_goal - - @rpc - def stop(self) -> None: - """Stop the navigation module and clean up resources.""" - self.stop_navigation() - try: - self._running = False - - self._local_pointcloud_subject.on_completed() - self._global_map_subject.on_completed() - - except Exception as e: - logger.error(f"Error during shutdown: {e}") - finally: - super().stop() - - -ros_nav = ROSNav.blueprint - - -def deploy(dimos: DimosCluster): # type: ignore[no-untyped-def] - nav = dimos.deploy(ROSNav) # type: ignore[attr-defined] - - # Existing ports on LCM transports - nav.pointcloud.transport = LCMTransport("/lidar", PointCloud2) - nav.global_map.transport = LCMTransport("/map", PointCloud2) - nav.goal_req.transport = LCMTransport("/goal_req", PoseStamped) - nav.goal_active.transport = LCMTransport("/goal_active", PoseStamped) - nav.path_active.transport = LCMTransport("/path_active", Path) - nav.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) - - # ROS In transports (receiving from ROS navigation stack) - nav.ros_goal_reached.transport = ROSTransport("/goal_reached", Bool) - nav.ros_cmd_vel.transport = ROSTransport("/cmd_vel", TwistStamped) - nav.ros_way_point.transport = ROSTransport("/way_point", PoseStamped) - nav.ros_registered_scan.transport = ROSTransport("/registered_scan", PointCloud2) - nav.ros_global_map.transport = ROSTransport("/terrain_map_ext", PointCloud2) - nav.ros_path.transport = ROSTransport("/path", Path) - nav.ros_tf.transport = ROSTransport("/tf", TFMessage) - - # ROS Out transports (publishing to ROS navigation stack) - nav.ros_goal_pose.transport = ROSTransport("/goal_pose", PoseStamped) - nav.ros_cancel_goal.transport = ROSTransport("/cancel_goal", Bool) - nav.ros_soft_stop.transport = ROSTransport("/stop", Int8) - nav.ros_joy.transport = ROSTransport("/joy", Joy) - - nav.start() - return nav - - -__all__ = ["ROSNav", "deploy", "ros_nav"] diff --git a/dimos/navigation/visual/query.py b/dimos/navigation/visual/query.py deleted file mode 100644 index 2e0951951e..0000000000 --- a/dimos/navigation/visual/query.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.models.qwen.video_query import BBox -from dimos.models.vl.base import VlModel -from dimos.msgs.sensor_msgs import Image -from dimos.utils.generic import extract_json_from_llm_response - - -def get_object_bbox_from_image( - vl_model: VlModel, image: Image, object_description: str -) -> BBox | None: - prompt = ( - f"Look at this image and find the '{object_description}'. " - "Return ONLY a JSON object with format: {'name': 'object_name', 'bbox': [x1, y1, x2, y2]} " - "where x1,y1 is the top-left and x2,y2 is the bottom-right corner of the bounding box. If not found, return None." - ) - - response = vl_model.query(image, prompt) - - result = extract_json_from_llm_response(response) - if not result: - return None - - try: - ret = tuple(map(float, result["bbox"])) - if len(ret) == 4: - return ret - except Exception: - pass - - return None diff --git a/dimos/navigation/visual_servoing/detection_navigation.py b/dimos/navigation/visual_servoing/detection_navigation.py deleted file mode 100644 index 5f89bd1faa..0000000000 --- a/dimos/navigation/visual_servoing/detection_navigation.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.sensor_msgs import CameraInfo as DimosLcmCameraInfo -import numpy as np - -from dimos.msgs.geometry_msgs import Transform, Twist, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox -from dimos.perception.detection.type.detection3d import Detection3DPC -from dimos.protocol.tf import LCMTF -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class DetectionNavigation: - _target_distance_3d: float = 1.5 # meters to maintain from person - _min_distance_3d: float = 0.8 # meters before backing up - _max_linear_speed_3d: float = 0.5 # m/s - _max_angular_speed_3d: float = 0.8 # rad/s - _linear_gain_3d: float = 0.8 - _angular_gain_3d: float = 1.5 - - _tf: LCMTF - _camera_info: CameraInfo - - def __init__(self, tf: LCMTF, camera_info: CameraInfo) -> None: - self._tf = tf - self._camera_info = camera_info - - def compute_twist_for_detection_3d( - self, pointcloud: PointCloud2, detection: Detection2DBBox, image: Image - ) -> Twist | None: - """Project a 2D detection to 3D using pointcloud and compute navigation twist. - - Args: - detection: 2D detection with bounding box - image: Current image frame - - Returns: - Twist command to navigate towards the detection's 3D position. - """ - - # Get transform from world frame to camera optical frame - world_to_optical = self._tf.get( - "camera_optical", pointcloud.frame_id, image.ts, time_tolerance=1.0 - ) - if world_to_optical is None: - logger.warning("Could not get camera transform") - return None - - lcm_camera_info = DimosLcmCameraInfo() - lcm_camera_info.K = self._camera_info.K - lcm_camera_info.width = self._camera_info.width - lcm_camera_info.height = self._camera_info.height - - # Project to 3D using the pointcloud - detection_3d = Detection3DPC.from_2d( - det=detection, - world_pointcloud=pointcloud, - camera_info=lcm_camera_info, - world_to_optical_transform=world_to_optical, - filters=[], # Skip filtering for faster processing in follow loop - ) - - if detection_3d is None: - logger.warning("3D projection failed") - return None - - # Get robot position to compute robust target - robot_transform = self._tf.get("world", "base_link", time_tolerance=1.0) - if robot_transform is None: - logger.warning("Could not get robot transform") - return None - - robot_pos = robot_transform.translation - - # Compute robust target position using front-most points - target_position = self._compute_robust_target_position(detection_3d.pointcloud, robot_pos) - if target_position is None: - logger.warning("Could not compute robust target position") - return None - - return self._compute_twist_from_3d(target_position, robot_transform) - - def _compute_robust_target_position( - self, pointcloud: PointCloud2, robot_pos: Vector3 - ) -> Vector3 | None: - """Compute a robust target position from the detection pointcloud. - - Instead of using the centroid of all points (which includes floor/background), - this method: - 1. Filters out floor points (z < 0.3m in world frame) - 2. Computes distance from robot to each remaining point - 3. Uses the 25th percentile of closest points to get the front surface - 4. Returns the centroid of those front-most points - - Args: - pointcloud: The detection's pointcloud in world frame - robot_pos: Robot's current position in world frame - - Returns: - Vector3 position representing the front of the detected object, - or None if not enough valid points. - """ - points, _ = pointcloud.as_numpy() - if len(points) < 10: - return None - - # Filter out floor points (keep points above 0.3m height) - height_mask = points[:, 2] > 0.3 - points = points[height_mask] - if len(points) < 10: - # Fall back to all points if height filtering removes too many - points, _ = pointcloud.as_numpy() - - # Compute 2D distance (XY plane) from robot to each point - dx = points[:, 0] - robot_pos.x - dy = points[:, 1] - robot_pos.y - distances = np.sqrt(dx * dx + dy * dy) - - # Use 25th percentile of distances to find front-most points - distance_threshold = np.percentile(distances, 25) - - # Get points that are within the front 25% - front_mask = distances <= distance_threshold - front_points = points[front_mask] - - if len(front_points) < 3: - # Fall back to median distance point - median_dist = np.median(distances) - close_mask = np.abs(distances - median_dist) < 0.3 - front_points = points[close_mask] - if len(front_points) < 3: - return None - - # Compute centroid of front-most points - centroid = front_points.mean(axis=0) - return Vector3(centroid[0], centroid[1], centroid[2]) - - def _compute_twist_from_3d(self, target_position: Vector3, robot_transform: Transform) -> Twist: - """Compute twist command to navigate towards a 3D target position. - - Args: - target_position: 3D position of the target in world frame. - robot_transform: Robot's current transform in world frame. - - Returns: - Twist command for the robot. - """ - robot_pos = robot_transform.translation - - # Compute vector from robot to target in world frame - dx = target_position.x - robot_pos.x - dy = target_position.y - robot_pos.y - distance = np.sqrt(dx * dx + dy * dy) - print(f"Distance to target: {distance:.2f} m") - - # Compute angle to target in world frame - angle_to_target = np.arctan2(dy, dx) - - # Get robot's current heading from transform - robot_yaw = robot_transform.rotation.to_euler().z - - # Angle error (how much to turn) - angle_error = angle_to_target - robot_yaw - # Normalize to [-pi, pi] - while angle_error > np.pi: - angle_error -= 2 * np.pi - while angle_error < -np.pi: - angle_error += 2 * np.pi - - # Compute angular velocity (turn towards target) - angular_z = angle_error * self._angular_gain_3d - angular_z = float( - np.clip(angular_z, -self._max_angular_speed_3d, self._max_angular_speed_3d) - ) - - # Compute linear velocity based on distance - distance_error = distance - self._target_distance_3d - - if distance < self._min_distance_3d: - # Too close, back up - linear_x = -self._max_linear_speed_3d * 0.6 - else: - # Move forward based on distance error, reduce speed when turning - turn_factor = 1.0 - min(abs(angle_error) / np.pi, 0.7) - linear_x = distance_error * self._linear_gain_3d * turn_factor - linear_x = float( - np.clip(linear_x, -self._max_linear_speed_3d, self._max_linear_speed_3d) - ) - - return Twist( - linear=Vector3(linear_x, 0.0, 0.0), - angular=Vector3(0.0, 0.0, angular_z), - ) diff --git a/dimos/navigation/visual_servoing/visual_servoing_2d.py b/dimos/navigation/visual_servoing/visual_servoing_2d.py deleted file mode 100644 index 032b5f3370..0000000000 --- a/dimos/navigation/visual_servoing/visual_servoing_2d.py +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np - -from dimos.msgs.geometry_msgs import Twist, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo - - -class VisualServoing2D: - """2D visual servoing controller for tracking objects using bounding boxes. - - Uses camera intrinsics to convert pixel coordinates to normalized camera - coordinates and estimates distance based on known object width. - """ - - # Target distance to maintain from object (meters). - _target_distance: float = 1.5 - - # Minimum distance before backing up (meters). - _min_distance: float = 0.8 - - # Maximum forward/backward speed (m/s). - _max_linear_speed: float = 0.5 - - # Maximum turning speed (rad/s). - _max_angular_speed: float = 0.8 - - # Assumed real-world width of tracked object (meters). - _assumed_object_width: float = 0.45 - - # Proportional gain for angular velocity control. - _angular_gain: float = 1.0 - - # Proportional gain for linear velocity control. - _linear_gain: float = 0.8 - - # Speed factor when backing up (multiplied by max_linear_speed). - _backup_speed_factor: float = 0.6 - - # Multiplier for x_norm when calculating turn factor. - _turn_factor_multiplier: float = 2.0 - - # Maximum speed reduction due to turning (turn_factor ranges from 1-this to 1). - _turn_factor_max_reduction: float = 0.7 - - _rotation_requires_linear_movement: bool = False - - # Camera intrinsics for coordinate conversion. - _camera_info: CameraInfo - - def __init__( - self, camera_info: CameraInfo, rotation_requires_linear_movement: bool = False - ) -> None: - self._camera_info = camera_info - self._rotation_requires_linear_movement = rotation_requires_linear_movement - - def compute_twist( - self, - bbox: tuple[float, float, float, float], - image_width: int, - ) -> Twist: - """Compute twist command to servo towards the tracked object. - - Args: - bbox: Bounding box (x1, y1, x2, y2) in pixels. - image_width: Width of the image. - - Returns: - Twist command for the robot. - """ - x1, _, x2, _ = bbox - bbox_center_x = (x1 + x2) / 2.0 - - # Get normalized x coordinate using inverse K matrix - # Positive = object is to the right of optical center - x_norm = self._get_normalized_x(bbox_center_x) - - estimated_distance = self._estimate_distance(bbox) - - if estimated_distance is None: - return Twist.zero() - - # Calculate distance error (positive = too far, need to move forward) - distance_error = estimated_distance - self._target_distance - - # Compute angular velocity (turn towards object) - # Negative because positive angular.z is counter-clockwise (left turn) - angular_z = -x_norm * self._angular_gain - angular_z = float(np.clip(angular_z, -self._max_angular_speed, self._max_angular_speed)) - - # Compute linear velocity - ALWAYS move forward/backward based on distance. - # Reduce forward speed when turning sharply to maintain stability. - turn_factor = 1.0 - min( - abs(x_norm) * self._turn_factor_multiplier, self._turn_factor_max_reduction - ) - - if estimated_distance < self._min_distance: - # Too close, back up (don't reduce speed for backing up) - linear_x = -self._max_linear_speed * self._backup_speed_factor - else: - # Move forward based on distance error with proportional gain - linear_x = distance_error * self._linear_gain * turn_factor - linear_x = float(np.clip(linear_x, -self._max_linear_speed, self._max_linear_speed)) - - # Enforce minimum linear speed when turning - if self._rotation_requires_linear_movement and abs(angular_z) < 0.02: - linear_x = max(linear_x, 0.1) - - return Twist( - linear=Vector3(linear_x, 0.0, 0.0), - angular=Vector3(0.0, 0.0, angular_z), - ) - - def _get_normalized_x(self, pixel_x: float) -> float: - """Convert pixel x coordinate to normalized camera coordinate. - - Uses inverse K matrix: x_norm = (pixel_x - cx) / fx - - Args: - pixel_x: x coordinate in pixels - - Returns: - Normalized x coordinate (tan of angle from optical center) - """ - fx = self._camera_info.K[0] # focal length x - cx = self._camera_info.K[2] # optical center x - return (pixel_x - cx) / fx - - def _estimate_distance(self, bbox: tuple[float, float, float, float]) -> float | None: - """Estimate distance to object based on bounding box size and camera intrinsics. - - Uses the pinhole camera model: - pixel_width / fx = real_width / distance - distance = (real_width * fx) / pixel_width - - Uses bbox width instead of height because ground robot can't see full - person height when close. Width (shoulders) is more consistently visible. - - Args: - bbox: Bounding box (x1, y1, x2, y2) in pixels. - - Returns: - Estimated distance in meters, or None if bbox is invalid. - """ - bbox_width = bbox[2] - bbox[0] # x2 - x1 - - if bbox_width <= 0: - return None - - # Pinhole camera model: distance = (real_width * fx) / pixel_width - fx = self._camera_info.K[0] # focal length x in pixels - estimated_distance = (self._assumed_object_width * fx) / bbox_width - - return estimated_distance diff --git a/dimos/perception/__init__.py b/dimos/perception/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/perception/common/__init__.py b/dimos/perception/common/__init__.py deleted file mode 100644 index 5902f54bb8..0000000000 --- a/dimos/perception/common/__init__.py +++ /dev/null @@ -1,81 +0,0 @@ -from .utils import ( - BoundingBox2D, - CameraInfo, - Detection2D, - Detection3D, - Header, - Image, - ObjectData, - Pose, - Quaternion, - Union, - Vector, - Vector3, - bbox2d_to_corners, - colorize_depth, - combine_object_data, - cp, - cv2, - detection_results_to_object_data, - draw_bounding_box, - draw_object_detection_visualization, - draw_segmentation_mask, - extract_pose_from_detection3d, - find_clicked_detection, - load_camera_info, - load_camera_info_opencv, - logger, - np, - point_in_bbox, - project_2d_points_to_3d, - project_2d_points_to_3d_cpu, - project_2d_points_to_3d_cuda, - project_3d_points_to_2d, - project_3d_points_to_2d_cpu, - project_3d_points_to_2d_cuda, - rectify_image, - setup_logger, - torch, - yaml, -) - -__all__ = [ - "BoundingBox2D", - "CameraInfo", - "Detection2D", - "Detection3D", - "Header", - "Image", - "ObjectData", - "Pose", - "Quaternion", - "Union", - "Vector", - "Vector3", - "bbox2d_to_corners", - "colorize_depth", - "combine_object_data", - "cp", - "cv2", - "detection_results_to_object_data", - "draw_bounding_box", - "draw_object_detection_visualization", - "draw_segmentation_mask", - "extract_pose_from_detection3d", - "find_clicked_detection", - "load_camera_info", - "load_camera_info_opencv", - "logger", - "np", - "point_in_bbox", - "project_2d_points_to_3d", - "project_2d_points_to_3d_cpu", - "project_2d_points_to_3d_cuda", - "project_3d_points_to_2d", - "project_3d_points_to_2d_cpu", - "project_3d_points_to_2d_cuda", - "rectify_image", - "setup_logger", - "torch", - "yaml", -] diff --git a/dimos/perception/common/utils.py b/dimos/perception/common/utils.py deleted file mode 100644 index c5f550ade3..0000000000 --- a/dimos/perception/common/utils.py +++ /dev/null @@ -1,860 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Union - -import cv2 -from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.vision_msgs import ( - BoundingBox2D, - Detection2D, - Detection3D, -) -import numpy as np -import torch -import yaml # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.std_msgs import Header -from dimos.types.manipulation import ObjectData -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -__all__ = [ - "BoundingBox2D", - "CameraInfo", - "Detection2D", - "Detection3D", - "Header", - "Image", - "ObjectData", - "Pose", - "Quaternion", - "Union", - "Vector", - "Vector3", - "bbox2d_to_corners", - "colorize_depth", - "combine_object_data", - "cp", - "cv2", - "detection_results_to_object_data", - "draw_bounding_box", - "draw_object_detection_visualization", - "draw_segmentation_mask", - "extract_pose_from_detection3d", - "find_clicked_detection", - "load_camera_info", - "load_camera_info_opencv", - "logger", - "np", - "point_in_bbox", - "project_2d_points_to_3d", - "project_2d_points_to_3d_cpu", - "project_2d_points_to_3d_cuda", - "project_3d_points_to_2d", - "project_3d_points_to_2d_cpu", - "project_3d_points_to_2d_cuda", - "rectify_image", - "setup_logger", - "torch", - "yaml", -] - -# Optional CuPy support -try: # pragma: no cover - optional dependency - import cupy as cp # type: ignore[import-not-found, import-untyped] - - _HAS_CUDA = True -except Exception: # pragma: no cover - optional dependency - cp = None - _HAS_CUDA = False - - -def _is_cu_array(x) -> bool: # type: ignore[no-untyped-def] - return _HAS_CUDA and cp is not None and isinstance(x, cp.ndarray) - - -def _to_numpy(x): # type: ignore[no-untyped-def] - return cp.asnumpy(x) if _is_cu_array(x) else x - - -def _to_cupy(x): # type: ignore[no-untyped-def] - if _HAS_CUDA and cp is not None and isinstance(x, np.ndarray): - try: - return cp.asarray(x) - except Exception: - return x - return x - - -def load_camera_info(yaml_path: str, frame_id: str = "camera_link") -> CameraInfo: - """ - Load ROS-style camera_info YAML file and convert to CameraInfo LCM message. - - Args: - yaml_path: Path to camera_info YAML file (ROS format) - frame_id: Frame ID for the camera (default: "camera_link") - - Returns: - CameraInfo: LCM CameraInfo message with all calibration data - """ - with open(yaml_path) as f: - camera_info_data = yaml.safe_load(f) - - # Extract image dimensions - width = camera_info_data.get("image_width", 1280) - height = camera_info_data.get("image_height", 720) - - # Extract camera matrix (K) - already in row-major format - K = camera_info_data["camera_matrix"]["data"] - - # Extract distortion coefficients - D = camera_info_data["distortion_coefficients"]["data"] - - # Extract rectification matrix (R) if available, else use identity - R = camera_info_data.get("rectification_matrix", {}).get("data", [1, 0, 0, 0, 1, 0, 0, 0, 1]) - - # Extract projection matrix (P) if available - P = camera_info_data.get("projection_matrix", {}).get("data", None) - - # If P not provided, construct from K - if P is None: - fx = K[0] - fy = K[4] - cx = K[2] - cy = K[5] - P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] - - # Create header - header = Header(frame_id) - - # Create and return CameraInfo message - return CameraInfo( - D_length=len(D), - header=header, - height=height, - width=width, - distortion_model=camera_info_data.get("distortion_model", "plumb_bob"), - D=D, - K=K, - R=R, - P=P, - binning_x=0, - binning_y=0, - ) - - -def load_camera_info_opencv(yaml_path: str) -> tuple[np.ndarray, np.ndarray]: # type: ignore[type-arg] - """ - Load ROS-style camera_info YAML file and convert to OpenCV camera matrix and distortion coefficients. - - Args: - yaml_path: Path to camera_info YAML file (ROS format) - - Returns: - K: 3x3 camera intrinsic matrix - dist: 1xN distortion coefficients array (for plumb_bob model) - """ - with open(yaml_path) as f: - camera_info = yaml.safe_load(f) - - # Extract camera matrix (K) - camera_matrix_data = camera_info["camera_matrix"]["data"] - K = np.array(camera_matrix_data).reshape(3, 3) - - # Extract distortion coefficients - dist_coeffs_data = camera_info["distortion_coefficients"]["data"] - dist = np.array(dist_coeffs_data) - - # Ensure dist is 1D array for OpenCV compatibility - if dist.ndim == 2: - dist = dist.flatten() - - return K, dist - - -def rectify_image(image: Image, camera_matrix: np.ndarray, dist_coeffs: np.ndarray) -> Image: # type: ignore[type-arg] - """CPU rectification using OpenCV. Preserves backend by caller. - - Returns an Image with numpy or cupy data depending on caller choice. - """ - rect = cv2.undistort(image.data, camera_matrix, dist_coeffs) - return Image(data=rect, format=image.format, frame_id=image.frame_id, ts=image.ts) - - -def project_3d_points_to_2d_cuda( - points_3d: "cp.ndarray", camera_intrinsics: Union[list[float], "cp.ndarray"] -) -> "cp.ndarray": - xp = cp - pts = points_3d.astype(xp.float64, copy=False) - mask = pts[:, 2] > 0 - if not bool(xp.any(mask)): - return xp.zeros((0, 2), dtype=xp.int32) - valid = pts[mask] - if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: - fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] - else: - K = camera_intrinsics.astype(xp.float64, copy=False) # type: ignore[union-attr] - fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] - u = (valid[:, 0] * fx / valid[:, 2]) + cx - v = (valid[:, 1] * fy / valid[:, 2]) + cy - return xp.stack([u, v], axis=1).astype(xp.int32) - - -def project_3d_points_to_2d_cpu( - points_3d: np.ndarray, # type: ignore[type-arg] - camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] -) -> np.ndarray: # type: ignore[type-arg] - pts = np.asarray(points_3d, dtype=np.float64) - valid_mask = pts[:, 2] > 0 - if not np.any(valid_mask): - return np.zeros((0, 2), dtype=np.int32) - valid_points = pts[valid_mask] - if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: - fx, fy, cx, cy = [float(v) for v in camera_intrinsics] - else: - K = np.array(camera_intrinsics, dtype=np.float64) - fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] - u = (valid_points[:, 0] * fx / valid_points[:, 2]) + cx - v = (valid_points[:, 1] * fy / valid_points[:, 2]) + cy - return np.column_stack([u, v]).astype(np.int32) - - -def project_3d_points_to_2d( - points_3d: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], # type: ignore[type-arg] -) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] - """ - Project 3D points to 2D image coordinates using camera intrinsics. - - Args: - points_3d: Nx3 array of 3D points (X, Y, Z) - camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix - - Returns: - Nx2 array of 2D image coordinates (u, v) - """ - if len(points_3d) == 0: - return ( - cp.zeros((0, 2), dtype=cp.int32) - if _is_cu_array(points_3d) - else np.zeros((0, 2), dtype=np.int32) - ) - - # Filter out points with zero or negative depth - if _is_cu_array(points_3d) or _is_cu_array(camera_intrinsics): - xp = cp - pts = points_3d if _is_cu_array(points_3d) else xp.asarray(points_3d) - K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics - return project_3d_points_to_2d_cuda(pts, K) - return project_3d_points_to_2d_cpu(np.asarray(points_3d), np.asarray(camera_intrinsics)) - - -def project_2d_points_to_3d_cuda( - points_2d: "cp.ndarray", - depth_values: "cp.ndarray", - camera_intrinsics: Union[list[float], "cp.ndarray"], -) -> "cp.ndarray": - xp = cp - pts = points_2d.astype(xp.float64, copy=False) - depths = depth_values.astype(xp.float64, copy=False) - valid = depths > 0 - if not bool(xp.any(valid)): - return xp.zeros((0, 3), dtype=xp.float32) - uv = pts[valid] - Z = depths[valid] - if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: - fx, fy, cx, cy = [xp.asarray(v, dtype=xp.float64) for v in camera_intrinsics] - else: - K = camera_intrinsics.astype(xp.float64, copy=False) # type: ignore[union-attr] - fx, fy, cx, cy = K[0, 0], K[1, 1], K[0, 2], K[1, 2] - X = (uv[:, 0] - cx) * Z / fx - Y = (uv[:, 1] - cy) * Z / fy - return xp.stack([X, Y, Z], axis=1).astype(xp.float32) - - -def project_2d_points_to_3d_cpu( - points_2d: np.ndarray, # type: ignore[type-arg] - depth_values: np.ndarray, # type: ignore[type-arg] - camera_intrinsics: list[float] | np.ndarray, # type: ignore[type-arg] -) -> np.ndarray: # type: ignore[type-arg] - pts = np.asarray(points_2d, dtype=np.float64) - depths = np.asarray(depth_values, dtype=np.float64) - valid_mask = depths > 0 - if not np.any(valid_mask): - return np.zeros((0, 3), dtype=np.float32) - valid_points_2d = pts[valid_mask] - valid_depths = depths[valid_mask] - if isinstance(camera_intrinsics, list) and len(camera_intrinsics) == 4: - fx, fy, cx, cy = [float(v) for v in camera_intrinsics] - else: - camera_matrix = np.array(camera_intrinsics, dtype=np.float64) - fx = camera_matrix[0, 0] - fy = camera_matrix[1, 1] - cx = camera_matrix[0, 2] - cy = camera_matrix[1, 2] - X = (valid_points_2d[:, 0] - cx) * valid_depths / fx - Y = (valid_points_2d[:, 1] - cy) * valid_depths / fy - Z = valid_depths - return np.column_stack([X, Y, Z]).astype(np.float32) - - -def project_2d_points_to_3d( - points_2d: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - depth_values: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - camera_intrinsics: Union[list[float], np.ndarray, "cp.ndarray"], # type: ignore[type-arg] -) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] - """ - Project 2D image points to 3D coordinates using depth values and camera intrinsics. - - Args: - points_2d: Nx2 array of 2D image coordinates (u, v) - depth_values: N-length array of depth values (Z coordinates) for each point - camera_intrinsics: Camera parameters as [fx, fy, cx, cy] list or 3x3 matrix - - Returns: - Nx3 array of 3D points (X, Y, Z) - """ - if len(points_2d) == 0: - return ( - cp.zeros((0, 3), dtype=cp.float32) - if _is_cu_array(points_2d) - else np.zeros((0, 3), dtype=np.float32) - ) - - # Ensure depth_values is a numpy array - if _is_cu_array(points_2d) or _is_cu_array(depth_values) or _is_cu_array(camera_intrinsics): - xp = cp - pts = points_2d if _is_cu_array(points_2d) else xp.asarray(points_2d) - depths = depth_values if _is_cu_array(depth_values) else xp.asarray(depth_values) - K = camera_intrinsics if _is_cu_array(camera_intrinsics) else camera_intrinsics - return project_2d_points_to_3d_cuda(pts, depths, K) - return project_2d_points_to_3d_cpu( - np.asarray(points_2d), np.asarray(depth_values), np.asarray(camera_intrinsics) - ) - - -def colorize_depth( - depth_img: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - max_depth: float = 5.0, - overlay_stats: bool = True, -) -> Union[np.ndarray, "cp.ndarray"] | None: # type: ignore[type-arg] - """ - Normalize and colorize depth image using COLORMAP_JET with optional statistics overlay. - - Args: - depth_img: Depth image (H, W) in meters - max_depth: Maximum depth value for normalization - overlay_stats: Whether to overlay depth statistics on the image - - Returns: - Colorized depth image (H, W, 3) in RGB format, or None if input is None - """ - if depth_img is None: - return None - - was_cu = _is_cu_array(depth_img) - xp = cp if was_cu else np - depth = depth_img if was_cu else np.asarray(depth_img) - - valid_mask = xp.isfinite(depth) & (depth > 0) - depth_norm = xp.zeros_like(depth, dtype=xp.float32) - if bool(valid_mask.any() if not was_cu else xp.any(valid_mask)): - depth_norm = xp.where(valid_mask, xp.clip(depth / max_depth, 0, 1), depth_norm) - - # Use CPU for colormap/text; convert back to GPU if needed - depth_norm_np = _to_numpy(depth_norm) # type: ignore[no-untyped-call] - depth_colored = cv2.applyColorMap((depth_norm_np * 255).astype(np.uint8), cv2.COLORMAP_JET) - depth_rgb_np = cv2.cvtColor(depth_colored, cv2.COLOR_BGR2RGB) - depth_rgb_np = (depth_rgb_np * 0.6).astype(np.uint8) - - if overlay_stats and (np.any(_to_numpy(valid_mask))): # type: ignore[no-untyped-call] - valid_depths = _to_numpy(depth)[_to_numpy(valid_mask)] # type: ignore[no-untyped-call] - min_depth = float(np.min(valid_depths)) - max_depth_actual = float(np.max(valid_depths)) - h, w = depth_rgb_np.shape[:2] - center_y, center_x = h // 2, w // 2 - center_region = _to_numpy( # type: ignore[no-untyped-call] - depth - )[max(0, center_y - 2) : min(h, center_y + 3), max(0, center_x - 2) : min(w, center_x + 3)] - center_mask = np.isfinite(center_region) & (center_region > 0) - if center_mask.any(): - center_depth = float(np.median(center_region[center_mask])) - else: - depth_np = _to_numpy(depth) # type: ignore[no-untyped-call] - vm_np = _to_numpy(valid_mask) # type: ignore[no-untyped-call] - center_depth = float(depth_np[center_y, center_x]) if vm_np[center_y, center_x] else 0.0 - - font = cv2.FONT_HERSHEY_SIMPLEX - font_scale = 0.6 - thickness = 1 - line_type = cv2.LINE_AA - text_color = (255, 255, 255) - bg_color = (0, 0, 0) - padding = 5 - - min_text = f"Min: {min_depth:.2f}m" - (text_w, text_h), _ = cv2.getTextSize(min_text, font, font_scale, thickness) - cv2.rectangle( - depth_rgb_np, - (padding, padding), - (padding + text_w + 4, padding + text_h + 6), - bg_color, - -1, - ) - cv2.putText( - depth_rgb_np, - min_text, - (padding + 2, padding + text_h + 2), - font, - font_scale, - text_color, - thickness, - line_type, - ) - - max_text = f"Max: {max_depth_actual:.2f}m" - (text_w, text_h), _ = cv2.getTextSize(max_text, font, font_scale, thickness) - cv2.rectangle( - depth_rgb_np, - (w - padding - text_w - 4, padding), - (w - padding, padding + text_h + 6), - bg_color, - -1, - ) - cv2.putText( - depth_rgb_np, - max_text, - (w - padding - text_w - 2, padding + text_h + 2), - font, - font_scale, - text_color, - thickness, - line_type, - ) - - if center_depth > 0: - center_text = f"{center_depth:.2f}m" - (text_w, text_h), _ = cv2.getTextSize(center_text, font, font_scale, thickness) - center_text_x = center_x - text_w // 2 - center_text_y = center_y + text_h // 2 - cross_size = 10 - cross_color = (255, 255, 255) - cv2.line( - depth_rgb_np, - (center_x - cross_size, center_y), - (center_x + cross_size, center_y), - cross_color, - 1, - ) - cv2.line( - depth_rgb_np, - (center_x, center_y - cross_size), - (center_x, center_y + cross_size), - cross_color, - 1, - ) - cv2.rectangle( - depth_rgb_np, - (center_text_x - 2, center_text_y - text_h - 2), - (center_text_x + text_w + 2, center_text_y + 2), - bg_color, - -1, - ) - cv2.putText( - depth_rgb_np, - center_text, - (center_text_x, center_text_y), - font, - font_scale, - text_color, - thickness, - line_type, - ) - - return _to_cupy(depth_rgb_np) if was_cu else depth_rgb_np # type: ignore[no-untyped-call] - - -def draw_bounding_box( - image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - bbox: list[float], - color: tuple[int, int, int] = (0, 255, 0), - thickness: int = 2, - label: str | None = None, - confidence: float | None = None, - object_id: int | None = None, - font_scale: float = 0.6, -) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] - """ - Draw a bounding box with optional label on an image. - - Args: - image: Image to draw on (H, W, 3) - bbox: Bounding box [x1, y1, x2, y2] - color: RGB color tuple for the box - thickness: Line thickness for the box - label: Optional class label - confidence: Optional confidence score - object_id: Optional object ID - font_scale: Font scale for text - - Returns: - Image with bounding box drawn - """ - was_cu = _is_cu_array(image) - img_np = _to_numpy(image) # type: ignore[no-untyped-call] - x1, y1, x2, y2 = map(int, bbox) - cv2.rectangle(img_np, (x1, y1), (x2, y2), color, thickness) - - # Create label text - text_parts = [] - if label is not None: - text_parts.append(str(label)) - if object_id is not None: - text_parts.append(f"ID: {object_id}") - if confidence is not None: - text_parts.append(f"({confidence:.2f})") - - if text_parts: - text = ", ".join(text_parts) - - # Draw text background - text_size = cv2.getTextSize(text, cv2.FONT_HERSHEY_SIMPLEX, font_scale, 1)[0] - cv2.rectangle( - img_np, - (x1, y1 - text_size[1] - 5), - (x1 + text_size[0], y1), - (0, 0, 0), - -1, - ) - - # Draw text - cv2.putText( - img_np, - text, - (x1, y1 - 5), - cv2.FONT_HERSHEY_SIMPLEX, - font_scale, - (255, 255, 255), - 1, - ) - - return _to_cupy(img_np) if was_cu else img_np # type: ignore[no-untyped-call] - - -def draw_segmentation_mask( - image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - mask: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - color: tuple[int, int, int] = (0, 200, 200), - alpha: float = 0.5, - draw_contours: bool = True, - contour_thickness: int = 2, -) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] - """ - Draw segmentation mask overlay on an image. - - Args: - image: Image to draw on (H, W, 3) - mask: Segmentation mask (H, W) - boolean or uint8 - color: RGB color for the mask - alpha: Transparency factor (0.0 = transparent, 1.0 = opaque) - draw_contours: Whether to draw mask contours - contour_thickness: Thickness of contour lines - - Returns: - Image with mask overlay drawn - """ - if mask is None: - return image - - was_cu = _is_cu_array(image) - img_np = _to_numpy(image) # type: ignore[no-untyped-call] - mask_np = _to_numpy(mask) # type: ignore[no-untyped-call] - - try: - mask_np = mask_np.astype(np.uint8) - colored_mask = np.zeros_like(img_np) - colored_mask[mask_np > 0] = color - mask_area = mask_np > 0 - img_np[mask_area] = cv2.addWeighted( - img_np[mask_area], 1 - alpha, colored_mask[mask_area], alpha, 0 - ) - if draw_contours: - contours, _ = cv2.findContours(mask_np, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - cv2.drawContours(img_np, contours, -1, color, contour_thickness) - except Exception as e: - logger.warning(f"Error drawing segmentation mask: {e}") - - return _to_cupy(img_np) if was_cu else img_np # type: ignore[no-untyped-call] - - -def draw_object_detection_visualization( - image: Union[np.ndarray, "cp.ndarray"], # type: ignore[type-arg] - objects: list[ObjectData], - draw_masks: bool = False, - bbox_color: tuple[int, int, int] = (0, 255, 0), - mask_color: tuple[int, int, int] = (0, 200, 200), - font_scale: float = 0.6, -) -> Union[np.ndarray, "cp.ndarray"]: # type: ignore[type-arg] - """ - Create object detection visualization with bounding boxes and optional masks. - - Args: - image: Base image to draw on (H, W, 3) - objects: List of ObjectData with detection information - draw_masks: Whether to draw segmentation masks - bbox_color: Default color for bounding boxes - mask_color: Default color for segmentation masks - font_scale: Font scale for text labels - - Returns: - Image with detection visualization - """ - was_cu = _is_cu_array(image) - viz_image = _to_numpy(image).copy() # type: ignore[no-untyped-call] - - for obj in objects: - try: - # Draw segmentation mask first (if enabled and available) - if draw_masks and "segmentation_mask" in obj and obj["segmentation_mask"] is not None: - viz_image = draw_segmentation_mask( - viz_image, obj["segmentation_mask"], color=mask_color, alpha=0.5 - ) - - # Draw bounding box - if "bbox" in obj and obj["bbox"] is not None: - # Use object's color if available, otherwise default - color = bbox_color - if "color" in obj and obj["color"] is not None: - obj_color = obj["color"] - if isinstance(obj_color, np.ndarray): - color = tuple(int(c) for c in obj_color) # type: ignore[assignment] - elif isinstance(obj_color, list | tuple): - color = tuple(int(c) for c in obj_color[:3]) - - viz_image = draw_bounding_box( - viz_image, - obj["bbox"], - color=color, - label=obj.get("label"), - confidence=obj.get("confidence"), - object_id=obj.get("object_id"), - font_scale=font_scale, - ) - - except Exception as e: - logger.warning(f"Error drawing object visualization: {e}") - - return _to_cupy(viz_image) if was_cu else viz_image # type: ignore[no-untyped-call] - - -def detection_results_to_object_data( - bboxes: list[list[float]], - track_ids: list[int], - class_ids: list[int], - confidences: list[float], - names: list[str], - masks: list[np.ndarray] | None = None, # type: ignore[type-arg] - source: str = "detection", -) -> list[ObjectData]: - """ - Convert detection/segmentation results to ObjectData format. - - Args: - bboxes: List of bounding boxes [x1, y1, x2, y2] - track_ids: List of tracking IDs - class_ids: List of class indices - confidences: List of detection confidences - names: List of class names - masks: Optional list of segmentation masks - source: Source type ("detection" or "segmentation") - - Returns: - List of ObjectData dictionaries - """ - objects = [] - - for i in range(len(bboxes)): - # Calculate basic properties from bbox - bbox = bboxes[i] - width = bbox[2] - bbox[0] - height = bbox[3] - bbox[1] - bbox[0] + width / 2 - bbox[1] + height / 2 - - # Create ObjectData - object_data: ObjectData = { - "object_id": track_ids[i] if i < len(track_ids) else i, - "bbox": bbox, - "depth": -1.0, # Will be populated by depth estimation or point cloud processing - "confidence": confidences[i] if i < len(confidences) else 1.0, - "class_id": class_ids[i] if i < len(class_ids) else 0, - "label": names[i] if i < len(names) else f"{source}_object", - "movement_tolerance": 1.0, # Default to freely movable - "segmentation_mask": masks[i].cpu().numpy() # type: ignore[attr-defined, typeddict-item] - if masks and i < len(masks) and isinstance(masks[i], torch.Tensor) - else masks[i] - if masks and i < len(masks) - else None, - # Initialize 3D properties (will be populated by point cloud processing) - "position": Vector(0, 0, 0), # type: ignore[arg-type] - "rotation": Vector(0, 0, 0), # type: ignore[arg-type] - "size": { - "width": 0.0, - "height": 0.0, - "depth": 0.0, - }, - } - objects.append(object_data) - - return objects - - -def combine_object_data( - list1: list[ObjectData], list2: list[ObjectData], overlap_threshold: float = 0.8 -) -> list[ObjectData]: - """ - Combine two ObjectData lists, removing duplicates based on segmentation mask overlap. - """ - combined = list1.copy() - used_ids = set(obj.get("object_id", 0) for obj in list1) - next_id = max(used_ids) + 1 if used_ids else 1 - - for obj2 in list2: - obj_copy = obj2.copy() - - # Handle duplicate object_id - if obj_copy.get("object_id", 0) in used_ids: - obj_copy["object_id"] = next_id - next_id += 1 - used_ids.add(obj_copy["object_id"]) - - # Check mask overlap - mask2 = obj2.get("segmentation_mask") - m2 = _to_numpy(mask2) if mask2 is not None else None # type: ignore[no-untyped-call] - if m2 is None or np.sum(m2 > 0) == 0: - combined.append(obj_copy) - continue - - mask2_area = np.sum(m2 > 0) - is_duplicate = False - - for obj1 in list1: - mask1 = obj1.get("segmentation_mask") - if mask1 is None: - continue - - m1 = _to_numpy(mask1) # type: ignore[no-untyped-call] - intersection = np.sum((m1 > 0) & (m2 > 0)) - if intersection / mask2_area >= overlap_threshold: - is_duplicate = True - break - - if not is_duplicate: - combined.append(obj_copy) - - return combined - - -def point_in_bbox(point: tuple[int, int], bbox: list[float]) -> bool: - """ - Check if a point is inside a bounding box. - - Args: - point: (x, y) coordinates - bbox: Bounding box [x1, y1, x2, y2] - - Returns: - True if point is inside bbox - """ - x, y = point - x1, y1, x2, y2 = bbox - return x1 <= x <= x2 and y1 <= y <= y2 - - -def bbox2d_to_corners(bbox_2d: BoundingBox2D) -> tuple[float, float, float, float]: - """ - Convert BoundingBox2D from center format to corner format. - - Args: - bbox_2d: BoundingBox2D with center and size - - Returns: - Tuple of (x1, y1, x2, y2) corner coordinates - """ - center_x = bbox_2d.center.position.x - center_y = bbox_2d.center.position.y - half_width = bbox_2d.size_x / 2.0 - half_height = bbox_2d.size_y / 2.0 - - x1 = center_x - half_width - y1 = center_y - half_height - x2 = center_x + half_width - y2 = center_y + half_height - - return x1, y1, x2, y2 - - -def find_clicked_detection( - click_pos: tuple[int, int], detections_2d: list[Detection2D], detections_3d: list[Detection3D] -) -> Detection3D | None: - """ - Find which detection was clicked based on 2D bounding boxes. - - Args: - click_pos: (x, y) click position - detections_2d: List of Detection2D objects - detections_3d: List of Detection3D objects (must be 1:1 correspondence) - - Returns: - Corresponding Detection3D object if found, None otherwise - """ - click_x, click_y = click_pos - - for i, det_2d in enumerate(detections_2d): - if det_2d.bbox and i < len(detections_3d): - x1, y1, x2, y2 = bbox2d_to_corners(det_2d.bbox) - - if x1 <= click_x <= x2 and y1 <= click_y <= y2: - return detections_3d[i] - - return None - - -def extract_pose_from_detection3d(detection3d: Detection3D): # type: ignore[no-untyped-def] - """Extract PoseStamped from Detection3D message. - - Args: - detection3d: Detection3D message - - Returns: - Pose or None if no valid detection - """ - if not detection3d or not detection3d.bbox or not detection3d.bbox.center: - return None - - # Extract position - pos = detection3d.bbox.center.position - position = Vector3(pos.x, pos.y, pos.z) - - # Extract orientation - orient = detection3d.bbox.center.orientation - orientation = Quaternion(orient.x, orient.y, orient.z, orient.w) - - pose = Pose(position=position, orientation=orientation) - return pose diff --git a/dimos/perception/demo_object_scene_registration.py b/dimos/perception/demo_object_scene_registration.py deleted file mode 100644 index c02f7d2984..0000000000 --- a/dimos/perception/demo_object_scene_registration.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.camera.realsense import realsense_camera -from dimos.hardware.sensors.camera.zed import zed_camera -from dimos.perception.detection.detectors.yoloe import YoloePromptMode -from dimos.perception.object_scene_registration import object_scene_registration_module -from dimos.robot.foxglove_bridge import foxglove_bridge - -camera_choice = "zed" - -if camera_choice == "realsense": - camera_module = realsense_camera(enable_pointcloud=False) -elif camera_choice == "zed": - camera_module = zed_camera(enable_pointcloud=False) -else: - raise ValueError(f"Invalid camera choice: {camera_choice}") - -demo_object_scene_registration = autoconnect( - camera_module, - object_scene_registration_module(target_frame="world", prompt_mode=YoloePromptMode.LRPC), - foxglove_bridge(), - agent(), -).global_config(viewer_backend="foxglove") diff --git a/dimos/perception/detection/__init__.py b/dimos/perception/detection/__init__.py deleted file mode 100644 index ae9f8cb14d..0000000000 --- a/dimos/perception/detection/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "detectors": ["Detector", "Yolo2DDetector"], - "module2D": ["Detection2DModule"], - "module3D": ["Detection3DModule"], - }, -) diff --git a/dimos/perception/detection/conftest.py b/dimos/perception/detection/conftest.py deleted file mode 100644 index 3b24422c47..0000000000 --- a/dimos/perception/detection/conftest.py +++ /dev/null @@ -1,303 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Generator -import functools -from typing import TypedDict - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -from dimos_lcm.foxglove_msgs.SceneUpdate import SceneUpdate -from dimos_lcm.visualization_msgs.MarkerArray import MarkerArray -import pytest - -from dimos.core import LCMTransport -from dimos.msgs.geometry_msgs import Transform -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module2D import Detection2DModule -from dimos.perception.detection.module3D import Detection3DModule -from dimos.perception.detection.moduleDB import ObjectDBModule -from dimos.perception.detection.type import ( - Detection2D, - Detection3DPC, - ImageDetections2D, - ImageDetections3DPC, -) -from dimos.protocol.tf import TF -from dimos.robot.unitree.go2 import connection -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.data import get_data -from dimos.utils.testing import TimedSensorReplay - - -class Moment(TypedDict, total=False): - odom_frame: Odometry - lidar_frame: PointCloud2 - image_frame: Image - camera_info: CameraInfo - transforms: list[Transform] - tf: TF - annotations: ImageAnnotations | None - detections: ImageDetections3DPC | None - markers: MarkerArray | None - scene_update: SceneUpdate | None - - -class Moment2D(Moment): - detections2d: ImageDetections2D - - -class Moment3D(Moment): - detections3dpc: ImageDetections3DPC - - -@pytest.fixture(scope="session") -def tf(): - t = TF() - yield t - t.stop() - - -@pytest.fixture(scope="session") -def get_moment(tf): - @functools.lru_cache(maxsize=1) - def moment_provider(**kwargs) -> Moment: - print("MOMENT PROVIDER ARGS:", kwargs) - seek = kwargs.get("seek", 10.0) - - data_dir = "unitree_go2_lidar_corrected" - get_data(data_dir) - - lidar_frame_result = TimedSensorReplay(f"{data_dir}/lidar").find_closest_seek(seek) - if lidar_frame_result is None: - raise ValueError("No lidar frame found") - lidar_frame: PointCloud2 = lidar_frame_result - - image_frame = TimedSensorReplay( - f"{data_dir}/video", - ).find_closest(lidar_frame.ts) - - if image_frame is None: - raise ValueError("No image frame found") - - image_frame.frame_id = "camera_optical" - - odom_frame = TimedSensorReplay(f"{data_dir}/odom", autocast=Odometry.from_msg).find_closest( - lidar_frame.ts - ) - - if odom_frame is None: - raise ValueError("No odom frame found") - - transforms = connection.GO2Connection._odom_to_tf(odom_frame) - - tf.receive_transform(*transforms) - - return { - "odom_frame": odom_frame, - "lidar_frame": lidar_frame, - "image_frame": image_frame, - "camera_info": connection._camera_info_static(), - "transforms": transforms, - "tf": tf, - } - - return moment_provider - - -@pytest.fixture(scope="session") -def publish_moment(): - def publisher(moment: Moment | Moment2D | Moment3D) -> None: - detections2d_val = moment.get("detections2d") - if detections2d_val: - # 2d annotations - annotations: LCMTransport[ImageAnnotations] = LCMTransport( - "/annotations", ImageAnnotations - ) - assert isinstance(detections2d_val, ImageDetections2D) - annotations.publish(detections2d_val.to_foxglove_annotations()) - - detections: LCMTransport[Detection2DArray] = LCMTransport( - "/detections", Detection2DArray - ) - detections.publish(detections2d_val.to_ros_detection2d_array()) - - annotations.lcm.stop() - detections.lcm.stop() - - detections3dpc_val = moment.get("detections3dpc") - if detections3dpc_val: - scene_update: LCMTransport[SceneUpdate] = LCMTransport("/scene_update", SceneUpdate) - # 3d scene update - assert isinstance(detections3dpc_val, ImageDetections3DPC) - scene_update.publish(detections3dpc_val.to_foxglove_scene_update()) - scene_update.lcm.stop() - - lidar_frame = moment.get("lidar_frame") - if lidar_frame: - lidar: LCMTransport[PointCloud2] = LCMTransport("/lidar", PointCloud2) - lidar.publish(lidar_frame) - lidar.lcm.stop() - - image_frame = moment.get("image_frame") - if image_frame: - image: LCMTransport[Image] = LCMTransport("/image", Image) - image.publish(image_frame) - image.lcm.stop() - - camera_info_val = moment.get("camera_info") - if camera_info_val: - camera_info: LCMTransport[CameraInfo] = LCMTransport("/camera_info", CameraInfo) - camera_info.publish(camera_info_val) - camera_info.lcm.stop() - - tf = moment.get("tf") - transforms = moment.get("transforms") - if tf is not None and transforms is not None: - tf.publish(*transforms) - - # moduleDB.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) - # moduleDB.target.transport = LCMTransport("/target", PoseStamped) - - return publisher - - -@pytest.fixture(scope="session") -def imageDetections2d(get_moment_2d) -> ImageDetections2D: - moment = get_moment_2d() - assert len(moment["detections2d"]) > 0, "No detections found in the moment" - return moment["detections2d"] - - -@pytest.fixture(scope="session") -def detection2d(get_moment_2d) -> Detection2D: - moment = get_moment_2d() - assert len(moment["detections2d"]) > 0, "No detections found in the moment" - return moment["detections2d"][0] - - -@pytest.fixture(scope="session") -def detections3dpc(get_moment_3dpc) -> Detection3DPC: - moment = get_moment_3dpc(seek=10.0) - assert len(moment["detections3dpc"]) > 0, "No detections found in the moment" - return moment["detections3dpc"] - - -@pytest.fixture(scope="session") -def detection3dpc(detections3dpc) -> Detection3DPC: - return detections3dpc[0] - - -@pytest.fixture(scope="session") -def get_moment_2d(get_moment) -> Generator[Callable[[], Moment2D], None, None]: - from dimos.perception.detection.detectors import Yolo2DDetector - - module = Detection2DModule(detector=lambda: Yolo2DDetector(device="cpu")) - - @functools.lru_cache(maxsize=1) - def moment_provider(**kwargs) -> Moment2D: - moment = get_moment(**kwargs) - detections = module.process_image_frame(moment.get("image_frame")) - - return { - **moment, - "detections2d": detections, - } - - yield moment_provider - - module._close_module() - - -@pytest.fixture(scope="session") -def get_moment_3dpc(get_moment_2d) -> Generator[Callable[[], Moment3D], None, None]: - module: Detection3DModule | None = None - - @functools.lru_cache(maxsize=1) - def moment_provider(**kwargs) -> Moment3D: - nonlocal module - moment = get_moment_2d(**kwargs) - - if not module: - module = Detection3DModule(camera_info=moment["camera_info"]) - - lidar_frame = moment.get("lidar_frame") - if lidar_frame is None: - raise ValueError("No lidar frame found") - - camera_transform = moment["tf"].get("camera_optical", lidar_frame.frame_id) - if camera_transform is None: - raise ValueError("No camera_optical transform in tf") - - detections3dpc = module.process_frame( - moment["detections2d"], moment["lidar_frame"], camera_transform - ) - - return { - **moment, - "detections3dpc": detections3dpc, - } - - yield moment_provider - if module is not None: - module._close_module() - - -@pytest.fixture(scope="session") -def object_db_module(get_moment): - """Create and populate an ObjectDBModule with detections from multiple frames.""" - from dimos.perception.detection.detectors import Yolo2DDetector - - module2d = Detection2DModule(detector=lambda: Yolo2DDetector(device="cpu")) - module3d = Detection3DModule(camera_info=connection._camera_info_static()) - moduleDB = ObjectDBModule(camera_info=connection._camera_info_static()) - - # Process 5 frames to build up object history - for i in range(5): - seek_value = 10.0 + (i * 2) - moment = get_moment(seek=seek_value) - - # Process 2D detections - imageDetections2d = module2d.process_image_frame(moment["image_frame"]) - - # Get camera transform - camera_transform = moment["tf"].get("camera_optical", moment.get("lidar_frame").frame_id) - - # Process 3D detections - imageDetections3d = module3d.process_frame( - imageDetections2d, moment["lidar_frame"], camera_transform - ) - - # Add to database - moduleDB.add_detections(imageDetections3d) - - yield moduleDB - - module2d._close_module() - module3d._close_module() - moduleDB._close_module() - - -@pytest.fixture(scope="session") -def first_object(object_db_module): - """Get the first object from the database.""" - objects = list(object_db_module.objects.values()) - assert len(objects) > 0, "No objects found in database" - return objects[0] - - -@pytest.fixture(scope="session") -def all_objects(object_db_module): - """Get all objects from the database.""" - return list(object_db_module.objects.values()) diff --git a/dimos/perception/detection/detectors/__init__.py b/dimos/perception/detection/detectors/__init__.py deleted file mode 100644 index 2f151fe3ef..0000000000 --- a/dimos/perception/detection/detectors/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -# from dimos.perception.detection.detectors.detic import Detic2DDetector -from dimos.perception.detection.detectors.types import Detector -from dimos.perception.detection.detectors.yolo import Yolo2DDetector - -__all__ = [ - "Detector", - "Yolo2DDetector", -] diff --git a/dimos/perception/detection/detectors/config/custom_tracker.yaml b/dimos/perception/detection/detectors/config/custom_tracker.yaml deleted file mode 100644 index 7a6748ebf6..0000000000 --- a/dimos/perception/detection/detectors/config/custom_tracker.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license - -# Default Ultralytics settings for BoT-SORT tracker when using mode="track" -# For documentation and examples see https://docs.ultralytics.com/modes/track/ -# For BoT-SORT source code see https://github.com/NirAharon/BoT-SORT - -tracker_type: botsort # tracker type, ['botsort', 'bytetrack'] -track_high_thresh: 0.4 # threshold for the first association -track_low_thresh: 0.2 # threshold for the second association -new_track_thresh: 0.5 # threshold for init new track if the detection does not match any tracks -track_buffer: 100 # buffer to calculate the time when to remove tracks -match_thresh: 0.4 # threshold for matching tracks -fuse_score: False # Whether to fuse confidence scores with the iou distances before matching -# min_box_area: 10 # threshold for min box areas(for tracker evaluation, not used for now) - -# BoT-SORT settings -gmc_method: sparseOptFlow # method of global motion compensation -# ReID model related thresh (not supported yet) -proximity_thresh: 0.6 -appearance_thresh: 0.35 -with_reid: False diff --git a/dimos/perception/detection/detectors/conftest.py b/dimos/perception/detection/detectors/conftest.py deleted file mode 100644 index 6a2c041a8b..0000000000 --- a/dimos/perception/detection/detectors/conftest.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector -from dimos.perception.detection.detectors.yolo import Yolo2DDetector -from dimos.perception.detection.detectors.yoloe import Yoloe2DDetector, YoloePromptMode -from dimos.utils.data import get_data - - -@pytest.fixture(scope="session") -def test_image(): - """Load the test image used for detector tests.""" - return Image.from_file(get_data("cafe.jpg")) - - -@pytest.fixture(scope="session") -def person_detector(): - """Create a YoloPersonDetector instance.""" - return YoloPersonDetector() - - -@pytest.fixture(scope="session") -def bbox_detector(): - """Create a Yolo2DDetector instance for general object detection.""" - return Yolo2DDetector() - - -@pytest.fixture(scope="session") -def yoloe_detector(): - """Create a Yoloe2DDetector instance for general object detection.""" - return Yoloe2DDetector(prompt_mode=YoloePromptMode.LRPC) diff --git a/dimos/perception/detection/detectors/person/test_person_detectors.py b/dimos/perception/detection/detectors/person/test_person_detectors.py deleted file mode 100644 index 2ed7cdc7dc..0000000000 --- a/dimos/perception/detection/detectors/person/test_person_detectors.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.perception.detection.type import Detection2DPerson, ImageDetections2D - - -@pytest.fixture(scope="session") -def people(person_detector, test_image): - return person_detector.process_image(test_image) - - -@pytest.fixture(scope="session") -def person(people): - return people[0] - - -def test_person_detection(people) -> None: - """Test that we can detect people with pose keypoints.""" - assert len(people) > 0 - - # Check first person - person = people[0] - assert isinstance(person, Detection2DPerson) - assert person.confidence > 0 - assert len(person.bbox) == 4 # bbox is now a tuple - assert person.keypoints.shape == (17, 2) - assert person.keypoint_scores.shape == (17,) - - -def test_person_properties(people) -> None: - """Test Detection2DPerson object properties and methods.""" - person = people[0] - - # Test bounding box properties - assert person.width > 0 - assert person.height > 0 - assert len(person.center) == 2 - - # Test keypoint access - nose_xy, nose_conf = person.get_keypoint("nose") - assert nose_xy.shape == (2,) - assert 0 <= nose_conf <= 1 - - # Test visible keypoints - visible = person.get_visible_keypoints(threshold=0.5) - assert len(visible) > 0 - assert all(isinstance(name, str) for name, _, _ in visible) - assert all(xy.shape == (2,) for _, xy, _ in visible) - assert all(0 <= conf <= 1 for _, _, conf in visible) - - -def test_person_normalized_coords(people) -> None: - """Test normalized coordinates if available.""" - person = people[0] - - if person.keypoints_normalized is not None: - assert person.keypoints_normalized.shape == (17, 2) - # Check all values are in 0-1 range - assert (person.keypoints_normalized >= 0).all() - assert (person.keypoints_normalized <= 1).all() - - if person.bbox_normalized is not None: - assert person.bbox_normalized.shape == (4,) - assert (person.bbox_normalized >= 0).all() - assert (person.bbox_normalized <= 1).all() - - -def test_multiple_people(people) -> None: - """Test that multiple people can be detected.""" - print(f"\nDetected {len(people)} people in test image") - - for i, person in enumerate(people[:3]): # Show first 3 - print(f"\nPerson {i}:") - print(f" Confidence: {person.confidence:.3f}") - print(f" Size: {person.width:.1f} x {person.height:.1f}") - - visible = person.get_visible_keypoints(threshold=0.8) - print(f" High-confidence keypoints (>0.8): {len(visible)}") - for name, xy, conf in visible[:5]: - print(f" {name}: ({xy[0]:.1f}, {xy[1]:.1f}) conf={conf:.3f}") - - -def test_image_detections2d_structure(people) -> None: - """Test that process_image returns ImageDetections2D.""" - assert isinstance(people, ImageDetections2D) - assert len(people.detections) > 0 - assert all(isinstance(d, Detection2DPerson) for d in people.detections) - - -def test_invalid_keypoint(test_image) -> None: - """Test error handling for invalid keypoint names.""" - # Create a dummy Detection2DPerson - import numpy as np - - person = Detection2DPerson( - # Detection2DBBox fields - bbox=(0.0, 0.0, 100.0, 100.0), - track_id=0, - class_id=0, - confidence=0.9, - name="person", - ts=test_image.ts, - image=test_image, - # Detection2DPerson fields - keypoints=np.zeros((17, 2)), - keypoint_scores=np.zeros(17), - ) - - with pytest.raises(ValueError): - person.get_keypoint("invalid_keypoint") - - -def test_person_annotations(person) -> None: - # Test text annotations - text_anns = person.to_text_annotation() - print(f"\nText annotations: {len(text_anns)}") - for i, ann in enumerate(text_anns): - print(f" {i}: {ann.text}") - assert len(text_anns) == 3 # confidence, name/track_id, keypoints count - assert any("keypoints:" in ann.text for ann in text_anns) - - # Test points annotations - points_anns = person.to_points_annotation() - print(f"\nPoints annotations: {len(points_anns)}") - - # Count different types (use actual LCM constants) - from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation - - bbox_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.LINE_LOOP) # 2 - keypoint_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.POINTS) # 1 - skeleton_count = sum(1 for ann in points_anns if ann.type == PointsAnnotation.LINE_LIST) # 4 - - print(f" - Bounding boxes: {bbox_count}") - print(f" - Keypoint circles: {keypoint_count}") - print(f" - Skeleton lines: {skeleton_count}") - - assert bbox_count >= 1 # At least the person bbox - assert keypoint_count >= 1 # At least some visible keypoints - assert skeleton_count >= 1 # At least some skeleton connections - - # Test full image annotations - img_anns = person.to_image_annotations() - assert img_anns.texts_length == len(text_anns) - assert img_anns.points_length == len(points_anns) - - print("\n✓ Person annotations working correctly!") - print(f" - {len(person.get_visible_keypoints(0.5))}/17 visible keypoints") diff --git a/dimos/perception/detection/detectors/person/yolo.py b/dimos/perception/detection/detectors/person/yolo.py deleted file mode 100644 index 519f45f2f6..0000000000 --- a/dimos/perception/detection/detectors/person/yolo.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ultralytics import YOLO # type: ignore[attr-defined, import-not-found] - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.types import Detector -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.data import get_data -from dimos.utils.gpu_utils import is_cuda_available -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class YoloPersonDetector(Detector): - def __init__( - self, - model_path: str = "models_yolo", - model_name: str = "yolo11n-pose.pt", - device: str | None = None, - ) -> None: - self.model = YOLO(get_data(model_path) / model_name, task="track") - - self.tracker = get_data(model_path) / "botsort.yaml" - - if device: - self.device = device - return - - if is_cuda_available(): # type: ignore[no-untyped-call] - self.device = "cuda" - logger.info("Using CUDA for YOLO person detector") - else: - self.device = "cpu" - logger.info("Using CPU for YOLO person detector") - - def process_image(self, image: Image) -> ImageDetections2D: - """Process image and return detection results. - - Args: - image: Input image - - Returns: - ImageDetections2D containing Detection2DPerson objects with pose keypoints - """ - results = self.model.track( - source=image.to_opencv(), - verbose=False, - conf=0.5, - tracker=self.tracker, - persist=True, - device=self.device, - ) - return ImageDetections2D.from_ultralytics_result(image, results) - - def stop(self) -> None: - """ - Clean up resources used by the detector, including tracker threads. - """ - if hasattr(self.model, "predictor") and self.model.predictor is not None: - predictor = self.model.predictor - if hasattr(predictor, "trackers") and predictor.trackers: - for tracker in predictor.trackers: - if hasattr(tracker, "tracker") and hasattr(tracker.tracker, "gmc"): - gmc = tracker.tracker.gmc - if hasattr(gmc, "executor") and gmc.executor is not None: - gmc.executor.shutdown(wait=True) - self.model.predictor = None diff --git a/dimos/perception/detection/detectors/test_bbox_detectors.py b/dimos/perception/detection/detectors/test_bbox_detectors.py deleted file mode 100644 index 32a509061a..0000000000 --- a/dimos/perception/detection/detectors/test_bbox_detectors.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations -import pytest -from reactivex.disposable import CompositeDisposable - -from dimos.core import LCMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import Detection2D, ImageDetections2D - - -@pytest.fixture(params=["bbox_detector", "person_detector", "yoloe_detector"], scope="session") -def detector(request): - """Parametrized fixture that provides both bbox and person detectors.""" - return request.getfixturevalue(request.param) - - -@pytest.fixture(scope="session") -def get_topic_annotations(): - disposables = CompositeDisposable() - - def topic_annotations(suffix: str = "unnamed"): - annotations: LCMTransport[ImageAnnotations] = LCMTransport( - f"/annotations_{suffix}", ImageAnnotations - ) - disposables.add(annotations) - return annotations - - yield topic_annotations - disposables.dispose() - - -@pytest.fixture(scope="session") -def detections(detector, test_image, topic_image, get_topic_annotations): - """Get ImageDetections2D from any detector.""" - topic_image.publish(test_image) - detections = detector.process_image(test_image) - annotations = detections.to_foxglove_annotations() - print("annotations:", annotations) - topic_annotations = get_topic_annotations(detector.__class__.__name__) - topic_annotations.publish(annotations) - return detections - - -@pytest.fixture(scope="session") -def topic_image(): - image: LCMTransport[Image] = LCMTransport("/color_image", Image) - yield image - image.lcm.stop() - - -def test_detection_basic(detections) -> None: - """Test that we can detect objects with all detectors.""" - assert len(detections.detections) > 0 - - # Check first detection - detection = detections.detections[0] - assert isinstance(detection, Detection2D) - assert detection.confidence > 0 - assert len(detection.bbox) == 4 # bbox is a tuple (x1, y1, x2, y2) - assert detection.class_id >= 0 - assert detection.name is not None - - -def test_detection_bbox_properties(detections) -> None: - """Test Detection2D bbox properties work for all detectors.""" - detection = detections.detections[0] - - # Test bounding box is valid - x1, y1, x2, y2 = detection.bbox - assert x2 > x1, "x2 should be greater than x1" - assert y2 > y1, "y2 should be greater than y1" - assert all(coord >= 0 for coord in detection.bbox), "Coordinates should be non-negative" - - # Test bbox volume - volume = detection.bbox_2d_volume() - assert volume > 0 - expected_volume = (x2 - x1) * (y2 - y1) - assert abs(volume - expected_volume) < 0.01 - - # Test center calculation - center_x, center_y, width, height = detection.get_bbox_center() - assert center_x == (x1 + x2) / 2.0 - assert center_y == (y1 + y2) / 2.0 - assert width == x2 - x1 - assert height == y2 - y1 - - -def test_detection_cropped_image(detections, test_image) -> None: - """Test cropping image to detection bbox.""" - detection = detections.detections[0] - - # Test cropped image - cropped = detection.cropped_image(padding=20) - assert cropped is not None - - # Cropped image should be smaller than original (usually) - if test_image.shape: - assert cropped.shape[0] <= test_image.shape[0] - assert cropped.shape[1] <= test_image.shape[1] - - -def test_detection_annotations(detections) -> None: - """Test annotation generation for detections.""" - detection = detections.detections[0] - - # Test text annotations - all detections should have at least 2 - text_annotations = detection.to_text_annotation() - assert len(text_annotations) >= 2 # confidence and name/track_id (person has keypoints too) - - # Test points annotations - at least bbox - points_annotations = detection.to_points_annotation() - assert len(points_annotations) >= 1 # At least the bbox polygon - - # Test image annotations - annotations = detection.to_image_annotations() - assert annotations.texts_length >= 2 - assert annotations.points_length >= 1 - - -def test_detection_ros_conversion(detections) -> None: - """Test conversion to ROS Detection2D message.""" - detection = detections.detections[0] - - ros_det = detection.to_ros_detection2d() - - # Check bbox conversion - center_x, center_y, width, height = detection.get_bbox_center() - assert abs(ros_det.bbox.center.position.x - center_x) < 0.01 - assert abs(ros_det.bbox.center.position.y - center_y) < 0.01 - assert abs(ros_det.bbox.size_x - width) < 0.01 - assert abs(ros_det.bbox.size_y - height) < 0.01 - - # Check confidence and class_id - assert len(ros_det.results) > 0 - assert ros_det.results[0].hypothesis.score == detection.confidence - assert ros_det.results[0].hypothesis.class_id == detection.class_id - - -def test_detection_is_valid(detections) -> None: - """Test bbox validation.""" - detection = detections.detections[0] - - # Detection from real detector should be valid - assert detection.is_valid() - - -def test_image_detections2d_structure(detections) -> None: - """Test that process_image returns ImageDetections2D.""" - assert isinstance(detections, ImageDetections2D) - assert len(detections.detections) > 0 - assert all(isinstance(d, Detection2D) for d in detections.detections) - - -def test_multiple_detections(detections) -> None: - """Test that multiple objects can be detected.""" - print(f"\nDetected {len(detections.detections)} objects in test image") - - for i, detection in enumerate(detections.detections[:5]): # Show first 5 - print(f"\nDetection {i}:") - print(f" Class: {detection.name} (id: {detection.class_id})") - print(f" Confidence: {detection.confidence:.3f}") - print( - f" Bbox: ({detection.bbox[0]:.1f}, {detection.bbox[1]:.1f}, {detection.bbox[2]:.1f}, {detection.bbox[3]:.1f})" - ) - print(f" Track ID: {detection.track_id}") - - -def test_detection_string_representation(detections) -> None: - """Test string representation of detections.""" - detection = detections.detections[0] - str_repr = str(detection) - - # Should contain class name (either Detection2DBBox or Detection2DPerson) - assert "Detection2D" in str_repr - - # Should show object name - assert detection.name in str_repr or f"class_{detection.class_id}" in str_repr diff --git a/dimos/perception/detection/detectors/types.py b/dimos/perception/detection/detectors/types.py deleted file mode 100644 index e85c5ae18e..0000000000 --- a/dimos/perception/detection/detectors/types.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type import ImageDetections2D - - -class Detector(ABC): - @abstractmethod - def process_image(self, image: Image) -> ImageDetections2D: ... diff --git a/dimos/perception/detection/detectors/yolo.py b/dimos/perception/detection/detectors/yolo.py deleted file mode 100644 index c9a65a120e..0000000000 --- a/dimos/perception/detection/detectors/yolo.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ultralytics import YOLO # type: ignore[attr-defined, import-not-found] - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.types import Detector -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.data import get_data -from dimos.utils.gpu_utils import is_cuda_available -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class Yolo2DDetector(Detector): - def __init__( - self, - model_path: str = "models_yolo", - model_name: str = "yolo11n.pt", - device: str | None = None, - ) -> None: - self.model = YOLO( - get_data(model_path) / model_name, - task="detect", - ) - - if device: - self.device = device - return - - if is_cuda_available(): # type: ignore[no-untyped-call] - self.device = "cuda" - logger.debug("Using CUDA for YOLO 2d detector") - else: - self.device = "cpu" - logger.debug("Using CPU for YOLO 2d detector") - - def process_image(self, image: Image) -> ImageDetections2D: - """ - Process an image and return detection results. - - Args: - image: Input image - - Returns: - ImageDetections2D containing all detected objects - """ - results = self.model.track( - source=image.to_opencv(), - device=self.device, - conf=0.5, - iou=0.6, - persist=True, - verbose=False, - ) - - return ImageDetections2D.from_ultralytics_result(image, results) - - def stop(self) -> None: - """ - Clean up resources used by the detector, including tracker threads. - """ - if hasattr(self.model, "predictor") and self.model.predictor is not None: - predictor = self.model.predictor - if hasattr(predictor, "trackers") and predictor.trackers: - for tracker in predictor.trackers: - if hasattr(tracker, "tracker") and hasattr(tracker.tracker, "gmc"): - gmc = tracker.tracker.gmc - if hasattr(gmc, "executor") and gmc.executor is not None: - gmc.executor.shutdown(wait=True) - self.model.predictor = None diff --git a/dimos/perception/detection/detectors/yoloe.py b/dimos/perception/detection/detectors/yoloe.py deleted file mode 100644 index 9c9881209c..0000000000 --- a/dimos/perception/detection/detectors/yoloe.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from enum import Enum -import threading -from typing import Any - -import numpy as np -from numpy.typing import NDArray -from ultralytics import YOLOE # type: ignore[attr-defined, import-not-found] - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.detectors.types import Detector -from dimos.perception.detection.type import ImageDetections2D -from dimos.utils.data import get_data -from dimos.utils.gpu_utils import is_cuda_available - - -class YoloePromptMode(Enum): - """YOLO-E prompt modes.""" - - LRPC = "lrpc" - PROMPT = "prompt" - - -class Yoloe2DDetector(Detector): - def __init__( - self, - model_path: str = "models_yoloe", - model_name: str | None = None, - device: str | None = None, - prompt_mode: YoloePromptMode = YoloePromptMode.LRPC, - exclude_class_ids: list[int] | None = None, - max_area_ratio: float | None = 0.3, - ) -> None: - """ - Initialize YOLO-E 2D detector. - - Args: - model_path: Path to model directory (fetched via get_data from LFS). - model_name: Model filename. Defaults based on prompt_mode. - device: Device to run inference on ('cuda', 'cpu', or None for auto). - prompt_mode: LRPC for prompt-free detection, PROMPT for text/visual prompting. - exclude_class_ids: Class IDs to filter out from results (pass [] to disable). - max_area_ratio: Maximum bbox area ratio (0-1) relative to image. - """ - if model_name is None: - if prompt_mode == YoloePromptMode.LRPC: - model_name = "yoloe-11s-seg-pf.pt" - else: - model_name = "yoloe-11s-seg.pt" - - self.model = YOLOE(get_data(model_path) / model_name) - self.prompt_mode = prompt_mode - self._visual_prompts: dict[str, NDArray[Any]] | None = None - self.max_area_ratio = max_area_ratio - self._lock = threading.Lock() - - if prompt_mode == YoloePromptMode.PROMPT: - self.set_prompts(text=["nothing"]) - self.exclude_class_ids = set(exclude_class_ids) if exclude_class_ids else set() - - if self.max_area_ratio is not None and not (0.0 < self.max_area_ratio <= 1.0): - raise ValueError("max_area_ratio must be in the range (0, 1].") - - if device: - self.device = device - elif is_cuda_available(): # type: ignore[no-untyped-call] - self.device = "cuda" - else: - self.device = "cpu" - - def set_prompts( - self, - text: list[str] | None = None, - bboxes: NDArray[np.float64] | None = None, - ) -> None: - """ - Set prompts for detection. Provide either text or bboxes, not both. - - Args: - text: List of class names to detect. - bboxes: Bounding boxes in xyxy format, shape (N, 4). - """ - if text is not None and bboxes is not None: - raise ValueError("Provide either text or bboxes, not both.") - if text is None and bboxes is None: - raise ValueError("Must provide either text or bboxes.") - - with self._lock: - self.model.predictor = None - if text is not None: - self.model.set_classes(text, self.model.get_text_pe(text)) # type: ignore[no-untyped-call] - self._visual_prompts = None - else: - cls = np.arange(len(bboxes), dtype=np.int16) # type: ignore[arg-type] - self._visual_prompts = {"bboxes": bboxes, "cls": cls} # type: ignore[dict-item] - - def process_image(self, image: Image) -> "ImageDetections2D[Any]": - """ - Process an image and return detection results. - - Args: - image: Input image - - Returns: - ImageDetections2D containing all detected objects - """ - track_kwargs = { - "source": image.to_opencv(), - "device": self.device, - "conf": 0.6, - "iou": 0.6, - "persist": True, - "verbose": False, - } - - with self._lock: - if self._visual_prompts is not None: - track_kwargs["visual_prompts"] = self._visual_prompts - - results = self.model.track(**track_kwargs) # type: ignore[arg-type] - - detections = ImageDetections2D.from_ultralytics_result(image, results) - return self._apply_filters(image, detections) - - def _apply_filters( - self, - image: Image, - detections: "ImageDetections2D[Any]", - ) -> "ImageDetections2D[Any]": - if not self.exclude_class_ids and self.max_area_ratio is None: - return detections - - predicates = [] - - if self.exclude_class_ids: - predicates.append(lambda det: det.class_id not in self.exclude_class_ids) - - if self.max_area_ratio is not None: - image_area = image.width * image.height - - def area_filter(det): # type: ignore[no-untyped-def] - if image_area <= 0: - return True - return (det.bbox_2d_volume() / image_area) <= self.max_area_ratio - - predicates.append(area_filter) - - filtered = detections.detections - for predicate in predicates: - filtered = [det for det in filtered if predicate(det)] # type: ignore[no-untyped-call] - - return ImageDetections2D(image, filtered) - - def stop(self) -> None: - """Clean up resources used by the detector.""" - if hasattr(self.model, "predictor") and self.model.predictor is not None: - predictor = self.model.predictor - if hasattr(predictor, "trackers") and predictor.trackers: - for tracker in predictor.trackers: - if hasattr(tracker, "tracker") and hasattr(tracker.tracker, "gmc"): - gmc = tracker.tracker.gmc - if hasattr(gmc, "executor") and gmc.executor is not None: - gmc.executor.shutdown(wait=True) - self.model.predictor = None diff --git a/dimos/perception/detection/module2D.py b/dimos/perception/detection/module2D.py deleted file mode 100644 index cfca3b2192..0000000000 --- a/dimos/perception/detection/module2D.py +++ /dev/null @@ -1,179 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, -) -from reactivex import operators as ops -from reactivex.observable import Observable -from reactivex.subject import Subject - -from dimos import spec -from dimos.core import DimosCluster, In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image -from dimos.msgs.sensor_msgs.Image import sharpness_barrier -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.detectors import Detector # type: ignore[attr-defined] -from dimos.perception.detection.detectors.yolo import Yolo2DDetector -from dimos.perception.detection.type import Filter2D, ImageDetections2D -from dimos.utils.decorators.decorators import simple_mcache -from dimos.utils.reactive import backpressure - - -@dataclass -class Config(ModuleConfig): - max_freq: float = 10 - detector: Callable[[Any], Detector] | None = Yolo2DDetector - publish_detection_images: bool = True - camera_info: CameraInfo = None # type: ignore[assignment] - filter: list[Filter2D] | Filter2D | None = None - - def __post_init__(self) -> None: - if self.filter is None: - self.filter = [] - elif not isinstance(self.filter, list): - self.filter = [self.filter] - - -class Detection2DModule(Module): - default_config = Config - config: Config - detector: Detector - - color_image: In[Image] - - detections: Out[Detection2DArray] - annotations: Out[ImageAnnotations] - - detected_image_0: Out[Image] - detected_image_1: Out[Image] - detected_image_2: Out[Image] - - cnt: int = 0 - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self.detector = self.config.detector() # type: ignore[call-arg, misc] - self.vlm_detections_subject = Subject() # type: ignore[var-annotated] - self.previous_detection_count = 0 - - def process_image_frame(self, image: Image) -> ImageDetections2D: - imageDetections = self.detector.process_image(image) - if not self.config.filter: - return imageDetections - return imageDetections.filter(*self.config.filter) # type: ignore[misc, return-value] - - @simple_mcache - def sharp_image_stream(self) -> Observable[Image]: - return backpressure( - self.color_image.pure_observable().pipe( - sharpness_barrier(self.config.max_freq), - ) - ) - - @simple_mcache - def detection_stream_2d(self) -> Observable[ImageDetections2D]: - return backpressure(self.sharp_image_stream().pipe(ops.map(self.process_image_frame))) - - def track(self, detections: ImageDetections2D) -> None: - sensor_frame = self.tf.get("sensor", "camera_optical", detections.image.ts, 5.0) - - if not sensor_frame: - return - - if not detections.detections: - return - - sensor_frame.child_frame_id = "sensor_frame" - transforms = [sensor_frame] - - current_count = len(detections.detections) - max_count = max(current_count, self.previous_detection_count) - - # Publish transforms for all detection slots up to max_count - for index in range(max_count): - if index < current_count: - # Active detection - compute real position - detection = detections.detections[index] - position_3d = self.pixel_to_3d( # type: ignore[attr-defined] - detection.center_bbox, - self.config.camera_info, - assumed_depth=1.0, - ) - else: - # No detection at this index - publish zero transform - position_3d = Vector3(0.0, 0.0, 0.0) - - transforms.append( - Transform( - frame_id=sensor_frame.child_frame_id, - child_frame_id=f"det_{index}", - ts=detections.image.ts, - translation=position_3d, - ) - ) - - self.previous_detection_count = current_count - self.tf.publish(*transforms) - - @rpc - def start(self) -> None: - # self.detection_stream_2d().subscribe(self.track) - - self.detection_stream_2d().subscribe( - lambda det: self.detections.publish(det.to_ros_detection2d_array()) - ) - - self.detection_stream_2d().subscribe( - lambda det: self.annotations.publish(det.to_foxglove_annotations()) - ) - - def publish_cropped_images(detections: ImageDetections2D) -> None: - for index, detection in enumerate(detections[:3]): - image_topic = getattr(self, "detected_image_" + str(index)) - image_topic.publish(detection.cropped_image()) - - if self.config.publish_detection_images: - self.detection_stream_2d().subscribe(publish_cropped_images) - - @rpc - def stop(self) -> None: - return super().stop() # type: ignore[no-any-return] - - -def deploy( # type: ignore[no-untyped-def] - dimos: DimosCluster, - camera: spec.Camera, - prefix: str = "/detector2d", - **kwargs, -) -> Detection2DModule: - from dimos.core import LCMTransport - - detector = Detection2DModule(**kwargs) - detector.color_image.connect(camera.color_image) - - detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) - detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) - - detector.detected_image_0.transport = LCMTransport(f"{prefix}/image/0", Image) - detector.detected_image_1.transport = LCMTransport(f"{prefix}/image/1", Image) - detector.detected_image_2.transport = LCMTransport(f"{prefix}/image/2", Image) - - detector.start() - return detector diff --git a/dimos/perception/detection/module3D.py b/dimos/perception/detection/module3D.py deleted file mode 100644 index d275fbc85f..0000000000 --- a/dimos/perception/detection/module3D.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import TYPE_CHECKING, Any - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, -) -from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] -from reactivex import operators as ops -from reactivex.observable import Observable - -from dimos import spec -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.stream import In, Out -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module2D import Detection2DModule -from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D -from dimos.perception.detection.type.detection3d import Detection3DPC -from dimos.perception.detection.type.detection3d.imageDetections3DPC import ImageDetections3DPC -from dimos.types.timestamped import align_timestamped -from dimos.utils.reactive import backpressure - -if TYPE_CHECKING: - from dask.distributed import Client as DimosCluster -else: - DimosCluster = Any - - -class Detection3DModule(Detection2DModule): - color_image: In[Image] - pointcloud: In[PointCloud2] - - detections: Out[Detection2DArray] - annotations: Out[ImageAnnotations] - scene_update: Out[SceneUpdate] - - # just for visualization, - # emits latest pointclouds of detected objects in a frame - detected_pointcloud_0: Out[PointCloud2] - detected_pointcloud_1: Out[PointCloud2] - detected_pointcloud_2: Out[PointCloud2] - - # just for visualization, emits latest top 3 detections in a frame - detected_image_0: Out[Image] - detected_image_1: Out[Image] - detected_image_2: Out[Image] - - detection_3d_stream: Observable[ImageDetections3DPC] | None = None - - def process_frame( - self, - detections: ImageDetections2D, - pointcloud: PointCloud2, - transform: Transform, - ) -> ImageDetections3DPC: - if not transform: - return ImageDetections3DPC(detections.image, []) - - detection3d_list: list[Detection3DPC] = [] - for detection in detections: - detection3d = Detection3DPC.from_2d( - detection, - world_pointcloud=pointcloud, - camera_info=self.config.camera_info, - world_to_optical_transform=transform, - ) - if detection3d is not None: - detection3d_list.append(detection3d) - - return ImageDetections3DPC(detections.image, detection3d_list) - - def pixel_to_3d( - self, - pixel: tuple[int, int], - assumed_depth: float = 1.0, - ) -> Vector3: - """Unproject 2D pixel coordinates to 3D position in camera optical frame. - - Args: - camera_info: Camera calibration information - assumed_depth: Assumed depth in meters (default 1.0m from camera) - - Returns: - Vector3 position in camera optical frame coordinates - """ - # Extract camera intrinsics - fx, fy = self.config.camera_info.K[0], self.config.camera_info.K[4] - cx, cy = self.config.camera_info.K[2], self.config.camera_info.K[5] - - # Unproject pixel to normalized camera coordinates - x_norm = (pixel[0] - cx) / fx - y_norm = (pixel[1] - cy) / fy - - # Create 3D point at assumed depth in camera optical frame - # Camera optical frame: X right, Y down, Z forward - return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) - - @skill - def ask_vlm(self, question: str) -> str: - """asks a visual model about the view of the robot, for example - is the bannana in the trunk? - """ - from dimos.models.vl.qwen import QwenVlModel - - model = QwenVlModel() - image = self.color_image.get_next() - return model.query(image, question) - - # @skill - @rpc - def nav_vlm(self, question: str) -> str: - """ - query visual model about the view in front of the camera - you can ask to mark objects like: - - "red cup on the table left of the pencil" - "laptop on the desk" - "a person wearing a red shirt" - """ - from dimos.models.vl.qwen import QwenVlModel - - model = QwenVlModel() - image = self.color_image.get_next() - result = model.query_detections(image, question) - - print("VLM result:", result, "for", image, "and question", question) - - if isinstance(result, str) or not result or not len(result): - return None # type: ignore[return-value] - - detections: ImageDetections2D = result - - print(detections) - if not len(detections): - print("No 2d detections") - return None # type: ignore[return-value] - - pc = self.pointcloud.get_next() - transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) - - detections3d = self.process_frame(detections, pc, transform) - - if len(detections3d): - return detections3d[0].pose # type: ignore[no-any-return] - print("No 3d detections, projecting 2d") - - center = detections[0].get_bbox_center() - return PoseStamped( - ts=detections.image.ts, - frame_id="world", - position=self.pixel_to_3d(center, assumed_depth=1.5), - orientation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) - - @rpc - def start(self) -> None: - super().start() - - def detection2d_to_3d(args): # type: ignore[no-untyped-def] - detections, pc = args - transform = self.tf.get("camera_optical", pc.frame_id, detections.image.ts, 5.0) - return self.process_frame(detections, pc, transform) - - self.detection_stream_3d = align_timestamped( - backpressure(self.detection_stream_2d()), - self.pointcloud.observable(), # type: ignore[no-untyped-call] - match_tolerance=0.25, - buffer_size=20.0, - ).pipe(ops.map(detection2d_to_3d)) - - self.detection_stream_3d.subscribe(self._publish_detections) - - @rpc - def stop(self) -> None: - super().stop() - - def _publish_detections(self, detections: ImageDetections3DPC) -> None: - if not detections: - return - - for index, detection in enumerate(detections[:3]): - pointcloud_topic = getattr(self, "detected_pointcloud_" + str(index)) - pointcloud_topic.publish(detection.pointcloud) - - self.scene_update.publish(detections.to_foxglove_scene_update()) - - -def deploy( # type: ignore[no-untyped-def] - dimos: DimosCluster, - lidar: spec.Pointcloud, - camera: spec.Camera, - prefix: str = "/detector3d", - **kwargs, -) -> Detection3DModule: - from dimos.core import LCMTransport - - detector = dimos.deploy(Detection3DModule, camera_info=camera.hardware_camera_info, **kwargs) # type: ignore[attr-defined] - - detector.image.connect(camera.color_image) - detector.pointcloud.connect(lidar.pointcloud) - - detector.annotations.transport = LCMTransport(f"{prefix}/annotations", ImageAnnotations) - detector.detections.transport = LCMTransport(f"{prefix}/detections", Detection2DArray) - detector.scene_update.transport = LCMTransport(f"{prefix}/scene_update", SceneUpdate) - - detector.detected_image_0.transport = LCMTransport(f"{prefix}/image/0", Image) - detector.detected_image_1.transport = LCMTransport(f"{prefix}/image/1", Image) - detector.detected_image_2.transport = LCMTransport(f"{prefix}/image/2", Image) - - detector.detected_pointcloud_0.transport = LCMTransport(f"{prefix}/pointcloud/0", PointCloud2) - detector.detected_pointcloud_1.transport = LCMTransport(f"{prefix}/pointcloud/1", PointCloud2) - detector.detected_pointcloud_2.transport = LCMTransport(f"{prefix}/pointcloud/2", PointCloud2) - - detector.start() - - return detector # type: ignore[no-any-return] - - -detection3d_module = Detection3DModule.blueprint - -__all__ = ["Detection3DModule", "deploy", "detection3d_module"] diff --git a/dimos/perception/detection/moduleDB.py b/dimos/perception/detection/moduleDB.py deleted file mode 100644 index bc0a346a59..0000000000 --- a/dimos/perception/detection/moduleDB.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections.abc import Callable -from copy import copy -import threading -import time -from typing import Any - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, -) -from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] -from reactivex.observable import Observable - -from dimos.core.core import rpc -from dimos.core.stream import In, Out -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module3D import Detection3DModule -from dimos.perception.detection.type.detection3d import Detection3DPC -from dimos.perception.detection.type.detection3d.imageDetections3DPC import ImageDetections3DPC -from dimos.perception.detection.type.utils import TableStr - - -# Represents an object in space, as collection of 3d detections over time -class Object3D(Detection3DPC): - best_detection: Detection3DPC | None = None - center: Vector3 | None = None # type: ignore[assignment] - track_id: str | None = None # type: ignore[assignment] - detections: int = 0 - - def to_repr_dict(self) -> dict[str, Any]: - if self.center is None: - center_str = "None" - else: - center_str = ( - "[" + ", ".join(list(map(lambda n: f"{n:1f}", self.center.to_list()))) + "]" - ) - return { - "object_id": self.track_id, - "detections": self.detections, - "center": center_str, - } - - def __init__( # type: ignore[no-untyped-def] - self, track_id: str, detection: Detection3DPC | None = None, *args, **kwargs - ) -> None: - if detection is None: - return - self.ts = detection.ts - self.track_id = track_id - self.class_id = detection.class_id - self.name = detection.name - self.confidence = detection.confidence - self.pointcloud = detection.pointcloud - self.bbox = detection.bbox - self.transform = detection.transform - self.center = detection.center - self.frame_id = detection.frame_id - self.detections = self.detections + 1 - self.best_detection = detection - - def __add__(self, detection: Detection3DPC) -> "Object3D": - if self.track_id is None: - raise ValueError("Cannot add detection to object with None track_id") - new_object = Object3D(self.track_id) - new_object.bbox = detection.bbox - new_object.confidence = max(self.confidence, detection.confidence) - new_object.ts = max(self.ts, detection.ts) - new_object.track_id = self.track_id - new_object.class_id = self.class_id - new_object.name = self.name - new_object.transform = self.transform - new_object.pointcloud = self.pointcloud + detection.pointcloud - new_object.frame_id = self.frame_id - new_object.center = (self.center + detection.center) / 2 - new_object.detections = self.detections + 1 - - if detection.bbox_2d_volume() > self.bbox_2d_volume(): - new_object.best_detection = detection - else: - new_object.best_detection = self.best_detection - - return new_object - - def get_image(self) -> Image | None: - return self.best_detection.image if self.best_detection else None - - def scene_entity_label(self) -> str: - return f"{self.name} ({self.detections})" - - def agent_encode(self): # type: ignore[no-untyped-def] - return { - "id": self.track_id, - "name": self.name, - "detections": self.detections, - "last_seen": f"{round(time.time() - self.ts)}s ago", - # "position": self.to_pose().position.agent_encode(), - } - - def to_pose(self) -> PoseStamped: - if self.best_detection is None or self.center is None: - raise ValueError("Cannot compute pose without best_detection and center") - - optical_inverse = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ).inverse() - - print("transform is", self.best_detection.transform) - - global_transform = optical_inverse + self.best_detection.transform - - print("inverse optical is", global_transform) - - print("obj center is", self.center) - global_pose = global_transform.to_pose() - print("Global pose:", global_pose) - global_pose.frame_id = self.best_detection.frame_id - print("remap to", self.best_detection.frame_id) - return PoseStamped( - position=self.center, orientation=Quaternion(), frame_id=self.best_detection.frame_id - ) - - -class ObjectDBModule(Detection3DModule, TableStr): - cnt: int = 0 - objects: dict[str, Object3D] - object_stream: Observable[Object3D] | None = None - - goto: Callable[[PoseStamped], Any] | None = None - - color_image: In[Image] - pointcloud: In[PointCloud2] - - detections: Out[Detection2DArray] - annotations: Out[ImageAnnotations] - - detected_pointcloud_0: Out[PointCloud2] - detected_pointcloud_1: Out[PointCloud2] - detected_pointcloud_2: Out[PointCloud2] - - detected_image_0: Out[Image] - detected_image_1: Out[Image] - detected_image_2: Out[Image] - - scene_update: Out[SceneUpdate] - - target: Out[PoseStamped] - - remembered_locations: dict[str, PoseStamped] - - @rpc - def start(self) -> None: - Detection3DModule.start(self) - - def update_objects(imageDetections: ImageDetections3DPC) -> None: - for detection in imageDetections.detections: - self.add_detection(detection) - - def scene_thread() -> None: - while True: - scene_update = self.to_foxglove_scene_update() - self.scene_update.publish(scene_update) - time.sleep(1.0) - - threading.Thread(target=scene_thread, daemon=True).start() - - self.detection_stream_3d.subscribe(update_objects) - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self.goto = None - self.objects = {} - self.remembered_locations = {} - - def closest_object(self, detection: Detection3DPC) -> Object3D | None: - # Filter objects to only those with matching names - matching_objects = [obj for obj in self.objects.values() if obj.name == detection.name] - - if not matching_objects: - return None - - # Sort by distance - distances = sorted(matching_objects, key=lambda obj: detection.center.distance(obj.center)) - - return distances[0] - - def add_detections(self, detections: list[Detection3DPC]) -> list[Object3D]: - return [ - detection for detection in map(self.add_detection, detections) if detection is not None - ] - - def add_detection(self, detection: Detection3DPC): # type: ignore[no-untyped-def] - """Add detection to existing object or create new one.""" - closest = self.closest_object(detection) - if closest and closest.bounding_box_intersects(detection): - return self.add_to_object(closest, detection) - else: - return self.create_new_object(detection) - - def add_to_object(self, closest: Object3D, detection: Detection3DPC): # type: ignore[no-untyped-def] - new_object = closest + detection - if closest.track_id is not None: - self.objects[closest.track_id] = new_object - return new_object - - def create_new_object(self, detection: Detection3DPC): # type: ignore[no-untyped-def] - new_object = Object3D(f"obj_{self.cnt}", detection) - if new_object.track_id is not None: - self.objects[new_object.track_id] = new_object - self.cnt += 1 - return new_object - - def agent_encode(self) -> str: - ret = [] - for obj in copy(self.objects).values(): - # we need at least 3 detectieons to consider it a valid object - # for this to be serious we need a ratio of detections within the window of observations - if len(obj.detections) < 4: # type: ignore[arg-type] - continue - ret.append(str(obj.agent_encode())) # type: ignore[no-untyped-call] - if not ret: - return "No objects detected yet." - return "\n".join(ret) - - # @rpc - # def vlm_query(self, description: str) -> Object3D | None: - # imageDetections2D = super().ask_vlm(description) - # print("VLM query found", imageDetections2D, "detections") - # time.sleep(3) - - # if not imageDetections2D.detections: - # return None - - # ret = [] - # for obj in self.objects.values(): - # if obj.ts != imageDetections2D.ts: - # print( - # "Skipping", - # obj.track_id, - # "ts", - # obj.ts, - # "!=", - # imageDetections2D.ts, - # ) - # continue - # if obj.class_id != -100: - # continue - # if obj.name != imageDetections2D.detections[0].name: - # print("Skipping", obj.name, "!=", imageDetections2D.detections[0].name) - # continue - # ret.append(obj) - # ret.sort(key=lambda x: x.ts) - - # return ret[0] if ret else None - - def lookup(self, label: str) -> list[Detection3DPC]: - """Look up a detection by label.""" - return [] - - @rpc - def stop(self): # type: ignore[no-untyped-def] - return super().stop() - - def goto_object(self, object_id: str) -> Object3D | None: - """Go to object by id.""" - return self.objects.get(object_id, None) - - def to_foxglove_scene_update(self) -> "SceneUpdate": - """Convert all detections to a Foxglove SceneUpdate message. - - Returns: - SceneUpdate containing SceneEntity objects for all detections - """ - - # Create SceneUpdate message with all detections - scene_update = SceneUpdate() - scene_update.deletions_length = 0 - scene_update.deletions = [] - scene_update.entities = [] - - for obj in self.objects: - try: - scene_update.entities.append( - obj.to_foxglove_scene_entity(entity_id=f"{obj.name}_{obj.track_id}") # type: ignore[attr-defined] - ) - except Exception: - pass - - scene_update.entities_length = len(scene_update.entities) - return scene_update - - def __len__(self) -> int: - return len(self.objects.values()) - - -detection_db_module = ObjectDBModule.blueprint - -__all__ = ["ObjectDBModule", "detection_db_module"] diff --git a/dimos/perception/detection/objectDB.py b/dimos/perception/detection/objectDB.py deleted file mode 100644 index 9af8058c55..0000000000 --- a/dimos/perception/detection/objectDB.py +++ /dev/null @@ -1,321 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import threading -import time -from typing import TYPE_CHECKING, Any - -import open3d as o3d # type: ignore[import-untyped] - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs import Vector3 - from dimos.perception.detection.type.detection3d.object import Object - -logger = setup_logger() - - -class ObjectDB: - """Spatial memory database for 3D object detections. - - Maintains two tiers of objects internally: - - _pending_objects: Recently detected objects (detection_count < threshold) - - _objects: Confirmed permanent objects (detection_count >= threshold) - - Deduplication uses two heuristics: - 1. track_id match from YOLOE tracker (recent match) - 2. Center distance within threshold (spatial proximity match) - """ - - def __init__( - self, - distance_threshold: float = 0.2, - min_detections_for_permanent: int = 6, - pending_ttl_s: float = 5.0, - track_id_ttl_s: float = 5.0, - ) -> None: - self._distance_threshold = distance_threshold - self._min_detections = min_detections_for_permanent - self._pending_ttl_s = pending_ttl_s - self._track_id_ttl_s = track_id_ttl_s - - # Internal storage - keyed by object_id - self._pending_objects: dict[str, Object] = {} - self._objects: dict[str, Object] = {} # Permanent objects - - # track_id -> object_id mapping for fast lookup - self._track_id_map: dict[int, str] = {} - self._last_add_stats: dict[str, int] = {} - - self._lock = threading.RLock() - - # ───────────────────────────────────────────────────────────────── - # Public Methods - # ───────────────────────────────────────────────────────────────── - - def add_objects(self, objects: list[Object]) -> list[Object]: - """Add multiple objects to the database with deduplication. - - Args: - objects: List of Object instances from object_scene_registration - - Returns: - List of updated/created Object instances - """ - stats = { - "input": len(objects), - "created": 0, - "updated": 0, - "promoted": 0, - "matched_track": 0, - "matched_distance": 0, - } - - results: list[Object] = [] - now = time.time() - with self._lock: - self._prune_stale_pending(now) - for obj in objects: - matched, reason = self._match(obj, now) - if matched is None: - results.append(self._insert_pending(obj, now)) - stats["created"] += 1 - continue - - self._update_existing(matched, obj, now) - results.append(matched) - stats["updated"] += 1 - if reason == "track": - stats["matched_track"] += 1 - elif reason == "distance": - stats["matched_distance"] += 1 - if self._check_promotion(matched): - stats["promoted"] += 1 - - stats["pending"] = len(self._pending_objects) - stats["permanent"] = len(self._objects) - self._last_add_stats = stats - return results - - def get_last_add_stats(self) -> dict[str, int]: - with self._lock: - return dict(self._last_add_stats) - - def get_objects(self) -> list[Object]: - """Get all permanent objects (detection_count >= threshold).""" - with self._lock: - return list(self._objects.values()) - - def get_all_objects(self) -> list[Object]: - """Get all objects (both pending and permanent).""" - with self._lock: - return list(self._pending_objects.values()) + list(self._objects.values()) - - def promote(self, object_id: str) -> bool: - """Promote an object from pending to permanent.""" - with self._lock: - if object_id in self._pending_objects: - self._objects[object_id] = self._pending_objects.pop(object_id) - return True - return object_id in self._objects - - def find_by_name(self, name: str) -> list[Object]: - """Find all permanent objects with matching name.""" - with self._lock: - return [obj for obj in self._objects.values() if obj.name == name] - - def find_by_object_id(self, object_id: str) -> Object | None: - """Find an object by its object_id (searches pending and permanent).""" - with self._lock: - if object_id in self._objects: - return self._objects[object_id] - if object_id in self._pending_objects: - return self._pending_objects[object_id] - return None - - def find_nearest( - self, - position: Vector3, - name: str | None = None, - ) -> Object | None: - """Find nearest permanent object to a position, optionally filtered by name. - - Args: - position: Position to search from - name: Optional name filter - - Returns: - Nearest Object or None if no objects found - """ - with self._lock: - candidates = [ - obj - for obj in self._objects.values() - if obj.center is not None and (name is None or obj.name == name) - ] - - if not candidates: - return None - - return min(candidates, key=lambda obj: position.distance(obj.center)) # type: ignore[arg-type] - - def clear(self) -> None: - """Clear all objects from the database.""" - with self._lock: - # Drop Open3D pointcloud references before clearing to reduce shutdown warnings. - for obj in list(self._pending_objects.values()) + list(self._objects.values()): - obj.pointcloud = PointCloud2( - pointcloud=o3d.geometry.PointCloud(), - frame_id=obj.pointcloud.frame_id, - ts=obj.pointcloud.ts, - ) - self._pending_objects.clear() - self._objects.clear() - self._track_id_map.clear() - logger.info("ObjectDB cleared") - - def get_stats(self) -> dict[str, int]: - """Get statistics about the database.""" - with self._lock: - return { - "pending_count": len(self._pending_objects), - "permanent_count": len(self._objects), - "total_count": len(self._pending_objects) + len(self._objects), - } - - # ───────────────────────────────────────────────────────────────── - # Internal Methods - # ───────────────────────────────────────────────────────────────── - - def _match(self, obj: Object, now: float) -> tuple[Object | None, str | None]: - if obj.track_id >= 0: - matched = self._match_by_track_id(obj.track_id, now) - if matched is not None: - return matched, "track" - - matched = self._match_by_distance(obj) - if matched is not None: - return matched, "distance" - return None, None - - def _insert_pending(self, obj: Object, now: float) -> Object: - if not obj.ts: - obj.ts = now - self._pending_objects[obj.object_id] = obj - if obj.track_id >= 0: - self._track_id_map[obj.track_id] = obj.object_id - logger.info(f"Created new pending object {obj.object_id} ({obj.name})") - return obj - - def _update_existing(self, existing: Object, obj: Object, now: float) -> None: - existing.update_object(obj) - existing.ts = obj.ts or now - if obj.track_id >= 0: - self._track_id_map[obj.track_id] = existing.object_id - - def _match_by_track_id(self, track_id: int, now: float) -> Object | None: - """Find object with matching track_id from YOLOE.""" - if track_id < 0: - return None - - object_id = self._track_id_map.get(track_id) - if object_id is None: - return None - - # Check in permanent objects first - if object_id in self._objects: - obj = self._objects[object_id] - elif object_id in self._pending_objects: - obj = self._pending_objects[object_id] - else: - del self._track_id_map[track_id] - return None - - last_seen = obj.ts if obj.ts else now - if now - last_seen > self._track_id_ttl_s: - del self._track_id_map[track_id] - return None - - return obj - - def _match_by_distance(self, obj: Object) -> Object | None: - """Find object within distance threshold.""" - if obj.center is None: - return None - - # Combine all objects and filter by valid center - all_objects = list(self._objects.values()) + list(self._pending_objects.values()) - candidates = [ - o - for o in all_objects - if o.center is not None and obj.center.distance(o.center) < self._distance_threshold - ] - - if not candidates: - return None - - return min(candidates, key=lambda o: obj.center.distance(o.center)) # type: ignore[union-attr] - - def _prune_stale_pending(self, now: float) -> None: - if self._pending_ttl_s <= 0: - return - cutoff = now - self._pending_ttl_s - stale_ids = [ - obj_id for obj_id, obj in self._pending_objects.items() if (obj.ts or now) < cutoff - ] - for obj_id in stale_ids: - del self._pending_objects[obj_id] - for track_id, mapped_id in list(self._track_id_map.items()): - if mapped_id == obj_id: - del self._track_id_map[track_id] - - def _check_promotion(self, obj: Object) -> bool: - """Move object from pending to permanent if threshold met.""" - if obj.detections_count >= self._min_detections: - # Check if it's in pending - if obj.object_id in self._pending_objects: - # Promote to permanent - del self._pending_objects[obj.object_id] - self._objects[obj.object_id] = obj - logger.info( - f"Promoted object {obj.object_id} ({obj.name}) to permanent " - f"with {obj.detections_count} detections" - ) - return True - return False - - # ───────────────────────────────────────────────────────────────── - # Agent encoding - # ───────────────────────────────────────────────────────────────── - - def agent_encode(self) -> list[dict[str, Any]]: - """Encode permanent objects for agent consumption.""" - with self._lock: - return [obj.agent_encode() for obj in self._objects.values()] - - def __len__(self) -> int: - """Return number of permanent objects.""" - with self._lock: - return len(self._objects) - - def __repr__(self) -> str: - with self._lock: - return f"ObjectDB(permanent={len(self._objects)}, pending={len(self._pending_objects)})" - - -__all__ = ["ObjectDB"] diff --git a/dimos/perception/detection/person_tracker.py b/dimos/perception/detection/person_tracker.py deleted file mode 100644 index 50082742f0..0000000000 --- a/dimos/perception/detection/person_tracker.py +++ /dev/null @@ -1,126 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Any - -from reactivex import operators as ops -from reactivex.observable import Observable - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Transform, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.type import ImageDetections2D -from dimos.types.timestamped import align_timestamped -from dimos.utils.reactive import backpressure - - -class PersonTracker(Module): - detections: In[Detection2DArray] - color_image: In[Image] - target: Out[PoseStamped] - - camera_info: CameraInfo - - def __init__(self, cameraInfo: CameraInfo, **kwargs: Any) -> None: - super().__init__(**kwargs) - self.camera_info = cameraInfo - - def center_to_3d( - self, - pixel: tuple[float, float], - camera_info: CameraInfo, - assumed_depth: float = 1.0, - ) -> Vector3: - """Unproject 2D pixel coordinates to 3D position in camera_link frame. - - Args: - camera_info: Camera calibration information - assumed_depth: Assumed depth in meters (default 1.0m from camera) - - Returns: - Vector3 position in camera_link frame coordinates (Z up, X forward) - """ - # Extract camera intrinsics - fx, fy = camera_info.K[0], camera_info.K[4] - cx, cy = camera_info.K[2], camera_info.K[5] - - # Unproject pixel to normalized camera coordinates - x_norm = (pixel[0] - cx) / fx - y_norm = (pixel[1] - cy) / fy - - # Create 3D point at assumed depth in camera optical frame - # Camera optical frame: X right, Y down, Z forward - x_optical = x_norm * assumed_depth - y_optical = y_norm * assumed_depth - z_optical = assumed_depth - - # Transform from camera optical frame to camera_link frame - # Optical: X right, Y down, Z forward - # Link: X forward, Y left, Z up - # Transformation: x_link = z_optical, y_link = -x_optical, z_link = -y_optical - return Vector3(z_optical, -x_optical, -y_optical) - - def detections_stream(self) -> Observable[ImageDetections2D]: - return backpressure( - align_timestamped( - self.color_image.pure_observable(), - self.detections.pure_observable().pipe( - ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] - ), - match_tolerance=0.0, - buffer_size=2.0, - ).pipe( - ops.map( - lambda pair: ImageDetections2D.from_ros_detection2d_array(*pair) # type: ignore[misc, arg-type] - ) - ) - ) - - @rpc - def start(self) -> None: - self.detections_stream().subscribe(self.track) - - @rpc - def stop(self) -> None: - super().stop() - - def track(self, detections2D: ImageDetections2D) -> None: - if len(detections2D) == 0: - return - - target = max(detections2D.detections, key=lambda det: det.bbox_2d_volume()) - vector = self.center_to_3d(target.center_bbox, self.camera_info, 2.0) - - pose_in_camera = PoseStamped( - ts=detections2D.ts, - position=vector, - frame_id="camera_link", - ) - - tf_world_to_camera = self.tf.get("world", "camera_link", detections2D.ts, 5.0) - if not tf_world_to_camera: - return - - tf_camera_to_target = Transform.from_pose("target", pose_in_camera) - tf_world_to_target = tf_world_to_camera + tf_camera_to_target - pose_in_world = tf_world_to_target.to_pose(ts=detections2D.ts) - - self.target.publish(pose_in_world) - - -person_tracker_module = PersonTracker.blueprint - -__all__ = ["PersonTracker", "person_tracker_module"] diff --git a/dimos/perception/detection/reid/__init__.py b/dimos/perception/detection/reid/__init__.py deleted file mode 100644 index 31d50a894b..0000000000 --- a/dimos/perception/detection/reid/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem -from dimos.perception.detection.reid.module import Config, ReidModule -from dimos.perception.detection.reid.type import IDSystem, PassthroughIDSystem - -__all__ = [ - "Config", - "EmbeddingIDSystem", - # ID Systems - "IDSystem", - "PassthroughIDSystem", - # Module - "ReidModule", -] diff --git a/dimos/perception/detection/reid/embedding_id_system.py b/dimos/perception/detection/reid/embedding_id_system.py deleted file mode 100644 index 15bb491f5c..0000000000 --- a/dimos/perception/detection/reid/embedding_id_system.py +++ /dev/null @@ -1,266 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from typing import Literal - -import numpy as np - -from dimos.models.embedding.base import Embedding, EmbeddingModel -from dimos.perception.detection.reid.type import IDSystem -from dimos.perception.detection.type import Detection2DBBox - - -class EmbeddingIDSystem(IDSystem): - """Associates short-term track_ids to long-term unique detection IDs via embedding similarity. - - Maintains: - - All embeddings per track_id (as numpy arrays) for robust group comparison - - Negative constraints from co-occurrence (tracks in same frame = different objects) - - Mapping from track_id to unique long-term ID - """ - - def __init__( - self, - model: Callable[[], EmbeddingModel], - padding: int = 0, - similarity_threshold: float = 0.63, - comparison_mode: Literal["max", "mean", "top_k_mean"] = "top_k_mean", - top_k: int = 30, - max_embeddings_per_track: int = 500, - min_embeddings_for_matching: int = 10, - ) -> None: - """Initialize track associator. - - Args: - model: Callable (class or function) that returns an embedding model for feature extraction - padding: Padding to add around detection bbox when cropping (default: 0) - similarity_threshold: Minimum similarity for associating tracks (0-1) - comparison_mode: How to aggregate similarities between embedding groups - - "max": Use maximum similarity between any pair - - "mean": Use mean of all pairwise similarities - - "top_k_mean": Use mean of top-k similarities - top_k: Number of top similarities to average (if using top_k_mean) - max_embeddings_per_track: Maximum number of embeddings to keep per track - min_embeddings_for_matching: Minimum embeddings before attempting to match tracks - """ - # Call model factory (class or function) to get model instance - self.model = model() - - # Call start if available (Resource interface) - if hasattr(self.model, "start"): - self.model.start() - - self.padding = padding - self.similarity_threshold = similarity_threshold - self.comparison_mode = comparison_mode - self.top_k = top_k - self.max_embeddings_per_track = max_embeddings_per_track - self.min_embeddings_for_matching = min_embeddings_for_matching - - # Track embeddings (list of all embeddings as numpy arrays) - self.track_embeddings: dict[int, list[np.ndarray]] = {} # type: ignore[type-arg] - - # Negative constraints (track_ids that co-occurred = different objects) - self.negative_pairs: dict[int, set[int]] = {} - - # Track ID to long-term unique ID mapping - self.track_to_long_term: dict[int, int] = {} - self.long_term_counter: int = 0 - - # Similarity history for optional adaptive thresholding - self.similarity_history: list[float] = [] - - def register_detection(self, detection: Detection2DBBox) -> int: - """ - Register detection and return long-term ID. - - Args: - detection: Detection to register - - Returns: - Long-term unique ID for this detection - """ - # Extract embedding from detection's cropped image - cropped_image = detection.cropped_image(padding=self.padding) - embedding = self.model.embed(cropped_image) - assert not isinstance(embedding, list), "Expected single embedding for single image" - # Move embedding to CPU immediately to free GPU memory - embedding = embedding.to_cpu() - - # Update and associate track - self.update_embedding(detection.track_id, embedding) - return self.associate(detection.track_id) - - def update_embedding(self, track_id: int, new_embedding: Embedding) -> None: - """Add new embedding to track's embedding collection. - - Args: - track_id: Short-term track ID from detector - new_embedding: New embedding to add to collection - """ - # Convert to numpy array (already on CPU from feature extractor) - new_vec = new_embedding.to_numpy() - - # Ensure normalized for cosine similarity - norm = np.linalg.norm(new_vec) - if norm > 0: - new_vec = new_vec / norm - - if track_id not in self.track_embeddings: - self.track_embeddings[track_id] = [] - - embeddings = self.track_embeddings[track_id] - embeddings.append(new_vec) - - # Keep only most recent embeddings if limit exceeded - if len(embeddings) > self.max_embeddings_per_track: - embeddings.pop(0) # Remove oldest - - def _compute_group_similarity( - self, - query_embeddings: list[np.ndarray], # type: ignore[type-arg] - candidate_embeddings: list[np.ndarray], # type: ignore[type-arg] - ) -> float: - """Compute similarity between two groups of embeddings. - - Args: - query_embeddings: List of embeddings for query track - candidate_embeddings: List of embeddings for candidate track - - Returns: - Aggregated similarity score - """ - # Compute all pairwise similarities efficiently - query_matrix = np.stack(query_embeddings) # [M, D] - candidate_matrix = np.stack(candidate_embeddings) # [N, D] - - # Cosine similarity via matrix multiplication (already normalized) - similarities = query_matrix @ candidate_matrix.T # [M, N] - - if self.comparison_mode == "max": - # Maximum similarity across all pairs - return float(np.max(similarities)) - - elif self.comparison_mode == "mean": - # Mean of all pairwise similarities - return float(np.mean(similarities)) - - elif self.comparison_mode == "top_k_mean": - # Mean of top-k similarities - flat_sims = similarities.flatten() - k = min(self.top_k, len(flat_sims)) - top_k_sims = np.partition(flat_sims, -k)[-k:] - return float(np.mean(top_k_sims)) - - else: - raise ValueError(f"Unknown comparison mode: {self.comparison_mode}") - - def add_negative_constraints(self, track_ids: list[int]) -> None: - """Record that these track_ids co-occurred in same frame (different objects). - - Args: - track_ids: List of track_ids present in current frame - """ - # All pairs of track_ids in same frame can't be same object - for i, tid1 in enumerate(track_ids): - for tid2 in track_ids[i + 1 :]: - self.negative_pairs.setdefault(tid1, set()).add(tid2) - self.negative_pairs.setdefault(tid2, set()).add(tid1) - - def associate(self, track_id: int) -> int: - """Associate track_id to long-term unique detection ID. - - Args: - track_id: Short-term track ID to associate - - Returns: - Long-term unique detection ID - """ - # Already has assignment - if track_id in self.track_to_long_term: - return self.track_to_long_term[track_id] - - # Need embeddings to compare - if track_id not in self.track_embeddings or not self.track_embeddings[track_id]: - # Create new ID if no embeddings yet - new_id = self.long_term_counter - self.long_term_counter += 1 - self.track_to_long_term[track_id] = new_id - return new_id - - # Get query embeddings - query_embeddings = self.track_embeddings[track_id] - - # Don't attempt matching until we have enough embeddings for the query track - if len(query_embeddings) < self.min_embeddings_for_matching: - # Not ready yet - return -1 - return -1 - - # Build candidate list (only tracks with assigned long_term_ids) - best_similarity = -1.0 - best_track_id = None - - for other_tid, other_embeddings in self.track_embeddings.items(): - # Skip self - if other_tid == track_id: - continue - - # Skip if negative constraint (co-occurred) - if other_tid in self.negative_pairs.get(track_id, set()): - continue - - # Skip if no long_term_id yet - if other_tid not in self.track_to_long_term: - continue - - # Skip if not enough embeddings - if len(other_embeddings) < self.min_embeddings_for_matching: - continue - - # Compute group similarity - similarity = self._compute_group_similarity(query_embeddings, other_embeddings) - - if similarity > best_similarity: - best_similarity = similarity - best_track_id = other_tid - - # Check if best match exceeds threshold - if best_track_id is not None and best_similarity >= self.similarity_threshold: - matched_long_term_id = self.track_to_long_term[best_track_id] - print( - f"Track {track_id}: matched with track {best_track_id} " - f"(long_term_id={matched_long_term_id}, similarity={best_similarity:.4f}, " - f"mode={self.comparison_mode}, embeddings: {len(query_embeddings)} vs {len(self.track_embeddings[best_track_id])}), threshold: {self.similarity_threshold}" - ) - - # Track similarity history - self.similarity_history.append(best_similarity) - - # Associate with existing long_term_id - self.track_to_long_term[track_id] = matched_long_term_id - return matched_long_term_id - - # Create new unique detection ID - new_id = self.long_term_counter - self.long_term_counter += 1 - self.track_to_long_term[track_id] = new_id - - if best_track_id is not None: - print( - f"Track {track_id}: creating new ID {new_id} " - f"(best similarity={best_similarity:.4f} with id={self.track_to_long_term[best_track_id]} below threshold={self.similarity_threshold})" - ) - - return new_id diff --git a/dimos/perception/detection/reid/module.py b/dimos/perception/detection/reid/module.py deleted file mode 100644 index 0a359746d3..0000000000 --- a/dimos/perception/detection/reid/module.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, - TextAnnotation, -) -from dimos_lcm.foxglove_msgs.Point2 import Point2 -from reactivex import operators as ops -from reactivex.observable import Observable - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import In, Out -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem -from dimos.perception.detection.reid.type import IDSystem -from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D -from dimos.types.timestamped import align_timestamped, to_ros_stamp -from dimos.utils.reactive import backpressure - - -class Config(ModuleConfig): - idsystem: IDSystem - - -class ReidModule(Module): - default_config = Config - - detections: In[Detection2DArray] - image: In[Image] - annotations: Out[ImageAnnotations] - - def __init__(self, idsystem: IDSystem | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - if idsystem is None: - try: - from dimos.models.embedding import TorchReIDModel - - idsystem = EmbeddingIDSystem(model=TorchReIDModel, padding=0) # type: ignore[arg-type] - except Exception as e: - raise RuntimeError( - "TorchReIDModel not available. Please install with: pip install dimos[torchreid]" - ) from e - - self.idsystem = idsystem - - def detections_stream(self) -> Observable[ImageDetections2D]: - return backpressure( - align_timestamped( - self.image.pure_observable(), - self.detections.pure_observable().pipe( - ops.filter(lambda d: d.detections_length > 0) # type: ignore[attr-defined] - ), - match_tolerance=0.0, - buffer_size=2.0, - ).pipe(ops.map(lambda pair: ImageDetections2D.from_ros_detection2d_array(*pair))) # type: ignore[misc, arg-type] - ) - - @rpc - def start(self) -> None: - self.detections_stream().subscribe(self.ingress) - - @rpc - def stop(self) -> None: - super().stop() - - def ingress(self, imageDetections: ImageDetections2D) -> None: - text_annotations = [] - - for detection in imageDetections: - # Register detection and get long-term ID - long_term_id = self.idsystem.register_detection(detection) - - # Skip annotation if not ready yet (long_term_id == -1) - if long_term_id == -1: - continue - - # Create text annotation for long_term_id above the detection - x1, y1, _, _ = detection.bbox - font_size = imageDetections.image.width / 60 - - text_annotations.append( - TextAnnotation( - timestamp=to_ros_stamp(detection.ts), - position=Point2(x=x1, y=y1 - font_size * 1.5), - text=f"PERSON: {long_term_id}", - font_size=font_size, - text_color=Color(r=0.0, g=1.0, b=1.0, a=1.0), # Cyan - background_color=Color(r=0.0, g=0.0, b=0.0, a=0.8), - ) - ) - - # Publish annotations (even if empty to clear previous annotations) - annotations = ImageAnnotations( - texts=text_annotations, - texts_length=len(text_annotations), - points=[], - points_length=0, - ) - self.annotations.publish(annotations) diff --git a/dimos/perception/detection/reid/test_embedding_id_system.py b/dimos/perception/detection/reid/test_embedding_id_system.py deleted file mode 100644 index b9e6f591ee..0000000000 --- a/dimos/perception/detection/reid/test_embedding_id_system.py +++ /dev/null @@ -1,276 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem -from dimos.utils.data import get_data - - -@pytest.fixture(scope="session") -def mobileclip_model(): - """Load MobileCLIP model once for all tests.""" - from dimos.models.embedding.mobileclip import MobileCLIPModel - - model = MobileCLIPModel() # Uses default MobileCLIP2-S4 - model.start() - return model - - -@pytest.fixture -def track_associator(mobileclip_model): - """Create fresh EmbeddingIDSystem for each test.""" - return EmbeddingIDSystem( - model=lambda: mobileclip_model, - similarity_threshold=0.75, - min_embeddings_for_matching=1, # Allow matching with single embedding for tests - ) - - -@pytest.fixture(scope="session") -def test_image(): - """Load test image.""" - return Image.from_file(get_data("cafe.jpg")).to_rgb() - - -@pytest.mark.gpu -def test_update_embedding_single(track_associator, mobileclip_model, test_image) -> None: - """Test updating embedding for a single track.""" - embedding = mobileclip_model.embed(test_image) - - # First update - track_associator.update_embedding(track_id=1, new_embedding=embedding) - - assert 1 in track_associator.track_embeddings - assert len(track_associator.track_embeddings[1]) == 1 - - # Verify embedding is stored as numpy array and normalized - emb_vec = track_associator.track_embeddings[1][0] - assert isinstance(emb_vec, np.ndarray) - norm = np.linalg.norm(emb_vec) - assert abs(norm - 1.0) < 0.01, "Embedding should be normalized" - - -@pytest.mark.gpu -def test_update_embedding_multiple(track_associator, mobileclip_model, test_image) -> None: - """Test storing multiple embeddings per track.""" - embedding1 = mobileclip_model.embed(test_image) - embedding2 = mobileclip_model.embed(test_image) - - # Add first embedding - track_associator.update_embedding(track_id=1, new_embedding=embedding1) - first_vec = track_associator.track_embeddings[1][0].copy() - - # Add second embedding (same image, should be very similar) - track_associator.update_embedding(track_id=1, new_embedding=embedding2) - - # Should have 2 embeddings now - assert len(track_associator.track_embeddings[1]) == 2 - - # Both should be normalized - for emb in track_associator.track_embeddings[1]: - norm = np.linalg.norm(emb) - assert abs(norm - 1.0) < 0.01, "Embedding should be normalized" - - # Second embedding should be similar to first (same image) - second_vec = track_associator.track_embeddings[1][1] - similarity = float(np.dot(first_vec, second_vec)) - assert similarity > 0.99, "Same image should produce very similar embeddings" - - -@pytest.mark.gpu -def test_negative_constraints(track_associator) -> None: - """Test negative constraint recording.""" - # Simulate frame with 3 tracks - track_ids = [1, 2, 3] - track_associator.add_negative_constraints(track_ids) - - # Check that all pairs are recorded - assert 2 in track_associator.negative_pairs[1] - assert 3 in track_associator.negative_pairs[1] - assert 1 in track_associator.negative_pairs[2] - assert 3 in track_associator.negative_pairs[2] - assert 1 in track_associator.negative_pairs[3] - assert 2 in track_associator.negative_pairs[3] - - -@pytest.mark.gpu -def test_associate_new_track(track_associator, mobileclip_model, test_image) -> None: - """Test associating a new track creates new long_term_id.""" - embedding = mobileclip_model.embed(test_image) - track_associator.update_embedding(track_id=1, new_embedding=embedding) - - # First association should create new long_term_id - long_term_id = track_associator.associate(track_id=1) - - assert long_term_id == 0, "First track should get long_term_id=0" - assert track_associator.track_to_long_term[1] == 0 - assert track_associator.long_term_counter == 1 - - -@pytest.mark.gpu -def test_associate_similar_tracks(track_associator, mobileclip_model, test_image) -> None: - """Test associating similar tracks to same long_term_id.""" - # Create embeddings from same image (should be very similar) - embedding1 = mobileclip_model.embed(test_image) - embedding2 = mobileclip_model.embed(test_image) - - # Add first track - track_associator.update_embedding(track_id=1, new_embedding=embedding1) - long_term_id_1 = track_associator.associate(track_id=1) - - # Add second track with similar embedding - track_associator.update_embedding(track_id=2, new_embedding=embedding2) - long_term_id_2 = track_associator.associate(track_id=2) - - # Should get same long_term_id (similarity > 0.75) - assert long_term_id_1 == long_term_id_2, "Similar tracks should get same long_term_id" - assert track_associator.long_term_counter == 1, "Only one long_term_id should be created" - - -@pytest.mark.gpu -def test_associate_with_negative_constraint(track_associator, mobileclip_model, test_image) -> None: - """Test that negative constraints prevent association.""" - # Create similar embeddings - embedding1 = mobileclip_model.embed(test_image) - embedding2 = mobileclip_model.embed(test_image) - - # Add first track - track_associator.update_embedding(track_id=1, new_embedding=embedding1) - long_term_id_1 = track_associator.associate(track_id=1) - - # Add negative constraint (tracks co-occurred) - track_associator.add_negative_constraints([1, 2]) - - # Add second track with similar embedding - track_associator.update_embedding(track_id=2, new_embedding=embedding2) - long_term_id_2 = track_associator.associate(track_id=2) - - # Should get different long_term_ids despite high similarity - assert long_term_id_1 != long_term_id_2, ( - "Co-occurring tracks should get different long_term_ids" - ) - assert track_associator.long_term_counter == 2, "Two long_term_ids should be created" - - -@pytest.mark.gpu -def test_associate_different_objects(track_associator, mobileclip_model, test_image) -> None: - """Test that dissimilar embeddings get different long_term_ids.""" - # Create embeddings for image and text (very different) - image_emb = mobileclip_model.embed(test_image) - text_emb = mobileclip_model.embed_text("a dog") - - # Add first track (image) - track_associator.update_embedding(track_id=1, new_embedding=image_emb) - long_term_id_1 = track_associator.associate(track_id=1) - - # Add second track (text - very different embedding) - track_associator.update_embedding(track_id=2, new_embedding=text_emb) - long_term_id_2 = track_associator.associate(track_id=2) - - # Should get different long_term_ids (similarity < 0.75) - assert long_term_id_1 != long_term_id_2, "Different objects should get different long_term_ids" - assert track_associator.long_term_counter == 2 - - -@pytest.mark.gpu -def test_associate_returns_cached(track_associator, mobileclip_model, test_image) -> None: - """Test that repeated calls return same long_term_id.""" - embedding = mobileclip_model.embed(test_image) - track_associator.update_embedding(track_id=1, new_embedding=embedding) - - # First call - long_term_id_1 = track_associator.associate(track_id=1) - - # Second call should return cached result - long_term_id_2 = track_associator.associate(track_id=1) - - assert long_term_id_1 == long_term_id_2 - assert track_associator.long_term_counter == 1, "Should not create new ID" - - -@pytest.mark.gpu -def test_associate_no_embedding(track_associator) -> None: - """Test that associate creates new ID for track without embedding.""" - # Track with no embedding gets assigned a new ID - long_term_id = track_associator.associate(track_id=999) - assert long_term_id == 0, "Track without embedding should get new long_term_id" - assert track_associator.long_term_counter == 1 - - -@pytest.mark.gpu -def test_embeddings_stored_as_numpy(track_associator, mobileclip_model, test_image) -> None: - """Test that embeddings are stored as numpy arrays for efficient CPU comparisons.""" - embedding = mobileclip_model.embed(test_image) - track_associator.update_embedding(track_id=1, new_embedding=embedding) - - # Embeddings should be stored as numpy arrays - emb_list = track_associator.track_embeddings[1] - assert isinstance(emb_list, list) - assert len(emb_list) == 1 - assert isinstance(emb_list[0], np.ndarray) - - # Add more embeddings - embedding2 = mobileclip_model.embed(test_image) - track_associator.update_embedding(track_id=1, new_embedding=embedding2) - - assert len(track_associator.track_embeddings[1]) == 2 - for emb in track_associator.track_embeddings[1]: - assert isinstance(emb, np.ndarray) - - -@pytest.mark.gpu -def test_similarity_threshold_configurable(mobileclip_model) -> None: - """Test that similarity threshold is configurable.""" - associator_strict = EmbeddingIDSystem(model=lambda: mobileclip_model, similarity_threshold=0.95) - associator_loose = EmbeddingIDSystem(model=lambda: mobileclip_model, similarity_threshold=0.50) - - assert associator_strict.similarity_threshold == 0.95 - assert associator_loose.similarity_threshold == 0.50 - - -@pytest.mark.gpu -def test_multi_track_scenario(track_associator, mobileclip_model, test_image) -> None: - """Test realistic scenario with multiple tracks across frames.""" - # Frame 1: Track 1 appears - emb1 = mobileclip_model.embed(test_image) - track_associator.update_embedding(1, emb1) - track_associator.add_negative_constraints([1]) - lt1 = track_associator.associate(1) - - # Frame 2: Track 1 and Track 2 appear (different objects) - text_emb = mobileclip_model.embed_text("a dog") - track_associator.update_embedding(1, emb1) # Update embedding - track_associator.update_embedding(2, text_emb) - track_associator.add_negative_constraints([1, 2]) # Co-occur = different - lt2 = track_associator.associate(2) - - # Track 2 should get different ID due to negative constraint - assert lt1 != lt2 - - # Frame 3: Track 1 disappears, Track 3 appears (same as Track 1) - emb3 = mobileclip_model.embed(test_image) - track_associator.update_embedding(3, emb3) - track_associator.add_negative_constraints([2, 3]) - lt3 = track_associator.associate(3) - - # Track 3 should match Track 1 (not co-occurring, similar embedding) - assert lt3 == lt1 - - print("\nMulti-track scenario results:") - print(f" Track 1 -> long_term_id {lt1}") - print(f" Track 2 -> long_term_id {lt2} (different object, co-occurred)") - print(f" Track 3 -> long_term_id {lt3} (re-identified as Track 1)") diff --git a/dimos/perception/detection/reid/test_module.py b/dimos/perception/detection/reid/test_module.py deleted file mode 100644 index d962da6b6c..0000000000 --- a/dimos/perception/detection/reid/test_module.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.core import LCMTransport -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.perception.detection.reid.embedding_id_system import EmbeddingIDSystem -from dimos.perception.detection.reid.module import ReidModule - - -@pytest.mark.tool -def test_reid_ingress(imageDetections2d) -> None: - try: - from dimos.models.embedding import TorchReIDModel - except Exception: - pytest.skip("TorchReIDModel not available") - - # Create TorchReID-based IDSystem for testing - reid_model = TorchReIDModel(model_name="osnet_x1_0") - reid_model.start() - idsystem = EmbeddingIDSystem( - model=lambda: reid_model, - padding=20, - similarity_threshold=0.75, - ) - - reid_module = ReidModule(idsystem=idsystem, warmup=False) - print("Processing detections through ReidModule...") - reid_module.annotations._transport = LCMTransport("/annotations", ImageAnnotations) - reid_module.ingress(imageDetections2d) - reid_module._close_module() - print("✓ ReidModule ingress test completed successfully") diff --git a/dimos/perception/detection/reid/type.py b/dimos/perception/detection/reid/type.py deleted file mode 100644 index 61571e418f..0000000000 --- a/dimos/perception/detection/reid/type.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox - -if TYPE_CHECKING: - from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D - - -class IDSystem(ABC): - """Abstract base class for ID assignment systems.""" - - def register_detections(self, detections: ImageDetections2D) -> None: - """Register multiple detections.""" - for detection in detections.detections: - if isinstance(detection, Detection2DBBox): - self.register_detection(detection) - - @abstractmethod - def register_detection(self, detection: Detection2DBBox) -> int: - """ - Register a single detection, returning assigned (long term) ID. - - Args: - detection: Detection to register - - Returns: - Long-term unique ID for this detection - """ - ... - - -class PassthroughIDSystem(IDSystem): - """Simple ID system that returns track_id with no object permanence.""" - - def register_detection(self, detection: Detection2DBBox) -> int: - """Return detection's track_id as long-term ID (no permanence).""" - return detection.track_id diff --git a/dimos/perception/detection/test_moduleDB.py b/dimos/perception/detection/test_moduleDB.py deleted file mode 100644 index 23885a1c60..0000000000 --- a/dimos/perception/detection/test_moduleDB.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import time - -from lcm_msgs.foxglove_msgs import SceneUpdate -import pytest - -from dimos.core import LCMTransport -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.moduleDB import ObjectDBModule -from dimos.robot.unitree.go2 import connection as go2_connection - - -@pytest.mark.module -def test_moduleDB(dimos_cluster) -> None: - connection = go2_connection.deploy(dimos_cluster, "fake") - - moduleDB = dimos_cluster.deploy( - ObjectDBModule, - camera_info=go2_connection._camera_info_static(), - goto=lambda obj_id: print(f"Going to {obj_id}"), - ) - moduleDB.image.connect(connection.color_image) - moduleDB.pointcloud.connect(connection.lidar) - - moduleDB.annotations.transport = LCMTransport("/annotations", ImageAnnotations) - moduleDB.detections.transport = LCMTransport("/detections", Detection2DArray) - - moduleDB.detected_pointcloud_0.transport = LCMTransport("/detected/pointcloud/0", PointCloud2) - moduleDB.detected_pointcloud_1.transport = LCMTransport("/detected/pointcloud/1", PointCloud2) - moduleDB.detected_pointcloud_2.transport = LCMTransport("/detected/pointcloud/2", PointCloud2) - - moduleDB.detected_image_0.transport = LCMTransport("/detected/image/0", Image) - moduleDB.detected_image_1.transport = LCMTransport("/detected/image/1", Image) - moduleDB.detected_image_2.transport = LCMTransport("/detected/image/2", Image) - - moduleDB.scene_update.transport = LCMTransport("/scene_update", SceneUpdate) - moduleDB.target.transport = LCMTransport("/target", PoseStamped) - - connection.start() - moduleDB.start() - - time.sleep(4) - print("VLM RES", moduleDB.navigate_to_object_in_view("white floor")) - time.sleep(30) diff --git a/dimos/perception/detection/type/__init__.py b/dimos/perception/detection/type/__init__.py deleted file mode 100644 index 00cf943db3..0000000000 --- a/dimos/perception/detection/type/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "detection2d": [ - "Detection2D", - "Detection2DBBox", - "Detection2DPerson", - "Detection2DPoint", - "Filter2D", - "ImageDetections2D", - ], - "detection3d": [ - "Detection3D", - "Detection3DBBox", - "Detection3DPC", - "ImageDetections3DPC", - "PointCloudFilter", - "height_filter", - "radius_outlier", - "raycast", - "statistical", - ], - "imageDetections": ["ImageDetections"], - "utils": ["TableStr"], - }, -) diff --git a/dimos/perception/detection/type/detection2d/__init__.py b/dimos/perception/detection/type/detection2d/__init__.py deleted file mode 100644 index dc81916679..0000000000 --- a/dimos/perception/detection/type/detection2d/__init__.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.perception.detection.type.detection2d.base import Detection2D, Filter2D -from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox -from dimos.perception.detection.type.detection2d.imageDetections2D import ImageDetections2D -from dimos.perception.detection.type.detection2d.person import Detection2DPerson -from dimos.perception.detection.type.detection2d.point import Detection2DPoint -from dimos.perception.detection.type.detection2d.seg import Detection2DSeg - -__all__ = [ - "Detection2D", - "Detection2DBBox", - "Detection2DPerson", - "Detection2DPoint", - "Detection2DSeg", - "Filter2D", - "ImageDetections2D", -] diff --git a/dimos/perception/detection/type/detection2d/base.py b/dimos/perception/detection/type/detection2d/base.py deleted file mode 100644 index ee9374af8c..0000000000 --- a/dimos/perception/detection/type/detection2d/base.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import abstractmethod -from collections.abc import Callable - -from dimos_lcm.vision_msgs import Detection2D as ROSDetection2D - -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.sensor_msgs import Image -from dimos.types.timestamped import Timestamped - - -class Detection2D(Timestamped): - """Abstract base class for 2D detections.""" - - @abstractmethod - def cropped_image(self, padding: int = 20) -> Image: - """Return a cropped version of the image focused on the detection area.""" - ... - - @abstractmethod - def to_image_annotations(self) -> ImageAnnotations: - """Convert detection to Foxglove ImageAnnotations for visualization.""" - ... - - @abstractmethod - def to_ros_detection2d(self) -> ROSDetection2D: - """Convert detection to ROS Detection2D message.""" - ... - - @abstractmethod - def is_valid(self) -> bool: - """Check if the detection is valid.""" - ... - - -Filter2D = Callable[[Detection2D], bool] diff --git a/dimos/perception/detection/type/detection2d/bbox.py b/dimos/perception/detection/type/detection2d/bbox.py deleted file mode 100644 index 32109dffd3..0000000000 --- a/dimos/perception/detection/type/detection2d/bbox.py +++ /dev/null @@ -1,408 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -import hashlib -from typing import TYPE_CHECKING, Any - -from typing_extensions import Self - -if TYPE_CHECKING: - from ultralytics.engine.results import Results # type: ignore[import-not-found] - - from dimos.msgs.sensor_msgs import Image - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - PointsAnnotation, - TextAnnotation, -) -from dimos_lcm.foxglove_msgs.Point2 import Point2 -from dimos_lcm.vision_msgs import ( - BoundingBox2D, - Detection2D as ROSDetection2D, - ObjectHypothesis, - ObjectHypothesisWithPose, - Point2D, - Pose2D, -) -from rich.console import Console -from rich.text import Text - -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.msgs.std_msgs import Header -from dimos.perception.detection.type.detection2d.base import Detection2D -from dimos.types.timestamped import to_ros_stamp, to_timestamp -from dimos.utils.decorators.decorators import simple_mcache - -Bbox = tuple[float, float, float, float] -CenteredBbox = tuple[float, float, float, float] - - -def _hash_to_color(name: str) -> str: - """Generate a consistent color for a given name using hash.""" - # List of rich colors to choose from - colors = [ - "cyan", - "magenta", - "yellow", - "blue", - "green", - "red", - "bright_cyan", - "bright_magenta", - "bright_yellow", - "bright_blue", - "bright_green", - "bright_red", - "purple", - "white", - "pink", - ] - - # Hash the name and pick a color - hash_value = hashlib.md5(name.encode()).digest()[0] - return colors[hash_value % len(colors)] - - -@dataclass -class Detection2DBBox(Detection2D): - bbox: Bbox - track_id: int - class_id: int - confidence: float - name: str - ts: float - image: Image - - def to_repr_dict(self) -> dict[str, Any]: - """Return a dictionary representation of the detection for display purposes.""" - x1, y1, x2, y2 = self.bbox - return { - "name": self.name, - "class": str(self.class_id), - "track": str(self.track_id), - "conf": f"{self.confidence:.2f}", - "bbox": f"[{x1:.0f},{y1:.0f},{x2:.0f},{y2:.0f}]", - } - - def center_to_3d( - self, - pixel: tuple[int, int], - camera_info: CameraInfo, # type: ignore[name-defined] - assumed_depth: float = 1.0, - ) -> PoseStamped: # type: ignore[name-defined] - """Unproject 2D pixel coordinates to 3D position in camera optical frame. - - Args: - camera_info: Camera calibration information - assumed_depth: Assumed depth in meters (default 1.0m from camera) - - Returns: - Vector3 position in camera optical frame coordinates - """ - # Extract camera intrinsics - fx, fy = camera_info.K[0], camera_info.K[4] - cx, cy = camera_info.K[2], camera_info.K[5] - - # Unproject pixel to normalized camera coordinates - x_norm = (pixel[0] - cx) / fx - y_norm = (pixel[1] - cy) / fy - - # Create 3D point at assumed depth in camera optical frame - # Camera optical frame: X right, Y down, Z forward - return Vector3(x_norm * assumed_depth, y_norm * assumed_depth, assumed_depth) # type: ignore[name-defined] - - # return focused image, only on the bbox - def cropped_image(self, padding: int = 20) -> Image: - """Return a cropped version of the image focused on the bounding box. - - Args: - padding: Pixels to add around the bounding box (default: 20) - - Returns: - Cropped Image containing only the detection area plus padding - """ - x1, y1, x2, y2 = map(int, self.bbox) - return self.image.crop( - x1 - padding, y1 - padding, x2 - x1 + 2 * padding, y2 - y1 + 2 * padding - ) - - def __str__(self) -> str: - console = Console(force_terminal=True, legacy_windows=False) - d = self.to_repr_dict() - - # Build the string representation - parts = [ - Text(f"{self.__class__.__name__}("), - ] - - # Add any extra fields (e.g., points for Detection3D) - extra_keys = [k for k in d.keys() if k not in ["class"]] - for key in extra_keys: - if d[key] == "None": - parts.append(Text(f"{key}={d[key]}", style="dim")) - else: - parts.append(Text(f"{key}={d[key]}", style=_hash_to_color(key))) - - parts.append(Text(")")) - - # Render to string - with console.capture() as capture: - console.print(*parts, end="") - return capture.get().strip() - - @property - def center_bbox(self) -> tuple[float, float]: - """Get center point of bounding box.""" - x1, y1, x2, y2 = self.bbox - return ((x1 + x2) / 2, (y1 + y2) / 2) - - def bbox_2d_volume(self) -> float: - x1, y1, x2, y2 = self.bbox - width = max(0.0, x2 - x1) - height = max(0.0, y2 - y1) - return width * height - - @simple_mcache - def is_valid(self) -> bool: - """Check if detection bbox is valid. - - Validates that: - - Bounding box has positive dimensions - - Bounding box is within image bounds (if image has shape) - - Returns: - True if bbox is valid, False otherwise - """ - x1, y1, x2, y2 = self.bbox - - # Check positive dimensions - if x2 <= x1 or y2 <= y1: - return False - - # Check if within image bounds (if image has shape) - if self.image.shape: - h, w = self.image.shape[:2] - if not (0 <= x1 <= w and 0 <= y1 <= h and 0 <= x2 <= w and 0 <= y2 <= h): - return False - - return True - - @classmethod - def from_ultralytics_result(cls, result: Results, idx: int, image: Image) -> Detection2DBBox: - """Create Detection2DBBox from ultralytics Results object. - - Args: - result: Ultralytics Results object containing detection data - idx: Index of the detection in the results - image: Source image - - Returns: - Detection2DBBox instance - """ - if result.boxes is None: - raise ValueError("Result has no boxes") - - # Extract bounding box coordinates - bbox_array = result.boxes.xyxy[idx].cpu().numpy() - bbox: Bbox = ( - float(bbox_array[0]), - float(bbox_array[1]), - float(bbox_array[2]), - float(bbox_array[3]), - ) - - # Extract confidence - confidence = float(result.boxes.conf[idx].cpu()) - - # Extract class ID and name - class_id = int(result.boxes.cls[idx].cpu()) - name = ( - result.names.get(class_id, f"class_{class_id}") - if hasattr(result, "names") - else f"class_{class_id}" - ) - - # Extract track ID if available - track_id = -1 - if hasattr(result.boxes, "id") and result.boxes.id is not None: - track_id = int(result.boxes.id[idx].cpu()) - - return cls( - bbox=bbox, - track_id=track_id, - class_id=class_id, - confidence=confidence, - name=name, - ts=image.ts, - image=image, - ) - - def get_bbox_center(self) -> CenteredBbox: - x1, y1, x2, y2 = self.bbox - center_x = (x1 + x2) / 2.0 - center_y = (y1 + y2) / 2.0 - width = float(x2 - x1) - height = float(y2 - y1) - return (center_x, center_y, width, height) - - def to_ros_bbox(self) -> BoundingBox2D: - center_x, center_y, width, height = self.get_bbox_center() - return BoundingBox2D( - center=Pose2D( - position=Point2D(x=center_x, y=center_y), - theta=0.0, - ), - size_x=width, - size_y=height, - ) - - def lcm_encode(self): # type: ignore[no-untyped-def] - return self.to_image_annotations().lcm_encode() - - def to_text_annotation(self) -> list[TextAnnotation]: - x1, y1, _x2, y2 = self.bbox - - font_size = self.image.width / 80 - - # Build label text - exclude class_id if it's -1 (VLM detection) - if self.class_id == -1: - label_text = f"{self.name}_{self.track_id}" - else: - label_text = f"{self.name}_{self.class_id}_{self.track_id}" - - annotations = [ - TextAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=x1, y=y1), - text=label_text, - font_size=font_size, - text_color=Color(r=1.0, g=1.0, b=1.0, a=1), - background_color=Color(r=0, g=0, b=0, a=1), - ), - ] - - # Only show confidence if it's not 1.0 - if self.confidence != 1.0: - annotations.append( - TextAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=x1, y=y2 + font_size), - text=f"confidence: {self.confidence:.3f}", - font_size=font_size, - text_color=Color(r=1.0, g=1.0, b=1.0, a=1), - background_color=Color(r=0, g=0, b=0, a=1), - ) - ) - - return annotations - - def to_points_annotation(self) -> list[PointsAnnotation]: - x1, y1, x2, y2 = self.bbox - - thickness = 1 - - # Use consistent color based on object name, brighter for outline - outline_color = Color.from_string(self.name, alpha=1.0, brightness=1.25) - - return [ - PointsAnnotation( - timestamp=to_ros_stamp(self.ts), - outline_color=outline_color, - fill_color=Color.from_string(self.name, alpha=0.2), - thickness=thickness, - points_length=4, - points=[ - Point2(x1, y1), - Point2(x1, y2), - Point2(x2, y2), - Point2(x2, y1), - ], - type=PointsAnnotation.LINE_LOOP, - ) - ] - - # this is almost never called directly since this is a single detection - # and ImageAnnotations message normally contains multiple detections annotations - # so ImageDetections2D and ImageDetections3D normally implements this for whole image - def to_image_annotations(self) -> ImageAnnotations: - points = self.to_points_annotation() - texts = self.to_text_annotation() - - return ImageAnnotations( - texts=texts, - texts_length=len(texts), - points=points, - points_length=len(points), - ) - - @classmethod - def from_ros_detection2d(cls, ros_det: ROSDetection2D, **kwargs) -> Self: # type: ignore[no-untyped-def] - """Convert from ROS Detection2D message to Detection2D object.""" - # Extract bbox from ROS format - center_x = ros_det.bbox.center.position.x - center_y = ros_det.bbox.center.position.y - width = ros_det.bbox.size_x - height = ros_det.bbox.size_y - - # Convert centered bbox to corner format - x1 = center_x - width / 2.0 - y1 = center_y - height / 2.0 - x2 = center_x + width / 2.0 - y2 = center_y + height / 2.0 - bbox = (x1, y1, x2, y2) - - # Extract hypothesis info - class_id = 0 - confidence = 0.0 - if ros_det.results: - hypothesis = ros_det.results[0].hypothesis - class_id = hypothesis.class_id - confidence = hypothesis.score - - # Extract track_id - track_id = int(ros_det.id) if ros_det.id.isdigit() else 0 - - # Extract timestamp - ts = to_timestamp(ros_det.header.stamp) - - name = kwargs.pop("name", f"class_{class_id}") - - return cls( - bbox=bbox, - track_id=track_id, - class_id=class_id, - confidence=confidence, - name=name, - ts=ts, - **kwargs, - ) - - def to_ros_detection2d(self) -> ROSDetection2D: - return ROSDetection2D( - header=Header(self.ts, "camera_link"), - bbox=self.to_ros_bbox(), - results=[ - ObjectHypothesisWithPose( - ObjectHypothesis( - class_id=self.class_id, - score=self.confidence, - ) - ) - ], - id=str(self.track_id), - ) diff --git a/dimos/perception/detection/type/detection2d/imageDetections2D.py b/dimos/perception/detection/type/detection2d/imageDetections2D.py deleted file mode 100644 index 34033a9c50..0000000000 --- a/dimos/perception/detection/type/detection2d/imageDetections2D.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Generic - -from typing_extensions import TypeVar - -from dimos.perception.detection.type.detection2d.base import Detection2D -from dimos.perception.detection.type.detection2d.bbox import Detection2DBBox -from dimos.perception.detection.type.detection2d.person import Detection2DPerson -from dimos.perception.detection.type.detection2d.seg import Detection2DSeg -from dimos.perception.detection.type.imageDetections import ImageDetections - -if TYPE_CHECKING: - from ultralytics.engine.results import Results - - from dimos.msgs.sensor_msgs import Image - from dimos.msgs.vision_msgs import Detection2DArray - -T2D = TypeVar("T2D", bound=Detection2D, default=Detection2DBBox) - - -class ImageDetections2D(ImageDetections[T2D], Generic[T2D]): - @classmethod - def from_ros_detection2d_array( - cls, - image: Image, - ros_detections: Detection2DArray, - **kwargs: Any, - ) -> ImageDetections2D[Detection2DBBox]: - """Convert from ROS Detection2DArray message to ImageDetections2D object.""" - detections: list[Detection2DBBox] = [] - for ros_det in ros_detections.detections: - detection = Detection2DBBox.from_ros_detection2d(ros_det, image=image, **kwargs) - if detection.is_valid(): - detections.append(detection) - - return ImageDetections2D(image=image, detections=detections) - - @classmethod - def from_ultralytics_result( - cls, - image: Image, - results: list[Results], - ) -> ImageDetections2D[Detection2DBBox]: - """Create ImageDetections2D from ultralytics Results. - - Dispatches to appropriate Detection2D subclass based on result type: - - If masks present: creates Detection2DSeg - - If keypoints present: creates Detection2DPerson - - Otherwise: creates Detection2DBBox - - Args: - image: Source image - results: List of ultralytics Results objects - - Returns: - ImageDetections2D containing appropriate detection types - """ - - detections: list[Detection2DBBox] = [] - for result in results: - if result.boxes is None: - continue - - num_detections = len(result.boxes.xyxy) - for i in range(num_detections): - detection: Detection2DBBox - if result.masks is not None: - # Segmentation detection with mask - detection = Detection2DSeg.from_ultralytics_result(result, i, image) - elif result.keypoints is not None: - # Pose detection with keypoints - detection = Detection2DPerson.from_ultralytics_result(result, i, image) - else: - # Regular bbox detection - detection = Detection2DBBox.from_ultralytics_result(result, i, image) - if detection.is_valid(): - detections.append(detection) - - return ImageDetections2D(image=image, detections=detections) diff --git a/dimos/perception/detection/type/detection2d/person.py b/dimos/perception/detection/type/detection2d/person.py deleted file mode 100644 index efb12ebdbc..0000000000 --- a/dimos/perception/detection/type/detection2d/person.py +++ /dev/null @@ -1,345 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass - -# Import for type checking only to avoid circular imports -from typing import TYPE_CHECKING - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - PointsAnnotation, - TextAnnotation, -) -from dimos_lcm.foxglove_msgs.Point2 import Point2 -import numpy as np - -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.msgs.sensor_msgs import Image -from dimos.perception.detection.type.detection2d.bbox import Bbox, Detection2DBBox -from dimos.types.timestamped import to_ros_stamp -from dimos.utils.decorators.decorators import simple_mcache - -if TYPE_CHECKING: - from ultralytics.engine.results import Results # type: ignore[import-not-found] - - -@dataclass -class Detection2DPerson(Detection2DBBox): - """Represents a detected person with pose keypoints.""" - - # Pose keypoints - additional fields beyond Detection2DBBox - keypoints: np.ndarray # type: ignore[type-arg] # [17, 2] - x,y coordinates - keypoint_scores: np.ndarray # type: ignore[type-arg] # [17] - confidence scores - - # Optional normalized coordinates - bbox_normalized: np.ndarray | None = None # type: ignore[type-arg] # [x1, y1, x2, y2] in 0-1 range - keypoints_normalized: np.ndarray | None = None # type: ignore[type-arg] # [17, 2] in 0-1 range - - # Image dimensions for context - image_width: int | None = None - image_height: int | None = None - - # Keypoint names (class attribute) - KEYPOINT_NAMES = [ - "nose", - "left_eye", - "right_eye", - "left_ear", - "right_ear", - "left_shoulder", - "right_shoulder", - "left_elbow", - "right_elbow", - "left_wrist", - "right_wrist", - "left_hip", - "right_hip", - "left_knee", - "right_knee", - "left_ankle", - "right_ankle", - ] - - @classmethod - def from_ultralytics_result( - cls, result: "Results", idx: int, image: Image - ) -> "Detection2DPerson": - """Create Detection2DPerson from ultralytics Results object with pose keypoints. - - Args: - result: Ultralytics Results object containing detection and keypoint data - idx: Index of the detection in the results - image: Source image - - Returns: - Detection2DPerson instance - - Raises: - ValueError: If the result doesn't contain keypoints or is not a person detection - """ - # Validate that this is a pose detection result - if not hasattr(result, "keypoints") or result.keypoints is None: - raise ValueError( - "Cannot create Detection2DPerson from result without keypoints. " - "This appears to be a regular detection result, not a pose detection. " - "Use Detection2DBBox.from_ultralytics_result() instead." - ) - - if not hasattr(result, "boxes") or result.boxes is None: - raise ValueError("Cannot create Detection2DPerson from result without bounding boxes") - - # Check if this is actually a person detection (class 0 in COCO) - class_id = int(result.boxes.cls[idx].cpu()) - if class_id != 0: # Person is class 0 in COCO - class_name = ( - result.names.get(class_id, f"class_{class_id}") - if hasattr(result, "names") - else f"class_{class_id}" - ) - raise ValueError( - f"Cannot create Detection2DPerson from non-person detection. " - f"Got class {class_id} ({class_name}), expected class 0 (person)." - ) - - # Extract bounding box as tuple for Detection2DBBox - bbox_array = result.boxes.xyxy[idx].cpu().numpy() - - bbox: Bbox = ( - float(bbox_array[0]), - float(bbox_array[1]), - float(bbox_array[2]), - float(bbox_array[3]), - ) - - bbox_norm = ( - result.boxes.xyxyn[idx].cpu().numpy() if hasattr(result.boxes, "xyxyn") else None - ) - - confidence = float(result.boxes.conf[idx].cpu()) - class_id = int(result.boxes.cls[idx].cpu()) - - # Extract keypoints - if result.keypoints.xy is None or result.keypoints.conf is None: - raise ValueError("Keypoints xy or conf data is missing from the result") - - keypoints = result.keypoints.xy[idx].cpu().numpy() - keypoint_scores = result.keypoints.conf[idx].cpu().numpy() - keypoints_norm = ( - result.keypoints.xyn[idx].cpu().numpy() - if hasattr(result.keypoints, "xyn") and result.keypoints.xyn is not None - else None - ) - - # Get image dimensions - height, width = result.orig_shape - - # Extract track ID if available - track_id = idx # Use index as default - if hasattr(result.boxes, "id") and result.boxes.id is not None: - track_id = int(result.boxes.id[idx].cpu()) - - # Get class name - name = result.names.get(class_id, "person") if hasattr(result, "names") else "person" - - return cls( - # Detection2DBBox fields - bbox=bbox, - track_id=track_id, - class_id=class_id, - confidence=confidence, - name=name, - ts=image.ts, - image=image, - # Person specific fields - keypoints=keypoints, - keypoint_scores=keypoint_scores, - bbox_normalized=bbox_norm, - keypoints_normalized=keypoints_norm, - image_width=width, - image_height=height, - ) - - @classmethod - def from_yolo(cls, result: "Results", idx: int, image: Image) -> "Detection2DPerson": - """Alias for from_ultralytics_result for backward compatibility.""" - return cls.from_ultralytics_result(result, idx, image) - - @classmethod - def from_ros_detection2d(cls, *args, **kwargs) -> "Detection2DPerson": # type: ignore[no-untyped-def] - """Conversion from ROS Detection2D is not supported for Detection2DPerson. - - The ROS Detection2D message format does not include keypoint data, - which is required for Detection2DPerson. Use Detection2DBBox for - round-trip ROS conversions, or store keypoints separately. - - Raises: - NotImplementedError: Always raised as this conversion is impossible - """ - raise NotImplementedError( - "Cannot convert from ROS Detection2D to Detection2DPerson. " - "The ROS Detection2D message format does not contain keypoint data " - "(keypoints and keypoint_scores) which are required fields for Detection2DPerson. " - "Consider using Detection2DBBox for ROS conversions, or implement a custom " - "message format that includes pose keypoints." - ) - - def get_keypoint(self, name: str) -> tuple[np.ndarray, float]: # type: ignore[type-arg] - """Get specific keypoint by name. - Returns: - Tuple of (xy_coordinates, confidence_score) - """ - if name not in self.KEYPOINT_NAMES: - raise ValueError(f"Invalid keypoint name: {name}. Must be one of {self.KEYPOINT_NAMES}") - - idx = self.KEYPOINT_NAMES.index(name) - return self.keypoints[idx], self.keypoint_scores[idx] - - def get_visible_keypoints(self, threshold: float = 0.5) -> list[tuple[str, np.ndarray, float]]: # type: ignore[type-arg] - """Get all keypoints above confidence threshold. - Returns: - List of tuples: (keypoint_name, xy_coordinates, confidence) - """ - visible = [] - for i, (name, score) in enumerate( - zip(self.KEYPOINT_NAMES, self.keypoint_scores, strict=False) - ): - if score > threshold: - visible.append((name, self.keypoints[i], score)) - return visible - - @simple_mcache - def is_valid(self) -> bool: - valid_keypoints = sum(1 for score in self.keypoint_scores if score > 0.8) - return valid_keypoints >= 5 - - @property - def width(self) -> float: - """Get width of bounding box.""" - x1, _, x2, _ = self.bbox - return x2 - x1 - - @property - def height(self) -> float: - """Get height of bounding box.""" - _, y1, _, y2 = self.bbox - return y2 - y1 - - @property - def center(self) -> tuple[float, float]: - """Get center point of bounding box.""" - x1, y1, x2, y2 = self.bbox - return ((x1 + x2) / 2, (y1 + y2) / 2) - - def to_points_annotation(self) -> list[PointsAnnotation]: - """Override to include keypoint visualizations along with bounding box.""" - annotations = [] - - # First add the bounding box from parent class - annotations.extend(super().to_points_annotation()) - - # Add keypoints as circles - visible_keypoints = self.get_visible_keypoints(threshold=0.3) - - # Create points for visible keypoints - if visible_keypoints: - keypoint_points = [] - for _name, xy, _conf in visible_keypoints: - keypoint_points.append(Point2(float(xy[0]), float(xy[1]))) - - # Add keypoints as circles - annotations.append( - PointsAnnotation( - timestamp=to_ros_stamp(self.ts), - outline_color=Color(r=0.0, g=1.0, b=0.0, a=1.0), # Green outline - fill_color=Color(r=0.0, g=1.0, b=0.0, a=0.5), # Semi-transparent green - thickness=2.0, - points_length=len(keypoint_points), - points=keypoint_points, - type=PointsAnnotation.POINTS, # Draw as individual points/circles - ) - ) - - # Add skeleton connections (COCO skeleton) - skeleton_connections = [ - # Face - (0, 1), - (0, 2), - (1, 3), - (2, 4), # nose to eyes, eyes to ears - # Arms - (5, 6), # shoulders - (5, 7), - (7, 9), # left arm - (6, 8), - (8, 10), # right arm - # Torso - (5, 11), - (6, 12), - (11, 12), # shoulders to hips, hip to hip - # Legs - (11, 13), - (13, 15), # left leg - (12, 14), - (14, 16), # right leg - ] - - # Draw skeleton lines between connected keypoints - for start_idx, end_idx in skeleton_connections: - if ( - start_idx < len(self.keypoint_scores) - and end_idx < len(self.keypoint_scores) - and self.keypoint_scores[start_idx] > 0.3 - and self.keypoint_scores[end_idx] > 0.3 - ): - start_point = Point2( - float(self.keypoints[start_idx][0]), float(self.keypoints[start_idx][1]) - ) - end_point = Point2( - float(self.keypoints[end_idx][0]), float(self.keypoints[end_idx][1]) - ) - - annotations.append( - PointsAnnotation( - timestamp=to_ros_stamp(self.ts), - outline_color=Color(r=0.0, g=0.8, b=1.0, a=0.8), # Cyan - thickness=1.5, - points_length=2, - points=[start_point, end_point], - type=PointsAnnotation.LINE_LIST, - ) - ) - - return annotations - - def to_text_annotation(self) -> list[TextAnnotation]: - """Override to include pose information in text annotations.""" - # Get base annotations from parent - annotations = super().to_text_annotation() - - # Add pose-specific info - visible_count = len(self.get_visible_keypoints(threshold=0.5)) - x1, _y1, _x2, y2 = self.bbox - - annotations.append( - TextAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=x1, y=y2 + 40), # Below confidence text - text=f"keypoints: {visible_count}/17", - font_size=18, - text_color=Color(r=0.0, g=1.0, b=0.0, a=1), - background_color=Color(r=0, g=0, b=0, a=0.7), - ) - ) - - return annotations diff --git a/dimos/perception/detection/type/detection2d/point.py b/dimos/perception/detection/type/detection2d/point.py deleted file mode 100644 index 216ec57b82..0000000000 --- a/dimos/perception/detection/type/detection2d/point.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - CircleAnnotation, - TextAnnotation, -) -from dimos_lcm.foxglove_msgs.Point2 import Point2 -from dimos_lcm.vision_msgs import ( - BoundingBox2D, - Detection2D as ROSDetection2D, - ObjectHypothesis, - ObjectHypothesisWithPose, - Point2D, - Pose2D, -) - -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.msgs.std_msgs import Header -from dimos.perception.detection.type.detection2d.base import Detection2D -from dimos.types.timestamped import to_ros_stamp - -if TYPE_CHECKING: - from dimos.msgs.sensor_msgs import Image - - -@dataclass -class Detection2DPoint(Detection2D): - """A 2D point detection, visualized as a circle.""" - - x: float - y: float - name: str - ts: float - image: Image - track_id: int = -1 - class_id: int = -1 - confidence: float = 1.0 - - def to_repr_dict(self) -> dict[str, str]: - """Return a dictionary representation for display purposes.""" - return { - "name": self.name, - "track": str(self.track_id), - "conf": f"{self.confidence:.2f}", - "point": f"({self.x:.0f},{self.y:.0f})", - } - - def cropped_image(self, padding: int = 20) -> Image: - """Return a cropped version of the image focused on the point. - - Args: - padding: Pixels to add around the point (default: 20) - - Returns: - Cropped Image containing the area around the point - """ - x, y = int(self.x), int(self.y) - return self.image.crop( - x - padding, - y - padding, - 2 * padding, - 2 * padding, - ) - - @property - def diameter(self) -> float: - return self.image.width / 40 - - def to_circle_annotation(self) -> list[CircleAnnotation]: - """Return circle annotations for visualization.""" - return [ - CircleAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=self.x, y=self.y), - diameter=self.diameter, - thickness=1.0, - fill_color=Color.from_string(self.name, alpha=0.3), - outline_color=Color.from_string(self.name, alpha=1.0, brightness=1.25), - ) - ] - - def to_text_annotation(self) -> list[TextAnnotation]: - """Return text annotations for visualization.""" - font_size = self.image.width / 80 - - # Build label text - if self.class_id == -1: - if self.track_id == -1: - label_text = self.name - else: - label_text = f"{self.name}_{self.track_id}" - else: - label_text = f"{self.name}_{self.class_id}_{self.track_id}" - - annotations = [ - TextAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=self.x + self.diameter / 2, y=self.y + self.diameter / 2), - text=label_text, - font_size=font_size, - text_color=Color(r=1.0, g=1.0, b=1.0, a=1), - background_color=Color(r=0, g=0, b=0, a=1), - ), - ] - - # Only show confidence if it's not 1.0 - if self.confidence != 1.0: - annotations.append( - TextAnnotation( - timestamp=to_ros_stamp(self.ts), - position=Point2(x=self.x + self.diameter / 2 + 2, y=self.y + font_size + 2), - text=f"{self.confidence:.2f}", - font_size=font_size, - text_color=Color(r=1.0, g=1.0, b=1.0, a=1), - background_color=Color(r=0, g=0, b=0, a=1), - ) - ) - - return annotations - - def to_image_annotations(self) -> ImageAnnotations: - """Convert detection to Foxglove ImageAnnotations for visualization.""" - texts = self.to_text_annotation() - circles = self.to_circle_annotation() - - return ImageAnnotations( - texts=texts, - texts_length=len(texts), - points=[], - points_length=0, - circles=circles, - circles_length=len(circles), - ) - - def to_ros_detection2d(self) -> ROSDetection2D: - """Convert point to ROS Detection2D message (as zero-size bbox at point).""" - return ROSDetection2D( - header=Header(self.ts, "camera_link"), - bbox=BoundingBox2D( - center=Pose2D( - position=Point2D(x=self.x, y=self.y), - theta=0.0, - ), - size_x=0.0, - size_y=0.0, - ), - results=[ - ObjectHypothesisWithPose( - ObjectHypothesis( - class_id=self.class_id, - score=self.confidence, - ) - ) - ], - id=str(self.track_id), - ) - - def is_valid(self) -> bool: - """Check if the point is within image bounds.""" - if self.image.shape: - h, w = self.image.shape[:2] - return bool(0 <= self.x <= w and 0 <= self.y <= h) - return True - - def lcm_encode(self): # type: ignore[no-untyped-def] - return self.to_image_annotations().lcm_encode() diff --git a/dimos/perception/detection/type/detection2d/seg.py b/dimos/perception/detection/type/detection2d/seg.py deleted file mode 100644 index 5d4d55d0c3..0000000000 --- a/dimos/perception/detection/type/detection2d/seg.py +++ /dev/null @@ -1,206 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -import cv2 -from dimos_lcm.foxglove_msgs.ImageAnnotations import PointsAnnotation -from dimos_lcm.foxglove_msgs.Point2 import Point2 -import numpy as np -import torch - -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.perception.detection.type.detection2d.bbox import Bbox, Detection2DBBox -from dimos.types.timestamped import to_ros_stamp - -if TYPE_CHECKING: - from ultralytics.engine.results import Results - - from dimos.msgs.sensor_msgs import Image - - -@dataclass -class Detection2DSeg(Detection2DBBox): - """Represents a detection with a segmentation mask.""" - - mask: np.ndarray[Any, np.dtype[np.uint8]] # Binary mask [H, W], uint8 0 or 255 - - @classmethod - def from_sam2_result( - cls, - mask: np.ndarray[Any, Any] | torch.Tensor, - obj_id: int, - image: Image, - class_id: int = 0, - name: str = "object", - confidence: float = 1.0, - ) -> Detection2DSeg: - """Create Detection2DSeg from SAM output (single object). - - Args: - mask: Segmentation mask (logits or binary). Shape [H, W] or [1, H, W]. - obj_id: Tracking ID of the object. - image: Source image. - class_id: Class ID (default 0). - name: Class name (default "object"). - confidence: Confidence score (default 1.0). - - Returns: - Detection2DSeg instance. - """ - # Convert mask to numpy if tensor - if isinstance(mask, torch.Tensor): - mask = mask.detach().cpu().numpy() - - # Handle dimensions (EdgeTAM might return [1, H, W] or [H, W]) - if mask.ndim == 3: - mask = mask.squeeze() - - # Binarize if it's logits (usually < 0 is background, > 0 is foreground) - # or if it's boolean - if mask.dtype == bool: - mask = mask.astype(np.uint8) * 255 - elif np.issubdtype(mask.dtype, np.floating): - mask = (mask > 0.0).astype(np.uint8) * 255 - - # Calculate bbox - y_indices, x_indices = np.where(mask > 0) - if len(x_indices) > 0: - x1_val, y1_val = float(np.min(x_indices)), float(np.min(y_indices)) - x2_val, y2_val = float(np.max(x_indices)), float(np.max(y_indices)) - else: - x1_val = y1_val = x2_val = y2_val = 0.0 - - bbox = (x1_val, y1_val, x2_val, y2_val) - - return cls( - bbox=bbox, - track_id=obj_id, - class_id=class_id, - confidence=confidence, - name=name, - ts=image.ts, - image=image, - mask=mask.astype(np.uint8), # type: ignore[arg-type] - ) - - @classmethod - def from_ultralytics_result(cls, result: Results, idx: int, image: Image) -> Detection2DSeg: - """Create Detection2DSeg from ultralytics Results object with segmentation mask. - - Args: - result: Ultralytics Results object containing detection and mask data - idx: Index of the detection in the results - image: Source image - - Returns: - Detection2DSeg instance - """ - if result.boxes is None: - raise ValueError("Result has no boxes") - - # Extract bounding box coordinates - bbox_array = result.boxes.xyxy[idx].cpu().numpy() - bbox: Bbox = ( - float(bbox_array[0]), - float(bbox_array[1]), - float(bbox_array[2]), - float(bbox_array[3]), - ) - - # Extract confidence - confidence = float(result.boxes.conf[idx].cpu()) - - # Extract class ID and name - class_id = int(result.boxes.cls[idx].cpu()) - if hasattr(result, "names") and result.names is not None: - if isinstance(result.names, dict): - name = result.names.get(class_id, f"class_{class_id}") - elif isinstance(result.names, list) and class_id < len(result.names): - name = result.names[class_id] - else: - name = f"class_{class_id}" - else: - name = f"class_{class_id}" - - # Extract track ID if available - track_id = -1 - if hasattr(result.boxes, "id") and result.boxes.id is not None: - track_id = int(result.boxes.id[idx].cpu()) - - # Extract mask - mask = np.zeros((image.height, image.width), dtype=np.uint8) - if result.masks is not None and idx < len(result.masks.data): - mask_tensor = result.masks.data[idx] - mask_np = mask_tensor.cpu().numpy() - - # Resize mask to image size if needed - if mask_np.shape != (image.height, image.width): - mask_np = cv2.resize( - mask_np.astype(np.float32), - (image.width, image.height), - interpolation=cv2.INTER_LINEAR, - ) - - # Binarize mask - mask = (mask_np > 0.5).astype(np.uint8) * 255 # type: ignore[assignment] - - return cls( - bbox=bbox, - track_id=track_id, - class_id=class_id, - confidence=confidence, - name=name, - ts=image.ts, - image=image, - mask=mask, - ) - - def to_points_annotation(self) -> list[PointsAnnotation]: - """Override to include mask outline.""" - annotations = super().to_points_annotation() - - # Find contours - contours, _ = cv2.findContours(self.mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - for contour in contours: - # Simplify contour to reduce points - epsilon = 0.005 * cv2.arcLength(contour, True) - approx = cv2.approxPolyDP(contour, epsilon, True) - - points = [] - for i in range(len(approx)): - x_coord = float(approx[i, 0, 0]) - y_coord = float(approx[i, 0, 1]) - points.append(Point2(x=x_coord, y=y_coord)) - - if len(points) < 3: - continue - - annotations.append( - PointsAnnotation( - timestamp=to_ros_stamp(self.ts), - outline_color=Color.from_string(str(self.class_id), alpha=1.0, brightness=1.25), - fill_color=Color.from_string(str(self.track_id), alpha=0.4), - thickness=1.0, - points_length=len(points), - points=points, - type=PointsAnnotation.LINE_LOOP, - ) - ) - - return annotations diff --git a/dimos/perception/detection/type/detection2d/test_bbox.py b/dimos/perception/detection/type/detection2d/test_bbox.py deleted file mode 100644 index 5a76b41601..0000000000 --- a/dimos/perception/detection/type/detection2d/test_bbox.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pytest - - -def test_detection2d(detection2d) -> None: - # def test_detection_basic_properties(detection2d): - """Test basic detection properties.""" - assert detection2d.track_id >= 0 - assert detection2d.class_id >= 0 - assert 0.0 <= detection2d.confidence <= 1.0 - assert detection2d.name is not None - assert detection2d.ts > 0 - - # def test_bounding_box_format(detection2d): - """Test bounding box format and validity.""" - bbox = detection2d.bbox - assert len(bbox) == 4, "Bounding box should have 4 values" - - x1, y1, x2, y2 = bbox - assert x2 > x1, "x2 should be greater than x1" - assert y2 > y1, "y2 should be greater than y1" - assert x1 >= 0, "x1 should be non-negative" - assert y1 >= 0, "y1 should be non-negative" - - # def test_bbox_2d_volume(detection2d): - """Test bounding box volume calculation.""" - volume = detection2d.bbox_2d_volume() - assert volume > 0, "Bounding box volume should be positive" - - # Calculate expected volume - x1, y1, x2, y2 = detection2d.bbox - expected_volume = (x2 - x1) * (y2 - y1) - assert volume == pytest.approx(expected_volume, abs=0.001) - - # def test_bbox_center_calculation(detection2d): - """Test bounding box center calculation.""" - center_bbox = detection2d.get_bbox_center() - assert len(center_bbox) == 4, "Center bbox should have 4 values" - - center_x, center_y, width, height = center_bbox - x1, y1, x2, y2 = detection2d.bbox - - # Verify center calculations - assert center_x == pytest.approx((x1 + x2) / 2.0, abs=0.001) - assert center_y == pytest.approx((y1 + y2) / 2.0, abs=0.001) - assert width == pytest.approx(x2 - x1, abs=0.001) - assert height == pytest.approx(y2 - y1, abs=0.001) - - # def test_cropped_image(detection2d): - """Test cropped image generation.""" - padding = 20 - cropped = detection2d.cropped_image(padding=padding) - - assert cropped is not None, "Cropped image should not be None" - - # The actual cropped image is (260, 192, 3) - assert cropped.width == 192 - assert cropped.height == 260 - assert cropped.shape == (260, 192, 3) - - # def test_to_ros_bbox(detection2d): - """Test ROS bounding box conversion.""" - ros_bbox = detection2d.to_ros_bbox() - - assert ros_bbox is not None - assert hasattr(ros_bbox, "center") - assert hasattr(ros_bbox, "size_x") - assert hasattr(ros_bbox, "size_y") - - # Verify values match - center_x, center_y, width, height = detection2d.get_bbox_center() - assert ros_bbox.center.position.x == pytest.approx(center_x, abs=0.001) - assert ros_bbox.center.position.y == pytest.approx(center_y, abs=0.001) - assert ros_bbox.size_x == pytest.approx(width, abs=0.001) - assert ros_bbox.size_y == pytest.approx(height, abs=0.001) diff --git a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py b/dimos/perception/detection/type/detection2d/test_imageDetections2D.py deleted file mode 100644 index 83487d2c25..0000000000 --- a/dimos/perception/detection/type/detection2d/test_imageDetections2D.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pytest - -from dimos.perception.detection.type import ImageDetections2D - - -def test_from_ros_detection2d_array(get_moment_2d) -> None: - moment = get_moment_2d() - - detections2d = moment["detections2d"] - - test_image = detections2d.image - - # Convert to ROS detection array - ros_array = detections2d.to_ros_detection2d_array() - - # Convert back to ImageDetections2D - recovered = ImageDetections2D.from_ros_detection2d_array(test_image, ros_array) - - # Verify we got the same number of detections - assert len(recovered.detections) == len(detections2d.detections) - - # Verify the detection matches - original_det = detections2d.detections[0] - recovered_det = recovered.detections[0] - - # Check bbox is approximately the same (allow 1 pixel tolerance due to float conversion) - for orig_val, rec_val in zip(original_det.bbox, recovered_det.bbox, strict=False): - assert orig_val == pytest.approx(rec_val, abs=1.0) - - # Check other properties - assert recovered_det.track_id == original_det.track_id - assert recovered_det.class_id == original_det.class_id - assert recovered_det.confidence == pytest.approx(original_det.confidence, abs=0.01) - - print("\nSuccessfully round-tripped detection through ROS format:") - print(f" Original bbox: {original_det.bbox}") - print(f" Recovered bbox: {recovered_det.bbox}") - print(f" Track ID: {recovered_det.track_id}") - print(f" Confidence: {recovered_det.confidence:.3f}") diff --git a/dimos/perception/detection/type/detection2d/test_person.py b/dimos/perception/detection/type/detection2d/test_person.py deleted file mode 100644 index 06c5883ae2..0000000000 --- a/dimos/perception/detection/type/detection2d/test_person.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import pytest - - -def test_person_ros_confidence() -> None: - """Test that Detection2DPerson preserves confidence when converting to ROS format.""" - - from dimos.msgs.sensor_msgs import Image - from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector - from dimos.perception.detection.type.detection2d.person import Detection2DPerson - from dimos.utils.data import get_data - - # Load test image - image_path = get_data("cafe.jpg") - image = Image.from_file(image_path) - - # Run pose detection - detector = YoloPersonDetector(device="cpu") - detections = detector.process_image(image) - - # Find a Detection2DPerson (should have at least one person in cafe.jpg) - person_detections = [d for d in detections.detections if isinstance(d, Detection2DPerson)] - assert len(person_detections) > 0, "No person detections found in cafe.jpg" - - # Test each person detection - for person_det in person_detections: - original_confidence = person_det.confidence - assert 0.0 <= original_confidence <= 1.0, "Confidence should be between 0 and 1" - - # Convert to ROS format - ros_det = person_det.to_ros_detection2d() - - # Extract confidence from ROS message - assert len(ros_det.results) > 0, "ROS detection should have results" - ros_confidence = ros_det.results[0].hypothesis.score - - # Verify confidence is preserved (allow small floating point tolerance) - assert original_confidence == pytest.approx(ros_confidence, abs=0.001), ( - f"Confidence mismatch: {original_confidence} != {ros_confidence}" - ) - - print("\nSuccessfully preserved confidence in ROS conversion for Detection2DPerson:") - print(f" Original confidence: {original_confidence:.3f}") - print(f" ROS confidence: {ros_confidence:.3f}") - print(f" Track ID: {person_det.track_id}") - print(f" Visible keypoints: {len(person_det.get_visible_keypoints(threshold=0.3))}/17") - - -def test_person_from_ros_raises() -> None: - """Test that Detection2DPerson.from_ros_detection2d() raises NotImplementedError.""" - from dimos.perception.detection.type.detection2d.person import Detection2DPerson - - with pytest.raises(NotImplementedError) as exc_info: - Detection2DPerson.from_ros_detection2d() - - # Verify the error message is informative - error_msg = str(exc_info.value) - assert "keypoint data" in error_msg.lower() - assert "Detection2DBBox" in error_msg diff --git a/dimos/perception/detection/type/detection3d/__init__.py b/dimos/perception/detection/type/detection3d/__init__.py deleted file mode 100644 index 53ab73259e..0000000000 --- a/dimos/perception/detection/type/detection3d/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.perception.detection.type.detection3d.base import Detection3D -from dimos.perception.detection.type.detection3d.bbox import Detection3DBBox -from dimos.perception.detection.type.detection3d.imageDetections3DPC import ImageDetections3DPC -from dimos.perception.detection.type.detection3d.pointcloud import Detection3DPC -from dimos.perception.detection.type.detection3d.pointcloud_filters import ( - PointCloudFilter, - height_filter, - radius_outlier, - raycast, - statistical, -) - -__all__ = [ - "Detection3D", - "Detection3DBBox", - "Detection3DPC", - "ImageDetections3DPC", - "PointCloudFilter", - "height_filter", - "radius_outlier", - "raycast", - "statistical", -] diff --git a/dimos/perception/detection/type/detection3d/base.py b/dimos/perception/detection/type/detection3d/base.py deleted file mode 100644 index b036584f3e..0000000000 --- a/dimos/perception/detection/type/detection3d/base.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import abstractmethod -from dataclasses import dataclass, field -from typing import TYPE_CHECKING - -from dimos.msgs.geometry_msgs import Transform -from dimos.perception.detection.type.detection2d import Detection2DBBox - -if TYPE_CHECKING: - from dimos_lcm.sensor_msgs import CameraInfo - - -@dataclass -class Detection3D(Detection2DBBox): - """Abstract base class for 3D detections.""" - - frame_id: str = "" - transform: Transform = field(default_factory=Transform.identity) - - @classmethod - @abstractmethod - def from_2d( - cls, - det: Detection2DBBox, - distance: float, - camera_info: CameraInfo, - world_to_optical_transform: Transform, - ) -> Detection3D | None: - """Create a 3D detection from a 2D detection.""" - ... diff --git a/dimos/perception/detection/type/detection3d/bbox.py b/dimos/perception/detection/type/detection3d/bbox.py deleted file mode 100644 index cf7f4ea3cc..0000000000 --- a/dimos/perception/detection/type/detection3d/bbox.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass, field -import functools -from typing import Any - -from dimos_lcm.vision_msgs import ObjectHypothesis, ObjectHypothesisWithPose - -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Transform, Vector3 -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection3D -from dimos.perception.detection.type.detection2d import Detection2DBBox - - -@dataclass -class Detection3DBBox(Detection2DBBox): - """3D bounding box detection with center, size, and orientation. - - Represents a 3D detection as an oriented bounding box in world space. - """ - - center: Vector3 # Center point in world frame - size: Vector3 # Width, height, depth - transform: Transform | None = None # Camera to world transform - frame_id: str = "" # Frame ID (e.g., "world", "map") - orientation: Quaternion = field(default_factory=lambda: Quaternion(0.0, 0.0, 0.0, 1.0)) - - @functools.cached_property - def pose(self) -> PoseStamped: - """Convert detection to a PoseStamped using bounding box center. - - Returns pose in world frame with the detection's orientation. - """ - return PoseStamped( - ts=self.ts, - frame_id=self.frame_id, - position=self.center, - orientation=self.orientation, - ) - - def to_detection3d_msg(self) -> Detection3D: - """Convert to ROS Detection3D message.""" - msg = Detection3D() - msg.header = Header(self.ts, self.frame_id) - - # Results - msg.results = [ - ObjectHypothesisWithPose( - hypothesis=ObjectHypothesis( - class_id=str(self.class_id), - score=self.confidence, - ) - ) - ] - - # Bounding Box - msg.bbox.center = Pose( - position=self.center, - orientation=self.orientation, - ) - msg.bbox.size = self.size - - return msg - - def to_repr_dict(self) -> dict[str, Any]: - # Calculate distance from camera - if self.transform is None: - return super().to_repr_dict() - camera_pos = self.transform.translation - distance = (self.center - camera_pos).magnitude() - - parent_dict = super().to_repr_dict() - # Remove bbox key if present - parent_dict.pop("bbox", None) - - return { - **parent_dict, - "dist": f"{distance:.2f}m", - "size": f"[{self.size.x:.2f},{self.size.y:.2f},{self.size.z:.2f}]", - } diff --git a/dimos/perception/detection/type/detection3d/imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/imageDetections3DPC.py deleted file mode 100644 index 0fbb1a7c59..0000000000 --- a/dimos/perception/detection/type/detection3d/imageDetections3DPC.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from lcm_msgs.foxglove_msgs import SceneUpdate # type: ignore[import-not-found] - -from dimos.perception.detection.type.detection3d.pointcloud import Detection3DPC -from dimos.perception.detection.type.imageDetections import ImageDetections - - -class ImageDetections3DPC(ImageDetections[Detection3DPC]): - """Specialized class for 3D detections in an image.""" - - def to_foxglove_scene_update(self) -> SceneUpdate: - """Convert all detections to a Foxglove SceneUpdate message. - - Returns: - SceneUpdate containing SceneEntity objects for all detections - """ - - # Create SceneUpdate message with all detections - scene_update = SceneUpdate() - scene_update.deletions_length = 0 - scene_update.deletions = [] - scene_update.entities = [] - - # Process each detection - for i, detection in enumerate(self.detections): - entity = detection.to_foxglove_scene_entity(entity_id=f"detection_{detection.name}_{i}") - scene_update.entities.append(entity) - - scene_update.entities_length = len(scene_update.entities) - return scene_update diff --git a/dimos/perception/detection/type/detection3d/object.py b/dimos/perception/detection/type/detection3d/object.py deleted file mode 100644 index 43702917e1..0000000000 --- a/dimos/perception/detection/type/detection3d/object.py +++ /dev/null @@ -1,367 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass, field -import time -from typing import TYPE_CHECKING, Any -import uuid - -import cv2 -from dimos_lcm.geometry_msgs import Pose -import numpy as np -import open3d as o3d # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection3D as ROSDetection3D, Detection3DArray -from dimos.perception.detection.type.detection2d.seg import Detection2DSeg -from dimos.perception.detection.type.detection3d.base import Detection3D - -if TYPE_CHECKING: - from dimos_lcm.sensor_msgs import CameraInfo - - from dimos.perception.detection.type.detection2d import ImageDetections2D - - -@dataclass(kw_only=True) -class Object(Detection3D): - """3D object detection combining bounding box and pointcloud representations. - - Represents a detected object in 3D space with support for accumulating - multiple detections over time. - """ - - object_id: str = field(default_factory=lambda: uuid.uuid4().hex[:8]) - center: Vector3 - size: Vector3 - pose: PoseStamped - pointcloud: PointCloud2 - camera_transform: Transform | None = None - mask: np.ndarray[Any, np.dtype[np.uint8]] | None = None - detections_count: int = 1 - - def update_object(self, other: Object) -> None: - """Update this object with data from another detection. - - Accumulates pointclouds by transforming the new pointcloud to world frame - and adding it to the existing pointcloud. Updates center and camera_transform, - and increments the detections_count. - - Args: - other: Another Object instance with newer detection data. - """ - # Accumulate pointclouds if transforms are available - if other.camera_transform is not None: - # Transform new pointcloud to world frame and add to existing - # transformed_pc = other.pointcloud.transform(other.camera_transform) - # self.pointcloud = self.pointcloud + transformed_pc - - # Recompute center from accumulated pointcloud - self.pointcloud = other.pointcloud - pc_center = other.pointcloud.center - self.center = Vector3(pc_center.x, pc_center.y, pc_center.z) - else: - # No transform available, just replace - self.pointcloud = other.pointcloud - self.center = other.center - - self.camera_transform = other.camera_transform - self.size = other.size - self.pose = other.pose - self.track_id = other.track_id - self.mask = other.mask - self.name = other.name - self.bbox = other.bbox - self.confidence = other.confidence - self.class_id = other.class_id - self.ts = other.ts - self.frame_id = other.frame_id - self.image = other.image - self.detections_count += 1 - - def get_oriented_bounding_box(self) -> Any: - """Get oriented bounding box of the pointcloud.""" - return self.pointcloud.get_oriented_bounding_box() - - def scene_entity_label(self) -> str: - """Get label for scene visualization.""" - if self.detections_count > 1: - return f"{self.name} ({self.detections_count})" - return f"{self.track_id}/{self.name} ({self.confidence:.0%})" - - def to_detection3d_msg(self) -> ROSDetection3D: - """Convert to ROS Detection3D message.""" - obb = self.get_oriented_bounding_box() # type: ignore[no-untyped-call] - orientation = Quaternion.from_rotation_matrix(obb.R) - - msg = ROSDetection3D() - msg.header = Header(self.ts, self.frame_id) - msg.id = str(self.track_id) - msg.bbox.center = Pose( - position=Vector3(obb.center[0], obb.center[1], obb.center[2]), - orientation=orientation, - ) - msg.bbox.size = Vector3(obb.extent[0], obb.extent[1], obb.extent[2]) - - return msg - - def agent_encode(self) -> dict[str, Any]: - """Encode for agent consumption.""" - return { - "object_id": self.object_id, - "track_id": self.track_id, - "name": self.name, - "detections": self.detections_count, - "last_seen": f"{round(time.time() - self.ts)}s ago", - } - - def to_dict(self) -> dict[str, Any]: - """Convert object to dictionary with all relevant data.""" - return { - "object_id": self.object_id, - "track_id": self.track_id, - "class_id": self.class_id, - "name": self.name, - "mask": self.mask, - "pointcloud": self.pointcloud.as_numpy(), - "image": self.image.as_numpy() if self.image else None, - } - - @classmethod - def from_2d_to_list( - cls, - detections_2d: ImageDetections2D[Detection2DSeg], - color_image: Image, - depth_image: Image, - camera_info: CameraInfo, - camera_transform: Transform | None = None, - depth_scale: float = 1.0, - depth_trunc: float = 10.0, - statistical_nb_neighbors: int = 10, - statistical_std_ratio: float = 0.5, - voxel_downsample: float = 0.005, - mask_erode_pixels: int = 3, - ) -> list[Object]: - """Create 3D Objects from 2D detections and RGBD images. - - Uses Open3D's optimized RGBD projection for efficient processing. - - Args: - detections_2d: 2D detections with segmentation masks - color_image: RGB color image - depth_image: Depth image (in meters if depth_scale=1.0) - camera_info: Camera intrinsics - camera_transform: Optional transform from camera frame to world frame. - If provided, pointclouds will be transformed to world frame. - depth_scale: Scale factor for depth (1.0 for meters, 1000.0 for mm) - depth_trunc: Maximum depth value in meters - statistical_nb_neighbors: Neighbors for statistical outlier removal - statistical_std_ratio: Std ratio for statistical outlier removal - voxel_downsample: Voxel size (meters) for downsampling before filtering. Set <= 0 to skip. - mask_erode_pixels: Number of pixels to erode the mask by to remove - noisy depth edge points. Set to 0 to disable. - - Returns: - List of Object instances with pointclouds - """ - color_cv = color_image.to_opencv() - if color_cv.ndim == 3 and color_cv.shape[2] == 3: - color_cv = cv2.cvtColor(color_cv, cv2.COLOR_BGR2RGB) - - depth_cv = depth_image.to_opencv() - h, w = depth_cv.shape[:2] - - # Build Open3D camera intrinsics - fx, fy = camera_info.K[0], camera_info.K[4] - cx, cy = camera_info.K[2], camera_info.K[5] - intrinsic_o3d = o3d.camera.PinholeCameraIntrinsic(w, h, fx, fy, cx, cy) - - objects: list[Object] = [] - - for det in detections_2d.detections: - if isinstance(det, Detection2DSeg): - mask = det.mask - store_mask = det.mask - else: - mask = np.zeros((h, w), dtype=np.uint8) - x1, y1, x2, y2 = map(int, det.bbox) - x1, y1 = max(0, x1), max(0, y1) - x2, y2 = min(w, x2), min(h, y2) - mask[y1:y2, x1:x2] = 255 - store_mask = mask - - if mask_erode_pixels > 0: - mask_uint8 = mask.astype(np.uint8) - if mask_uint8.max() == 1: - mask_uint8 = mask_uint8 * 255 # type: ignore[assignment] - kernel_size = 2 * mask_erode_pixels + 1 - erode_kernel = cv2.getStructuringElement( - cv2.MORPH_ELLIPSE, (kernel_size, kernel_size) - ) - mask = cv2.erode(mask_uint8, erode_kernel) # type: ignore[assignment] - - depth_masked = depth_cv.copy() - depth_masked[mask == 0] = 0 - - rgbd = o3d.geometry.RGBDImage.create_from_color_and_depth( - o3d.geometry.Image(color_cv.astype(np.uint8)), - o3d.geometry.Image(depth_masked.astype(np.float32)), - depth_scale=depth_scale, - depth_trunc=depth_trunc, - convert_rgb_to_intensity=False, - ) - pcd = o3d.geometry.PointCloud.create_from_rgbd_image(rgbd, intrinsic_o3d) - - pc0 = PointCloud2( - pcd, - frame_id=depth_image.frame_id, - ts=depth_image.ts, - ).voxel_downsample(voxel_downsample) - - pcd_filtered, _ = pc0.pointcloud.remove_statistical_outlier( - nb_neighbors=statistical_nb_neighbors, - std_ratio=statistical_std_ratio, - ) - - if len(pcd_filtered.points) < 10: - continue - - pc = PointCloud2( - pcd_filtered, - frame_id=depth_image.frame_id, - ts=depth_image.ts, - ) - - # Transform pointcloud to world frame if camera_transform is provided - if camera_transform is not None: - pc = pc.transform(camera_transform) - frame_id = camera_transform.frame_id - else: - frame_id = depth_image.frame_id - - # Compute center from pointcloud - obb = pc.pointcloud.get_oriented_bounding_box() - center = Vector3(obb.center[0], obb.center[1], obb.center[2]) - size = Vector3(obb.extent[0], obb.extent[1], obb.extent[2]) - orientation = Quaternion.from_rotation_matrix(obb.R) - pose = PoseStamped( - ts=det.ts, - frame_id=frame_id, - position=center, - orientation=orientation, - ) - - objects.append( - cls( - bbox=det.bbox, - track_id=det.track_id, - class_id=det.class_id, - confidence=det.confidence, - name=det.name, - ts=det.ts, - image=det.image, - frame_id=frame_id, - pointcloud=pc, - center=center, - size=size, - pose=pose, - camera_transform=camera_transform, - mask=store_mask, - ) - ) - - return objects - - -def aggregate_pointclouds(objects: list[Object]) -> PointCloud2: - """Aggregate all object pointclouds into a single colored pointcloud. - - Each object's points are colored based on its track_id. - - Args: - objects: List of Object instances with pointclouds - - Returns: - Combined PointCloud2 with all points colored by object (empty if no points). - """ - if not objects: - return PointCloud2(pointcloud=o3d.geometry.PointCloud(), frame_id="", ts=0.0) - - all_points = [] - all_colors = [] - - for obj in objects: - points, colors = obj.pointcloud.as_numpy() - if len(points) == 0: - continue - - try: - seed = int(obj.object_id, 16) - except (ValueError, TypeError): - seed = abs(hash(obj.object_id)) - np.random.seed(abs(seed) % (2**32 - 1)) - track_color = np.random.randint(50, 255, 3) / 255.0 - - if colors is not None: - blended = np.clip(0.6 * colors + 0.4 * track_color, 0.0, 1.0) - else: - blended = np.tile(track_color, (len(points), 1)) - - all_points.append(points) - all_colors.append(blended) - - if not all_points: - return PointCloud2( - pointcloud=o3d.geometry.PointCloud(), frame_id=objects[0].frame_id, ts=objects[0].ts - ) - - combined_points = np.vstack(all_points) - combined_colors = np.vstack(all_colors) - - pc = PointCloud2.from_numpy( - combined_points, - frame_id=objects[0].frame_id, - timestamp=objects[0].ts, - ) - pcd = pc.pointcloud - pcd.colors = o3d.utility.Vector3dVector(combined_colors) - pc.pointcloud = pcd - - return pc - - -def to_detection3d_array(objects: list[Object]) -> Detection3DArray: - """Convert a list of Objects to a ROS Detection3DArray message. - - Args: - objects: List of Object instances - - Returns: - Detection3DArray ROS message - """ - array = Detection3DArray() - - if objects: - array.header = Header(objects[0].ts, objects[0].frame_id) - - for obj in objects: - array.detections.append(obj.to_detection3d_msg()) - - return array - - -__all__ = ["Object", "aggregate_pointclouds", "to_detection3d_array"] diff --git a/dimos/perception/detection/type/detection3d/pointcloud.py b/dimos/perception/detection/type/detection3d/pointcloud.py deleted file mode 100644 index 7edceb17a5..0000000000 --- a/dimos/perception/detection/type/detection3d/pointcloud.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass, field -import functools -from typing import TYPE_CHECKING, Any - -from lcm_msgs.builtin_interfaces import Duration # type: ignore[import-not-found] -from lcm_msgs.foxglove_msgs import ( # type: ignore[import-not-found] - CubePrimitive, - SceneEntity, - TextPrimitive, -) -from lcm_msgs.geometry_msgs import ( # type: ignore[import-not-found] - Point, - Pose, - Quaternion, - Vector3 as LCMVector3, -) -import numpy as np - -from dimos.msgs.foxglove_msgs.Color import Color -from dimos.msgs.geometry_msgs import PoseStamped, Transform, Vector3 -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.perception.detection.type.detection3d.base import Detection3D -from dimos.perception.detection.type.detection3d.pointcloud_filters import ( - PointCloudFilter, - radius_outlier, - raycast, - statistical, -) -from dimos.types.timestamped import to_ros_stamp - -if TYPE_CHECKING: - from dimos_lcm.sensor_msgs import CameraInfo - - from dimos.perception.detection.type.detection2d import Detection2DBBox - - -@dataclass -class Detection3DPC(Detection3D): - pointcloud: PointCloud2 = field(default_factory=PointCloud2) - - @functools.cached_property - def center(self) -> Vector3: - return Vector3(*self.pointcloud.center) - - @functools.cached_property - def pose(self) -> PoseStamped: - """Convert detection to a PoseStamped using pointcloud center. - - Returns pose in world frame with identity rotation. - The pointcloud is already in world frame. - """ - return PoseStamped( - ts=self.ts, - frame_id=self.frame_id, - position=self.center, - orientation=(0.0, 0.0, 0.0, 1.0), # Identity quaternion - ) - - def get_bounding_box(self): # type: ignore[no-untyped-def] - """Get axis-aligned bounding box of the detection's pointcloud.""" - return self.pointcloud.get_axis_aligned_bounding_box() - - def get_oriented_bounding_box(self): # type: ignore[no-untyped-def] - """Get oriented bounding box of the detection's pointcloud.""" - return self.pointcloud.get_oriented_bounding_box() - - def get_bounding_box_dimensions(self) -> tuple[float, float, float]: - """Get dimensions (width, height, depth) of the detection's bounding box.""" - return self.pointcloud.get_bounding_box_dimensions() - - def bounding_box_intersects(self, other: Detection3DPC) -> bool: - """Check if this detection's bounding box intersects with another's.""" - return self.pointcloud.bounding_box_intersects(other.pointcloud) - - def to_repr_dict(self) -> dict[str, Any]: - # Calculate distance from camera - # The pointcloud is in world frame, and transform gives camera position in world - center_world = self.center - # Camera position in world frame is the translation part of the transform - camera_pos = self.transform.translation - # Use Vector3 subtraction and magnitude - distance = (center_world - camera_pos).magnitude() - - parent_dict = super().to_repr_dict() - # Remove bbox key if present - parent_dict.pop("bbox", None) - - return { - **parent_dict, - "dist": f"{distance:.2f}m", - "points": str(len(self.pointcloud)), - } - - def to_foxglove_scene_entity(self, entity_id: str | None = None) -> SceneEntity: - """Convert detection to a Foxglove SceneEntity with cube primitive and text label. - - Args: - entity_id: Optional custom entity ID. If None, generates one from name and hash. - - Returns: - SceneEntity with cube bounding box and text label - """ - - # Create a cube primitive for the bounding box - cube = CubePrimitive() - - # Get the axis-aligned bounding box - aabb = self.get_bounding_box() # type: ignore[no-untyped-call] - - # Set pose from axis-aligned bounding box - cube.pose = Pose() - cube.pose.position = Point() - # Get center of the axis-aligned bounding box - aabb_center = aabb.get_center() - cube.pose.position.x = aabb_center[0] - cube.pose.position.y = aabb_center[1] - cube.pose.position.z = aabb_center[2] - - # For axis-aligned box, use identity quaternion (no rotation) - cube.pose.orientation = Quaternion() - cube.pose.orientation.x = 0 - cube.pose.orientation.y = 0 - cube.pose.orientation.z = 0 - cube.pose.orientation.w = 1 - - # Set size from axis-aligned bounding box - cube.size = LCMVector3() - aabb_extent = aabb.get_extent() - cube.size.x = aabb_extent[0] # width - cube.size.y = aabb_extent[1] # height - cube.size.z = aabb_extent[2] # depth - - # Set color based on name hash - cube.color = Color.from_string(self.name, alpha=0.2) - - # Create text label - text = TextPrimitive() - text.pose = Pose() - text.pose.position = Point() - text.pose.position.x = aabb_center[0] - text.pose.position.y = aabb_center[1] - text.pose.position.z = aabb_center[2] + aabb_extent[2] / 2 + 0.1 # Above the box - text.pose.orientation = Quaternion() - text.pose.orientation.x = 0 - text.pose.orientation.y = 0 - text.pose.orientation.z = 0 - text.pose.orientation.w = 1 - text.billboard = True - text.font_size = 20.0 - text.scale_invariant = True - text.color = Color() - text.color.r = 1.0 - text.color.g = 1.0 - text.color.b = 1.0 - text.color.a = 1.0 - text.text = self.scene_entity_label() - - # Create scene entity - entity = SceneEntity() - entity.timestamp = to_ros_stamp(self.ts) - entity.frame_id = self.frame_id - entity.id = str(self.track_id) - entity.lifetime = Duration() - entity.lifetime.sec = 0 # Persistent - entity.lifetime.nanosec = 0 - entity.frame_locked = False - - # Initialize all primitive arrays - entity.metadata_length = 0 - entity.metadata = [] - entity.arrows_length = 0 - entity.arrows = [] - entity.cubes_length = 1 - entity.cubes = [cube] - entity.spheres_length = 0 - entity.spheres = [] - entity.cylinders_length = 0 - entity.cylinders = [] - entity.lines_length = 0 - entity.lines = [] - entity.triangles_length = 0 - entity.triangles = [] - entity.texts_length = 1 - entity.texts = [text] - entity.models_length = 0 - entity.models = [] - - return entity - - def scene_entity_label(self) -> str: - return f"{self.track_id}/{self.name} ({self.confidence:.0%})" - - @classmethod - def from_2d( # type: ignore[override] - cls, - det: Detection2DBBox, - world_pointcloud: PointCloud2, - camera_info: CameraInfo, - world_to_optical_transform: Transform, - # filters are to be adjusted based on the sensor noise characteristics if feeding - # sensor data directly - filters: list[PointCloudFilter] | None = None, - ) -> Detection3DPC | None: - """Create a Detection3D from a 2D detection by projecting world pointcloud. - - This method handles: - 1. Projecting world pointcloud to camera frame - 2. Filtering points within the 2D detection bounding box - 3. Cleaning up the pointcloud (height filter, outlier removal) - 4. Hidden point removal from camera perspective - - Args: - det: The 2D detection - world_pointcloud: Full pointcloud in world frame - camera_info: Camera calibration info - world_to_camerlka_transform: Transform from world to camera frame - filters: List of functions to apply to the pointcloud for filtering - Returns: - Detection3D with filtered pointcloud, or None if no valid points - """ - # Set default filters if none provided - if filters is None: - filters = [ - # height_filter(0.1), - raycast(), - radius_outlier(), - statistical(), - ] - - # Extract camera parameters - fx, fy = camera_info.K[0], camera_info.K[4] - cx, cy = camera_info.K[2], camera_info.K[5] - image_width = camera_info.width - image_height = camera_info.height - - camera_matrix = np.array([[fx, 0, cx], [0, fy, cy], [0, 0, 1]]) - - # Convert pointcloud to numpy array - world_points, _ = world_pointcloud.as_numpy() - - # Project points to camera frame - points_homogeneous = np.hstack([world_points, np.ones((world_points.shape[0], 1))]) - extrinsics_matrix = world_to_optical_transform.to_matrix() - points_camera = (extrinsics_matrix @ points_homogeneous.T).T - - # Filter out points behind the camera - valid_mask = points_camera[:, 2] > 0 - points_camera = points_camera[valid_mask] - world_points = world_points[valid_mask] - - if len(world_points) == 0: - return None - - # Project to 2D - points_2d_homogeneous = (camera_matrix @ points_camera[:, :3].T).T - points_2d = points_2d_homogeneous[:, :2] / points_2d_homogeneous[:, 2:3] - - # Filter points within image bounds - in_image_mask = ( - (points_2d[:, 0] >= 0) - & (points_2d[:, 0] < image_width) - & (points_2d[:, 1] >= 0) - & (points_2d[:, 1] < image_height) - ) - points_2d = points_2d[in_image_mask] - world_points = world_points[in_image_mask] - - if len(world_points) == 0: - return None - - # Extract bbox from Detection2D - x_min, y_min, x_max, y_max = det.bbox - - # Find points within this detection box (with small margin) - margin = 5 # pixels - in_box_mask = ( - (points_2d[:, 0] >= x_min - margin) - & (points_2d[:, 0] <= x_max + margin) - & (points_2d[:, 1] >= y_min - margin) - & (points_2d[:, 1] <= y_max + margin) - ) - - detection_points = world_points[in_box_mask] - - if detection_points.shape[0] == 0: - # print(f"No points found in detection bbox after projection. {det.name}") - return None - - # Create initial pointcloud for this detection - initial_pc = PointCloud2.from_numpy( - detection_points, - frame_id=world_pointcloud.frame_id, - timestamp=world_pointcloud.ts, - ) - - # Apply filters - each filter gets all arguments - detection_pc = initial_pc - for filter_func in filters: - result = filter_func(det, detection_pc, camera_info, world_to_optical_transform) - if result is None: - return None - detection_pc = result - - # Final check for empty pointcloud - if len(detection_pc.pointcloud.points) == 0: - return None - - # Create Detection3D with filtered pointcloud - return cls( - image=det.image, - bbox=det.bbox, - track_id=det.track_id, - class_id=det.class_id, - confidence=det.confidence, - name=det.name, - ts=det.ts, - pointcloud=detection_pc, - transform=world_to_optical_transform, - frame_id=world_pointcloud.frame_id, - ) diff --git a/dimos/perception/detection/type/detection3d/pointcloud_filters.py b/dimos/perception/detection/type/detection3d/pointcloud_filters.py deleted file mode 100644 index 984e04bc99..0000000000 --- a/dimos/perception/detection/type/detection3d/pointcloud_filters.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections.abc import Callable - -from dimos_lcm.sensor_msgs import CameraInfo - -from dimos.msgs.geometry_msgs import Transform -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.perception.detection.type.detection2d import Detection2DBBox - -# Filters take Detection2DBBox, PointCloud2, CameraInfo, Transform and return filtered PointCloud2 or None -PointCloudFilter = Callable[ - [Detection2DBBox, PointCloud2, CameraInfo, Transform], PointCloud2 | None -] - - -def height_filter(height: float = 0.1) -> PointCloudFilter: - return lambda det, pc, ci, tf: pc.filter_by_height(height) - - -def statistical(nb_neighbors: int = 40, std_ratio: float = 0.5) -> PointCloudFilter: - def filter_func( - det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform - ) -> PointCloud2 | None: - try: - statistical, _removed = pc.pointcloud.remove_statistical_outlier( - nb_neighbors=nb_neighbors, std_ratio=std_ratio - ) - return PointCloud2(statistical, pc.frame_id, pc.ts) - except Exception: - # print("statistical filter failed:", e) - return None - - return filter_func - - -def raycast() -> PointCloudFilter: - def filter_func( - det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform - ) -> PointCloud2 | None: - try: - camera_pos = tf.inverse().translation - camera_pos_np = camera_pos.to_numpy() - _, visible_indices = pc.pointcloud.hidden_point_removal(camera_pos_np, radius=100.0) - visible_pcd = pc.pointcloud.select_by_index(visible_indices) - return PointCloud2(visible_pcd, pc.frame_id, pc.ts) - except Exception: - # print("raycast filter failed:", e) - return None - - return filter_func - - -def radius_outlier(min_neighbors: int = 20, radius: float = 0.3) -> PointCloudFilter: - """ - Remove isolated points: keep only points that have at least `min_neighbors` - neighbors within `radius` meters (same units as your point cloud). - """ - - def filter_func( - det: Detection2DBBox, pc: PointCloud2, ci: CameraInfo, tf: Transform - ) -> PointCloud2 | None: - filtered_pcd, _removed = pc.pointcloud.remove_radius_outlier( - nb_points=min_neighbors, radius=radius - ) - return PointCloud2(filtered_pcd, pc.frame_id, pc.ts) - - return filter_func diff --git a/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py b/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py deleted file mode 100644 index cca8b862d4..0000000000 --- a/dimos/perception/detection/type/detection3d/test_imageDetections3DPC.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - - -@pytest.mark.skip -def test_to_foxglove_scene_update(detections3dpc) -> None: - # Convert to scene update - scene_update = detections3dpc.to_foxglove_scene_update() - - # Verify scene update structure - assert scene_update is not None - assert scene_update.deletions_length == 0 - assert len(scene_update.deletions) == 0 - assert scene_update.entities_length == len(detections3dpc.detections) - assert len(scene_update.entities) == len(detections3dpc.detections) - - # Verify each entity corresponds to a detection - for _i, (entity, detection) in enumerate( - zip(scene_update.entities, detections3dpc.detections, strict=False) - ): - assert entity.id == str(detection.track_id) - assert entity.frame_id == detection.frame_id - assert entity.cubes_length == 1 - assert entity.texts_length == 1 diff --git a/dimos/perception/detection/type/detection3d/test_pointcloud.py b/dimos/perception/detection/type/detection3d/test_pointcloud.py deleted file mode 100644 index ad1c5cdf1b..0000000000 --- a/dimos/perception/detection/type/detection3d/test_pointcloud.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest - - -def test_detection3dpc(detection3dpc) -> None: - # def test_oriented_bounding_box(detection3dpc): - """Test oriented bounding box calculation and values.""" - obb = detection3dpc.get_oriented_bounding_box() - assert obb is not None, "Oriented bounding box should not be None" - - # Verify OBB center values - assert obb.center[0] == pytest.approx(-3.36002, abs=0.1) - assert obb.center[1] == pytest.approx(-0.196446, abs=0.1) - assert obb.center[2] == pytest.approx(0.220184, abs=0.1) - - # Verify OBB extent values - assert obb.extent[0] == pytest.approx(0.531275, abs=0.12) - assert obb.extent[1] == pytest.approx(0.461054, abs=0.1) - assert obb.extent[2] == pytest.approx(0.155, abs=0.1) - - # def test_bounding_box_dimensions(detection3dpc): - """Test bounding box dimension calculation.""" - dims = detection3dpc.get_bounding_box_dimensions() - assert len(dims) == 3, "Bounding box dimensions should have 3 values" - assert dims[0] == pytest.approx(0.350, abs=0.1) - assert dims[1] == pytest.approx(0.250, abs=0.1) - assert dims[2] == pytest.approx(0.550, abs=0.1) - - # def test_axis_aligned_bounding_box(detection3dpc): - """Test axis-aligned bounding box calculation.""" - aabb = detection3dpc.get_bounding_box() - assert aabb is not None, "Axis-aligned bounding box should not be None" - - # Verify AABB min values - assert aabb.min_bound[0] == pytest.approx(-3.575, abs=0.1) - assert aabb.min_bound[1] == pytest.approx(-0.375, abs=0.1) - assert aabb.min_bound[2] == pytest.approx(-0.075, abs=0.1) - - # Verify AABB max values - assert aabb.max_bound[0] == pytest.approx(-3.075, abs=0.1) - assert aabb.max_bound[1] == pytest.approx(-0.125, abs=0.1) - assert aabb.max_bound[2] == pytest.approx(0.475, abs=0.1) - - # def test_point_cloud_properties(detection3dpc): - """Test point cloud data and boundaries.""" - points, _ = detection3dpc.pointcloud.as_numpy() - assert len(points) > 60 - assert detection3dpc.pointcloud.frame_id == "world", ( - f"Expected frame_id 'world', got '{detection3dpc.pointcloud.frame_id}'" - ) - - min_pt = np.min(points, axis=0) - max_pt = np.max(points, axis=0) - center = np.mean(points, axis=0) - - # Verify point cloud boundaries - assert min_pt[0] == pytest.approx(-3.575, abs=0.1) - assert min_pt[1] == pytest.approx(-0.375, abs=0.1) - assert min_pt[2] == pytest.approx(-0.075, abs=0.1) - - assert max_pt[0] == pytest.approx(-3.075, abs=0.1) - assert max_pt[1] == pytest.approx(-0.125, abs=0.1) - assert max_pt[2] == pytest.approx(0.475, abs=0.1) - - assert center[0] == pytest.approx(-3.326, abs=0.1) - assert center[1] == pytest.approx(-0.202, abs=0.1) - assert center[2] == pytest.approx(0.160, abs=0.1) - - # def test_foxglove_scene_entity_generation(detection3dpc): - """Test Foxglove scene entity creation and structure.""" - entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") - - # Verify entity metadata - assert entity.id == "1", f"Expected entity ID '1', got '{entity.id}'" - assert entity.frame_id == "world", f"Expected frame_id 'world', got '{entity.frame_id}'" - assert entity.cubes_length == 1, f"Expected 1 cube, got {entity.cubes_length}" - assert entity.texts_length == 1, f"Expected 1 text, got {entity.texts_length}" - - # def test_foxglove_cube_properties(detection3dpc): - """Test Foxglove cube primitive properties.""" - entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") - cube = entity.cubes[0] - - # Verify position - assert cube.pose.position.x == pytest.approx(-3.325, abs=0.1) - assert cube.pose.position.y == pytest.approx(-0.250, abs=0.1) - assert cube.pose.position.z == pytest.approx(0.200, abs=0.1) - - # Verify size - assert cube.size.x == pytest.approx(0.350, abs=0.1) - assert cube.size.y == pytest.approx(0.250, abs=0.1) - assert cube.size.z == pytest.approx(0.550, abs=0.1) - - # Verify color (green with alpha) - assert cube.color.r == pytest.approx(0.08235294117647059, abs=0.1) - assert cube.color.g == pytest.approx(0.7176470588235294, abs=0.1) - assert cube.color.b == pytest.approx(0.28627450980392155, abs=0.1) - assert cube.color.a == pytest.approx(0.2, abs=0.1) - - # def test_foxglove_text_label(detection3dpc): - """Test Foxglove text label properties.""" - entity = detection3dpc.to_foxglove_scene_entity("test_entity_123") - text = entity.texts[0] - - assert text.text in ["1/suitcase (81%)", "1/suitcase (82%)"], ( - f"Expected text '1/suitcase (81%)' or '1/suitcase (82%)', got '{text.text}'" - ) - assert text.pose.position.x == pytest.approx(-3.325, abs=0.1) - assert text.pose.position.y == pytest.approx(-0.250, abs=0.1) - assert text.pose.position.z == pytest.approx(0.575, abs=0.1) - assert text.font_size == 20.0, f"Expected font size 20.0, got {text.font_size}" - - # def test_detection_pose(detection3dpc): - """Test detection pose and frame information.""" - assert detection3dpc.pose.x == pytest.approx(-3.327, abs=0.1) - assert detection3dpc.pose.y == pytest.approx(-0.202, abs=0.1) - assert detection3dpc.pose.z == pytest.approx(0.160, abs=0.1) - assert detection3dpc.pose.frame_id == "world", ( - f"Expected frame_id 'world', got '{detection3dpc.pose.frame_id}'" - ) diff --git a/dimos/perception/detection/type/imageDetections.py b/dimos/perception/detection/type/imageDetections.py deleted file mode 100644 index 12a1f4efb9..0000000000 --- a/dimos/perception/detection/type/imageDetections.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from functools import reduce -from operator import add -from typing import TYPE_CHECKING, Generic, TypeVar - -from dimos_lcm.vision_msgs import Detection2DArray - -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.std_msgs import Header -from dimos.perception.detection.type.utils import TableStr - -if TYPE_CHECKING: - from collections.abc import Callable, Iterator - - from dimos.msgs.sensor_msgs import Image - from dimos.perception.detection.type.detection2d.base import Detection2D - - T = TypeVar("T", bound=Detection2D) -else: - from dimos.perception.detection.type.detection2d.base import Detection2D - - T = TypeVar("T", bound=Detection2D) - - -class ImageDetections(Generic[T], TableStr): - image: Image - detections: list[T] - - @property - def ts(self) -> float: - return self.image.ts - - def __init__(self, image: Image, detections: list[T] | None = None) -> None: - self.image = image - self.detections = detections or [] - for det in self.detections: - if not det.ts: - det.ts = image.ts - - def __len__(self) -> int: - return len(self.detections) - - def __iter__(self) -> Iterator: # type: ignore[type-arg] - return iter(self.detections) - - def __getitem__(self, index): # type: ignore[no-untyped-def] - return self.detections[index] - - def filter(self, *predicates: Callable[[T], bool]) -> ImageDetections[T]: - """Filter detections using one or more predicate functions. - - Multiple predicates are applied in cascade (all must return True). - - Args: - *predicates: Functions that take a detection and return True to keep it - - Returns: - A new ImageDetections instance with filtered detections - """ - filtered_detections = self.detections - for predicate in predicates: - filtered_detections = [det for det in filtered_detections if predicate(det)] - return ImageDetections(self.image, filtered_detections) - - def to_ros_detection2d_array(self) -> Detection2DArray: - return Detection2DArray( - detections_length=len(self.detections), - header=Header(self.image.ts, "camera_optical"), - detections=[det.to_ros_detection2d() for det in self.detections], - ) - - def to_foxglove_annotations(self) -> ImageAnnotations: - if not self.detections: - return ImageAnnotations( - texts=[], texts_length=0, points=[], points_length=0, circles=[], circles_length=0 - ) - return reduce(add, (det.to_image_annotations() for det in self.detections)) diff --git a/dimos/perception/detection/type/test_detection3d.py b/dimos/perception/detection/type/test_detection3d.py deleted file mode 100644 index b467df7ffe..0000000000 --- a/dimos/perception/detection/type/test_detection3d.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - - -def test_guess_projection(get_moment_2d, publish_moment) -> None: - moment = get_moment_2d() - for key, value in moment.items(): - print(key, "====================================") - print(value) - - moment.get("camera_info") - detection2d = moment.get("detections2d")[0] - tf = moment.get("tf") - tf.get("camera_optical", "world", detection2d.ts, 5.0) - - # for stash - # detection3d = Detection3D.from_2d(detection2d, 1.5, camera_info, transform) - # print(detection3d) - - # foxglove bridge needs 2 messages per topic to pass to foxglove - publish_moment(moment) - time.sleep(0.1) - publish_moment(moment) diff --git a/dimos/perception/detection/type/test_object3d.py b/dimos/perception/detection/type/test_object3d.py deleted file mode 100644 index 7057fbb9cb..0000000000 --- a/dimos/perception/detection/type/test_object3d.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.perception.detection.moduleDB import Object3D -from dimos.perception.detection.type.detection3d import ImageDetections3DPC - - -def test_first_object(first_object) -> None: - # def test_object3d_properties(first_object): - """Test basic properties of an Object3D.""" - assert first_object.track_id is not None - assert isinstance(first_object.track_id, str) - assert first_object.name is not None - assert first_object.class_id >= 0 - assert 0.0 <= first_object.confidence <= 1.0 - assert first_object.ts > 0 - assert first_object.frame_id is not None - assert first_object.best_detection is not None - - # def test_object3d_center(first_object): - """Test Object3D center calculation.""" - assert first_object.center is not None - assert hasattr(first_object.center, "x") - assert hasattr(first_object.center, "y") - assert hasattr(first_object.center, "z") - - # Center should be within reasonable bounds - assert -10 < first_object.center.x < 10 - assert -10 < first_object.center.y < 10 - assert -10 < first_object.center.z < 10 - - -def test_object3d_repr_dict(first_object) -> None: - """Test to_repr_dict method.""" - repr_dict = first_object.to_repr_dict() - - assert "object_id" in repr_dict - assert "detections" in repr_dict - assert "center" in repr_dict - - assert repr_dict["object_id"] == first_object.track_id - assert repr_dict["detections"] == first_object.detections - - # Center should be formatted as string with coordinates - assert isinstance(repr_dict["center"], str) - assert repr_dict["center"].startswith("[") - assert repr_dict["center"].endswith("]") - - # def test_object3d_scene_entity_label(first_object): - """Test scene entity label generation.""" - label = first_object.scene_entity_label() - - assert isinstance(label, str) - assert first_object.name in label - assert f"({first_object.detections})" in label - - # def test_object3d_agent_encode(first_object): - """Test agent encoding.""" - encoded = first_object.agent_encode() - - assert isinstance(encoded, dict) - assert "id" in encoded - assert "name" in encoded - assert "detections" in encoded - assert "last_seen" in encoded - - assert encoded["id"] == first_object.track_id - assert encoded["name"] == first_object.name - assert encoded["detections"] == first_object.detections - assert encoded["last_seen"].endswith("s ago") - - # def test_object3d_image_property(first_object): - """Test get_image method returns best_detection's image.""" - assert first_object.get_image() is not None - assert first_object.get_image() is first_object.best_detection.image - - -def test_all_objeects(all_objects) -> None: - # def test_object3d_multiple_detections(all_objects): - """Test objects that have been built from multiple detections.""" - # Find objects with multiple detections - multi_detection_objects = [obj for obj in all_objects if obj.detections > 1] - - if multi_detection_objects: - obj = multi_detection_objects[0] - - # Since detections is now a counter, we can only test that we have multiple detections - # and that best_detection exists - assert obj.detections > 1 - assert obj.best_detection is not None - assert obj.confidence is not None - assert obj.ts > 0 - - # Test that best_detection has reasonable properties - assert obj.best_detection.bbox_2d_volume() > 0 - - # def test_object_db_module_objects_structure(all_objects): - """Test the structure of objects in the database.""" - for obj in all_objects: - assert isinstance(obj, Object3D) - assert hasattr(obj, "track_id") - assert hasattr(obj, "detections") - assert hasattr(obj, "best_detection") - assert hasattr(obj, "center") - assert obj.detections >= 1 - - -def test_objectdb_module(object_db_module) -> None: - # def test_object_db_module_populated(object_db_module): - """Test that ObjectDBModule is properly populated.""" - assert len(object_db_module.objects) > 0, "Database should contain objects" - assert object_db_module.cnt > 0, "Object counter should be greater than 0" - - # def test_object3d_addition(object_db_module): - """Test Object3D addition operator.""" - # Get existing objects from the database - objects = list(object_db_module.objects.values()) - if len(objects) < 2: - pytest.skip("Not enough objects in database") - - # Get detections from two different objects - det1 = objects[0].best_detection - det2 = objects[1].best_detection - - # Create a new object with the first detection - obj = Object3D("test_track_combined", det1) - - # Add the second detection from a different object - combined = obj + det2 - - assert combined.track_id == "test_track_combined" - assert combined.detections == 2 - - # Since detections is now a counter, we can't check if specific detections are in the list - # We can only verify the count and that best_detection is properly set - - # Best detection should be determined by the Object3D logic - assert combined.best_detection is not None - - # Center should be valid (no specific value check since we're using real detections) - assert hasattr(combined, "center") - assert combined.center is not None - - # def test_image_detections3d_scene_update(object_db_module): - """Test ImageDetections3DPC to Foxglove scene update conversion.""" - # Get some detections - objects = list(object_db_module.objects.values()) - if not objects: - pytest.skip("No objects in database") - - detections = [obj.best_detection for obj in objects[:3]] # Take up to 3 - - image_detections = ImageDetections3DPC(image=detections[0].image, detections=detections) - - scene_update = image_detections.to_foxglove_scene_update() - - assert scene_update is not None - assert scene_update.entities_length == len(detections) - - for i, entity in enumerate(scene_update.entities): - assert entity.id == str(detections[i].track_id) - assert entity.frame_id == detections[i].frame_id - assert entity.cubes_length == 1 - assert entity.texts_length == 1 diff --git a/dimos/perception/detection/type/utils.py b/dimos/perception/detection/type/utils.py deleted file mode 100644 index eb924cbd1a..0000000000 --- a/dimos/perception/detection/type/utils.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import hashlib - -from rich.console import Console -from rich.table import Table -from rich.text import Text - -from dimos.types.timestamped import to_timestamp - - -def _hash_to_color(name: str) -> str: - """Generate a consistent color for a given name using hash.""" - # List of rich colors to choose from - colors = [ - "cyan", - "magenta", - "yellow", - "blue", - "green", - "red", - "bright_cyan", - "bright_magenta", - "bright_yellow", - "bright_blue", - "bright_green", - "bright_red", - "purple", - "white", - "pink", - ] - - # Hash the name and pick a color - hash_value = hashlib.md5(name.encode()).digest()[0] - return colors[hash_value % len(colors)] - - -class TableStr: - """Mixin class that provides table-based string representation for detection collections.""" - - def __str__(self) -> str: - console = Console(force_terminal=True, legacy_windows=False) - - # Create a table for detections - table = Table( - title=f"{self.__class__.__name__} [{len(self.detections)} detections @ {to_timestamp(self.image.ts):.3f}]", # type: ignore[attr-defined] - show_header=True, - show_edge=True, - ) - - # Dynamically build columns based on the first detection's dict keys - if not self.detections: # type: ignore[attr-defined] - return ( - f" {self.__class__.__name__} [0 detections @ {to_timestamp(self.image.ts):.3f}]" # type: ignore[attr-defined] - ) - - # Cache all repr_dicts to avoid double computation - detection_dicts = [det.to_repr_dict() for det in self] # type: ignore[attr-defined] - - first_dict = detection_dicts[0] - table.add_column("#", style="dim") - for col in first_dict.keys(): - color = _hash_to_color(col) - table.add_column(col.title(), style=color) - - # Add each detection to the table - for i, d in enumerate(detection_dicts): - row = [str(i)] - - for key in first_dict.keys(): - if key == "conf": - # Color-code confidence - conf_color = ( - "green" - if float(d[key]) > 0.8 - else "yellow" - if float(d[key]) > 0.5 - else "red" - ) - row.append(Text(f"{d[key]}", style=conf_color)) # type: ignore[arg-type] - elif key == "points" and d.get(key) == "None": - row.append(Text(d.get(key, ""), style="dim")) # type: ignore[arg-type] - else: - row.append(str(d.get(key, ""))) - table.add_row(*row) - - with console.capture() as capture: - console.print(table) - return capture.get().strip() diff --git a/dimos/perception/experimental/__init__.py b/dimos/perception/experimental/__init__.py deleted file mode 100644 index 39ef33521d..0000000000 --- a/dimos/perception/experimental/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Experimental perception modules.""" diff --git a/dimos/perception/experimental/temporal_memory/README.md b/dimos/perception/experimental/temporal_memory/README.md deleted file mode 100644 index 9ef5f6cb22..0000000000 --- a/dimos/perception/experimental/temporal_memory/README.md +++ /dev/null @@ -1,32 +0,0 @@ -Temporal memory runs "Temporal/Spatial RAG" on streamed videos building an continuous entity-based -memory over time. It uses a VLM to extract evidence in sliding windows, tracks -entities across windows, maintains a rolling summary, and stores relations in a graph network. - -Methodology -1) Sample frames at a target FPS and analyze them in sliding windows. -2) Extract dense evidence with a VLM (caption + entities + relations). -3) Update rolling summary for global context. -4) Persist per-window evidence + entity graph for query-time context. - -Setup -- Put your OpenAI key in `.env`: - `OPENAI_API_KEY=...` -- Install dimensional dependencies - -Quickstart -To run: `dimos --replay run unitree-go2-temporal-memory` - -In another terminal: `humancli` to chat with the agent and run memory queries. - -Artifacts -By default, artifacts are written under `assets/temporal_memory`: -- `evidence.jsonl` (window evidence: captions, entities, relations) -- `state.json` (rolling summary + roster state) -- `entities.json` (current entity roster) -- `frames_index.jsonl` (timestamps for saved frames; written on stop) -- `entity_graph.db` (SQLite graph of relations/distances) - -Notes -- Evidence is extracted in sliding windows, so queries can refer to recent or past entities. -- Distance estimation can run in the background to enrich graph relations. -- If you want a different output directory, set `TemporalMemoryConfig(output_dir=...)`. diff --git a/dimos/perception/experimental/temporal_memory/__init__.py b/dimos/perception/experimental/temporal_memory/__init__.py deleted file mode 100644 index 3cc61601ce..0000000000 --- a/dimos/perception/experimental/temporal_memory/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Temporal memory package.""" - -from .temporal_memory import Frame, TemporalMemory, TemporalMemoryConfig, temporal_memory - -__all__ = [ - "Frame", - "TemporalMemory", - "TemporalMemoryConfig", - "temporal_memory", -] diff --git a/dimos/perception/experimental/temporal_memory/clip_filter.py b/dimos/perception/experimental/temporal_memory/clip_filter.py deleted file mode 100644 index d747899452..0000000000 --- a/dimos/perception/experimental/temporal_memory/clip_filter.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""CLIP-based frame filtering for selecting diverse frames from video windows.""" - -from typing import Any - -import numpy as np - -from dimos.msgs.sensor_msgs import Image -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -try: - import torch # type: ignore - - from dimos.models.embedding.clip import CLIPModel # type: ignore - - CLIP_AVAILABLE = True -except ImportError as e: - CLIP_AVAILABLE = False - logger.info(f"CLIP unavailable ({e}), using simple frame sampling") - - -def _get_image_data(image: Image) -> np.ndarray[Any, Any]: - """Extract numpy array from Image.""" - if not hasattr(image, "data"): - raise AttributeError(f"Image missing .data attribute: {type(image)}") - return image.data - - -if CLIP_AVAILABLE: - - class CLIPFrameFilter: - """Filter video frames using CLIP embeddings for diversity.""" - - def __init__(self, model_name: str = "ViT-B/32", device: str | None = None): - if not CLIP_AVAILABLE: - raise ImportError("CLIP not available. Install transformers[torch].") - - resolved_name = ( - "openai/clip-vit-base-patch32" if model_name == "ViT-B/32" else model_name - ) - if device is None: - self._model = CLIPModel(model_name=resolved_name) - else: - self._model = CLIPModel(model_name=resolved_name, device=device) - logger.info(f"Loading CLIP {resolved_name} on {self._model.device}") - - def _encode_images(self, images: list[Image]) -> "torch.Tensor": - """Encode images using CLIP.""" - embeddings = self._model.embed(*images) - if not isinstance(embeddings, list): - embeddings = [embeddings] - vectors = [e.to_torch(self._model.device) for e in embeddings] - return torch.stack(vectors) - - def select_diverse_frames(self, frames: list[Any], max_frames: int = 3) -> list[Any]: - """Select diverse frames using greedy farthest-point sampling in CLIP space.""" - if len(frames) <= max_frames: - return frames - - embeddings = self._encode_images([f.image for f in frames]) - - # Greedy farthest-point sampling - selected_indices = [0] # Always include first frame - remaining_indices = list(range(1, len(frames))) - - while len(selected_indices) < max_frames and remaining_indices: - # Compute similarities: (num_remaining, num_selected) - similarities = embeddings[remaining_indices] @ embeddings[selected_indices].T - # Find max similarity for each remaining frame - max_similarities = similarities.max(dim=1)[0] - # Select frame most different from all selected - best_idx = int(max_similarities.argmin().item()) - - selected_indices.append(remaining_indices[best_idx]) - remaining_indices.pop(best_idx) - - return [frames[i] for i in sorted(selected_indices)] - - def close(self) -> None: - """Clean up CLIP model.""" - if hasattr(self, "_model"): - self._model.stop() - del self._model - - -def select_diverse_frames_simple(frames: list[Any], max_frames: int = 3) -> list[Any]: - """Fallback frame selection: uniform sampling across window.""" - if len(frames) <= max_frames: - return frames - indices = [int(i * len(frames) / max_frames) for i in range(max_frames)] - return [frames[i] for i in indices] - - -def adaptive_keyframes( - frames: list[Any], - min_frames: int = 3, - max_frames: int = 5, - change_threshold: float = 15.0, -) -> list[Any]: - """Select frames based on visual change, adaptive count.""" - if len(frames) <= min_frames: - return frames - - # Compute frame-to-frame differences - try: - diffs = [ - np.abs( - _get_image_data(frames[i].image).astype(float) - - _get_image_data(frames[i - 1].image).astype(float) - ).mean() - for i in range(1, len(frames)) - ] - except (AttributeError, ValueError) as e: - logger.warning(f"Failed to compute frame diffs: {e}. Falling back to uniform sampling.") - return select_diverse_frames_simple(frames, max_frames) - - total_motion = sum(diffs) - n_frames = int(np.clip(total_motion / change_threshold, min_frames, max_frames)) - - # Always include first and last - keyframe_indices = {0, len(frames) - 1} - - # Add peaks in diff signal - for i in range(1, len(diffs) - 1): - if ( - diffs[i] > diffs[i - 1] - and diffs[i] > diffs[i + 1] - and diffs[i] > change_threshold * 0.5 - ): - keyframe_indices.add(i + 1) - - # Adjust count - if len(keyframe_indices) > n_frames: - # Keep first, last, and highest-diff peaks - middle = [i for i in keyframe_indices if i not in (0, len(frames) - 1)] - middle_by_diff = sorted(middle, key=lambda i: diffs[i - 1], reverse=True) - keyframe_indices = {0, len(frames) - 1, *middle_by_diff[: n_frames - 2]} - elif len(keyframe_indices) < n_frames: - # Fill uniformly from remaining - needed = n_frames - len(keyframe_indices) - candidates = sorted(set(range(len(frames))) - keyframe_indices) - if candidates: - step = max(1, len(candidates) // (needed + 1)) - keyframe_indices.update(candidates[::step][:needed]) - - return [frames[i] for i in sorted(keyframe_indices)] - - -__all__ = [ - "CLIP_AVAILABLE", - "adaptive_keyframes", - "select_diverse_frames_simple", -] - -if CLIP_AVAILABLE: - __all__.append("CLIPFrameFilter") diff --git a/dimos/perception/experimental/temporal_memory/entity_graph_db.py b/dimos/perception/experimental/temporal_memory/entity_graph_db.py deleted file mode 100644 index 7109459f40..0000000000 --- a/dimos/perception/experimental/temporal_memory/entity_graph_db.py +++ /dev/null @@ -1,1018 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Entity Graph Database for storing and querying entity relationships. - -Maintains three types of graphs: -1. Relations Graph: Interactions between entities (holds, looks_at, talks_to, etc.) -2. Distance Graph: Spatial distances between entities -3. Semantic Graph: Conceptual relationships (goes_with, part_of, used_for, etc.) - -All graphs share the same entity nodes but have different edge types. -""" - -import json -from pathlib import Path -import sqlite3 -import threading -from typing import TYPE_CHECKING, Any - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.models.vl.base import VlModel - from dimos.msgs.sensor_msgs import Image - -logger = setup_logger() - - -class EntityGraphDB: - """ - SQLite-based graph database for entity relationships. - - Thread-safe implementation using connection-per-thread pattern. - All graphs share the same entity nodes but maintain separate edge tables. - """ - - def __init__(self, db_path: str | Path) -> None: - """ - Initialize the entity graph database. - - Args: - db_path: Path to the SQLite database file - """ - self.db_path = Path(db_path) - self.db_path.parent.mkdir(parents=True, exist_ok=True) - - # Thread-local storage for connections - self._local = threading.local() - - # Initialize schema - self._init_schema() - - logger.info(f"EntityGraphDB initialized at {self.db_path}") - - def _get_connection(self) -> sqlite3.Connection: - """Get thread-local database connection.""" - if not hasattr(self._local, "conn"): - self._local.conn = sqlite3.connect(str(self.db_path)) - self._local.conn.row_factory = sqlite3.Row - return self._local.conn # type: ignore - - def _init_schema(self) -> None: - """Initialize database schema.""" - conn = self._get_connection() - cursor = conn.cursor() - - # Entities table (shared nodes) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS entities ( - entity_id TEXT PRIMARY KEY, - entity_type TEXT NOT NULL, - descriptor TEXT, - first_seen_ts REAL NOT NULL, - last_seen_ts REAL NOT NULL, - metadata TEXT - ) - """) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_entities_first_seen ON entities(first_seen_ts)" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_entities_last_seen ON entities(last_seen_ts)" - ) - - # Relations table (Graph 1: Interactions) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS relations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - relation_type TEXT NOT NULL, - subject_id TEXT NOT NULL, - object_id TEXT NOT NULL, - confidence REAL DEFAULT 1.0, - timestamp_s REAL NOT NULL, - evidence TEXT, - notes TEXT, - FOREIGN KEY (subject_id) REFERENCES entities(entity_id), - FOREIGN KEY (object_id) REFERENCES entities(entity_id) - ) - """) - cursor.execute("CREATE INDEX IF NOT EXISTS idx_relations_subject ON relations(subject_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_relations_object ON relations(object_id)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type)") - cursor.execute("CREATE INDEX IF NOT EXISTS idx_relations_time ON relations(timestamp_s)") - - # Distances table (Graph 2: Spatial) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS distances ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - entity_a_id TEXT NOT NULL, - entity_b_id TEXT NOT NULL, - distance_meters REAL, - distance_category TEXT, - confidence REAL DEFAULT 1.0, - timestamp_s REAL NOT NULL, - method TEXT, - FOREIGN KEY (entity_a_id) REFERENCES entities(entity_id), - FOREIGN KEY (entity_b_id) REFERENCES entities(entity_id) - ) - """) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_distances_pair ON distances(entity_a_id, entity_b_id)" - ) - cursor.execute("CREATE INDEX IF NOT EXISTS idx_distances_time ON distances(timestamp_s)") - - # Semantic relations table (Graph 3: Knowledge) - cursor.execute(""" - CREATE TABLE IF NOT EXISTS semantic_relations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - relation_type TEXT NOT NULL, - entity_a_id TEXT NOT NULL, - entity_b_id TEXT NOT NULL, - confidence REAL DEFAULT 1.0, - learned_from TEXT, - first_observed_ts REAL NOT NULL, - last_observed_ts REAL NOT NULL, - observation_count INTEGER DEFAULT 1, - FOREIGN KEY (entity_a_id) REFERENCES entities(entity_id), - FOREIGN KEY (entity_b_id) REFERENCES entities(entity_id) - ) - """) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_semantic_pair ON semantic_relations(entity_a_id, entity_b_id)" - ) - cursor.execute( - "CREATE INDEX IF NOT EXISTS idx_semantic_type ON semantic_relations(relation_type)" - ) - - conn.commit() - - def upsert_entity( - self, - entity_id: str, - entity_type: str, - descriptor: str, - timestamp_s: float, - metadata: dict[str, Any] | None = None, - ) -> None: - """ - Insert or update an entity. - - Args: - entity_id: Unique entity identifier (e.g., "E1") - entity_type: Type of entity (person, object, location, etc.) - descriptor: Text description of the entity - timestamp_s: Timestamp when entity was observed - metadata: Optional additional metadata - """ - conn = self._get_connection() - cursor = conn.cursor() - - metadata_json = json.dumps(metadata) if metadata else None - - cursor.execute( - """ - INSERT INTO entities (entity_id, entity_type, descriptor, first_seen_ts, last_seen_ts, metadata) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(entity_id) DO UPDATE SET - last_seen_ts = ?, - descriptor = COALESCE(excluded.descriptor, descriptor), - metadata = COALESCE(excluded.metadata, metadata) - """, - ( - entity_id, - entity_type, - descriptor, - timestamp_s, - timestamp_s, - metadata_json, - timestamp_s, - ), - ) - - conn.commit() - logger.debug(f"Upserted entity {entity_id} (type={entity_type})") - - def get_entity(self, entity_id: str) -> dict[str, Any] | None: - """ - Get an entity by ID. - """ - conn = self._get_connection() - cursor = conn.cursor() - - cursor.execute("SELECT * FROM entities WHERE entity_id = ?", (entity_id,)) - row = cursor.fetchone() - - if row is None: - return None - - return { - "entity_id": row["entity_id"], - "entity_type": row["entity_type"], - "descriptor": row["descriptor"], - "first_seen_ts": row["first_seen_ts"], - "last_seen_ts": row["last_seen_ts"], - "metadata": json.loads(row["metadata"]) if row["metadata"] else None, - } - - def get_all_entities(self, entity_type: str | None = None) -> list[dict[str, Any]]: - """Get all entities, optionally filtered by type.""" - conn = self._get_connection() - cursor = conn.cursor() - - if entity_type: - cursor.execute( - "SELECT * FROM entities WHERE entity_type = ? ORDER BY last_seen_ts DESC", - (entity_type,), - ) - else: - cursor.execute("SELECT * FROM entities ORDER BY last_seen_ts DESC") - - rows = cursor.fetchall() - return [ - { - "entity_id": row["entity_id"], - "entity_type": row["entity_type"], - "descriptor": row["descriptor"], - "first_seen_ts": row["first_seen_ts"], - "last_seen_ts": row["last_seen_ts"], - "metadata": json.loads(row["metadata"]) if row["metadata"] else None, - } - for row in rows - ] - - def get_entities_by_time( - self, - time_window: tuple[float, float], - first_seen: bool = True, - ) -> list[dict[str, Any]]: - """Get entities first/last seen within a time window. - - Args: - time_window: (start_ts, end_ts) tuple in seconds - first_seen: If True, filter by first_seen_ts. If False, filter by last_seen_ts. - - Returns: - List of entities seen within the time window - """ - conn = self._get_connection() - cursor = conn.cursor() - - ts_field = "first_seen_ts" if first_seen else "last_seen_ts" - cursor.execute( - f"SELECT * FROM entities WHERE {ts_field} BETWEEN ? AND ? ORDER BY {ts_field} DESC", - time_window, - ) - - rows = cursor.fetchall() - return [ - { - "entity_id": row["entity_id"], - "entity_type": row["entity_type"], - "descriptor": row["descriptor"], - "first_seen_ts": row["first_seen_ts"], - "last_seen_ts": row["last_seen_ts"], - "metadata": json.loads(row["metadata"]) if row["metadata"] else None, - } - for row in rows - ] - - def add_relation( - self, - relation_type: str, - subject_id: str, - object_id: str, - confidence: float, - timestamp_s: float, - evidence: list[str] | None = None, - notes: str | None = None, - ) -> None: - """ - Add a relation between two entities. - - Args: - relation_type: Type of relation (holds, looks_at, talks_to, etc.) - subject_id: Subject entity ID - object_id: Object entity ID - confidence: Confidence score (0.0 to 1.0) - timestamp_s: Timestamp when relation was observed - evidence: Optional list of evidence strings - notes: Optional notes - """ - conn = self._get_connection() - cursor = conn.cursor() - - evidence_json = json.dumps(evidence) if evidence else None - - cursor.execute( - """ - INSERT INTO relations (relation_type, subject_id, object_id, confidence, timestamp_s, evidence, notes) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - (relation_type, subject_id, object_id, confidence, timestamp_s, evidence_json, notes), - ) - - conn.commit() - logger.debug(f"Added relation: {subject_id} --{relation_type}--> {object_id}") - - def get_relations_for_entity( - self, - entity_id: str, - relation_type: str | None = None, - time_window: tuple[float, float] | None = None, - ) -> list[dict[str, Any]]: - """ - Get all relations involving an entity. - - Args: - entity_id: Entity ID - relation_type: Optional filter by relation type - time_window: Optional (start_ts, end_ts) tuple - - Returns: - List of relation dicts - """ - conn = self._get_connection() - cursor = conn.cursor() - - query = """ - SELECT * FROM relations - WHERE (subject_id = ? OR object_id = ?) - """ - params: list[Any] = [entity_id, entity_id] - - if relation_type: - query += " AND relation_type = ?" - params.append(relation_type) - - if time_window: - query += " AND timestamp_s BETWEEN ? AND ?" - params.extend(time_window) - - query += " ORDER BY timestamp_s DESC" - - cursor.execute(query, params) - rows = cursor.fetchall() - - return [ - { - "id": row["id"], - "relation_type": row["relation_type"], - "subject_id": row["subject_id"], - "object_id": row["object_id"], - "confidence": row["confidence"], - "timestamp_s": row["timestamp_s"], - "evidence": json.loads(row["evidence"]) if row["evidence"] else None, - "notes": row["notes"], - } - for row in rows - ] - - def get_recent_relations(self, limit: int = 50) -> list[dict[str, Any]]: - """Get most recent relations.""" - conn = self._get_connection() - cursor = conn.cursor() - - cursor.execute( - """ - SELECT * FROM relations - ORDER BY timestamp_s DESC - LIMIT ? - """, - (limit,), - ) - - rows = cursor.fetchall() - return [ - { - "id": row["id"], - "relation_type": row["relation_type"], - "subject_id": row["subject_id"], - "object_id": row["object_id"], - "confidence": row["confidence"], - "timestamp_s": row["timestamp_s"], - "evidence": json.loads(row["evidence"]) if row["evidence"] else None, - "notes": row["notes"], - } - for row in rows - ] - - # ==================== Distance Operations (Graph 2) ==================== - - def add_distance( - self, - entity_a_id: str, - entity_b_id: str, - distance_meters: float | None, - distance_category: str | None, - confidence: float, - timestamp_s: float, - method: str, - ) -> None: - """ - Add distance measurement between two entities. - - Args: - entity_a_id: First entity ID - entity_b_id: Second entity ID - distance_meters: Distance in meters (can be None if only categorical) - distance_category: Category (near/medium/far) - confidence: Confidence score - timestamp_s: Timestamp of measurement - method: Method used (vlm, depth_estimation, bbox) - """ - conn = self._get_connection() - cursor = conn.cursor() - - # Normalize order to avoid duplicates (store alphabetically) - if entity_a_id > entity_b_id: - entity_a_id, entity_b_id = entity_b_id, entity_a_id - - cursor.execute( - """ - INSERT INTO distances (entity_a_id, entity_b_id, distance_meters, distance_category, - confidence, timestamp_s, method) - VALUES (?, ?, ?, ?, ?, ?, ?) - """, - ( - entity_a_id, - entity_b_id, - distance_meters, - distance_category, - confidence, - timestamp_s, - method, - ), - ) - - conn.commit() - logger.debug( - f"Added distance: {entity_a_id} <--> {entity_b_id}: {distance_meters}m ({distance_category})" - ) - - def get_distance( - self, - entity_a_id: str, - entity_b_id: str, - ) -> dict[str, Any] | None: - """Get most recent distance between two entities. - - Args: - entity_a_id: First entity ID - entity_b_id: Second entity ID - - Returns: - Distance dict or None - """ - conn = self._get_connection() - cursor = conn.cursor() - - # Normalize order - if entity_a_id > entity_b_id: - entity_a_id, entity_b_id = entity_b_id, entity_a_id - - cursor.execute( - """ - SELECT * FROM distances - WHERE entity_a_id = ? AND entity_b_id = ? - ORDER BY timestamp_s DESC - LIMIT 1 - """, - (entity_a_id, entity_b_id), - ) - - row = cursor.fetchone() - if row is None: - return None - - return { - "entity_a_id": row["entity_a_id"], - "entity_b_id": row["entity_b_id"], - "distance_meters": row["distance_meters"], - "distance_category": row["distance_category"], - "confidence": row["confidence"], - "timestamp_s": row["timestamp_s"], - "method": row["method"], - } - - def get_distance_history( - self, - entity_a_id: str, - entity_b_id: str, - ) -> list[dict[str, Any]]: - """Get all distance measurements between two entities. - - Args: - entity_a_id: First entity ID - entity_b_id: Second entity ID - - Returns: - List of distance dicts, most recent first - """ - conn = self._get_connection() - cursor = conn.cursor() - - # Normalize order - if entity_a_id > entity_b_id: - entity_a_id, entity_b_id = entity_b_id, entity_a_id - - cursor.execute( - """ - SELECT * FROM distances - WHERE entity_a_id = ? AND entity_b_id = ? - ORDER BY timestamp_s DESC - """, - (entity_a_id, entity_b_id), - ) - - return [ - { - "entity_a_id": row["entity_a_id"], - "entity_b_id": row["entity_b_id"], - "distance_meters": row["distance_meters"], - "distance_category": row["distance_category"], - "confidence": row["confidence"], - "timestamp_s": row["timestamp_s"], - "method": row["method"], - } - for row in cursor.fetchall() - ] - - def get_nearby_entities( - self, - entity_id: str, - max_distance: float, - latest_only: bool = True, - ) -> list[dict[str, Any]]: - """ - Find entities within a distance threshold. - - Args: - entity_id: Reference entity ID - max_distance: Maximum distance in meters - latest_only: If True, use only latest measurements - - Returns: - List of nearby entities with distances - """ - conn = self._get_connection() - cursor = conn.cursor() - - if latest_only: - # Get latest distance for each pair - query = """ - SELECT d.*, e.entity_type, e.descriptor - FROM distances d - INNER JOIN entities e ON ( - CASE - WHEN d.entity_a_id = ? THEN e.entity_id = d.entity_b_id - WHEN d.entity_b_id = ? THEN e.entity_id = d.entity_a_id - END - ) - WHERE (d.entity_a_id = ? OR d.entity_b_id = ?) - AND d.distance_meters IS NOT NULL - AND d.distance_meters <= ? - AND d.id IN ( - SELECT MAX(id) FROM distances - WHERE (entity_a_id = d.entity_a_id AND entity_b_id = d.entity_b_id) - GROUP BY entity_a_id, entity_b_id - ) - ORDER BY d.distance_meters ASC - """ - cursor.execute(query, (entity_id, entity_id, entity_id, entity_id, max_distance)) - else: - query = """ - SELECT d.*, e.entity_type, e.descriptor - FROM distances d - INNER JOIN entities e ON ( - CASE - WHEN d.entity_a_id = ? THEN e.entity_id = d.entity_b_id - WHEN d.entity_b_id = ? THEN e.entity_id = d.entity_a_id - END - ) - WHERE (d.entity_a_id = ? OR d.entity_b_id = ?) - AND d.distance_meters IS NOT NULL - AND d.distance_meters <= ? - ORDER BY d.distance_meters ASC - """ - cursor.execute(query, (entity_id, entity_id, entity_id, entity_id, max_distance)) - - rows = cursor.fetchall() - return [ - { - "entity_id": row["entity_b_id"] - if row["entity_a_id"] == entity_id - else row["entity_a_id"], - "entity_type": row["entity_type"], - "descriptor": row["descriptor"], - "distance_meters": row["distance_meters"], - "distance_category": row["distance_category"], - "confidence": row["confidence"], - "timestamp_s": row["timestamp_s"], - } - for row in rows - ] - - def add_semantic_relation( - self, - relation_type: str, - entity_a_id: str, - entity_b_id: str, - confidence: float, - learned_from: str, - timestamp_s: float, - ) -> None: - """ - Add or update a semantic relation. - - Args: - relation_type: Relation type (goes_with, opposite_of, part_of, used_for) - entity_a_id: First entity ID - entity_b_id: Second entity ID - confidence: Confidence score - learned_from: Source (llm, knowledge_base, observation) - timestamp_s: Timestamp when learned - """ - conn = self._get_connection() - cursor = conn.cursor() - - # Normalize order for symmetric relations - if entity_a_id > entity_b_id: - entity_a_id, entity_b_id = entity_b_id, entity_a_id - - # Check if relation exists - cursor.execute( - """ - SELECT id, observation_count, confidence FROM semantic_relations - WHERE relation_type = ? AND entity_a_id = ? AND entity_b_id = ? - """, - (relation_type, entity_a_id, entity_b_id), - ) - - existing = cursor.fetchone() - - if existing: - # Update existing relation (increase confidence, increment count) - new_count = existing["observation_count"] + 1 - new_confidence = min( - 1.0, existing["confidence"] + 0.1 - ) # Increase confidence with observations - - cursor.execute( - """ - UPDATE semantic_relations - SET last_observed_ts = ?, - observation_count = ?, - confidence = ? - WHERE id = ? - """, - (timestamp_s, new_count, new_confidence, existing["id"]), - ) - else: - # Insert new relation - cursor.execute( - """ - INSERT INTO semantic_relations - (relation_type, entity_a_id, entity_b_id, confidence, learned_from, - first_observed_ts, last_observed_ts, observation_count) - VALUES (?, ?, ?, ?, ?, ?, ?, 1) - """, - ( - relation_type, - entity_a_id, - entity_b_id, - confidence, - learned_from, - timestamp_s, - timestamp_s, - ), - ) - - conn.commit() - logger.debug(f"Added semantic relation: {entity_a_id} --{relation_type}--> {entity_b_id}") - - def get_semantic_relations( - self, - entity_id: str | None = None, - relation_type: str | None = None, - ) -> list[dict[str, Any]]: - """ - Get semantic relations, optionally filtered. - - Args: - entity_id: Optional filter by entity - relation_type: Optional filter by relation type - - Returns: - List of semantic relation dicts - """ - conn = self._get_connection() - cursor = conn.cursor() - - query = "SELECT * FROM semantic_relations WHERE 1=1" - params: list[Any] = [] - - if entity_id: - query += " AND (entity_a_id = ? OR entity_b_id = ?)" - params.extend([entity_id, entity_id]) - - if relation_type: - query += " AND relation_type = ?" - params.append(relation_type) - - query += " ORDER BY confidence DESC, observation_count DESC" - - cursor.execute(query, params) - rows = cursor.fetchall() - - return [ - { - "id": row["id"], - "relation_type": row["relation_type"], - "entity_a_id": row["entity_a_id"], - "entity_b_id": row["entity_b_id"], - "confidence": row["confidence"], - "learned_from": row["learned_from"], - "first_observed_ts": row["first_observed_ts"], - "last_observed_ts": row["last_observed_ts"], - "observation_count": row["observation_count"], - } - for row in rows - ] - - # querying - - def get_entity_neighborhood( - self, - entity_id: str, - max_hops: int = 2, - include_distances: bool = True, - include_semantics: bool = True, - ) -> dict[str, Any]: - """ - Get entity neighborhood (BFS traversal). - - Args: - entity_id: Starting entity ID - max_hops: Maximum number of hops to traverse - include_distances: Include distance graph - include_semantics: Include semantic graph - - Returns: - Dict with entities, relations, distances, and semantics - """ - visited_entities = {entity_id} - current_level = {entity_id} - all_relations = [] - all_distances = [] - all_semantics = [] - - for _ in range(max_hops): - next_level = set() - - for ent_id in current_level: - # Get relations - relations = self.get_relations_for_entity(ent_id) - all_relations.extend(relations) - - for rel in relations: - other_id = ( - rel["object_id"] if rel["subject_id"] == ent_id else rel["subject_id"] - ) - if other_id not in visited_entities: - next_level.add(other_id) - visited_entities.add(other_id) - - # Get distances - if include_distances: - distances = self.get_nearby_entities(ent_id, max_distance=10.0) - all_distances.extend(distances) - for dist in distances: - other_id = dist["entity_id"] - if other_id not in visited_entities: - next_level.add(other_id) - visited_entities.add(other_id) - - # Get semantic relations - if include_semantics: - semantics = self.get_semantic_relations(entity_id=ent_id) - all_semantics.extend(semantics) - for sem in semantics: - other_id = ( - sem["entity_b_id"] - if sem["entity_a_id"] == ent_id - else sem["entity_a_id"] - ) - if other_id not in visited_entities: - next_level.add(other_id) - visited_entities.add(other_id) - - current_level = next_level - if not current_level: - break - - # Get all entity details - entities = [self.get_entity(ent_id) for ent_id in visited_entities] - entities = [e for e in entities if e is not None] - - return { - "center_entity": entity_id, - "entities": entities, - "relations": all_relations, - "distances": all_distances, - "semantic_relations": all_semantics, - "num_hops": max_hops, - } - - def get_stats(self) -> dict[str, Any]: - """Get database statistics.""" - conn = self._get_connection() - cursor = conn.cursor() - - cursor.execute("SELECT COUNT(*) as count FROM entities") - entity_count = cursor.fetchone()["count"] - - cursor.execute("SELECT COUNT(*) as count FROM relations") - relation_count = cursor.fetchone()["count"] - - cursor.execute("SELECT COUNT(*) as count FROM distances") - distance_count = cursor.fetchone()["count"] - - cursor.execute("SELECT COUNT(*) as count FROM semantic_relations") - semantic_count = cursor.fetchone()["count"] - - return { - "entities": entity_count, - "relations": relation_count, - "distances": distance_count, - "semantic_relations": semantic_count, - } - - def get_summary(self, recent_relations_limit: int = 5) -> dict[str, Any]: - """Get stats, all entities, and recent relations.""" - return { - "stats": self.get_stats(), - "entities": self.get_all_entities(), - "recent_relations": self.get_recent_relations(limit=recent_relations_limit), - } - - def save_window_data(self, parsed: dict[str, Any], timestamp_s: float) -> None: - """Save parsed window data (entities and relations) to the graph database.""" - try: - # Save new entities - for entity in parsed.get("new_entities", []): - self.upsert_entity( - entity_id=entity["id"], - entity_type=entity["type"], - descriptor=entity.get("descriptor", "unknown"), - timestamp_s=timestamp_s, - ) - - # Save existing entities (update last_seen) - for entity in parsed.get("entities_present", []): - if isinstance(entity, dict) and "id" in entity: - descriptor = entity.get("descriptor") - if descriptor: - self.upsert_entity( - entity_id=entity["id"], - entity_type=entity.get("type", "unknown"), - descriptor=descriptor, - timestamp_s=timestamp_s, - ) - else: - existing = self.get_entity(entity["id"]) - if existing: - self.upsert_entity( - entity_id=entity["id"], - entity_type=existing["entity_type"], - descriptor=existing["descriptor"], - timestamp_s=timestamp_s, - ) - - # Save relations - for relation in parsed.get("relations", []): - subject_id = ( - relation["subject"].split("|")[0] - if "|" in relation["subject"] - else relation["subject"] - ) - object_id = ( - relation["object"].split("|")[0] - if "|" in relation["object"] - else relation["object"] - ) - - self.add_relation( - relation_type=relation["type"], - subject_id=subject_id, - object_id=object_id, - confidence=relation.get("confidence", 1.0), - timestamp_s=timestamp_s, - evidence=relation.get("evidence"), - notes=relation.get("notes"), - ) - - except Exception as e: - logger.error(f"Failed to save window data to graph DB: {e}", exc_info=True) - - def estimate_and_save_distances( - self, - parsed: dict[str, Any], - frame_image: "Image", - vlm: "VlModel", - timestamp_s: float, - max_distance_pairs: int = 5, - ) -> None: - """Estimate distances between entities using VLM and save to database. - - Args: - parsed: Parsed window data containing entities - frame_image: Frame image to analyze - vlm: VLM instance for distance estimation - timestamp_s: Timestamp for the distance measurements - max_distance_pairs: Maximum number of entity pairs to estimate - """ - if not frame_image: - return - - # Import here to avoid circular dependency - from . import temporal_utils as tu - - # Collect entities with descriptors - # new_entities have descriptors from VLM - enriched_entities = [] - for entity in parsed.get("new_entities", []): - if isinstance(entity, dict) and "id" in entity: - enriched_entities.append( - {"id": entity["id"], "descriptor": entity.get("descriptor", "unknown")} - ) - - # entities_present only have IDs - need to fetch descriptors from DB - for entity in parsed.get("entities_present", []): - if isinstance(entity, dict) and "id" in entity: - entity_id = entity["id"] - # Fetch descriptor from DB - db_entity = self.get_entity(entity_id) - if db_entity: - enriched_entities.append( - {"id": entity_id, "descriptor": db_entity.get("descriptor", "unknown")} - ) - - if len(enriched_entities) < 2: - return - - # Generate pairs without existing distances - pairs = [ - (enriched_entities[i], enriched_entities[j]) - for i in range(len(enriched_entities)) - for j in range(i + 1, len(enriched_entities)) - if not self.get_distance(enriched_entities[i]["id"], enriched_entities[j]["id"]) - ][:max_distance_pairs] - - if not pairs: - return - - try: - response = vlm.query(frame_image, tu.build_batch_distance_estimation_prompt(pairs)) - for r in tu.parse_batch_distance_response(response, pairs): - if r["category"] in ("near", "medium", "far"): - self.add_distance( - entity_a_id=r["entity_a_id"], - entity_b_id=r["entity_b_id"], - distance_meters=r.get("distance_m"), - distance_category=r["category"], - confidence=r.get("confidence", 0.5), - timestamp_s=timestamp_s, - method="vlm", - ) - except Exception as e: - logger.warning(f"Failed to estimate distances: {e}", exc_info=True) - - def commit(self) -> None: - """Commit all pending transactions and ensure data is flushed to disk.""" - if hasattr(self._local, "conn"): - conn = self._local.conn - conn.commit() - # Force checkpoint to ensure WAL data is written to main database file - try: - conn.execute("PRAGMA wal_checkpoint(FULL)") - except Exception: - pass # Ignore if WAL is not enabled - - def close(self) -> None: - """Close database connection.""" - if hasattr(self._local, "conn"): - self._local.conn.close() - del self._local.conn diff --git a/dimos/perception/experimental/temporal_memory/temporal_memory.md b/dimos/perception/experimental/temporal_memory/temporal_memory.md deleted file mode 100644 index 0eaa3df893..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_memory.md +++ /dev/null @@ -1,39 +0,0 @@ -Dimensional Temporal Memory is a lightweight Video RAG pipeline for building -entity-centric memory over live or replayed video streams. It uses a VLM to -extract evidence in sliding windows, tracks entities across time, maintains a -rolling summary, and persists relations in a compact graph for query-time context. - -How It Works -1) Sample frames at a target FPS and analyze them in sliding windows. -2) Extract dense evidence with a VLM (caption + entities + relations). -3) Update a rolling summary for global context. -4) Persist per-window evidence and the entity graph for fast queries. - -Setup -- Add your OpenAI key to `.env`: - `OPENAI_API_KEY=...` -- Install dependencies (recommended set from repo install guide): - `uv sync --extra dev --extra cpu --extra sim --extra drone` - -`uv sync` installs the locked dependency set from `uv.lock` to match the repo's -known-good environment. `uv pip install ...` behaves like pip (ad-hoc installs) -and can drift from the lockfile. - -Quickstart -- Run Temporal Memory on a replay: - `dimos --replay run unitree-go2-temporal-memory` -- In another terminal, open a chat session: - `humancli` - -Artifacts -By default, artifacts are written under `assets/temporal_memory`: -- `evidence.jsonl` (window evidence: captions, entities, relations) -- `state.json` (rolling summary + roster state) -- `entities.json` (current entity roster) -- `frames_index.jsonl` (timestamps for saved frames; written on stop) -- `entity_graph.db` (SQLite graph of relations/distances) - -Notes -- Evidence is extracted in sliding windows; queries can reference recent or past entities. -- Distance estimation can run in the background to enrich graph relations. -- Change the output location via `TemporalMemoryConfig(output_dir=...)`. diff --git a/dimos/perception/experimental/temporal_memory/temporal_memory.py b/dimos/perception/experimental/temporal_memory/temporal_memory.py deleted file mode 100644 index 66b6fce911..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_memory.py +++ /dev/null @@ -1,665 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Temporal Memory module for creating entity-based temporal understanding of video streams. - -This module implements a sophisticated temporal memory system inspired by VideoRAG, -using VLM (Vision-Language Model) API calls to maintain entity rosters, rolling summaries, -and temporal relationships across video frames. -""" - -from collections import deque -from dataclasses import dataclass -import json -import os -from pathlib import Path -import threading -import time -from typing import Any - -from reactivex import Subject, interval -from reactivex.disposable import Disposable - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import In -from dimos.models.vl.base import VlModel -from dimos.msgs.sensor_msgs import Image -from dimos.msgs.sensor_msgs.Image import sharpness_barrier - -from . import temporal_utils as tu -from .clip_filter import ( - CLIP_AVAILABLE, - adaptive_keyframes, -) - -try: - from .clip_filter import CLIPFrameFilter -except ImportError: - CLIPFrameFilter = type(None) # type: ignore[misc,assignment] -from dimos.utils.logging_config import setup_logger - -from .entity_graph_db import EntityGraphDB - -logger = setup_logger() - -# Constants -MAX_RECENT_WINDOWS = 50 # Max recent windows to keep in memory - - -@dataclass -class Frame: - frame_index: int - timestamp_s: float - image: Image - - -@dataclass -class TemporalMemoryConfig(ModuleConfig): - # Frame processing - fps: float = 1.0 - window_s: float = 2.0 - stride_s: float = 2.0 - summary_interval_s: float = 10.0 - max_frames_per_window: int = 3 - frame_buffer_size: int = 50 - - # Output - output_dir: str | Path | None = "assets/temporal_memory" - - # VLM parameters - max_tokens: int = 900 - temperature: float = 0.2 - - # Frame filtering - use_clip_filtering: bool = True - clip_model: str = "ViT-B/32" - stale_scene_threshold: float = 5.0 - - # Graph database - persistent_memory: bool = True # Keep graph across sessions - clear_memory_on_start: bool = False # Wipe DB on startup - enable_distance_estimation: bool = True # Estimate entity distances - max_distance_pairs: int = 5 # Max entity pairs per window - - # Graph context - max_relations_per_entity: int = 10 # Max relations in query context - nearby_distance_meters: float = 5.0 # "Nearby" threshold - - -class TemporalMemory(Module): - """ - builds temporal understanding of video streams using vlms. - - processes frames reactively, maintains entity rosters, tracks temporal - relationships, builds rolling summaries. responds to queries about current - state and recent events. - """ - - color_image: In[Image] - - def __init__( - self, vlm: VlModel | None = None, config: TemporalMemoryConfig | None = None - ) -> None: - super().__init__() - - self._vlm = vlm # Can be None for blueprint usage - self.config: TemporalMemoryConfig = config or TemporalMemoryConfig() - - # single lock protects all state - self._state_lock = threading.Lock() - self._stopped = False - - # protected state - self._state = tu.default_state() - self._state["next_summary_at_s"] = float(self.config.summary_interval_s) - self._frame_buffer: deque[Frame] = deque(maxlen=self.config.frame_buffer_size) - self._recent_windows: deque[dict[str, Any]] = deque(maxlen=MAX_RECENT_WINDOWS) - self._frame_count = 0 - # Start at -inf so first analysis passes stride_s check regardless of elapsed time - self._last_analysis_time = -float("inf") - self._video_start_wall_time: float | None = None - - # Track background distance estimation threads - self._distance_threads: list[threading.Thread] = [] - - # clip filter - use instance state to avoid mutating shared config - self._clip_filter: CLIPFrameFilter | None = None - self._use_clip_filtering = self.config.use_clip_filtering - if self._use_clip_filtering and CLIP_AVAILABLE: - try: - self._clip_filter = CLIPFrameFilter(model_name=self.config.clip_model) - logger.info("clip filtering enabled") - except Exception as e: - logger.warning(f"clip init failed: {e}") - self._use_clip_filtering = False - elif self._use_clip_filtering: - logger.warning("clip not available") - self._use_clip_filtering = False - - # output directory - self._graph_db: EntityGraphDB | None - if self.config.output_dir: - self._output_path = Path(self.config.output_dir) - self._output_path.mkdir(parents=True, exist_ok=True) - self._evidence_file = self._output_path / "evidence.jsonl" - self._state_file = self._output_path / "state.json" - self._entities_file = self._output_path / "entities.json" - self._frames_index_file = self._output_path / "frames_index.jsonl" - - db_path = self._output_path / "entity_graph.db" - if not self.config.persistent_memory or self.config.clear_memory_on_start: - if db_path.exists(): - db_path.unlink() - reason = ( - "non-persistent mode" - if not self.config.persistent_memory - else "clear_memory_on_start=True" - ) - logger.info(f"Deleted existing database: {reason}") - - self._graph_db = EntityGraphDB(db_path=db_path) - - logger.info(f"artifacts save to: {self._output_path}") - else: - self._graph_db = None - - logger.info( - f"temporalmemory init: fps={self.config.fps}, " - f"window={self.config.window_s}s, stride={self.config.stride_s}s" - ) - - @property - def vlm(self) -> VlModel: - """Get or create VLM instance lazily.""" - if self._vlm is None: - from dimos.models.vl.openai import OpenAIVlModel - - api_key = os.getenv("OPENAI_API_KEY") - if not api_key: - raise ValueError( - "OPENAI_API_KEY environment variable not set. " - "Either set it or pass a vlm instance to TemporalMemory constructor." - ) - self._vlm = OpenAIVlModel(api_key=api_key) - logger.info("Created OpenAIVlModel from OPENAI_API_KEY environment variable") - return self._vlm - - @rpc - def start(self) -> None: - super().start() - - with self._state_lock: - self._stopped = False - if self._video_start_wall_time is None: - self._video_start_wall_time = time.time() - - def on_frame(image: Image) -> None: - with self._state_lock: - video_start = self._video_start_wall_time - if video_start is None: - return # Not started yet - if image.ts is not None: - timestamp_s = image.ts - video_start - else: - timestamp_s = time.time() - video_start - - frame = Frame( - frame_index=self._frame_count, - timestamp_s=timestamp_s, - image=image, - ) - self._frame_buffer.append(frame) - self._frame_count += 1 - - frame_subject: Subject[Image] = Subject() - self._disposables.add( - frame_subject.pipe(sharpness_barrier(self.config.fps)).subscribe(on_frame) - ) - - unsub_image = self.color_image.subscribe(frame_subject.on_next) - self._disposables.add(Disposable(unsub_image)) - - # Schedule window analysis every stride_s seconds - self._disposables.add( - interval(self.config.stride_s).subscribe(lambda _: self._analyze_window()) - ) - - logger.info("temporalmemory started") - - @rpc - def stop(self) -> None: - # Save state before clearing (bypass _stopped check by saving directly) - if self.config.output_dir: - try: - with self._state_lock: - state_copy = self._state.copy() - entity_roster = list(self._state.get("entity_roster", [])) - with open(self._state_file, "w") as f: - json.dump(state_copy, f, indent=2, ensure_ascii=False) - logger.info(f"saved state to {self._state_file}") - with open(self._entities_file, "w") as f: - json.dump(entity_roster, f, indent=2, ensure_ascii=False) - logger.info(f"saved {len(entity_roster)} entities") - except Exception as e: - logger.error(f"save failed during stop: {e}", exc_info=True) - - self.save_frames_index() - with self._state_lock: - self._stopped = True - - # Wait for background distance estimation threads to complete before closing DB - if self._distance_threads: - logger.info(f"Waiting for {len(self._distance_threads)} distance estimation threads...") - for thread in self._distance_threads: - thread.join(timeout=10.0) # Wait max 10s per thread - self._distance_threads.clear() - - if self._graph_db: - db_path = self._graph_db.db_path - self._graph_db.commit() # save all pending transactions - self._graph_db.close() - self._graph_db = None - - if not self.config.persistent_memory and db_path.exists(): - db_path.unlink() - logger.info("Deleted non-persistent database") - - if self._clip_filter: - self._clip_filter.close() - self._clip_filter = None - - with self._state_lock: - self._frame_buffer.clear() - self._recent_windows.clear() - self._state = tu.default_state() - - super().stop() - - # Stop all stream transports to clean up LCM/shared memory threads - # Note: We use public stream.transport API and rely on transport.stop() to clean up - for stream in list(self.inputs.values()) + list(self.outputs.values()): - if stream.transport is not None and hasattr(stream.transport, "stop"): - try: - stream.transport.stop() - except Exception as e: - logger.warning(f"Failed to stop stream transport: {e}") - - logger.info("temporalmemory stopped") - - def _get_window_frames(self) -> tuple[list[Frame], dict[str, Any]] | None: - """Extract window frames from buffer with guards.""" - with self._state_lock: - if not self._frame_buffer: - return None - current_time = self._frame_buffer[-1].timestamp_s - if current_time - self._last_analysis_time < self.config.stride_s: - return None - frames_needed = max(1, int(self.config.fps * self.config.window_s)) - if len(self._frame_buffer) < frames_needed: - return None - window_frames = list(self._frame_buffer)[-frames_needed:] - state_snapshot = self._state.copy() - return window_frames, state_snapshot - - def _query_vlm_for_window( - self, - window_frames: list[Frame], - state_snapshot: dict[str, Any], - w_start: float, - w_end: float, - ) -> str | None: - """Query VLM for window analysis.""" - query = tu.build_window_prompt( - w_start=w_start, w_end=w_end, frame_count=len(window_frames), state=state_snapshot - ) - try: - fmt = tu.get_structured_output_format() - if len(window_frames) > 1: - responses = self.vlm.query_batch( - [f.image for f in window_frames], query, response_format=fmt - ) - return responses[0] if responses else "" - else: - return self.vlm.query(window_frames[0].image, query, response_format=fmt) - except Exception as e: - logger.error(f"vlm query failed [{w_start:.1f}-{w_end:.1f}s]: {e}", exc_info=True) - return None - - def _save_window_artifacts(self, parsed: dict[str, Any], w_end: float) -> None: - """Save window data to graph DB and evidence file.""" - if self._graph_db: - self._graph_db.save_window_data(parsed, w_end) - if self.config.output_dir: - self._append_evidence(parsed) - - def _analyze_window(self) -> None: - """Analyze a temporal window of frames using VLM.""" - # Extract window frames with guards - result = self._get_window_frames() - if result is None: - return - window_frames, state_snapshot = result - w_start, w_end = window_frames[0].timestamp_s, window_frames[-1].timestamp_s - - # Skip if scene hasn't changed - if tu.is_scene_stale(window_frames, self.config.stale_scene_threshold): - with self._state_lock: - self._last_analysis_time = w_end - return - - # Select diverse frames for analysis - window_frames = ( - adaptive_keyframes( # TODO: unclear if clip vs. diverse vs. this solution is best - window_frames, max_frames=self.config.max_frames_per_window - ) - ) - logger.info(f"analyzing [{w_start:.1f}-{w_end:.1f}s] with {len(window_frames)} frames") - - # Query VLM and parse response - response_text = self._query_vlm_for_window(window_frames, state_snapshot, w_start, w_end) - if response_text is None: - with self._state_lock: - self._last_analysis_time = w_end - return - - parsed = tu.parse_window_response(response_text, w_start, w_end, len(window_frames)) - if "_error" in parsed: - logger.error(f"parse error: {parsed['_error']}") - # else: - # logger.info(f"parsed. caption: {parsed.get('caption', '')[:100]}") - - # Start distance estimation in background - if self._graph_db and window_frames and self.config.enable_distance_estimation: - mid_frame = window_frames[len(window_frames) // 2] - if mid_frame.image: - thread = threading.Thread( - target=self._graph_db.estimate_and_save_distances, - args=(parsed, mid_frame.image, self.vlm, w_end, self.config.max_distance_pairs), - daemon=True, - ) - thread.start() - self._distance_threads = [t for t in self._distance_threads if t.is_alive()] - self._distance_threads.append(thread) - - # Update temporal state - with self._state_lock: - needs_summary = tu.update_state_from_window( - self._state, parsed, w_end, self.config.summary_interval_s - ) - self._recent_windows.append(parsed) - self._last_analysis_time = w_end - - # Save artifacts - self._save_window_artifacts(parsed, w_end) - - # Trigger summary update if needed - if needs_summary: - logger.info(f"updating summary at t≈{w_end:.1f}s") - self._update_rolling_summary(w_end) - - # Periodic state saves - with self._state_lock: - window_count = len(self._recent_windows) - if window_count % 10 == 0: - self.save_state() - self.save_entities() - - def _update_rolling_summary(self, w_end: float) -> None: - with self._state_lock: - if self._stopped: - return - rolling_summary = str(self._state.get("rolling_summary", "")) - chunk_buffer = list(self._state.get("chunk_buffer", [])) - latest_frame = self._frame_buffer[-1].image if self._frame_buffer else None - - if not chunk_buffer or not latest_frame: - return - - prompt = tu.build_summary_prompt( - rolling_summary=rolling_summary, chunk_windows=chunk_buffer - ) - - try: - summary_text = self.vlm.query(latest_frame, prompt) - if summary_text and summary_text.strip(): - with self._state_lock: - if self._stopped: - return - tu.apply_summary_update( - self._state, summary_text, w_end, self.config.summary_interval_s - ) - logger.info(f"updated summary: {summary_text[:100]}...") - if self.config.output_dir and not self._stopped: - self.save_state() - self.save_entities() - except Exception as e: - logger.error(f"summary update failed: {e}", exc_info=True) - - @skill - def query(self, question: str) -> str: - """Answer a question about the video stream using temporal memory and graph knowledge. - - This skill analyzes the current video stream and temporal memory state - to answer questions about what is happening, what entities are present, - recent events, spatial relationships, and conceptual knowledge. - - The system automatically accesses three knowledge graphs: - - Interactions: relationships between entities (holds, looks_at, talks_to) - - Spatial: distance and proximity information - - Semantic: conceptual relationships (goes_with, used_for, etc.) - - Example: - query("What entities are currently visible?") - query("What did I do last week?") - query("Where did I leave my keys?") - query("What objects are near the person?") - - Args: - question (str): The question to ask about the video stream. - Examples: "What entities are visible?", "What happened recently?", - "Is there a person in the scene?", "What am I holding?" - - Returns: - str: Answer based on temporal memory, graph knowledge, and current frame. - """ - # read state - with self._state_lock: - entity_roster = list(self._state.get("entity_roster", [])) - rolling_summary = str(self._state.get("rolling_summary", "")) - last_present = list(self._state.get("last_present", [])) - recent_windows = list(self._recent_windows) - if self._frame_buffer: - latest_frame = self._frame_buffer[-1].image - current_video_time_s = self._frame_buffer[-1].timestamp_s - else: - latest_frame = None - current_video_time_s = 0.0 - - if not latest_frame: - return "no frames available" - - # build context from temporal state - # Include entities from last_present and recent windows (both entities_present and new_entities) - currently_present = {e["id"] for e in last_present if isinstance(e, dict) and "id" in e} - for window in recent_windows[-3:]: - # Add entities that were present - for entity in window.get("entities_present", []): - if isinstance(entity, dict) and isinstance(entity.get("id"), str): - currently_present.add(entity["id"]) - # Also include newly detected entities (they're present now) - for entity in window.get("new_entities", []): - if isinstance(entity, dict) and isinstance(entity.get("id"), str): - currently_present.add(entity["id"]) - - context = { - "entity_roster": entity_roster, - "rolling_summary": rolling_summary, - "currently_present_entities": sorted(currently_present), - "recent_windows_count": len(recent_windows), - "timestamp": time.time(), - } - - # enhance context with graph database knowledge - if self._graph_db: - # Extract time window from question using VLM - time_window_s = tu.extract_time_window(question, self.vlm, latest_frame) - - # Query graph for ALL entities in roster (not just currently present) - # This allows queries about entities that disappeared or were seen in the past - all_entity_ids = [e["id"] for e in entity_roster if isinstance(e, dict) and "id" in e] - - if all_entity_ids: - graph_context = tu.build_graph_context( - graph_db=self._graph_db, - entity_ids=all_entity_ids, - time_window_s=time_window_s, - max_relations_per_entity=self.config.max_relations_per_entity, - nearby_distance_meters=self.config.nearby_distance_meters, - current_video_time_s=current_video_time_s, - ) - context["graph_knowledge"] = graph_context - - # build query prompt using temporal utils - prompt = tu.build_query_prompt(question=question, context=context) - - # query vlm (slow, outside lock) - try: - answer_text = self.vlm.query(latest_frame, prompt) - return answer_text.strip() - except Exception as e: - logger.error(f"query failed: {e}", exc_info=True) - return f"error: {e}" - - @rpc - def clear_history(self) -> bool: - """Clear temporal memory state.""" - try: - with self._state_lock: - self._state = tu.default_state() - self._state["next_summary_at_s"] = float(self.config.summary_interval_s) - self._recent_windows.clear() - logger.info("cleared history") - return True - except Exception as e: - logger.error(f"clear_history failed: {e}", exc_info=True) - return False - - @rpc - def get_state(self) -> dict[str, Any]: - with self._state_lock: - return { - "entity_count": len(self._state.get("entity_roster", [])), - "entities": list(self._state.get("entity_roster", [])), - "rolling_summary": str(self._state.get("rolling_summary", "")), - "frame_count": self._frame_count, - "buffer_size": len(self._frame_buffer), - "recent_windows": len(self._recent_windows), - "currently_present": list(self._state.get("last_present", [])), - } - - @rpc - def get_entity_roster(self) -> list[dict[str, Any]]: - with self._state_lock: - return list(self._state.get("entity_roster", [])) - - @rpc - def get_rolling_summary(self) -> str: - with self._state_lock: - return str(self._state.get("rolling_summary", "")) - - @rpc - def get_graph_db_stats(self) -> dict[str, Any]: - """Get statistics and sample data from the graph database. - - Returns empty structures when no database is available (no-error pattern). - """ - if not self._graph_db: - return {"stats": {}, "entities": [], "recent_relations": []} - return self._graph_db.get_summary() - - @rpc - def save_state(self) -> bool: - if not self.config.output_dir: - return False - try: - with self._state_lock: - # Don't save if stopped (state has been cleared) - if self._stopped: - return False - state_copy = self._state.copy() - with open(self._state_file, "w") as f: - json.dump(state_copy, f, indent=2, ensure_ascii=False) - logger.info(f"saved state to {self._state_file}") - return True - except Exception as e: - logger.error(f"save state failed: {e}", exc_info=True) - return False - - def _append_evidence(self, evidence: dict[str, Any]) -> None: - try: - with open(self._evidence_file, "a") as f: - f.write(json.dumps(evidence, ensure_ascii=False) + "\n") - except Exception as e: - logger.error(f"append evidence failed: {e}", exc_info=True) - - def save_entities(self) -> bool: - if not self.config.output_dir: - return False - try: - with self._state_lock: - # Don't save if stopped (state has been cleared) - if self._stopped: - return False - entity_roster = list(self._state.get("entity_roster", [])) - with open(self._entities_file, "w") as f: - json.dump(entity_roster, f, indent=2, ensure_ascii=False) - logger.info(f"saved {len(entity_roster)} entities") - return True - except Exception as e: - logger.error(f"save entities failed: {e}", exc_info=True) - return False - - def save_frames_index(self) -> bool: - if not self.config.output_dir: - return False - try: - with self._state_lock: - frames = list(self._frame_buffer) - - frames_index = [ - { - "frame_index": f.frame_index, - "timestamp_s": f.timestamp_s, - "timestamp": tu.format_timestamp(f.timestamp_s), - } - for f in frames - ] - - if frames_index: - with open(self._frames_index_file, "w", encoding="utf-8") as f: - for rec in frames_index: - f.write(json.dumps(rec, ensure_ascii=False) + "\n") - logger.info(f"saved {len(frames_index)} frames") - return True - except Exception as e: - logger.error(f"save frames failed: {e}", exc_info=True) - return False - - -temporal_memory = TemporalMemory.blueprint - -__all__ = ["Frame", "TemporalMemory", "TemporalMemoryConfig", "temporal_memory"] diff --git a/dimos/perception/experimental/temporal_memory/temporal_memory_deploy.py b/dimos/perception/experimental/temporal_memory/temporal_memory_deploy.py deleted file mode 100644 index ab3cc7a0f5..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_memory_deploy.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Deployment helpers for TemporalMemory module. -""" - -import os -from typing import TYPE_CHECKING - -from dimos.core._dask_exports import DimosCluster -from dimos.models.vl.base import VlModel -from dimos.spec import Camera as CameraSpec - -from .temporal_memory import TemporalMemory, TemporalMemoryConfig - -if TYPE_CHECKING: - from dimos.msgs.sensor_msgs import Image - - -def deploy( - dimos: DimosCluster, - camera: CameraSpec, - vlm: VlModel | None = None, - config: TemporalMemoryConfig | None = None, -) -> TemporalMemory: - """Deploy TemporalMemory with a camera. - - Args: - dimos: Dimos cluster instance - camera: Camera module to connect to - vlm: Optional VLM instance (creates OpenAI VLM if None) - config: Optional temporal memory configuration - """ - if vlm is None: - from dimos.models.vl.openai import OpenAIVlModel - - api_key = os.getenv("OPENAI_API_KEY") - if not api_key: - raise ValueError("OPENAI_API_KEY environment variable not set") - vlm = OpenAIVlModel(api_key=api_key) - - temporal_memory = dimos.deploy(TemporalMemory, vlm=vlm, config=config) # type: ignore[attr-defined] - - if camera.color_image.transport is None: - from dimos.core.transport import JpegShmTransport - - transport: JpegShmTransport[Image] = JpegShmTransport("/temporal_memory/color_image") - camera.color_image.transport = transport - - temporal_memory.color_image.connect(camera.color_image) - temporal_memory.start() - return temporal_memory # type: ignore[return-value,no-any-return] diff --git a/dimos/perception/experimental/temporal_memory/temporal_memory_example.py b/dimos/perception/experimental/temporal_memory/temporal_memory_example.py deleted file mode 100644 index 8ba28bb174..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_memory_example.py +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Example usage of TemporalMemory module with a VLM. - -This example demonstrates how to: -1. Deploy a camera module -2. Deploy TemporalMemory with the camera -3. Query the temporal memory about entities and events -""" - -from pathlib import Path - -from dotenv import load_dotenv - -from dimos import core -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.hardware.sensors.camera.webcam import Webcam - -from .temporal_memory import TemporalMemoryConfig -from .temporal_memory_deploy import deploy - -# Load environment variables -load_dotenv() - - -def example_usage() -> None: - """Example of how to use TemporalMemory.""" - # Initialize variables to None for cleanup - temporal_memory = None - camera = None - dimos = None - - try: - # Create Dimos cluster - dimos = core.start(1) - # Deploy camera module - camera = dimos.deploy(CameraModule, hardware=lambda: Webcam(camera_index=0)) # type: ignore[attr-defined] - camera.start() - - # Deploy temporal memory using the deploy function - output_dir = Path("./temporal_memory_output") - temporal_memory = deploy( - dimos, - camera, - vlm=None, # Will auto-create OpenAIVlModel if None - config=TemporalMemoryConfig( - fps=1.0, # Process 1 frame per second - window_s=2.0, # Analyze 2-second windows - stride_s=2.0, # New window every 2 seconds - summary_interval_s=10.0, # Update rolling summary every 10 seconds - max_frames_per_window=3, # Max 3 frames per window - output_dir=output_dir, - ), - ) - - print("TemporalMemory deployed and started!") - print(f"Artifacts will be saved to: {output_dir}") - - # Let it run for a bit to build context - print("Building temporal context... (wait ~15 seconds)") - import time - - time.sleep(20) - - # Query the temporal memory - questions = [ - "Are there any people in the scene?", - "Describe the main activity happening now", - "What has happened in the last few seconds?", - "What entities are currently visible?", - ] - - for question in questions: - print(f"\nQuestion: {question}") - answer = temporal_memory.query(question) - print(f"Answer: {answer}") - - # Get current state - state = temporal_memory.get_state() - print("\n=== Current State ===") - print(f"Entity count: {state['entity_count']}") - print(f"Frame count: {state['frame_count']}") - print(f"Rolling summary: {state['rolling_summary']}") - print(f"Entities: {state['entities']}") - - # Get entity roster - entities = temporal_memory.get_entity_roster() - print("\n=== Entity Roster ===") - for entity in entities: - print(f" {entity['id']}: {entity['descriptor']}") - - # Check graph database stats - graph_stats = temporal_memory.get_graph_db_stats() - print("\n=== Graph Database Stats ===") - if "error" in graph_stats: - print(f"Error: {graph_stats['error']}") - else: - print(f"Stats: {graph_stats['stats']}") - print(f"\nEntities in DB ({len(graph_stats['entities'])}):") - for entity in graph_stats["entities"]: - print(f" {entity['entity_id']} ({entity['entity_type']}): {entity['descriptor']}") - print(f"\nRecent relations ({len(graph_stats['recent_relations'])}):") - for rel in graph_stats["recent_relations"]: - print( - f" {rel['subject_id']} --{rel['relation_type']}--> {rel['object_id']} (confidence: {rel['confidence']:.2f})" - ) - - # Stop when done - temporal_memory.stop() - camera.stop() - print("\nTemporalMemory stopped") - - finally: - if temporal_memory is not None: - temporal_memory.stop() - if camera is not None: - camera.stop() - if dimos is not None: - dimos.close_all() # type: ignore[attr-defined] - - -if __name__ == "__main__": - example_usage() diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/__init__.py b/dimos/perception/experimental/temporal_memory/temporal_utils/__init__.py deleted file mode 100644 index 64950bee8a..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Temporal memory utilities for temporal memory. includes helper functions -and prompts that are used to build the prompt for the VLM. -""" - -# Re-export everything from submodules -from .graph_utils import build_graph_context, extract_time_window -from .helpers import clamp_text, format_timestamp, is_scene_stale, next_entity_id_hint -from .parsers import parse_batch_distance_response, parse_window_response -from .prompts import ( - WINDOW_RESPONSE_SCHEMA, - build_batch_distance_estimation_prompt, - build_distance_estimation_prompt, - build_query_prompt, - build_summary_prompt, - build_window_prompt, - get_structured_output_format, -) -from .state import apply_summary_update, default_state, update_state_from_window - -__all__ = [ - # Schema - "WINDOW_RESPONSE_SCHEMA", - # State management - "apply_summary_update", - # Prompts - "build_batch_distance_estimation_prompt", - "build_distance_estimation_prompt", - # Graph utils - "build_graph_context", - "build_query_prompt", - "build_summary_prompt", - "build_window_prompt", - # Helpers - "clamp_text", - "default_state", - "extract_time_window", - "format_timestamp", - "get_structured_output_format", - "is_scene_stale", - "next_entity_id_hint", - # Parsers - "parse_batch_distance_response", - "parse_window_response", - "update_state_from_window", -] diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/graph_utils.py b/dimos/perception/experimental/temporal_memory/temporal_utils/graph_utils.py deleted file mode 100644 index 8d05f8c1e1..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/graph_utils.py +++ /dev/null @@ -1,226 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Graph database utility functions for temporal memory.""" - -import re -from typing import TYPE_CHECKING, Any - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.models.vl.base import VlModel - from dimos.msgs.sensor_msgs import Image - - from ..entity_graph_db import EntityGraphDB - -logger = setup_logger() - - -def extract_time_window( - question: str, - vlm: "VlModel", - latest_frame: "Image | None" = None, -) -> float | None: - """Extract time window from question using VLM with example-based learning. - - Uses a few example keywords as patterns, then asks VLM to extrapolate - similar time references and return seconds. - - Args: - question: User's question - vlm: VLM instance to use for extraction - latest_frame: Optional frame (required for VLM call, but image is ignored) - - Returns: - Time window in seconds, or None if no time reference found - """ - question_lower = question.lower() - - # Quick check for common patterns (fast path) - if "last week" in question_lower or "past week" in question_lower: - return 7 * 24 * 3600 - if "today" in question_lower or "last hour" in question_lower: - return 3600 - if "recently" in question_lower or "recent" in question_lower: - return 600 - - # Use VLM to extract time reference from question - # Provide examples and let VLM extrapolate similar patterns - # Note: latest_frame is required by VLM interface but image content is ignored - if not latest_frame: - return None - - extraction_prompt = f"""Extract any time reference from this question and convert it to seconds. - -Question: {question} - -Examples of time references and their conversions: -- "last week" or "past week" -> 604800 seconds (7 days) -- "yesterday" -> 86400 seconds (1 day) -- "today" or "last hour" -> 3600 seconds (1 hour) -- "recently" or "recent" -> 600 seconds (10 minutes) -- "few minutes ago" -> 300 seconds (5 minutes) -- "just now" -> 60 seconds (1 minute) - -Extrapolate similar patterns (e.g., "2 days ago", "this morning", "last month", etc.) -and convert to seconds. If no time reference is found, return "none". - -Return ONLY a number (seconds) or the word "none". Do not include any explanation.""" - - try: - response = vlm.query(latest_frame, extraction_prompt) - response = response.strip().lower() - - if "none" in response or not response: - return None - - # Extract number from response - numbers = re.findall(r"\d+(?:\.\d+)?", response) - if numbers: - seconds = float(numbers[0]) - # Sanity check: reasonable time windows (1 second to 1 year) - if 1 <= seconds <= 365 * 24 * 3600: - return seconds - except Exception as e: - logger.debug(f"Time extraction failed: {e}") - - return None - - -def build_graph_context( - graph_db: "EntityGraphDB", - entity_ids: list[str], - time_window_s: float | None = None, - max_relations_per_entity: int = 10, - nearby_distance_meters: float = 5.0, - current_video_time_s: float | None = None, -) -> dict[str, Any]: - """Build enriched context from graph database for given entities. - - Args: - graph_db: Entity graph database instance - entity_ids: List of entity IDs to get context for - time_window_s: Optional time window in seconds (e.g., 3600 for last hour) - max_relations_per_entity: Maximum relations to include per entity (default: 10) - nearby_distance_meters: Distance threshold for "nearby" entities (default: 5.0) - current_video_time_s: Current video timestamp in seconds (for time window queries). - If None, uses latest entity timestamp from DB as reference. - - Returns: - Dictionary with graph context including relationships, distances, and semantics - """ - if not graph_db or not entity_ids: - return {} - - try: - graph_context: dict[str, Any] = { - "relationships": [], - "spatial_info": [], - "semantic_knowledge": [], - "entity_timestamps": [], - } - - # Convert time_window_s to a (start_ts, end_ts) tuple if provided - # Use video-relative timestamps, not wall-clock time - time_window_tuple = None - if time_window_s is not None: - if current_video_time_s is not None: - ref_time = current_video_time_s - else: - # Fallback: get the latest timestamp from entities in DB - all_entities = graph_db.get_all_entities() - ref_time = max((e.get("last_seen_ts", 0) for e in all_entities), default=0) - time_window_tuple = (max(0, ref_time - time_window_s), ref_time) - - # Get entity timestamp information for visibility duration queries - for entity_id in entity_ids: - entity = graph_db.get_entity(entity_id) - if entity: - first_seen = entity.get("first_seen_ts") - last_seen = entity.get("last_seen_ts") - duration_s = None - if first_seen is not None and last_seen is not None: - duration_s = last_seen - first_seen - - graph_context["entity_timestamps"].append( - { - "entity_id": entity_id, - "first_seen_ts": first_seen, - "last_seen_ts": last_seen, - "duration_s": duration_s, - } - ) - - # Get recent relationships for each entity - for entity_id in entity_ids: - # Get relationships (Graph 1: interactions) - relations = graph_db.get_relations_for_entity( - entity_id=entity_id, - relation_type=None, # all types - time_window=time_window_tuple, - ) - for rel in relations[-max_relations_per_entity:]: - graph_context["relationships"].append( - { - "subject": rel["subject_id"], - "relation": rel["relation_type"], - "object": rel["object_id"], - "confidence": rel["confidence"], - "when": rel["timestamp_s"], - } - ) - - # Get spatial relationships (Graph 2: distances) - nearby = graph_db.get_nearby_entities( - entity_id=entity_id, max_distance=nearby_distance_meters, latest_only=True - ) - for dist in nearby: - graph_context["spatial_info"].append( - { - "entity_a": entity_id, - "entity_b": dist["entity_id"], - "distance": dist.get("distance_meters"), - "category": dist.get("distance_category"), - "confidence": dist["confidence"], - } - ) - - # Get semantic knowledge (Graph 3: conceptual relations) - semantic_rels = graph_db.get_semantic_relations( - entity_id=entity_id, - relation_type=None, - ) - for sem in semantic_rels: - graph_context["semantic_knowledge"].append( - { - "entity_a": sem["entity_a_id"], - "relation": sem["relation_type"], - "entity_b": sem["entity_b_id"], - "confidence": sem["confidence"], - "observations": sem["observation_count"], - } - ) - - # Get graph statistics for context - if entity_ids: - stats = graph_db.get_stats() - graph_context["total_entities"] = stats.get("entities", 0) - graph_context["total_relations"] = stats.get("relations", 0) - - return graph_context - - except Exception as e: - logger.warning(f"failed to build graph context: {e}") - return {} diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/helpers.py b/dimos/perception/experimental/temporal_memory/temporal_utils/helpers.py deleted file mode 100644 index 513feb65a4..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/helpers.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Helper utility functions for temporal memory.""" - -from typing import TYPE_CHECKING, Any - -import numpy as np - -if TYPE_CHECKING: - from ..temporal_memory import Frame - - -def next_entity_id_hint(roster: Any) -> str: - """Generate next entity ID based on existing roster (e.g., E1, E2, E3...).""" - if not isinstance(roster, list): - return "E1" - max_n = 0 - for e in roster: - if not isinstance(e, dict): - continue - eid = e.get("id") - if isinstance(eid, str) and eid.startswith("E"): - tail = eid[1:] - if tail.isdigit(): - max_n = max(max_n, int(tail)) - return f"E{max_n + 1}" - - -def clamp_text(text: str, max_chars: int) -> str: - """Clamp text to maximum characters.""" - if len(text) <= max_chars: - return text - return text[:max_chars] + "..." - - -def format_timestamp(seconds: float) -> str: - """Format seconds as MM:SS.mmm timestamp string.""" - m = int(seconds // 60) - s = seconds - 60 * m - return f"{m:02d}:{s:06.3f}" - - -def is_scene_stale(frames: list["Frame"], stale_threshold: float = 5.0) -> bool: - """Check if scene hasn't changed meaningfully between first and last frame. - - Args: - frames: List of frames to check - stale_threshold: Threshold for mean pixel difference (default: 5.0) - - Returns: - True if scene is stale (hasn't changed enough), False otherwise - """ - if len(frames) < 2: - return False - first_img = frames[0].image - last_img = frames[-1].image - if first_img is None or last_img is None: - return False - if not hasattr(first_img, "data") or not hasattr(last_img, "data"): - return False - diff = np.abs(first_img.data.astype(float) - last_img.data.astype(float)) - return bool(diff.mean() < stale_threshold) diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/parsers.py b/dimos/perception/experimental/temporal_memory/temporal_utils/parsers.py deleted file mode 100644 index a9b1a05d9f..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/parsers.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Response parsing functions for VLM outputs.""" - -from typing import Any - -from dimos.utils.llm_utils import extract_json - - -def parse_batch_distance_response( - response: str, entity_pairs: list[tuple[dict[str, Any], dict[str, Any]]] -) -> list[dict[str, Any]]: - """ - Parse batched distance estimation response. - - Args: - response: VLM response text - entity_pairs: Original entity pairs used in the prompt - - Returns: - List of dicts with keys: entity_a_id, entity_b_id, category, distance_m, confidence - """ - results = [] - lines = response.strip().split("\n") - - current_pair_idx = None - category = None - distance_m = None - confidence = 0.5 - - for line in lines: - line = line.strip() - - # Check for pair marker - if line.startswith("Pair "): - # Save previous pair if exists - if current_pair_idx is not None and category: - entity_a, entity_b = entity_pairs[current_pair_idx] - results.append( - { - "entity_a_id": entity_a["id"], - "entity_b_id": entity_b["id"], - "category": category, - "distance_m": distance_m, - "confidence": confidence, - } - ) - - # Start new pair - try: - pair_num = int(line.split()[1].rstrip(":")) - current_pair_idx = pair_num - 1 # Convert to 0-indexed - category = None - distance_m = None - confidence = 0.5 - except (IndexError, ValueError): - continue - - # Parse distance fields - elif line.startswith("category:"): - category = line.split(":", 1)[1].strip().lower() - elif line.startswith("distance_m:"): - try: - distance_m = float(line.split(":", 1)[1].strip()) - except (ValueError, IndexError): - pass - elif line.startswith("confidence:"): - try: - confidence = float(line.split(":", 1)[1].strip()) - except (ValueError, IndexError): - pass - - # Save last pair - if current_pair_idx is not None and category and current_pair_idx < len(entity_pairs): - entity_a, entity_b = entity_pairs[current_pair_idx] - results.append( - { - "entity_a_id": entity_a["id"], - "entity_b_id": entity_b["id"], - "category": category, - "distance_m": distance_m, - "confidence": confidence, - } - ) - - return results - - -def parse_window_response( - response_text: str, w_start: float, w_end: float, frame_count: int -) -> dict[str, Any]: - """ - Parse VLM response for a window analysis. - - Args: - response_text: Raw text response from VLM - w_start: Window start time - w_end: Window end time - frame_count: Number of frames in window - - Returns: - Parsed dictionary with defaults filled in. If parsing fails, returns - a dict with "_error" key instead of raising. - """ - # Try to extract JSON (handles code fences) - parsed = extract_json(response_text) - if parsed is None: - return { - "window": {"start_s": w_start, "end_s": w_end}, - "caption": "", - "entities_present": [], - "new_entities": [], - "relations": [], - "on_screen_text": [], - "_error": f"Failed to parse JSON from response: {response_text[:200]}...", - } - - # Ensure we return a dict (extract_json can return a list) - if isinstance(parsed, list): - # If we got a list, wrap it in a dict with a default structure - # This shouldn't happen with proper structured output, but handle gracefully - return { - "window": {"start_s": w_start, "end_s": w_end}, - "caption": "", - "entities_present": [], - "new_entities": [], - "relations": [], - "on_screen_text": [], - "_error": f"Unexpected list response: {parsed}", - } - - # Ensure it's a dict - if not isinstance(parsed, dict): - return { - "window": {"start_s": w_start, "end_s": w_end}, - "caption": "", - "entities_present": [], - "new_entities": [], - "relations": [], - "on_screen_text": [], - "_error": f"Expected dict or list, got {type(parsed)}: {parsed}", - } - - return parsed diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/prompts.py b/dimos/perception/experimental/temporal_memory/temporal_utils/prompts.py deleted file mode 100644 index 5269a3d67d..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/prompts.py +++ /dev/null @@ -1,359 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Prompt building functions for VLM queries.""" - -import json -from typing import Any - -from .helpers import clamp_text, next_entity_id_hint - -# JSON schema for window responses (from VideoRAG) -WINDOW_RESPONSE_SCHEMA = { - "type": "object", - "properties": { - "window": { - "type": "object", - "properties": {"start_s": {"type": "number"}, "end_s": {"type": "number"}}, - "required": ["start_s", "end_s"], - }, - "caption": {"type": "string"}, - "entities_present": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "confidence": {"type": "number", "minimum": 0, "maximum": 1}, - }, - "required": ["id"], - }, - }, - "new_entities": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": {"type": "string"}, - "type": { - "type": "string", - "enum": ["person", "object", "screen", "text", "location", "other"], - }, - "descriptor": {"type": "string"}, - }, - "required": ["id", "type"], - }, - }, - "relations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": {"type": "string"}, - "subject": {"type": "string"}, - "object": {"type": "string"}, - "confidence": {"type": "number", "minimum": 0, "maximum": 1}, - "evidence": {"type": "array", "items": {"type": "string"}}, - "notes": {"type": "string"}, - }, - "required": ["type", "subject", "object"], - }, - }, - "on_screen_text": {"type": "array", "items": {"type": "string"}}, - "uncertainties": {"type": "array", "items": {"type": "string"}}, - "confidence": {"type": "number", "minimum": 0, "maximum": 1}, - }, - "required": ["window", "caption"], -} - - -def build_window_prompt( - *, - w_start: float, - w_end: float, - frame_count: int, - state: dict[str, Any], -) -> str: - """ - Build comprehensive VLM prompt for analyzing a video window. - - This is adapted from videorag's build_window_messages() but formatted - as a single text prompt for VlModel.query() instead of OpenAI's messages format. - - Args: - w_start: Window start time in seconds - w_end: Window end time in seconds - frame_count: Number of frames in this window - state: Current temporal memory state (entity_roster, rolling_summary, etc.) - - Returns: - Formatted prompt string - """ - roster = state.get("entity_roster", []) - rolling_summary = state.get("rolling_summary", "") - next_id = next_entity_id_hint(roster) - - # System instructions (from VideoRAG) - system_context = """You analyze short sequences of video frames. -You must stay grounded in what is visible. -Do not identify real people or guess names/identities; describe people anonymously. -Extract general entities (people, objects, screens, text, locations) and relations between them. -Use stable entity IDs like E1, E2 based on the provided roster.""" - - # Main prompt (from VideoRAG's build_window_messages) - prompt = f"""{system_context} - -Time window: [{w_start:.3f}, {w_end:.3f}) seconds -Number of frames: {frame_count} - -Existing entity roster (may be empty): -{json.dumps(roster, ensure_ascii=False)} - -Rolling summary so far (may be empty): -{clamp_text(str(rolling_summary), 1500)} - -Task: -1) Write a dense, grounded caption describing what is visible across the frames in this time window. -2) Identify which existing roster entities appear in these frames. -3) Add any new salient entities (people/objects/screens/text/locations) with a short grounded descriptor. -4) Extract grounded relations/events between entities (e.g., looks_at, holds, uses, walks_past, speaks_to (inferred)). - -New entity IDs must start at: {next_id} - -Rules (important): -- You MUST stay grounded in what is visible in the provided frames. -- You MUST NOT mention any entity ID unless it appears in the provided roster OR you include it in new_entities in this same output. -- If the roster is empty, introduce any salient entities you reference (start with E1, E2, ...). -- Do not invent on-screen text: only include text you can read. -- If a relation is inferred (e.g., speaks_to without audio), include it but lower confidence and explain the visual cues. - -Output JSON ONLY with this schema: -{{ - "window": {{"start_s": {w_start:.3f}, "end_s": {w_end:.3f}}}, - "caption": "dense grounded description", - "entities_present": [{{"id": "E1", "confidence": 0.0-1.0}}], - "new_entities": [{{"id": "E3", "type": "person|object|screen|text|location|other", "descriptor": "..."}}], - "relations": [ - {{ - "type": "speaks_to|looks_at|holds|uses|moves|gesture|scene_change|other", - "subject": "E1|unknown", - "object": "E2|unknown", - "confidence": 0.0-1.0, - "evidence": ["describe which frames show this"], - "notes": "short, grounded" - }} - ], - "on_screen_text": ["verbatim snippets"], - "uncertainties": ["things that are unclear"], - "confidence": 0.0-1.0 -}} -""" - return prompt - - -def build_summary_prompt( - *, - rolling_summary: str, - chunk_windows: list[dict[str, Any]], -) -> str: - """ - Build prompt for updating rolling summary. - - This is adapted from videorag's build_summary_messages() but formatted - as a single text prompt for VlModel.query(). - - Args: - rolling_summary: Current rolling summary text - chunk_windows: List of recent window results to incorporate - - Returns: - Formatted prompt string - """ - # System context (from VideoRAG) - system_context = """You summarize timestamped video-window logs into a concise rolling summary. -Stay grounded in the provided window captions/relations. -Do not invent entities or rename entity IDs; preserve IDs like E1, E2 exactly. -You MAY incorporate new entity IDs if they appear in the provided chunk windows (e.g., in new_entities). -Be concise, but keep relevant entity continuity and key relations.""" - - prompt = f"""{system_context} - -Update the rolling summary using the newest chunk. - -Previous rolling summary (may be empty): -{clamp_text(rolling_summary, 2500)} - -New chunk windows (JSON): -{json.dumps(chunk_windows, ensure_ascii=False)} - -Output a concise summary as PLAIN TEXT (no JSON, no code fences). -Length constraints (important): -- Target <= 120 words total. -- Hard cap <= 900 characters. -""" - return prompt - - -def build_query_prompt( - *, - question: str, - context: dict[str, Any], -) -> str: - """ - Build prompt for querying temporal memory. - - Args: - question: User's question about the video stream - context: Context dict containing entity_roster, rolling_summary, etc. - - Returns: - Formatted prompt string - """ - currently_present = context.get("currently_present_entities", []) - currently_present_str = ( - f"Entities recently detected in recent windows: {currently_present}" - if currently_present - else "No entities were detected in recent windows (list is empty)" - ) - - prompt = f"""Answer the following question about the video stream using the provided context. - -**Question:** {question} - -**Context:** -{json.dumps(context, indent=2, ensure_ascii=False)} - -**Important Notes:** -- Entities have stable IDs like E1, E2, etc. -- The 'currently_present_entities' list contains entity IDs that were detected in recent video windows (not necessarily in the current frame you're viewing) -- {currently_present_str} -- The 'entity_roster' contains all known entities with their descriptions -- The 'rolling_summary' describes what has happened over time -- The 'graph_knowledge.entity_timestamps' contains visibility information for each entity: - - 'first_seen_ts': timestamp (in seconds) when the entity was first detected - - 'last_seen_ts': timestamp (in seconds) when the entity was last detected - - 'duration_s': total time span from first to last appearance (last_seen_ts - first_seen_ts) - - Use this information to answer questions about when entities appeared, disappeared, or how long they were visible -- If 'currently_present_entities' is empty, it means no entities were detected in recent windows, but entities may still exist in the roster from earlier -- Answer based on the provided context (entity_roster, rolling_summary, currently_present_entities, graph_knowledge) AND what you see in the current frame -- If the context says entities were present but you don't see them in the current frame, mention both: what was recently detected AND what you currently see -- For duration questions, use the 'duration_s' field from 'entity_timestamps' if available - -Provide a concise answer. -""" - return prompt - - -def build_distance_estimation_prompt( - *, - entity_a_descriptor: str, - entity_a_id: str, - entity_b_descriptor: str, - entity_b_id: str, -) -> str: - """ - Build prompt for estimating distance between two entities. - - Args: - entity_a_descriptor: Description of first entity - entity_a_id: ID of first entity - entity_b_descriptor: Description of second entity - entity_b_id: ID of second entity - - Returns: - Formatted prompt string for distance estimation - """ - prompt = f"""Look at this image and estimate the distance between these two entities: - -Entity A: {entity_a_descriptor} (ID: {entity_a_id}) -Entity B: {entity_b_descriptor} (ID: {entity_b_id}) - -Provide: -1. Distance category: "near" (< 1m), "medium" (1-3m), or "far" (> 3m) -2. Approximate distance in meters (best guess) -3. Confidence: 0.0-1.0 (how certain are you?) - -Respond in this format: -category: [near/medium/far] -distance_m: [number] -confidence: [0.0-1.0] -reasoning: [brief explanation]""" - return prompt - - -def build_batch_distance_estimation_prompt( - entity_pairs: list[tuple[dict[str, Any], dict[str, Any]]], -) -> str: - """ - Build prompt for estimating distances between multiple entity pairs in one call. - - Args: - entity_pairs: List of (entity_a, entity_b) tuples, each entity is a dict with 'id' and 'descriptor' - - Returns: - Formatted prompt string for batched distance estimation - """ - pairs_text = [] - for i, (entity_a, entity_b) in enumerate(entity_pairs, 1): - pairs_text.append( - f"Pair {i}:\n" - f" Entity A: {entity_a['descriptor']} (ID: {entity_a['id']})\n" - f" Entity B: {entity_b['descriptor']} (ID: {entity_b['id']})" - ) - - prompt = f"""Look at this image and estimate the distances between the following entity pairs: - -{chr(10).join(pairs_text)} - -For each pair, provide: -1. Distance category: "near" (< 1m), "medium" (1-3m), or "far" (> 3m) -2. Approximate distance in meters (best guess) -3. Confidence: 0.0-1.0 (how certain are you?) - -Respond in this format (one block per pair): -Pair 1: -category: [near/medium/far] -distance_m: [number] -confidence: [0.0-1.0] - -Pair 2: -category: [near/medium/far] -distance_m: [number] -confidence: [0.0-1.0] - -(etc.)""" - return prompt - - -def get_structured_output_format() -> dict[str, Any]: - """ - Get OpenAI-compatible structured output format for window responses. - - This uses the json_schema mode available in OpenAI API (GPT-4o mini) to enforce - the VideoRAG response schema. - - Returns: - Dictionary for response_format parameter: - {"type": "json_schema", "json_schema": {...}} - """ - - return { - "type": "json_schema", - "json_schema": { - "name": "video_window_analysis", - "description": "Analysis of a video window with entities and relations", - "schema": WINDOW_RESPONSE_SCHEMA, - "strict": False, # Allow additional fields - }, - } diff --git a/dimos/perception/experimental/temporal_memory/temporal_utils/state.py b/dimos/perception/experimental/temporal_memory/temporal_utils/state.py deleted file mode 100644 index 9cdfbe4931..0000000000 --- a/dimos/perception/experimental/temporal_memory/temporal_utils/state.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""State management functions for temporal memory.""" - -from typing import Any - - -def default_state() -> dict[str, Any]: - """Create default temporal memory state dictionary.""" - return { - "entity_roster": [], - "rolling_summary": "", - "chunk_buffer": [], - "next_summary_at_s": 0.0, - "last_present": [], - } - - -def update_state_from_window( - state: dict[str, Any], - parsed: dict[str, Any], - w_end: float, - summary_interval_s: float, -) -> bool: - """ - Update temporal memory state from a parsed window result. - - This implements the state update logic from VideoRAG's generate_evidence(). - - Args: - state: Current state dictionary (modified in place) - parsed: Parsed window result - w_end: Window end time - summary_interval_s: How often to trigger summary updates - - Returns: - True if summary update is needed, False otherwise - """ - # Skip if there was an error - if "_error" in parsed: - return False - - new_entities = parsed.get("new_entities", []) - present = parsed.get("entities_present", []) - - # Handle new entities - if new_entities: - roster = list(state.get("entity_roster", [])) - known = {e.get("id") for e in roster if isinstance(e, dict)} - for e in new_entities: - if isinstance(e, dict) and e.get("id") not in known: - roster.append(e) - known.add(e.get("id")) - state["entity_roster"] = roster - - # Handle referenced entities (auto-add if mentioned but not in roster) - roster = list(state.get("entity_roster", [])) - known = {e.get("id") for e in roster if isinstance(e, dict)} - referenced: set[str] = set() - for p in present or []: - if isinstance(p, dict) and isinstance(p.get("id"), str): - referenced.add(p["id"]) - for rel in parsed.get("relations") or []: - if isinstance(rel, dict): - for k in ("subject", "object"): - v = rel.get(k) - if isinstance(v, str) and v != "unknown": - referenced.add(v) - for rid in sorted(referenced): - if rid not in known: - roster.append( - { - "id": rid, - "type": "other", - "descriptor": "unknown (auto-added; rerun recommended)", - } - ) - known.add(rid) - state["entity_roster"] = roster - state["last_present"] = present - - # Add to chunk buffer - chunk_buffer = state.get("chunk_buffer", []) - if not isinstance(chunk_buffer, list): - chunk_buffer = [] - chunk_buffer.append( - { - "window": parsed.get("window"), - "caption": parsed.get("caption", ""), - "entities_present": parsed.get("entities_present", []), - "new_entities": parsed.get("new_entities", []), - "relations": parsed.get("relations", []), - "on_screen_text": parsed.get("on_screen_text", []), - } - ) - state["chunk_buffer"] = chunk_buffer - - # Check if summary update is needed - if summary_interval_s > 0: - next_at = float(state.get("next_summary_at_s", summary_interval_s)) - if w_end + 1e-6 >= next_at and chunk_buffer: - return True # Need to update summary - - return False - - -def apply_summary_update( - state: dict[str, Any], summary_text: str, w_end: float, summary_interval_s: float -) -> None: - """ - Apply a summary update to the state. - - Args: - state: State dictionary (modified in place) - summary_text: New summary text - w_end: Current window end time - summary_interval_s: Summary update interval - """ - if summary_text and summary_text.strip(): - state["rolling_summary"] = summary_text.strip() - state["chunk_buffer"] = [] - - # Advance next_summary_at_s - next_at = float(state.get("next_summary_at_s", summary_interval_s)) - while next_at <= w_end + 1e-6: - next_at += float(summary_interval_s) - state["next_summary_at_s"] = next_at diff --git a/dimos/perception/experimental/temporal_memory/test_temporal_memory_module.py b/dimos/perception/experimental/temporal_memory/test_temporal_memory_module.py deleted file mode 100644 index 1d0dab007b..0000000000 --- a/dimos/perception/experimental/temporal_memory/test_temporal_memory_module.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import os -import pathlib -import tempfile -import time - -from dotenv import load_dotenv -import pytest -from reactivex import operators as ops - -from dimos import core -from dimos.core import Module, Out, rpc -from dimos.models.vl.openai import OpenAIVlModel -from dimos.msgs.sensor_msgs import Image -from dimos.perception.experimental.temporal_memory import TemporalMemory, TemporalMemoryConfig -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger -from dimos.utils.testing import TimedSensorReplay - -# Load environment variables -load_dotenv() - -logger = setup_logger() - - -class VideoReplayModule(Module): - """Module that replays video data from TimedSensorReplay.""" - - video_out: Out[Image] - - def __init__(self, video_path: str) -> None: - super().__init__() - self.video_path = video_path - - @rpc - def start(self) -> None: - """Start replaying video data.""" - # Use TimedSensorReplay to replay video frames - video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) - - # Subscribe to the replay stream and publish to LCM - self._disposables.add( - video_replay.stream() - .pipe( - ops.sample(1), # Sample every 1 second - ops.take(10), # Only take 10 frames total - ) - .subscribe(self.video_out.publish) - ) - - logger.info("VideoReplayModule started") - - @rpc - def stop(self) -> None: - """Stop replaying video data.""" - # Stop all stream transports to clean up LCM loop threads - for stream in list(self.outputs.values()): - if stream.transport is not None and hasattr(stream.transport, "stop"): - stream.transport.stop() - stream._transport = None - super().stop() - logger.info("VideoReplayModule stopped") - - -@pytest.mark.skipif(bool(os.getenv("CI")), reason="LCM replay + dataset not CI-safe.") -@pytest.mark.skipif(not os.getenv("OPENAI_API_KEY"), reason="OPENAI_API_KEY not set.") -@pytest.mark.neverending -class TestTemporalMemoryModule: - @pytest.fixture(scope="function") - def temp_dir(self): - """Create a temporary directory for test data.""" - temp_dir = tempfile.mkdtemp(prefix="temporal_memory_test_") - yield temp_dir - - @pytest.fixture(scope="function") - def dimos_cluster(self): - """Create and cleanup Dimos cluster.""" - dimos = core.start(1) - yield dimos - dimos.close_all() - - @pytest.fixture(scope="function") - def video_module(self, dimos_cluster): - """Create and cleanup video replay module.""" - data_path = get_data("unitree_office_walk") - video_path = os.path.join(data_path, "video") - video_module = dimos_cluster.deploy(VideoReplayModule, video_path) - video_module.video_out.transport = core.LCMTransport("/test_video", Image) - yield video_module - try: - video_module.stop() - except Exception as e: - logger.warning(f"Failed to stop video_module: {e}") - - @pytest.fixture(scope="function") - def temporal_memory(self, dimos_cluster, temp_dir): - """Create and cleanup temporal memory module.""" - output_dir = os.path.join(temp_dir, "temporal_memory_output") - # Create OpenAIVlModel instance - api_key = os.getenv("OPENAI_API_KEY") - vlm = OpenAIVlModel(api_key=api_key) - - temporal_memory = dimos_cluster.deploy( - TemporalMemory, - vlm=vlm, - config=TemporalMemoryConfig( - fps=1.0, # Process 1 frame per second - window_s=2.0, # Analyze 2-second windows - stride_s=2.0, # New window every 2 seconds - summary_interval_s=10.0, # Update rolling summary every 10 seconds - max_frames_per_window=3, # Max 3 frames per window - output_dir=output_dir, - ), - ) - yield temporal_memory - try: - temporal_memory.stop() - except Exception as e: - logger.warning(f"Failed to stop temporal_memory: {e}") - - @pytest.mark.asyncio - async def test_temporal_memory_module_with_replay( - self, dimos_cluster, video_module, temporal_memory, temp_dir - ): - """Test TemporalMemory module with TimedSensorReplay inputs.""" - # Connect streams - temporal_memory.color_image.connect(video_module.video_out) - - # Start all modules - video_module.start() - temporal_memory.start() - logger.info("All modules started, processing in background...") - - # Wait for frames to be processed with timeout - timeout = 15.0 # 15 second timeout - start_time = time.time() - - # Keep checking state while modules are running - while (time.time() - start_time) < timeout: - state = temporal_memory.get_state() - if state["frame_count"] > 0: - logger.info( - f"Frames processing - Frame count: {state['frame_count']}, " - f"Buffer size: {state['buffer_size']}, " - f"Entity count: {state['entity_count']}" - ) - if state["frame_count"] >= 3: # Wait for at least 3 frames - break - await asyncio.sleep(0.5) - else: - # Timeout reached - state = temporal_memory.get_state() - logger.error( - f"Timeout after {timeout}s - Frame count: {state['frame_count']}, " - f"Buffer size: {state['buffer_size']}" - ) - raise AssertionError(f"No frames processed within {timeout} seconds") - - await asyncio.sleep(3) # Wait for more processing - - # Test get_state() RPC method - mid_state = temporal_memory.get_state() - logger.info( - f"Mid-test state - Frame count: {mid_state['frame_count']}, " - f"Entity count: {mid_state['entity_count']}, " - f"Recent windows: {mid_state['recent_windows']}" - ) - assert mid_state["frame_count"] >= state["frame_count"], ( - "Frame count should increase or stay same" - ) - - # Test query() RPC method - answer = temporal_memory.query("What entities are currently visible?") - logger.info(f"Query result: {answer[:200]}...") - assert len(answer) > 0, "Query should return a non-empty answer" - - # Test get_entity_roster() RPC method - entities = temporal_memory.get_entity_roster() - logger.info(f"Entity roster has {len(entities)} entities") - assert isinstance(entities, list), "Entity roster should be a list" - - # Test get_rolling_summary() RPC method - summary = temporal_memory.get_rolling_summary() - logger.info(f"Rolling summary: {summary[:200] if summary else 'empty'}...") - assert isinstance(summary, str), "Rolling summary should be a string" - - final_state = temporal_memory.get_state() - logger.info( - f"Final state - Frame count: {final_state['frame_count']}, " - f"Entity count: {final_state['entity_count']}, " - f"Recent windows: {final_state['recent_windows']}" - ) - - video_module.stop() - temporal_memory.stop() - logger.info("Stopped modules") - - # Wait a bit for file operations to complete - await asyncio.sleep(0.5) - - # Verify files were created - stop() already saved them - output_dir = os.path.join(temp_dir, "temporal_memory_output") - output_path = pathlib.Path(output_dir) - assert output_path.exists(), f"Output directory should exist: {output_dir}" - assert (output_path / "state.json").exists(), "state.json should exist" - assert (output_path / "entities.json").exists(), "entities.json should exist" - assert (output_path / "frames_index.jsonl").exists(), "frames_index.jsonl should exist" - - logger.info("All temporal memory module tests passed!") - - -if __name__ == "__main__": - pytest.main(["-v", "-s", __file__]) diff --git a/dimos/perception/object_scene_registration.py b/dimos/perception/object_scene_registration.py deleted file mode 100644 index cfc4ab8d3e..0000000000 --- a/dimos/perception/object_scene_registration.py +++ /dev/null @@ -1,358 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Any - -import cv2 -import numpy as np -from numpy.typing import NDArray -import open3d as o3d # type: ignore[import-untyped] - -from dimos.agents.annotation import skill -from dimos.core import In, Out, rpc -from dimos.core.module import Module -from dimos.msgs.foxglove_msgs import ImageAnnotations -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.msgs.sensor_msgs.Image import ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray -from dimos.perception.detection.detectors.yoloe import Yoloe2DDetector, YoloePromptMode -from dimos.perception.detection.objectDB import ObjectDB -from dimos.perception.detection.type import ImageDetections2D -from dimos.perception.detection.type.detection3d.object import ( - Object, - Object as DetObject, - aggregate_pointclouds, - to_detection3d_array, -) -from dimos.types.timestamped import align_timestamped -from dimos.utils.logging_config import setup_logger -from dimos.utils.reactive import backpressure - -logger = setup_logger() - - -class ObjectSceneRegistrationModule(Module): - """Module for detecting objects in camera images using YOLO-E with 2D and 3D detection.""" - - color_image: In[Image] - depth_image: In[Image] - camera_info: In[CameraInfo] - - detections_2d: Out[Detection2DArray] - detections_3d: Out[Detection3DArray] - objects: Out[list[DetObject]] - overlay: Out[ImageAnnotations] - pointcloud: Out[PointCloud2] - - _detector: Yoloe2DDetector | None = None - _camera_info: CameraInfo | None = None - _object_db: ObjectDB - _latest_depth_image: Image | None = None - _latest_camera_transform: Any = None - - def __init__( - self, - target_frame: str = "map", - prompt_mode: YoloePromptMode = YoloePromptMode.LRPC, - ) -> None: - super().__init__() - self._target_frame = target_frame - self._prompt_mode = prompt_mode - self._object_db = ObjectDB() - - @rpc - def start(self) -> None: - super().start() - - if self._prompt_mode == YoloePromptMode.LRPC: - model_name = "yoloe-11l-seg-pf.pt" - else: - model_name = "yoloe-11l-seg.pt" - - self._detector = Yoloe2DDetector( - model_name=model_name, - prompt_mode=self._prompt_mode, - ) - - self.camera_info.subscribe(lambda msg: setattr(self, "_camera_info", msg)) - - aligned_frames = align_timestamped( - self.color_image.observable(), # type: ignore[no-untyped-call] - self.depth_image.observable(), # type: ignore[no-untyped-call] - buffer_size=2.0, - match_tolerance=0.1, - ) - backpressure(aligned_frames).subscribe(self._on_aligned_frames) - - @rpc - def stop(self) -> None: - """Stop the module and clean up resources.""" - - if self._detector: - self._detector.stop() - self._detector = None - - self._object_db.clear() - - logger.info("ObjectSceneRegistrationModule stopped") - super().stop() - - @rpc - def set_prompts( - self, - text: list[str] | None = None, - bboxes: NDArray[np.float64] | None = None, - ) -> None: - """Set prompts for detection. Provide either text or bboxes, not both.""" - if self._detector is not None: - self._detector.set_prompts(text=text, bboxes=bboxes) - - @rpc - def select_object(self, track_id: int) -> dict[str, Any] | None: - """Get object data by track_id and promote to permanent.""" - for obj in self._object_db.get_all_objects(): - if obj.track_id == track_id: - self._object_db.promote(obj.object_id) - return obj.to_dict() - return None - - @rpc - def get_object_track_ids(self) -> list[int]: - """Get track_ids of all permanent objects.""" - return [obj.track_id for obj in self._object_db.get_all_objects()] - - @rpc - def get_detected_objects(self) -> list[dict[str, Any]]: - """Get all detected objects with object_id (UUID) and name.""" - return [obj.agent_encode() for obj in self._object_db.get_all_objects()] - - @rpc - def get_object_pointcloud_by_name(self, name: str) -> PointCloud2 | None: - """Get pointcloud for an object by class name.""" - objects = self._object_db.find_by_name(name) - return objects[0].pointcloud if objects else None - - @rpc - def get_object_pointcloud_by_object_id(self, object_id: str) -> PointCloud2 | None: - """Get pointcloud for an object by its stable object_id (searches all objects).""" - obj = self._object_db.find_by_object_id(object_id) - if obj is None: - logger.warning(f"No object found with object_id='{object_id}'") - return None - pc = obj.pointcloud - num_points = len(pc.pointcloud.points) if pc else 0 - logger.info(f"Found object '{object_id}' ({obj.name}) with {num_points} points") - return pc - - def _get_object_mask(self, object_id: str) -> NDArray[np.uint8] | None: - """Get dilated mask for an object by ID.""" - for obj in self._object_db.get_all_objects(): - if obj.object_id != object_id: - continue - if obj.mask is None: - return None - - mask = obj.mask.astype(np.uint8) - if mask.max() == 1: - mask = (mask * 255).astype(np.uint8) - - kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) - return cv2.dilate(mask, kernel).astype(np.uint8) - - return None - - @rpc - def get_full_scene_pointcloud( - self, - exclude_object_id: str | None = None, - depth_trunc: float = 2.0, - voxel_size: float = 0.01, - ) -> PointCloud2 | None: - """Get full scene pointcloud from depth, including table/surfaces for collision filtering.""" - if self._latest_depth_image is None or self._camera_info is None: - return None - - depth_cv = self._latest_depth_image.to_opencv() - h, w = depth_cv.shape[:2] - - # Zero out excluded object's depth - if exclude_object_id: - exclude_mask = self._get_object_mask(exclude_object_id) - if exclude_mask is not None: - depth_cv = depth_cv.copy() - depth_cv[exclude_mask > 0] = 0 - - # Build pointcloud from depth - fx, fy = self._camera_info.K[0], self._camera_info.K[4] - cx, cy = self._camera_info.K[2], self._camera_info.K[5] - intrinsic = o3d.camera.PinholeCameraIntrinsic(w, h, fx, fy, cx, cy) - - depth_o3d = o3d.geometry.Image(depth_cv.astype(np.float32)) - pcd = o3d.geometry.PointCloud.create_from_depth_image( - depth_o3d, intrinsic, depth_scale=1.0, depth_trunc=depth_trunc - ) - - if len(pcd.points) < 100: - return None - - pcd = pcd.voxel_down_sample(voxel_size) - - pc = PointCloud2( - pcd, - frame_id=self._latest_depth_image.frame_id, - ts=self._latest_depth_image.ts, - ) - - if self._latest_camera_transform is not None: - pc = pc.transform(self._latest_camera_transform) - - return pc - - @skill - def detect(self, *prompts: str) -> str: - """Detect objects matching the given text prompts. - - Do NOT call this tool multiple times for one query. Pass all objects in a single call. - For example, to detect a cup and mouse, call detect("cup", "mouse") not detect("cup") then detect("mouse"). - - Args: - prompts (str): Text descriptions of objects to detect (e.g., "person", "car", "dog") - - Returns: - str: Detected objects with their object_id (stable UUID) and name. - - Example: - detect("person", "car", "dog") - detect("cup") - """ - if not prompts: - return "No prompts provided." - if self._detector is None: - return "Detector not initialized." - - self._detector.set_prompts(text=list(prompts)) - time.sleep(2.0) - - detected = self.get_detected_objects() - if not detected: - return "No objects detected." - - obj_list = [f" - {obj['name']} (object_id='{obj['object_id']}')" for obj in detected] - return f"Detected {len(detected)} object(s):\n" + "\n".join(obj_list) - - @skill - def select(self, track_id: int) -> str: - """Select an object by track_id and promote it to permanent. - - Example: - select(5) - """ - result = self.select_object(track_id) - if result is None: - return f"No object found with track_id {track_id}." - return f"Selected object {track_id}: {result['name']}" - - def _on_aligned_frames(self, frames) -> None: # type: ignore[no-untyped-def] - color_msg, depth_msg = frames - self._process_images(color_msg, depth_msg) - - def _process_images(self, color_msg: Image, depth_msg: Image) -> None: - """Process synchronized color and depth images (runs in background thread).""" - if not self._detector or not self._camera_info: - return - - color_image = color_msg - # Convert depth to meters (float32) - depth_cv = depth_msg.to_opencv() - if depth_msg.format == ImageFormat.DEPTH16: - depth_cv = depth_cv.astype(np.float32) / 1000.0 - elif depth_cv.dtype != np.float32: - depth_cv = depth_cv.astype(np.float32) - depth_image = Image( - data=depth_cv, format=ImageFormat.DEPTH, frame_id=depth_msg.frame_id, ts=depth_msg.ts - ) - - # Run 2D detection - detections_2d: ImageDetections2D[Any] = self._detector.process_image(color_image) - - detections_2d_msg = Detection2DArray( - detections_length=len(detections_2d.detections), - header=Header(color_image.ts, color_image.frame_id or ""), - detections=[det.to_ros_detection2d() for det in detections_2d.detections], - ) - self.detections_2d.publish(detections_2d_msg) - - overlay_annotations = detections_2d.to_foxglove_annotations() - self.overlay.publish(overlay_annotations) - - # Process 3D detections - self._process_3d_detections(detections_2d, color_image, depth_image) - - def _process_3d_detections( - self, - detections_2d: ImageDetections2D[Any], - color_image: Image, - depth_image: Image, - ) -> None: - """Convert 2D detections to 3D and publish.""" - if self._camera_info is None: - return - - # Cache depth image for full scene pointcloud generation - self._latest_depth_image = depth_image - - # Look up transform from camera frame to target frame (e.g., map) - camera_transform = None - if self._target_frame != color_image.frame_id: - camera_transform = self.tf.get( - self._target_frame, - color_image.frame_id, - color_image.ts, - 0.1, - ) - if camera_transform is None: - logger.warning("Failed to lookup transform from camera frame to target frame") - return - - # Cache camera transform for full scene pointcloud - self._latest_camera_transform = camera_transform - - objects = Object.from_2d_to_list( - detections_2d=detections_2d, - color_image=color_image, - depth_image=depth_image, - camera_info=self._camera_info, - camera_transform=camera_transform, - ) - if not objects: - return - - # Add objects to spatial memory database - objects = self._object_db.add_objects(objects) - - detections_3d = to_detection3d_array(objects) - self.detections_3d.publish(detections_3d) - self.objects.publish(objects) - - objects_for_pc = self._object_db.get_objects() - aggregated_pc = aggregate_pointclouds(objects_for_pc) - self.pointcloud.publish(aggregated_pc) - return - - -object_scene_registration_module = ObjectSceneRegistrationModule.blueprint - -__all__ = ["ObjectSceneRegistrationModule", "object_scene_registration_module"] diff --git a/dimos/perception/object_tracker.py b/dimos/perception/object_tracker.py deleted file mode 100644 index da415ac32a..0000000000 --- a/dimos/perception/object_tracker.py +++ /dev/null @@ -1,641 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -import threading -import time - -import cv2 - -# Import LCM messages -from dimos_lcm.vision_msgs import ( - Detection2D, - Detection3D, - ObjectHypothesisWithPose, -) -import numpy as np -from numpy.typing import NDArray -from reactivex.disposable import Disposable - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import In, Out -from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import ( - CameraInfo, - Image, - ImageFormat, -) -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray -from dimos.protocol.tf import TF -from dimos.types.timestamped import align_timestamped -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import ( - euler_to_quaternion, - optical_to_robot_frame, - yaw_towards_point, -) - -logger = setup_logger() - - -@dataclass -class ObjectTrackingConfig(ModuleConfig): - frame_id: str = "camera_link" - - -class ObjectTracking(Module[ObjectTrackingConfig]): - """Module for object tracking with LCM input/output.""" - - # LCM inputs - color_image: In[Image] - depth: In[Image] - camera_info: In[CameraInfo] - - # LCM outputs - detection2darray: Out[Detection2DArray] - detection3darray: Out[Detection3DArray] - tracked_overlay: Out[Image] # Visualization output - - default_config = ObjectTrackingConfig - config: ObjectTrackingConfig - - def __init__( - self, reid_threshold: int = 10, reid_fail_tolerance: int = 5, **kwargs: object - ) -> None: - """ - Initialize an object tracking module using OpenCV's CSRT tracker with ORB re-ID. - - Args: - camera_intrinsics: Optional [fx, fy, cx, cy] camera parameters. - If None, will use camera_info input. - reid_threshold: Minimum good feature matches needed to confirm re-ID. - reid_fail_tolerance: Number of consecutive frames Re-ID can fail before - tracking is stopped. - """ - # Call parent Module init - super().__init__(**kwargs) - - self.camera_intrinsics = None - self.reid_threshold = reid_threshold - self.reid_fail_tolerance = reid_fail_tolerance - - self.tracker = None - self.tracking_bbox = None # Stores (x, y, w, h) for tracker initialization - self.tracking_initialized = False - self.orb = cv2.ORB_create() # type: ignore[attr-defined] - self.bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False) - self.original_des = None # Store original ORB descriptors - self.original_kps = None # Store original ORB keypoints - self.reid_fail_count = 0 # Counter for consecutive re-id failures - self.last_good_matches = [] # type: ignore[var-annotated] # Store good matches for visualization - self.last_roi_kps = None # Store last ROI keypoints for visualization - self.last_roi_bbox = None # Store last ROI bbox for visualization - self.reid_confirmed = False # Store current reid confirmation state - self.tracking_frame_count = 0 # Count frames since tracking started - self.reid_warmup_frames = 3 # Number of frames before REID starts - - self._frame_lock = threading.Lock() - self._latest_rgb_frame: np.ndarray | None = None # type: ignore[type-arg] - self._latest_depth_frame: np.ndarray | None = None # type: ignore[type-arg] - self._latest_camera_info: CameraInfo | None = None - - # Tracking thread control - self.tracking_thread: threading.Thread | None = None - self.stop_tracking = threading.Event() - self.tracking_rate = 30.0 # Hz - self.tracking_period = 1.0 / self.tracking_rate - - # Initialize TF publisher - self.tf = TF() - - # Store latest detections for RPC access - self._latest_detection2d: Detection2DArray | None = None - self._latest_detection3d: Detection3DArray | None = None - self._detection_event = threading.Event() - - @rpc - def start(self) -> None: - super().start() - - # Subscribe to aligned rgb and depth streams - def on_aligned_frames(frames_tuple) -> None: # type: ignore[no-untyped-def] - rgb_msg, depth_msg = frames_tuple - with self._frame_lock: - self._latest_rgb_frame = rgb_msg.data - - depth_data = depth_msg.data - # Convert from millimeters to meters if depth is DEPTH16 format - if depth_msg.format == ImageFormat.DEPTH16: - depth_data = depth_data.astype(np.float32) / 1000.0 - self._latest_depth_frame = depth_data - - # Create aligned observable for RGB and depth - aligned_frames = align_timestamped( - self.color_image.observable(), # type: ignore[no-untyped-call] - self.depth.observable(), # type: ignore[no-untyped-call] - buffer_size=2.0, # 2 second buffer - match_tolerance=0.5, # 500ms tolerance - ) - unsub = aligned_frames.subscribe(on_aligned_frames) - self._disposables.add(unsub) - - # Subscribe to camera info stream separately (doesn't need alignment) - def on_camera_info(camera_info_msg: CameraInfo) -> None: - self._latest_camera_info = camera_info_msg - # Extract intrinsics from camera info K matrix - # K is a 3x3 matrix in row-major order: [fx, 0, cx, 0, fy, cy, 0, 0, 1] - self.camera_intrinsics = [ # type: ignore[assignment] - camera_info_msg.K[0], - camera_info_msg.K[4], - camera_info_msg.K[2], - camera_info_msg.K[5], - ] - - unsub = self.camera_info.subscribe(on_camera_info) # type: ignore[assignment] - self._disposables.add(Disposable(unsub)) # type: ignore[arg-type] - - @rpc - def stop(self) -> None: - self.stop_track() - - self.stop_tracking.set() - - if self.tracking_thread and self.tracking_thread.is_alive(): - self.tracking_thread.join(timeout=2.0) - - super().stop() - - @rpc - def track( - self, - bbox: list[float], - ) -> dict: # type: ignore[type-arg] - """ - Initialize tracking with a bounding box and process current frame. - - Args: - bbox: Bounding box in format [x1, y1, x2, y2] - - Returns: - Dict containing tracking results with 2D and 3D detections - """ - if self._latest_rgb_frame is None: - logger.warning("No RGB frame available for tracking") - - # Initialize tracking - x1, y1, x2, y2 = map(int, bbox) - w, h = x2 - x1, y2 - y1 - if w <= 0 or h <= 0: - logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") - - # Set tracking parameters - self.tracking_bbox = (x1, y1, w, h) # type: ignore[assignment] # Store in (x, y, w, h) format - self.tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] - self.tracking_initialized = False - self.original_des = None - self.reid_fail_count = 0 - logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") - - # Extract initial features - roi = self._latest_rgb_frame[y1:y2, x1:x2] # type: ignore[index] - if roi.size > 0: - self.original_kps, self.original_des = self.orb.detectAndCompute(roi, None) - if self.original_des is None: - logger.warning("No ORB features found in initial ROI. REID will be disabled.") - else: - logger.info(f"Initial ORB features extracted: {len(self.original_des)}") - - # Initialize the tracker - init_success = self.tracker.init(self._latest_rgb_frame, self.tracking_bbox) # type: ignore[attr-defined] - if init_success: - self.tracking_initialized = True - self.tracking_frame_count = 0 # Reset frame counter - logger.info("Tracker initialized successfully.") - else: - logger.error("Tracker initialization failed.") - self.stop_track() - else: - logger.error("Empty ROI during tracker initialization.") - self.stop_track() - - # Start tracking thread - self._start_tracking_thread() - - # Return initial tracking result - return {"status": "tracking_started", "bbox": self.tracking_bbox} - - def reid(self, frame, current_bbox) -> bool: # type: ignore[no-untyped-def] - """Check if features in current_bbox match stored original features.""" - # During warm-up period, always return True - if self.tracking_frame_count < self.reid_warmup_frames: - return True - - if self.original_des is None: - return False - x1, y1, x2, y2 = map(int, current_bbox) - roi = frame[y1:y2, x1:x2] - if roi.size == 0: - return False # Empty ROI cannot match - - kps_current, des_current = self.orb.detectAndCompute(roi, None) - if des_current is None or len(des_current) < 2: - return False # Need at least 2 descriptors for knnMatch - - # Store ROI keypoints and bbox for visualization - self.last_roi_kps = kps_current - self.last_roi_bbox = [x1, y1, x2, y2] - - # Handle case where original_des has only 1 descriptor (cannot use knnMatch with k=2) - if len(self.original_des) < 2: - matches = self.bf.match(self.original_des, des_current) - self.last_good_matches = matches # Store all matches for visualization - good_matches = len(matches) - else: - matches = self.bf.knnMatch(self.original_des, des_current, k=2) - # Apply Lowe's ratio test robustly - good_matches_list = [] - good_matches = 0 - for match_pair in matches: - if len(match_pair) == 2: - m, n = match_pair - if m.distance < 0.75 * n.distance: - good_matches_list.append(m) - good_matches += 1 - self.last_good_matches = good_matches_list # Store good matches for visualization - - return good_matches >= self.reid_threshold - - def _start_tracking_thread(self) -> None: - """Start the tracking thread.""" - self.stop_tracking.clear() - self.tracking_thread = threading.Thread(target=self._tracking_loop, daemon=True) - self.tracking_thread.start() - logger.info("Started tracking thread") - - def _tracking_loop(self) -> None: - """Main tracking loop that runs in a separate thread.""" - while not self.stop_tracking.is_set() and self.tracking_initialized: - # Process tracking for current frame - self._process_tracking() - - # Sleep to maintain tracking rate - time.sleep(self.tracking_period) - - logger.info("Tracking loop ended") - - def _reset_tracking_state(self) -> None: - """Reset tracking state without stopping the thread.""" - self.tracker = None - self.tracking_bbox = None - self.tracking_initialized = False - self.original_des = None - self.original_kps = None - self.reid_fail_count = 0 # Reset counter - self.last_good_matches = [] - self.last_roi_kps = None - self.last_roi_bbox = None - self.reid_confirmed = False # Reset reid confirmation state - self.tracking_frame_count = 0 # Reset frame counter - - # Publish empty detections to clear any visualizations - empty_2d = Detection2DArray(detections_length=0, header=Header(), detections=[]) - empty_3d = Detection3DArray(detections_length=0, header=Header(), detections=[]) - self._latest_detection2d = empty_2d - self._latest_detection3d = empty_3d - self._detection_event.clear() - self.detection2darray.publish(empty_2d) - self.detection3darray.publish(empty_3d) - - @rpc - def stop_track(self) -> bool: - """ - Stop tracking the current object. - This resets the tracker and all tracking state. - - Returns: - bool: True if tracking was successfully stopped - """ - # Reset tracking state first - self._reset_tracking_state() - - # Stop tracking thread if running (only if called from outside the thread) - if self.tracking_thread and self.tracking_thread.is_alive(): - # Check if we're being called from within the tracking thread - if threading.current_thread() != self.tracking_thread: - self.stop_tracking.set() - self.tracking_thread.join(timeout=1.0) - self.tracking_thread = None - else: - # If called from within thread, just set the stop flag - self.stop_tracking.set() - - logger.info("Tracking stopped") - return True - - @rpc - def is_tracking(self) -> bool: - """ - Check if the tracker is currently tracking an object successfully. - - Returns: - bool: True if tracking is active and REID is confirmed, False otherwise - """ - return self.tracking_initialized and self.reid_confirmed - - def _process_tracking(self) -> None: - """Process current frame for tracking and publish detections.""" - if self.tracker is None or not self.tracking_initialized: - return - - # Get local copies of frames under lock - with self._frame_lock: - if self._latest_rgb_frame is None or self._latest_depth_frame is None: - return - frame = self._latest_rgb_frame.copy() - depth_frame = self._latest_depth_frame.copy() - tracker_succeeded = False - reid_confirmed_this_frame = False - final_success = False - current_bbox_x1y1x2y2 = None - - # Perform tracker update - tracker_succeeded, bbox_cv = self.tracker.update(frame) - if tracker_succeeded: - x, y, w, h = map(int, bbox_cv) - current_bbox_x1y1x2y2 = [x, y, x + w, y + h] - # Perform re-ID check - reid_confirmed_this_frame = self.reid(frame, current_bbox_x1y1x2y2) - self.reid_confirmed = reid_confirmed_this_frame # Store for is_tracking() RPC - - if reid_confirmed_this_frame: - self.reid_fail_count = 0 - else: - self.reid_fail_count += 1 - else: - self.reid_confirmed = False # No tracking if tracker failed - - # Determine final success - if tracker_succeeded: - if self.reid_fail_count >= self.reid_fail_tolerance: - logger.warning( - f"Re-ID failed consecutively {self.reid_fail_count} times. Target lost." - ) - final_success = False - self._reset_tracking_state() - else: - final_success = True - else: - final_success = False - if self.tracking_initialized: - logger.info("Tracker update failed. Stopping track.") - self._reset_tracking_state() - - self.tracking_frame_count += 1 - - if not reid_confirmed_this_frame and self.tracking_frame_count >= self.reid_warmup_frames: - return - - # Create detections if tracking succeeded - header = Header(self.frame_id) - detection2darray = Detection2DArray(detections_length=0, header=header, detections=[]) - detection3darray = Detection3DArray(detections_length=0, header=header, detections=[]) - - if final_success and current_bbox_x1y1x2y2 is not None: - x1, y1, x2, y2 = current_bbox_x1y1x2y2 - center_x = (x1 + x2) / 2.0 - center_y = (y1 + y2) / 2.0 - width = float(x2 - x1) - height = float(y2 - y1) - - # Create Detection2D - detection_2d = Detection2D() - detection_2d.id = "0" - detection_2d.results_length = 1 - detection_2d.header = header - - # Create hypothesis - hypothesis = ObjectHypothesisWithPose() - hypothesis.hypothesis.class_id = "tracked_object" - hypothesis.hypothesis.score = 1.0 - detection_2d.results = [hypothesis] - - # Create bounding box - detection_2d.bbox.center.position.x = center_x - detection_2d.bbox.center.position.y = center_y - detection_2d.bbox.center.theta = 0.0 - detection_2d.bbox.size_x = width - detection_2d.bbox.size_y = height - - detection2darray = Detection2DArray() - detection2darray.detections_length = 1 - detection2darray.header = header - detection2darray.detections = [detection_2d] - - # Create Detection3D if depth is available - if depth_frame is not None: - # Calculate 3D position using depth and camera intrinsics - depth_value = self._get_depth_from_bbox(current_bbox_x1y1x2y2, depth_frame) - if ( - depth_value is not None - and depth_value > 0 - and self.camera_intrinsics is not None - ): - fx, fy, cx, cy = self.camera_intrinsics - - # Convert pixel coordinates to 3D in optical frame - z_optical = depth_value - x_optical = (center_x - cx) * z_optical / fx - y_optical = (center_y - cy) * z_optical / fy - - # Create pose in optical frame - optical_pose = Pose() - optical_pose.position = Vector3(x_optical, y_optical, z_optical) - optical_pose.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) # Identity for now - - # Convert to robot frame - robot_pose = optical_to_robot_frame(optical_pose) - - # Calculate orientation: object facing towards camera (origin) - yaw = yaw_towards_point(robot_pose.position) - euler = Vector3(0.0, 0.0, yaw) # Only yaw, no roll/pitch - robot_pose.orientation = euler_to_quaternion(euler) - - # Estimate object size in meters - size_x = width * z_optical / fx - size_y = height * z_optical / fy - size_z = 0.1 # Default depth size - - # Create Detection3D - detection_3d = Detection3D() - detection_3d.id = "0" - detection_3d.results_length = 1 - detection_3d.header = header - - # Reuse hypothesis from 2D - detection_3d.results = [hypothesis] - - # Create 3D bounding box with robot frame pose - detection_3d.bbox.center = Pose() - detection_3d.bbox.center.position = robot_pose.position - detection_3d.bbox.center.orientation = robot_pose.orientation - detection_3d.bbox.size = Vector3(size_x, size_y, size_z) - - detection3darray = Detection3DArray() - detection3darray.detections_length = 1 - detection3darray.header = header - detection3darray.detections = [detection_3d] - - # Publish transform for tracked object - # The optical pose is in camera optical frame, so publish it relative to the camera frame - tracked_object_tf = Transform( - translation=robot_pose.position, - rotation=robot_pose.orientation, - frame_id=self.frame_id, # Use configured camera frame - child_frame_id="tracked_object", - ts=header.ts, - ) - self.tf.publish(tracked_object_tf) - - # Store latest detections for RPC access - self._latest_detection2d = detection2darray - self._latest_detection3d = detection3darray - - # Signal that new detections are available - if detection2darray.detections_length > 0 or detection3darray.detections_length > 0: - self._detection_event.set() - - # Publish detections - self.detection2darray.publish(detection2darray) - self.detection3darray.publish(detection3darray) - - # Create and publish visualization if tracking is active - if self.tracking_initialized: - # Convert single detection to list for visualization - detections_3d = ( - detection3darray.detections if detection3darray.detections_length > 0 else [] - ) - detections_2d = ( - detection2darray.detections if detection2darray.detections_length > 0 else [] - ) - - if detections_3d and detections_2d: - # Extract 2D bbox for visualization - det_2d = detections_2d[0] - bbox_2d = [] - if det_2d.bbox: - x1 = det_2d.bbox.center.position.x - det_2d.bbox.size_x / 2 - y1 = det_2d.bbox.center.position.y - det_2d.bbox.size_y / 2 - x2 = det_2d.bbox.center.position.x + det_2d.bbox.size_x / 2 - y2 = det_2d.bbox.center.position.y + det_2d.bbox.size_y / 2 - bbox_2d = [[x1, y1, x2, y2]] - - # Use frame directly for visualization - viz_image = frame.copy() - - # Draw bounding boxes - for bbox in bbox_2d: - x1, y1, x2, y2 = map(int, bbox) - cv2.rectangle(viz_image, (x1, y1), (x2, y2), (0, 255, 0), 2) - - # Overlay REID feature matches if available - if self.last_good_matches and self.last_roi_kps and self.last_roi_bbox: - viz_image = self._draw_reid_matches(viz_image) - - # Convert to Image message and publish - viz_msg = Image.from_numpy(viz_image) - self.tracked_overlay.publish(viz_msg) - - def _draw_reid_matches(self, image: NDArray[np.uint8]) -> NDArray[np.uint8]: # type: ignore[type-arg] - """Draw REID feature matches on the image.""" - viz_image: NDArray[np.uint8] = image.copy() # type: ignore[type-arg] - - x1, y1, _x2, _y2 = self.last_roi_bbox # type: ignore[misc] - - # Draw keypoints from current ROI in green - for kp in self.last_roi_kps: # type: ignore[attr-defined] - pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) # type: ignore[has-type] - cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) - - for match in self.last_good_matches: - current_kp = self.last_roi_kps[match.trainIdx] # type: ignore[index] - pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) # type: ignore[has-type] - - # Draw a larger circle for matched points in yellow - cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) # Yellow for matched points - - # Draw match strength indicator (smaller circle with intensity based on distance) - # Lower distance = better match = brighter color - intensity = int(255 * (1.0 - min(match.distance / 100.0, 1.0))) - cv2.circle(viz_image, pt_current, 2, (intensity, intensity, 255), -1) - - text = f"REID Matches: {len(self.last_good_matches)}/{len(self.last_roi_kps) if self.last_roi_kps else 0}" - cv2.putText(viz_image, text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) - - if self.tracking_frame_count < self.reid_warmup_frames: - status_text = ( - f"REID: WARMING UP ({self.tracking_frame_count}/{self.reid_warmup_frames})" - ) - status_color = (255, 255, 0) # Yellow - elif len(self.last_good_matches) >= self.reid_threshold: - status_text = "REID: CONFIRMED" - status_color = (0, 255, 0) # Green - else: - status_text = f"REID: WEAK ({self.reid_fail_count}/{self.reid_fail_tolerance})" - status_color = (0, 165, 255) # Orange - - cv2.putText( - viz_image, status_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, status_color, 2 - ) - - return viz_image - - def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: # type: ignore[type-arg] - """Calculate depth from bbox using the 25th percentile of closest points. - - Args: - bbox: Bounding box coordinates [x1, y1, x2, y2] - depth_frame: Depth frame to extract depth values from - - Returns: - Depth value or None if not available - """ - if depth_frame is None: - return None - - x1, y1, x2, y2 = bbox - - # Ensure bbox is within frame bounds - y1 = max(0, y1) - y2 = min(depth_frame.shape[0], y2) - x1 = max(0, x1) - x2 = min(depth_frame.shape[1], x2) - - # Extract depth values from the entire bbox - roi_depth = depth_frame[y1:y2, x1:x2] - - # Get valid (finite and positive) depth values - valid_depths = roi_depth[np.isfinite(roi_depth) & (roi_depth > 0)] - - if len(valid_depths) > 0: - depth_25th_percentile = float(np.percentile(valid_depths, 25)) - return depth_25th_percentile - - return None - - -object_tracking = ObjectTracking.blueprint - -__all__ = ["ObjectTracking", "object_tracking"] diff --git a/dimos/perception/object_tracker_2d.py b/dimos/perception/object_tracker_2d.py deleted file mode 100644 index 1264b0e92b..0000000000 --- a/dimos/perception/object_tracker_2d.py +++ /dev/null @@ -1,301 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -import logging -import threading -import time - -import cv2 - -# Import LCM messages -from dimos_lcm.vision_msgs import ( - BoundingBox2D, - Detection2D, - ObjectHypothesis, - ObjectHypothesisWithPose, - Point2D, - Pose2D, -) -import numpy as np -from numpy.typing import NDArray -from reactivex.disposable import Disposable - -from dimos.core.core import rpc -from dimos.core.module import Module, ModuleConfig -from dimos.core.stream import In, Out -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.INFO) - - -@dataclass -class ObjectTracker2DConfig(ModuleConfig): - frame_id: str = "camera_link" - - -class ObjectTracker2D(Module[ObjectTracker2DConfig]): - """Pure 2D object tracking module using OpenCV's CSRT tracker.""" - - color_image: In[Image] - - detection2darray: Out[Detection2DArray] - tracked_overlay: Out[Image] # Visualization output - - default_config = ObjectTracker2DConfig - config: ObjectTracker2DConfig - - def __init__(self, **kwargs: object) -> None: - """Initialize 2D object tracking module using OpenCV's CSRT tracker.""" - super().__init__(**kwargs) - - # Tracker state - self.tracker = None - self.tracking_bbox = None # Stores (x, y, w, h) - self.tracking_initialized = False - - # Stuck detection - self._last_bbox = None - self._stuck_count = 0 - self._max_stuck_frames = 10 # Higher threshold for stationary objects - - # Frame management - self._frame_lock = threading.Lock() - self._latest_rgb_frame: np.ndarray | None = None # type: ignore[type-arg] - self._frame_arrival_time: float | None = None - - # Tracking thread control - self.tracking_thread: threading.Thread | None = None - self.stop_tracking_event = threading.Event() - self.tracking_rate = 5.0 # Hz - self.tracking_period = 1.0 / self.tracking_rate - - # Store latest detection for RPC access - self._latest_detection2d: Detection2DArray | None = None - - @rpc - def start(self) -> None: - super().start() - - def on_frame(frame_msg: Image) -> None: - arrival_time = time.perf_counter() - with self._frame_lock: - self._latest_rgb_frame = frame_msg.data - self._frame_arrival_time = arrival_time - - unsub = self.color_image.subscribe(on_frame) - self._disposables.add(Disposable(unsub)) - logger.info("ObjectTracker2D module started") - - @rpc - def stop(self) -> None: - self.stop_track() - if self.tracking_thread and self.tracking_thread.is_alive(): - self.stop_tracking_event.set() - self.tracking_thread.join(timeout=2.0) - - super().stop() - - @rpc - def track(self, bbox: list[float]) -> dict: # type: ignore[type-arg] - """ - Initialize tracking with a bounding box. - - Args: - bbox: Bounding box in format [x1, y1, x2, y2] - - Returns: - Dict containing tracking status - """ - if self._latest_rgb_frame is None: - logger.warning("No RGB frame available for tracking") - return {"status": "no_frame"} - - # Initialize tracking - x1, y1, x2, y2 = map(int, bbox) - w, h = x2 - x1, y2 - y1 - if w <= 0 or h <= 0: - logger.warning(f"Invalid initial bbox provided: {bbox}. Tracking not started.") - return {"status": "invalid_bbox"} - - self.tracking_bbox = (x1, y1, w, h) # type: ignore[assignment] - self.tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] - self.tracking_initialized = False - logger.info(f"Tracking target set with bbox: {self.tracking_bbox}") - - # Convert RGB to BGR for CSRT (OpenCV expects BGR) - frame_bgr = cv2.cvtColor(self._latest_rgb_frame, cv2.COLOR_RGB2BGR) - init_success = self.tracker.init(frame_bgr, self.tracking_bbox) # type: ignore[attr-defined] - if init_success: - self.tracking_initialized = True - logger.info("Tracker initialized successfully.") - else: - logger.error("Tracker initialization failed.") - self.stop_track() - return {"status": "init_failed"} - - # Start tracking thread - self._start_tracking_thread() - - return {"status": "tracking_started", "bbox": self.tracking_bbox} - - def _start_tracking_thread(self) -> None: - """Start the tracking thread.""" - self.stop_tracking_event.clear() - self.tracking_thread = threading.Thread(target=self._tracking_loop, daemon=True) - self.tracking_thread.start() - logger.info("Started tracking thread") - - def _tracking_loop(self) -> None: - """Main tracking loop that runs in a separate thread.""" - while not self.stop_tracking_event.is_set() and self.tracking_initialized: - self._process_tracking() - time.sleep(self.tracking_period) - logger.info("Tracking loop ended") - - def _reset_tracking_state(self) -> None: - """Reset tracking state without stopping the thread.""" - self.tracker = None - self.tracking_bbox = None - self.tracking_initialized = False - self._last_bbox = None - self._stuck_count = 0 - - # Publish empty detection - empty_2d = Detection2DArray( - detections_length=0, header=Header(time.time(), self.frame_id), detections=[] - ) - self._latest_detection2d = empty_2d - self.detection2darray.publish(empty_2d) - - @rpc - def stop_track(self) -> bool: - """ - Stop tracking the current object. - - Returns: - bool: True if tracking was successfully stopped - """ - self._reset_tracking_state() - - # Stop tracking thread if running - if self.tracking_thread and self.tracking_thread.is_alive(): - if threading.current_thread() != self.tracking_thread: - self.stop_tracking_event.set() - self.tracking_thread.join(timeout=1.0) - self.tracking_thread = None - else: - self.stop_tracking_event.set() - - logger.info("Tracking stopped") - return True - - @rpc - def is_tracking(self) -> bool: - """ - Check if the tracker is currently tracking an object. - - Returns: - bool: True if tracking is active - """ - return self.tracking_initialized - - def _process_tracking(self) -> None: - """Process current frame for tracking and publish 2D detections.""" - if self.tracker is None or not self.tracking_initialized: - return - - # Get frame copy - with self._frame_lock: - if self._latest_rgb_frame is None: - return - frame = self._latest_rgb_frame.copy() - - # Convert RGB to BGR for CSRT (OpenCV expects BGR) - frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - - tracker_succeeded, bbox_cv = self.tracker.update(frame_bgr) - - if not tracker_succeeded: - logger.info("Tracker update failed. Stopping track.") - self._reset_tracking_state() - return - - # Extract bbox - x, y, w, h = map(int, bbox_cv) - current_bbox_x1y1x2y2 = [x, y, x + w, y + h] - x1, y1, x2, y2 = current_bbox_x1y1x2y2 - - # Check if tracker is stuck - if self._last_bbox is not None: - if (x1, y1, x2, y2) == self._last_bbox: - self._stuck_count += 1 - if self._stuck_count >= self._max_stuck_frames: - logger.warning(f"Tracker stuck for {self._stuck_count} frames. Stopping track.") - self._reset_tracking_state() - return - else: - self._stuck_count = 0 - - self._last_bbox = (x1, y1, x2, y2) - - center_x = (x1 + x2) / 2.0 - center_y = (y1 + y2) / 2.0 - width = float(x2 - x1) - height = float(y2 - y1) - - # Create 2D detection header - header = Header(time.time(), self.frame_id) - - # Create Detection2D with all fields in constructors - detection_2d = Detection2D( - id="0", - results_length=1, - header=header, - bbox=BoundingBox2D( - center=Pose2D(position=Point2D(x=center_x, y=center_y), theta=0.0), - size_x=width, - size_y=height, - ), - results=[ - ObjectHypothesisWithPose( - hypothesis=ObjectHypothesis(class_id="tracked_object", score=1.0) - ) - ], - ) - - detection2darray = Detection2DArray( - detections_length=1, header=header, detections=[detection_2d] - ) - - # Store and publish - self._latest_detection2d = detection2darray - self.detection2darray.publish(detection2darray) - - # Create visualization - viz_image = self._draw_visualization(frame, current_bbox_x1y1x2y2) - viz_copy = viz_image.copy() # Force copy needed to prevent frame reuse - viz_msg = Image.from_numpy(viz_copy, format=ImageFormat.RGB) - self.tracked_overlay.publish(viz_msg) - - def _draw_visualization(self, image: NDArray[np.uint8], bbox: list[int]) -> NDArray[np.uint8]: # type: ignore[type-arg] - """Draw tracking visualization.""" - viz_image: NDArray[np.uint8] = image.copy() # type: ignore[type-arg] - x1, y1, x2, y2 = bbox - cv2.rectangle(viz_image, (x1, y1), (x2, y2), (0, 255, 0), 2) - cv2.putText(viz_image, "TRACKING", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) - return viz_image diff --git a/dimos/perception/object_tracker_3d.py b/dimos/perception/object_tracker_3d.py deleted file mode 100644 index f8143dc861..0000000000 --- a/dimos/perception/object_tracker_3d.py +++ /dev/null @@ -1,307 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# Import LCM messages -import cv2 -from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.vision_msgs import ( - Detection3D, - ObjectHypothesisWithPose, -) -import numpy as np - -from dimos.core import In, Out, rpc -from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.msgs.vision_msgs import Detection2DArray, Detection3DArray -from dimos.perception.object_tracker_2d import ObjectTracker2D -from dimos.protocol.tf import TF -from dimos.types.timestamped import align_timestamped -from dimos.utils.logging_config import setup_logger -from dimos.utils.transform_utils import ( - euler_to_quaternion, - optical_to_robot_frame, - yaw_towards_point, -) - -logger = setup_logger() - - -class ObjectTracker3D(ObjectTracker2D): - """3D object tracking module extending ObjectTracker2D with depth capabilities.""" - - # Additional inputs (2D tracker already has color_image) - depth: In[Image] - camera_info: In[CameraInfo] - - # Additional outputs (2D tracker already has detection2darray and tracked_overlay) - detection3darray: Out[Detection3DArray] - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - """ - Initialize 3D object tracking module. - - Args: - **kwargs: Arguments passed to parent ObjectTracker2D - """ - super().__init__(**kwargs) - - # Additional state for 3D tracking - self.camera_intrinsics = None - self._latest_depth_frame: np.ndarray | None = None # type: ignore[type-arg] - self._latest_camera_info: CameraInfo | None = None - - # TF publisher for tracked object - self.tf = TF() - - # Store latest 3D detection - self._latest_detection3d: Detection3DArray | None = None - - @rpc - def start(self) -> None: - super().start() - - # Subscribe to aligned RGB and depth streams - def on_aligned_frames(frames_tuple) -> None: # type: ignore[no-untyped-def] - rgb_msg, depth_msg = frames_tuple - with self._frame_lock: - self._latest_rgb_frame = rgb_msg.data - - depth_data = depth_msg.data - # Convert from millimeters to meters if depth is DEPTH16 format - if depth_msg.format == ImageFormat.DEPTH16: - depth_data = depth_data.astype(np.float32) / 1000.0 - self._latest_depth_frame = depth_data - - # Create aligned observable for RGB and depth - aligned_frames = align_timestamped( - self.color_image.observable(), # type: ignore[no-untyped-call] - self.depth.observable(), # type: ignore[no-untyped-call] - buffer_size=2.0, # 2 second buffer - match_tolerance=0.5, # 500ms tolerance - ) - unsub = aligned_frames.subscribe(on_aligned_frames) - self._disposables.add(unsub) - - # Subscribe to camera info - def on_camera_info(camera_info_msg: CameraInfo) -> None: - self._latest_camera_info = camera_info_msg - # Extract intrinsics: K is [fx, 0, cx, 0, fy, cy, 0, 0, 1] - self.camera_intrinsics = [ # type: ignore[assignment] - camera_info_msg.K[0], - camera_info_msg.K[4], - camera_info_msg.K[2], - camera_info_msg.K[5], - ] - - self.camera_info.subscribe(on_camera_info) - - logger.info("ObjectTracker3D module started with aligned frame subscription") - - @rpc - def stop(self) -> None: - super().stop() - - def _process_tracking(self) -> None: - """Override to add 3D detection creation after 2D tracking.""" - # Call parent 2D tracking - super()._process_tracking() - - # Enhance with 3D if we have depth and a valid 2D detection - if ( - self._latest_detection2d - and self._latest_detection2d.detections_length > 0 - and self._latest_depth_frame is not None - and self.camera_intrinsics is not None - ): - detection_3d = self._create_detection3d_from_2d(self._latest_detection2d) - if detection_3d: - self._latest_detection3d = detection_3d - self.detection3darray.publish(detection_3d) - - # Update visualization with 3D info - with self._frame_lock: - if self._latest_rgb_frame is not None: - frame = self._latest_rgb_frame.copy() - - # Extract 2D bbox for visualization - det_2d = self._latest_detection2d.detections[0] - x1 = det_2d.bbox.center.position.x - det_2d.bbox.size_x / 2 - y1 = det_2d.bbox.center.position.y - det_2d.bbox.size_y / 2 - x2 = det_2d.bbox.center.position.x + det_2d.bbox.size_x / 2 - y2 = det_2d.bbox.center.position.y + det_2d.bbox.size_y / 2 - bbox_2d = [[x1, y1, x2, y2]] - - # Use frame directly for visualization - viz_image = frame.copy() - - # Draw bounding boxes - for bbox in bbox_2d: - x1, y1, x2, y2 = map(int, bbox) - cv2.rectangle(viz_image, (x1, y1), (x2, y2), (0, 255, 0), 2) - - # Overlay Re-ID matches - if self.last_good_matches and self.last_roi_kps and self.last_roi_bbox: - viz_image = self._draw_reid_overlay(viz_image) - - viz_msg = Image.from_numpy(viz_image) - self.tracked_overlay.publish(viz_msg) - - def _create_detection3d_from_2d(self, detection2d: Detection2DArray) -> Detection3DArray | None: - """Create 3D detection from 2D detection using depth.""" - if detection2d.detections_length == 0: - return None - - det_2d = detection2d.detections[0] - - # Get bbox center - center_x = det_2d.bbox.center.position.x - center_y = det_2d.bbox.center.position.y - width = det_2d.bbox.size_x - height = det_2d.bbox.size_y - - # Convert to bbox coordinates - x1 = int(center_x - width / 2) - y1 = int(center_y - height / 2) - x2 = int(center_x + width / 2) - y2 = int(center_y + height / 2) - - # Get depth value - depth_value = self._get_depth_from_bbox([x1, y1, x2, y2], self._latest_depth_frame) # type: ignore[arg-type] - - if depth_value is None or depth_value <= 0: - return None - - fx, fy, cx, cy = self.camera_intrinsics # type: ignore[misc] - - # Convert pixel coordinates to 3D in optical frame - z_optical = depth_value - x_optical = (center_x - cx) * z_optical / fx # type: ignore[has-type] - y_optical = (center_y - cy) * z_optical / fy # type: ignore[has-type] - - # Create pose in optical frame - optical_pose = Pose() - optical_pose.position = Vector3(x_optical, y_optical, z_optical) - optical_pose.orientation = Quaternion(0.0, 0.0, 0.0, 1.0) - - # Convert to robot frame - robot_pose = optical_to_robot_frame(optical_pose) - - # Calculate orientation: object facing towards camera - yaw = yaw_towards_point(robot_pose.position) - euler = Vector3(0.0, 0.0, yaw) - robot_pose.orientation = euler_to_quaternion(euler) - - # Estimate object size in meters - size_x = width * z_optical / fx # type: ignore[has-type] - size_y = height * z_optical / fy # type: ignore[has-type] - size_z = 0.1 # Default depth size - - # Create Detection3D - header = Header(self.frame_id) - detection_3d = Detection3D() - detection_3d.id = "0" - detection_3d.results_length = 1 - detection_3d.header = header - - # Create hypothesis - hypothesis = ObjectHypothesisWithPose() - hypothesis.hypothesis.class_id = "tracked_object" - hypothesis.hypothesis.score = 1.0 - detection_3d.results = [hypothesis] - - # Create 3D bounding box - detection_3d.bbox.center = Pose() - detection_3d.bbox.center.position = robot_pose.position - detection_3d.bbox.center.orientation = robot_pose.orientation - detection_3d.bbox.size = Vector3(size_x, size_y, size_z) - - detection3darray = Detection3DArray() - detection3darray.detections_length = 1 - detection3darray.header = header - detection3darray.detections = [detection_3d] - - # Publish TF for tracked object - tracked_object_tf = Transform( - translation=robot_pose.position, - rotation=robot_pose.orientation, - frame_id=self.frame_id, - child_frame_id="tracked_object", - ts=header.ts, - ) - self.tf.publish(tracked_object_tf) - - return detection3darray - - def _get_depth_from_bbox(self, bbox: list[int], depth_frame: np.ndarray) -> float | None: # type: ignore[type-arg] - """ - Calculate depth from bbox using the 25th percentile of closest points. - - Args: - bbox: Bounding box coordinates [x1, y1, x2, y2] - depth_frame: Depth frame to extract depth values from - - Returns: - Depth value or None if not available - """ - if depth_frame is None: - return None - - x1, y1, x2, y2 = bbox - - # Ensure bbox is within frame bounds - y1 = max(0, y1) - y2 = min(depth_frame.shape[0], y2) - x1 = max(0, x1) - x2 = min(depth_frame.shape[1], x2) - - # Extract depth values from the bbox - roi_depth = depth_frame[y1:y2, x1:x2] - - # Get valid (finite and positive) depth values - valid_depths = roi_depth[np.isfinite(roi_depth) & (roi_depth > 0)] - - if len(valid_depths) > 0: - return float(np.percentile(valid_depths, 25)) - - return None - - def _draw_reid_overlay(self, image: np.ndarray) -> np.ndarray: # type: ignore[type-arg] - """Draw Re-ID feature matches on visualization.""" - import cv2 - - viz_image: np.ndarray = image.copy() # type: ignore[type-arg] - x1, y1, _x2, _y2 = self.last_roi_bbox # type: ignore[attr-defined] - - # Draw keypoints - for kp in self.last_roi_kps: # type: ignore[attr-defined] - pt = (int(kp.pt[0] + x1), int(kp.pt[1] + y1)) - cv2.circle(viz_image, pt, 3, (0, 255, 0), -1) - - # Draw matches - for match in self.last_good_matches: # type: ignore[attr-defined] - current_kp = self.last_roi_kps[match.trainIdx] # type: ignore[attr-defined] - pt_current = (int(current_kp.pt[0] + x1), int(current_kp.pt[1] + y1)) - cv2.circle(viz_image, pt_current, 5, (0, 255, 255), 2) - - intensity = int(255 * (1.0 - min(match.distance / 100.0, 1.0))) - cv2.circle(viz_image, pt_current, 2, (intensity, intensity, 255), -1) - - # Draw match count - text = f"REID: {len(self.last_good_matches)}/{len(self.last_roi_kps)}" # type: ignore[attr-defined] - cv2.putText(viz_image, text, (10, 90), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2) - - return viz_image diff --git a/dimos/perception/spatial_perception.py b/dimos/perception/spatial_perception.py deleted file mode 100644 index d7f27c31dc..0000000000 --- a/dimos/perception/spatial_perception.py +++ /dev/null @@ -1,592 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Spatial Memory module for creating a semantic map of the environment. -""" - -from datetime import datetime -import os -import time -from typing import TYPE_CHECKING, Any, Optional -import uuid - -import cv2 -import numpy as np -from reactivex import Observable, interval, operators as ops -from reactivex.disposable import Disposable - -from dimos import spec -from dimos.agents_deprecated.memory.image_embedding import ImageEmbeddingProvider -from dimos.agents_deprecated.memory.spatial_vector_db import SpatialVectorDB -from dimos.agents_deprecated.memory.visual_memory import VisualMemory -from dimos.constants import DIMOS_PROJECT_ROOT -from dimos.core import DimosCluster, In, rpc -from dimos.core.module import Module -from dimos.msgs.sensor_msgs import Image -from dimos.types.robot_location import RobotLocation -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs import Vector3 - -_OUTPUT_DIR = DIMOS_PROJECT_ROOT / "assets" / "output" -_MEMORY_DIR = _OUTPUT_DIR / "memory" -_SPATIAL_MEMORY_DIR = _MEMORY_DIR / "spatial_memory" -_DB_PATH = _SPATIAL_MEMORY_DIR / "chromadb_data" -_VISUAL_MEMORY_PATH = _SPATIAL_MEMORY_DIR / "visual_memory.pkl" - - -logger = setup_logger() - - -class SpatialMemory(Module): - """ - A Dask module for building and querying Robot spatial memory. - - This module processes video frames and odometry data from LCM streams, - associates them with XY locations, and stores them in a vector database - for later retrieval via RPC calls. It also maintains a list of named - robot locations that can be queried by name. - """ - - # LCM inputs - color_image: In[Image] - - def __init__( - self, - collection_name: str = "spatial_memory", - embedding_model: str = "clip", - embedding_dimensions: int = 512, - min_distance_threshold: float = 0.01, # Min distance in meters to store a new frame - min_time_threshold: float = 1.0, # Min time in seconds to record a new frame - db_path: str | None = str(_DB_PATH), # Path for ChromaDB persistence - visual_memory_path: str | None = str( - _VISUAL_MEMORY_PATH - ), # Path for saving/loading visual memory - new_memory: bool = True, # Whether to create a new memory from scratch - output_dir: str | None = str( - _SPATIAL_MEMORY_DIR - ), # Directory for storing visual memory data - chroma_client: Any = None, # Optional ChromaDB client for persistence - visual_memory: Optional[ - "VisualMemory" - ] = None, # Optional VisualMemory instance for storing images - ) -> None: - """ - Initialize the spatial perception system. - - Args: - collection_name: Name of the vector database collection - embedding_model: Model to use for image embeddings ("clip", "resnet", etc.) - embedding_dimensions: Dimensions of the embedding vectors - min_distance_threshold: Minimum distance in meters to record a new frame - min_time_threshold: Minimum time in seconds to record a new frame - chroma_client: Optional ChromaDB client for persistent storage - visual_memory: Optional VisualMemory instance for storing images - output_dir: Directory for storing visual memory data if visual_memory is not provided - """ - self.collection_name = collection_name - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions - self.min_distance_threshold = min_distance_threshold - self.min_time_threshold = min_time_threshold - - # Set up paths for persistence - # Call parent Module init - super().__init__() - - self.db_path = db_path - self.visual_memory_path = visual_memory_path - - # Setup ChromaDB client if not provided - self._chroma_client = chroma_client - if chroma_client is None and db_path is not None: - # Create db directory if needed - os.makedirs(db_path, exist_ok=True) - - # Clean up existing DB if creating new memory - if new_memory and os.path.exists(db_path): - try: - logger.info("Creating new ChromaDB database (new_memory=True)") - # Try to delete any existing database files - import shutil - - for item in os.listdir(db_path): - item_path = os.path.join(db_path, item) - if os.path.isfile(item_path): - os.unlink(item_path) - elif os.path.isdir(item_path): - shutil.rmtree(item_path) - logger.info(f"Removed existing ChromaDB files from {db_path}") - except Exception as e: - logger.error(f"Error clearing ChromaDB directory: {e}") - - import chromadb - from chromadb.config import Settings - - self._chroma_client = chromadb.PersistentClient( - path=db_path, settings=Settings(anonymized_telemetry=False) - ) - - # Initialize or load visual memory - self._visual_memory = visual_memory - if visual_memory is None: - if new_memory or not os.path.exists(visual_memory_path or ""): - logger.info("Creating new visual memory") - self._visual_memory = VisualMemory(output_dir=output_dir) - else: - try: - logger.info(f"Loading existing visual memory from {visual_memory_path}...") - self._visual_memory = VisualMemory.load( - visual_memory_path, # type: ignore[arg-type] - output_dir=output_dir, - ) - logger.info(f"Loaded {self._visual_memory.count()} images from previous runs") - except Exception as e: - logger.error(f"Error loading visual memory: {e}") - self._visual_memory = VisualMemory(output_dir=output_dir) - - self.embedding_provider: ImageEmbeddingProvider = ImageEmbeddingProvider( - model_name=embedding_model, dimensions=embedding_dimensions - ) - - self.vector_db: SpatialVectorDB = SpatialVectorDB( - collection_name=collection_name, - chroma_client=self._chroma_client, - visual_memory=self._visual_memory, - embedding_provider=self.embedding_provider, - ) - - self.last_position: Vector3 | None = None - self.last_record_time: float | None = None - - self.frame_count: int = 0 - self.stored_frame_count: int = 0 - - # List to store robot locations - self.robot_locations: list[RobotLocation] = [] - - # Track latest data for processing - self._latest_video_frame: np.ndarray | None = None # type: ignore[type-arg] - self._process_interval = 1 - - logger.info(f"SpatialMemory initialized with model {embedding_model}") - - @rpc - def start(self) -> None: - super().start() - - # Subscribe to LCM streams - def set_video(image_msg: Image) -> None: - # Convert Image message to numpy array - if hasattr(image_msg, "data"): - frame = image_msg.data - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - self._latest_video_frame = frame - else: - logger.warning("Received image message without data attribute") - - unsub = self.color_image.subscribe(set_video) - self._disposables.add(Disposable(unsub)) - - # Start periodic processing using interval - unsub = interval(self._process_interval).subscribe(lambda _: self._process_frame()) # type: ignore[assignment] - self._disposables.add(unsub) - - @rpc - def stop(self) -> None: - # Save data before shutdown - self.save() - - if self._visual_memory: - self._visual_memory.clear() - - super().stop() - - def _process_frame(self) -> None: - """Process the latest frame with pose data if available.""" - tf = self.tf.get("world", "base_link") - - if tf is None: - return - - if self._latest_video_frame is None: - return - - # Create Pose object with position and orientation - current_pose = tf.to_pose() - - # Process the frame directly - try: - self.frame_count += 1 - - # Check distance constraint - if self.last_position is not None: - distance_moved = np.linalg.norm( - [ - current_pose.position.x - self.last_position.x, - current_pose.position.y - self.last_position.y, - current_pose.position.z - self.last_position.z, - ] - ) - if distance_moved < self.min_distance_threshold: - logger.debug( - f"Position has not moved enough: {distance_moved:.4f}m < {self.min_distance_threshold}m, skipping frame" - ) - return - - # Check time constraint - if self.last_record_time is not None: - time_elapsed = time.time() - self.last_record_time - if time_elapsed < self.min_time_threshold: - logger.debug( - f"Time since last record too short: {time_elapsed:.2f}s < {self.min_time_threshold}s, skipping frame" - ) - return - - current_time = time.time() - - # Get embedding for the frame - frame_embedding = self.embedding_provider.get_embedding(self._latest_video_frame) - - frame_id = f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" - # Get euler angles from quaternion orientation for metadata - euler = tf.rotation.to_euler() - - # Create metadata dictionary with primitive types only - metadata = { - "pos_x": float(current_pose.position.x), - "pos_y": float(current_pose.position.y), - "pos_z": float(current_pose.position.z), - "rot_x": float(euler.x), - "rot_y": float(euler.y), - "rot_z": float(euler.z), - "timestamp": current_time, - "frame_id": frame_id, - } - - # Store in vector database - self.vector_db.add_image_vector( - vector_id=frame_id, - image=self._latest_video_frame, - embedding=frame_embedding, - metadata=metadata, - ) - - # Update tracking variables - self.last_position = current_pose.position - self.last_record_time = current_time - self.stored_frame_count += 1 - - logger.info( - f"Stored frame at position ({current_pose.position.x:.2f}, {current_pose.position.y:.2f}, {current_pose.position.z:.2f}), " - f"rotation ({euler.x:.2f}, {euler.y:.2f}, {euler.z:.2f}) " - f"stored {self.stored_frame_count}/{self.frame_count} frames" - ) - - # Periodically save visual memory to disk - if self._visual_memory is not None and self.visual_memory_path is not None: - if self.stored_frame_count % 100 == 0: - self.save() - - except Exception as e: - logger.error(f"Error processing frame: {e}") - - @rpc - def query_by_location( - self, x: float, y: float, radius: float = 2.0, limit: int = 5 - ) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images near the specified location. - - Args: - x: X coordinate - y: Y coordinate - radius: Search radius in meters - limit: Maximum number of results to return - - Returns: - List of results, each containing the image and its metadata - """ - return self.vector_db.query_by_location(x, y, radius, limit) - - @rpc - def save(self) -> bool: - """ - Save the visual memory component to disk. - - Returns: - True if memory was saved successfully, False otherwise - """ - if self._visual_memory is not None and self.visual_memory_path is not None: - try: - saved_path = self._visual_memory.save(self.visual_memory_path) - logger.info(f"Saved {self._visual_memory.count()} images to {saved_path}") - return True - except Exception as e: - logger.error(f"Failed to save visual memory: {e}") - return False - - def process_stream(self, combined_stream: Observable) -> Observable: # type: ignore[type-arg] - """ - Process a combined stream of video frames and positions. - - This method handles a stream where each item already contains both the frame and position, - such as the stream created by combining video and transform streams with the - with_latest_from operator. - - Args: - combined_stream: Observable stream of dictionaries containing 'frame' and 'position' - - Returns: - Observable of processing results, including the stored frame and its metadata - """ - - def process_combined_data(data): # type: ignore[no-untyped-def] - self.frame_count += 1 - - frame = data.get("frame") - position_vec = data.get("position") # Use .get() for consistency - rotation_vec = data.get("rotation") # Get rotation data if available - - if position_vec is None or rotation_vec is None: - logger.info("No position or rotation data available, skipping frame") - return None - - # position_vec is already a Vector3, no need to recreate it - position_v3 = position_vec - - if self.last_position is not None: - distance_moved = np.linalg.norm( - [ - position_v3.x - self.last_position.x, - position_v3.y - self.last_position.y, - position_v3.z - self.last_position.z, - ] - ) - if distance_moved < self.min_distance_threshold: - logger.debug("Position has not moved, skipping frame") - return None - - if ( - self.last_record_time is not None - and (time.time() - self.last_record_time) < self.min_time_threshold - ): - logger.debug("Time since last record too short, skipping frame") - return None - - current_time = time.time() - - frame_embedding = self.embedding_provider.get_embedding(frame) - - frame_id = f"frame_{datetime.now().strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}" - - # Create metadata dictionary with primitive types only - metadata = { - "pos_x": float(position_v3.x), - "pos_y": float(position_v3.y), - "pos_z": float(position_v3.z), - "rot_x": float(rotation_vec.x), - "rot_y": float(rotation_vec.y), - "rot_z": float(rotation_vec.z), - "timestamp": current_time, - "frame_id": frame_id, - } - - self.vector_db.add_image_vector( - vector_id=frame_id, image=frame, embedding=frame_embedding, metadata=metadata - ) - - self.last_position = position_v3 - self.last_record_time = current_time - self.stored_frame_count += 1 - - logger.info( - f"Stored frame at position ({position_v3.x:.2f}, {position_v3.y:.2f}, {position_v3.z:.2f}), " - f"rotation ({rotation_vec.x:.2f}, {rotation_vec.y:.2f}, {rotation_vec.z:.2f}) " - f"stored {self.stored_frame_count}/{self.frame_count} frames" - ) - - # Create return dictionary with primitive-compatible values - return { - "frame": frame, - "position": (position_v3.x, position_v3.y, position_v3.z), - "rotation": (rotation_vec.x, rotation_vec.y, rotation_vec.z), - "frame_id": frame_id, - "timestamp": current_time, - } - - return combined_stream.pipe( - ops.map(process_combined_data), ops.filter(lambda result: result is not None) - ) - - @rpc - def query_by_image(self, image: np.ndarray, limit: int = 5) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images similar to the provided image. - - Args: - image: Query image - limit: Maximum number of results to return - - Returns: - List of results, each containing the image and its metadata - """ - embedding = self.embedding_provider.get_embedding(image) - return self.vector_db.query_by_embedding(embedding, limit) - - @rpc - def query_by_text(self, text: str, limit: int = 5) -> list[dict]: # type: ignore[type-arg] - """ - Query the vector database for images matching the provided text description. - - This method uses CLIP's text-to-image matching capability to find images - that semantically match the text query (e.g., "where is the kitchen"). - - Args: - text: Text query to search for - limit: Maximum number of results to return - - Returns: - List of results, each containing the image, its metadata, and similarity score - """ - logger.info(f"Querying spatial memory with text: '{text}'") - return self.vector_db.query_by_text(text, limit) - - @rpc - def add_robot_location(self, location: RobotLocation) -> bool: - """ - Add a named robot location to spatial memory. - - Args: - location: The RobotLocation object to add - - Returns: - True if successfully added, False otherwise - """ - try: - # Add to our list of robot locations - self.robot_locations.append(location) - logger.info(f"Added robot location '{location.name}' at position {location.position}") - return True - - except Exception as e: - logger.error(f"Error adding robot location: {e}") - return False - - @rpc - def add_named_location( - self, - name: str, - position: list[float] | None = None, - rotation: list[float] | None = None, - description: str | None = None, - ) -> bool: - """ - Add a named robot location to spatial memory using current or specified position. - - Args: - name: Name of the location - position: Optional position [x, y, z], uses current position if None - rotation: Optional rotation [roll, pitch, yaw], uses current rotation if None - description: Optional description of the location - - Returns: - True if successfully added, False otherwise - """ - tf = self.tf.get("world", "base_link") - if not tf: - logger.error("No position available for robot location") - return False - - # Create RobotLocation object - location = RobotLocation( # type: ignore[call-arg] - name=name, - position=tf.translation, - rotation=tf.rotation.to_euler(), - description=description or f"Location: {name}", - timestamp=time.time(), - ) - - return self.add_robot_location(location) # type: ignore[no-any-return] - - @rpc - def get_robot_locations(self) -> list[RobotLocation]: - """ - Get all stored robot locations. - - Returns: - List of RobotLocation objects - """ - return self.robot_locations - - @rpc - def find_robot_location(self, name: str) -> RobotLocation | None: - """ - Find a robot location by name. - - Args: - name: Name of the location to find - - Returns: - RobotLocation object if found, None otherwise - """ - # Simple search through our list of locations - for location in self.robot_locations: - if location.name.lower() == name.lower(): - return location - - return None - - @rpc - def get_stats(self) -> dict[str, int]: - """Get statistics about the spatial memory module. - - Returns: - Dictionary containing: - - frame_count: Total number of frames processed - - stored_frame_count: Number of frames actually stored - """ - return {"frame_count": self.frame_count, "stored_frame_count": self.stored_frame_count} - - @rpc - def tag_location(self, robot_location: RobotLocation) -> bool: - try: - self.vector_db.tag_location(robot_location) - except Exception: - return False - return True - - @rpc - def query_tagged_location(self, query: str) -> RobotLocation | None: - location, semantic_distance = self.vector_db.query_tagged_location(query) - if semantic_distance < 0.3: - return location - return None - - -def deploy( # type: ignore[no-untyped-def] - dimos: DimosCluster, - camera: spec.Camera, -): - spatial_memory = dimos.deploy(SpatialMemory, db_path="/tmp/spatial_memory_db") # type: ignore[attr-defined] - spatial_memory.color_image.connect(camera.color_image) - spatial_memory.start() - return spatial_memory - - -spatial_memory = SpatialMemory.blueprint - -__all__ = ["SpatialMemory", "deploy", "spatial_memory"] diff --git a/dimos/perception/test_spatial_memory.py b/dimos/perception/test_spatial_memory.py deleted file mode 100644 index d4b188ced3..0000000000 --- a/dimos/perception/test_spatial_memory.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import shutil -import tempfile -import time - -import numpy as np -import pytest -from reactivex import operators as ops - -from dimos.msgs.geometry_msgs import Pose -from dimos.perception.spatial_perception import SpatialMemory -from dimos.stream.video_provider import VideoProvider - - -@pytest.mark.heavy -class TestSpatialMemory: - @pytest.fixture(scope="class") - def temp_dir(self): - # Create a temporary directory for storing spatial memory data - temp_dir = tempfile.mkdtemp() - yield temp_dir - # Clean up - shutil.rmtree(temp_dir) - - @pytest.fixture(scope="class") - def spatial_memory(self, temp_dir): - # Create a single SpatialMemory instance to be reused across all tests - memory = SpatialMemory( - collection_name="test_collection", - embedding_model="clip", - new_memory=True, - db_path=os.path.join(temp_dir, "chroma_db"), - visual_memory_path=os.path.join(temp_dir, "visual_memory.pkl"), - output_dir=os.path.join(temp_dir, "images"), - min_distance_threshold=0.01, - min_time_threshold=0.01, - ) - yield memory - # Clean up - memory.stop() - - def test_spatial_memory_initialization(self, spatial_memory) -> None: - """Test SpatialMemory initializes correctly with CLIP model.""" - # Use the shared spatial_memory fixture - assert spatial_memory is not None - assert spatial_memory.embedding_model == "clip" - assert spatial_memory.embedding_provider is not None - - def test_image_embedding(self, spatial_memory) -> None: - """Test generating image embeddings using CLIP.""" - # Use the shared spatial_memory fixture - # Create a test image - use a simple colored square - test_image = np.zeros((224, 224, 3), dtype=np.uint8) - test_image[50:150, 50:150] = [0, 0, 255] # Blue square - - # Generate embedding - embedding = spatial_memory.embedding_provider.get_embedding(test_image) - - # Check embedding shape and characteristics - assert embedding is not None - assert isinstance(embedding, np.ndarray) - assert embedding.shape[0] == spatial_memory.embedding_dimensions - - # Check that embedding is normalized (unit vector) - assert np.isclose(np.linalg.norm(embedding), 1.0, atol=1e-5) - - # Test text embedding - text_embedding = spatial_memory.embedding_provider.get_text_embedding("a blue square") - assert text_embedding is not None - assert isinstance(text_embedding, np.ndarray) - assert text_embedding.shape[0] == spatial_memory.embedding_dimensions - assert np.isclose(np.linalg.norm(text_embedding), 1.0, atol=1e-5) - - def test_spatial_memory_processing(self, spatial_memory, temp_dir) -> None: - """Test processing video frames and building spatial memory with CLIP embeddings.""" - try: - # Use the shared spatial_memory fixture - memory = spatial_memory - - from dimos.utils.data import get_data - - video_path = get_data("assets") / "trimmed_video_office.mov" - assert os.path.exists(video_path), f"Test video not found: {video_path}" - video_provider = VideoProvider(dev_name="test_video", video_source=video_path) - video_stream = video_provider.capture_video_as_observable(realtime=False, fps=15) - - # Create a frame counter for position generation - frame_counter = 0 - - # Process each video frame directly - def process_frame(frame): - nonlocal frame_counter - - # Generate a unique position for this frame to ensure minimum distance threshold is met - pos = Pose(frame_counter * 0.5, frame_counter * 0.5, 0) - transform = {"position": pos, "timestamp": time.time()} - frame_counter += 1 - - # Create a dictionary with frame, position and rotation for SpatialMemory.process_stream - return { - "frame": frame, - "position": transform["position"], - "rotation": transform["position"], # Using position as rotation for testing - } - - # Create a stream that processes each frame - formatted_stream = video_stream.pipe(ops.map(process_frame)) - - # Process the stream using SpatialMemory's built-in processing - print("Creating spatial memory stream...") - spatial_stream = memory.process_stream(formatted_stream) - - # Stream is now created above using memory.process_stream() - - # Collect results from the stream - results = [] - - frames_processed = 0 - target_frames = 100 # Process more frames for thorough testing - - def on_next(result) -> None: - nonlocal results, frames_processed - if not result: # Skip None results - return - - results.append(result) - frames_processed += 1 - - # Stop processing after target frames - if frames_processed >= target_frames: - subscription.dispose() - - def on_error(error) -> None: - pytest.fail(f"Error in spatial stream: {error}") - - def on_completed() -> None: - pass - - # Subscribe and wait for results - subscription = spatial_stream.subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - # Wait for frames to be processed - timeout = 30.0 # seconds - start_time = time.time() - while frames_processed < target_frames and time.time() - start_time < timeout: - time.sleep(0.5) - - subscription.dispose() - - assert len(results) > 0, "Failed to process any frames with spatial memory" - - relevant_queries = ["office", "room with furniture"] - irrelevant_query = "star wars" - - for query in relevant_queries: - results = memory.query_by_text(query, limit=2) - print(f"\nResults for query: '{query}'") - - assert len(results) > 0, f"No results found for relevant query: {query}" - - similarities = [1 - r.get("distance") for r in results] - print(f"Similarities: {similarities}") - - assert any(d > 0.22 for d in similarities), ( - f"Expected at least one result with similarity > 0.22 for query '{query}'" - ) - - results = memory.query_by_text(irrelevant_query, limit=2) - print(f"\nResults for query: '{irrelevant_query}'") - - if results: - similarities = [1 - r.get("distance") for r in results] - print(f"Similarities: {similarities}") - - assert all(d < 0.25 for d in similarities), ( - f"Expected all results to have similarity < 0.25 for irrelevant query '{irrelevant_query}'" - ) - - except Exception as e: - pytest.fail(f"Error in test: {e}") - finally: - video_provider.dispose_all() - - -if __name__ == "__main__": - pytest.main(["-v", __file__]) diff --git a/dimos/perception/test_spatial_memory_module.py b/dimos/perception/test_spatial_memory_module.py deleted file mode 100644 index 98ec7a1212..0000000000 --- a/dimos/perception/test_spatial_memory_module.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import os -import tempfile -import time - -import pytest -from reactivex import operators as ops - -from dimos import core -from dimos.core import Module, Out, rpc -from dimos.msgs.sensor_msgs import Image -from dimos.perception.spatial_perception import SpatialMemory -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger -from dimos.utils.testing import TimedSensorReplay - -logger = setup_logger() - - -class VideoReplayModule(Module): - """Module that replays video data from TimedSensorReplay.""" - - video_out: Out[Image] - - def __init__(self, video_path: str) -> None: - super().__init__() - self.video_path = video_path - self._subscription = None - - @rpc - def start(self) -> None: - """Start replaying video data.""" - # Use TimedSensorReplay to replay video frames - video_replay = TimedSensorReplay(self.video_path, autocast=Image.from_numpy) - - # Subscribe to the replay stream and publish to LCM - self._subscription = ( - video_replay.stream() - .pipe( - ops.sample(2), # Sample every 2 seconds for resource-constrained systems - ops.take(5), # Only take 5 frames total - ) - .subscribe(self.video_out.publish) - ) - - logger.info("VideoReplayModule started") - - @rpc - def stop(self) -> None: - """Stop replaying video data.""" - if self._subscription: - self._subscription.dispose() - self._subscription = None - logger.info("VideoReplayModule stopped") - - -class OdometryReplayModule(Module): - """Module that replays odometry data from TimedSensorReplay.""" - - odom_out: Out[Odometry] - - def __init__(self, odom_path: str) -> None: - super().__init__() - self.odom_path = odom_path - self._subscription = None - - @rpc - def start(self) -> None: - """Start replaying odometry data.""" - # Use TimedSensorReplay to replay odometry - odom_replay = TimedSensorReplay(self.odom_path, autocast=Odometry.from_msg) - - # Subscribe to the replay stream and publish to LCM - self._subscription = ( - odom_replay.stream() - .pipe( - ops.sample(0.5), # Sample every 500ms - ops.take(10), # Only take 10 odometry updates total - ) - .subscribe(self.odom_out.publish) - ) - - logger.info("OdometryReplayModule started") - - @rpc - def stop(self) -> None: - """Stop replaying odometry data.""" - if self._subscription: - self._subscription.dispose() - self._subscription = None - logger.info("OdometryReplayModule stopped") - - -@pytest.mark.gpu -@pytest.mark.neverending -class TestSpatialMemoryModule: - @pytest.fixture(scope="function") - def temp_dir(self): - """Create a temporary directory for test data.""" - # Use standard tempfile module to ensure proper permissions - temp_dir = tempfile.mkdtemp(prefix="spatial_memory_test_") - - yield temp_dir - - @pytest.mark.asyncio - async def test_spatial_memory_module_with_replay(self, temp_dir): - """Test SpatialMemory module with TimedSensorReplay inputs.""" - - # Start Dask - dimos = core.start(1) - - try: - # Get test data paths - data_path = get_data("unitree_office_walk") - video_path = os.path.join(data_path, "video") - odom_path = os.path.join(data_path, "odom") - - # Deploy modules - # Video replay module - video_module = dimos.deploy(VideoReplayModule, video_path) - video_module.video_out.transport = core.LCMTransport("/test_video", Image) - - # Odometry replay module - odom_module = dimos.deploy(OdometryReplayModule, odom_path) - odom_module.odom_out.transport = core.LCMTransport("/test_odom", Odometry) - - # Spatial memory module - spatial_memory = dimos.deploy( - SpatialMemory, - collection_name="test_spatial_memory", - embedding_model="clip", - embedding_dimensions=512, - min_distance_threshold=0.5, # 0.5m for test - min_time_threshold=1.0, # 1 second - db_path=os.path.join(temp_dir, "chroma_db"), - visual_memory_path=os.path.join(temp_dir, "visual_memory.pkl"), - new_memory=True, - output_dir=os.path.join(temp_dir, "images"), - ) - - # Connect streams - spatial_memory.video.connect(video_module.video_out) - spatial_memory.odom.connect(odom_module.odom_out) - - # Start all modules - video_module.start() - odom_module.start() - spatial_memory.start() - logger.info("All modules started, processing in background...") - - # Wait for frames to be processed with timeout - timeout = 10.0 # 10 second timeout - start_time = time.time() - - # Keep checking stats while modules are running - while (time.time() - start_time) < timeout: - stats = spatial_memory.get_stats() - if stats["frame_count"] > 0 and stats["stored_frame_count"] > 0: - logger.info( - f"Frames processing - Frame count: {stats['frame_count']}, Stored: {stats['stored_frame_count']}" - ) - break - await asyncio.sleep(0.5) - else: - # Timeout reached - stats = spatial_memory.get_stats() - logger.error( - f"Timeout after {timeout}s - Frame count: {stats['frame_count']}, Stored: {stats['stored_frame_count']}" - ) - raise AssertionError(f"No frames processed within {timeout} seconds") - - await asyncio.sleep(2) - - mid_stats = spatial_memory.get_stats() - logger.info( - f"Mid-test stats - Frame count: {mid_stats['frame_count']}, Stored: {mid_stats['stored_frame_count']}" - ) - assert mid_stats["frame_count"] >= stats["frame_count"], ( - "Frame count should increase or stay same" - ) - - # Test query while modules are still running - try: - text_results = spatial_memory.query_by_text("office") - logger.info(f"Query by text 'office' returned {len(text_results)} results") - assert len(text_results) > 0, "Should have at least one result" - except Exception as e: - logger.warning(f"Query by text failed: {e}") - - final_stats = spatial_memory.get_stats() - logger.info( - f"Final stats - Frame count: {final_stats['frame_count']}, Stored: {final_stats['stored_frame_count']}" - ) - - video_module.stop() - odom_module.stop() - logger.info("Stopped replay modules") - - logger.info("All spatial memory module tests passed!") - - finally: - # Cleanup - if "dimos" in locals(): - dimos.close() - - -if __name__ == "__main__": - pytest.main(["-v", "-s", __file__]) - # test = TestSpatialMemoryModule() - # asyncio.run( - # test.test_spatial_memory_module_with_replay(tempfile.mkdtemp(prefix="spatial_memory_test_")) - # ) diff --git a/dimos/protocol/__init__.py b/dimos/protocol/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/protocol/encode/__init__.py b/dimos/protocol/encode/__init__.py deleted file mode 100644 index 87386a09e5..0000000000 --- a/dimos/protocol/encode/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -from abc import ABC, abstractmethod -import json -from typing import Generic, Protocol, TypeVar - -MsgT = TypeVar("MsgT") -EncodingT = TypeVar("EncodingT") - - -class LCMMessage(Protocol): - """Protocol for LCM message types that have encode/decode methods.""" - - def encode(self) -> bytes: - """Encode the message to bytes.""" - ... - - @staticmethod - def decode(data: bytes) -> "LCMMessage": - """Decode bytes to a message instance.""" - ... - - -# TypeVar for LCM message types -LCMMsgT = TypeVar("LCMMsgT", bound=LCMMessage) - - -class Encoder(ABC, Generic[MsgT, EncodingT]): - """Base class for message encoders/decoders.""" - - @staticmethod - @abstractmethod - def encode(msg: MsgT) -> EncodingT: - raise NotImplementedError("Subclasses must implement this method.") - - @staticmethod - @abstractmethod - def decode(data: EncodingT) -> MsgT: - raise NotImplementedError("Subclasses must implement this method.") - - -class JSON(Encoder[MsgT, bytes]): - @staticmethod - def encode(msg: MsgT) -> bytes: - return json.dumps(msg).encode("utf-8") - - @staticmethod - def decode(data: bytes) -> MsgT: - return json.loads(data.decode("utf-8")) # type: ignore[no-any-return] - - -class LCM(Encoder[LCMMsgT, bytes]): - """Encoder for LCM message types.""" - - @staticmethod - def encode(msg: LCMMsgT) -> bytes: - return msg.encode() - - @staticmethod - def decode(data: bytes) -> LCMMsgT: - # Note: This is a generic implementation. In practice, you would need - # to pass the specific message type to decode with. This method would - # typically be overridden in subclasses for specific message types. - raise NotImplementedError( - "LCM.decode requires a specific message type. Use LCMTypedEncoder[MessageType] instead." - ) - - -class LCMTypedEncoder(LCM, Generic[LCMMsgT]): # type: ignore[type-arg] - """Typed LCM encoder for specific message types.""" - - def __init__(self, message_type: type[LCMMsgT]) -> None: - self.message_type = message_type - - @staticmethod - def decode(data: bytes) -> LCMMsgT: - # This is a generic implementation and should be overridden in specific instances - raise NotImplementedError( - "LCMTypedEncoder.decode must be overridden with a specific message type" - ) - - -def create_lcm_typed_encoder(message_type: type[LCMMsgT]) -> type[LCMTypedEncoder[LCMMsgT]]: - """Factory function to create a typed LCM encoder for a specific message type.""" - - class SpecificLCMEncoder(LCMTypedEncoder): # type: ignore[type-arg] - @staticmethod - def decode(data: bytes) -> LCMMsgT: - return message_type.decode(data) # type: ignore[return-value] - - return SpecificLCMEncoder diff --git a/dimos/protocol/mcp/README.md b/dimos/protocol/mcp/README.md deleted file mode 100644 index 233e852669..0000000000 --- a/dimos/protocol/mcp/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# DimOS MCP Server - -Expose DimOS robot skills to Claude Code via Model Context Protocol. - -## Setup - -```bash -uv sync --extra base --extra unitree -``` - -Add to Claude Code (one command): -```bash -claude mcp add --transport stdio dimos --scope project -- python -m dimos.protocol.mcp -``` - - -## Usage - -**Terminal 1** - Start DimOS: -```bash -uv run dimos run unitree-go2-agentic-mcp -``` - -**Claude Code** - Use robot skills: -``` -> move forward 1 meter -> go to the kitchen -> tag this location as "desk" -``` - -## How It Works - -1. `MCPModule` in the blueprint starts a TCP server on port 9990 -2. Claude Code spawns the bridge (`--bridge`) which connects to `localhost:9990` -3. Skills are exposed as MCP tools (e.g., `relative_move`, `navigate_with_text`) diff --git a/dimos/protocol/mcp/__init__.py b/dimos/protocol/mcp/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/protocol/mcp/__main__.py b/dimos/protocol/mcp/__main__.py deleted file mode 100644 index a58e59d367..0000000000 --- a/dimos/protocol/mcp/__main__.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""CLI entry point for Dimensional MCP Bridge. - -Connects Claude Code (or other MCP clients) to a running DimOS agent. - -Usage: - python -m dimos.protocol.mcp # Bridge to running DimOS on default port -""" - -from __future__ import annotations - -import asyncio - -from dimos.protocol.mcp.bridge import main as bridge_main - - -def main() -> None: - """Main entry point - connects to running DimOS via bridge.""" - asyncio.run(bridge_main()) - - -if __name__ == "__main__": - main() diff --git a/dimos/protocol/mcp/bridge.py b/dimos/protocol/mcp/bridge.py deleted file mode 100644 index 0b09997798..0000000000 --- a/dimos/protocol/mcp/bridge.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -"""MCP Bridge - Connects stdio (Claude Code) to TCP (DimOS Agent).""" - -import asyncio -import os -import sys - -DEFAULT_PORT = 9990 - - -async def main() -> None: - port = int(os.environ.get("MCP_PORT", DEFAULT_PORT)) - host = os.environ.get("MCP_HOST", "localhost") - - reader, writer = await asyncio.open_connection(host, port) - sys.stderr.write(f"MCP Bridge connected to {host}:{port}\n") - - async def stdin_to_tcp() -> None: - loop = asyncio.get_event_loop() - while True: - line = await loop.run_in_executor(None, sys.stdin.readline) - if not line: - break - writer.write(line.encode()) - await writer.drain() - - async def tcp_to_stdout() -> None: - while True: - data = await reader.readline() - if not data: - break - sys.stdout.write(data.decode()) - sys.stdout.flush() - - await asyncio.gather(stdin_to_tcp(), tcp_to_stdout()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/dimos/protocol/mcp/mcp.py b/dimos/protocol/mcp/mcp.py deleted file mode 100644 index 78d19c64db..0000000000 --- a/dimos/protocol/mcp/mcp.py +++ /dev/null @@ -1,139 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from __future__ import annotations - -import asyncio -import json -from typing import TYPE_CHECKING, Any - -from dimos.core import Module, rpc -from dimos.core.rpc_client import RpcCall, RPCClient - -if TYPE_CHECKING: - from dimos.core.module import SkillInfo - - -class MCPModule(Module): - _skills: list[SkillInfo] - _rpc_calls: dict[str, RpcCall] - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._skills = [] - self._rpc_calls = {} - self._server: asyncio.AbstractServer | None = None - self._server_future: object | None = None - - @rpc - def start(self) -> None: - super().start() - self._start_server() - - @rpc - def stop(self) -> None: - if self._server: - self._server.close() - loop = self._loop - assert loop is not None - asyncio.run_coroutine_threadsafe(self._server.wait_closed(), loop).result() - self._server = None - if self._server_future and hasattr(self._server_future, "cancel"): - self._server_future.cancel() - super().stop() - - @rpc - def on_system_modules(self, modules: list[RPCClient]) -> None: - assert self.rpc is not None - self._skills = [skill for module in modules for skill in (module.get_skills() or [])] - self._rpc_calls = { - skill.func_name: RpcCall(None, self.rpc, skill.func_name, skill.class_name, []) - for skill in self._skills - } - - def _start_server(self, port: int = 9990) -> None: - async def handle_client(reader, writer) -> None: # type: ignore[no-untyped-def] - while True: - if not (data := await reader.readline()): - break - response = await self._handle_request(json.loads(data.decode())) - writer.write(json.dumps(response).encode() + b"\n") - await writer.drain() - writer.close() - - async def start_server() -> None: - self._server = await asyncio.start_server(handle_client, "0.0.0.0", port) - await self._server.serve_forever() - - loop = self._loop - assert loop is not None - self._server_future = asyncio.run_coroutine_threadsafe(start_server(), loop) - - async def _handle_request(self, request: dict[str, Any]) -> dict[str, Any]: - method = request.get("method", "") - params = request.get("params", {}) or {} - req_id = request.get("id") - if method == "initialize": - init_result = { - "protocolVersion": "2024-11-05", - "capabilities": {"tools": {}}, - "serverInfo": {"name": "dimensional", "version": "1.0.0"}, - } - return {"jsonrpc": "2.0", "id": req_id, "result": init_result} - if method == "tools/list": - tools = [] - for skill in self._skills: - schema = json.loads(skill.args_schema) - tools.append( - { - "name": skill.func_name, - "description": schema.get("description", ""), - "inputSchema": schema, - } - ) - return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": tools}} - if method == "tools/call": - name = params.get("name") - args = params.get("arguments") or {} - if not isinstance(name, str): - return { - "jsonrpc": "2.0", - "id": req_id, - "error": {"code": -32602, "message": "Missing or invalid tool name"}, - } - if not isinstance(args, dict): - args = {} - rpc_call = self._rpc_calls.get(name) - if rpc_call is None: - return { - "jsonrpc": "2.0", - "id": req_id, - "result": {"content": [{"type": "text", "text": "Skill not found"}]}, - } - try: - result = await asyncio.get_event_loop().run_in_executor( - None, lambda: rpc_call(**args) - ) - text = str(result) if result is not None else "Completed" - except Exception as e: - text = f"Error: {e}" - return { - "jsonrpc": "2.0", - "id": req_id, - "result": {"content": [{"type": "text", "text": text}]}, - } - return { - "jsonrpc": "2.0", - "id": req_id, - "error": {"code": -32601, "message": f"Unknown: {method}"}, - } diff --git a/dimos/protocol/mcp/test_mcp_module.py b/dimos/protocol/mcp/test_mcp_module.py deleted file mode 100644 index 050e24f13b..0000000000 --- a/dimos/protocol/mcp/test_mcp_module.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import asyncio -import json -from pathlib import Path -from unittest.mock import MagicMock - -from dimos.core.module import SkillInfo -from dimos.protocol.mcp.mcp import MCPModule - - -def _make_mcp(skills: list[SkillInfo], call_results: dict[str, object]) -> MCPModule: - """Create an MCPModule with pre-populated skills and mock RPC calls.""" - mcp = MCPModule.__new__(MCPModule) - mcp._skills = skills - mcp._rpc_calls = {} - for skill in skills: - mock_call = MagicMock() - if skill.func_name in call_results: - mock_call.return_value = call_results[skill.func_name] - else: - mock_call.return_value = None - mcp._rpc_calls[skill.func_name] = mock_call - return mcp - - -def test_unitree_blueprint_has_mcp() -> None: - contents = Path( - "dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_mcp.py" - ).read_text() - assert "agentic_mcp" in contents - assert "MCPModule.blueprint()" in contents - - -def test_mcp_module_request_flow() -> None: - schema = json.dumps( - { - "type": "object", - "description": "Add two numbers", - "properties": {"x": {"type": "integer"}, "y": {"type": "integer"}}, - "required": ["x", "y"], - } - ) - skills = [SkillInfo(class_name="TestSkills", func_name="add", args_schema=schema)] - - mcp = _make_mcp(skills, {"add": 5}) - - response = asyncio.run(mcp._handle_request({"method": "tools/list", "id": 1})) - assert response["result"]["tools"][0]["name"] == "add" - assert response["result"]["tools"][0]["description"] == "Add two numbers" - - response = asyncio.run( - mcp._handle_request( - { - "method": "tools/call", - "id": 2, - "params": {"name": "add", "arguments": {"x": 2, "y": 3}}, - } - ) - ) - assert response["result"]["content"][0]["text"] == "5" - - -def test_mcp_module_handles_errors() -> None: - schema = json.dumps({"type": "object", "properties": {}}) - skills = [ - SkillInfo(class_name="TestSkills", func_name="ok_skill", args_schema=schema), - SkillInfo(class_name="TestSkills", func_name="fail_skill", args_schema=schema), - ] - - mcp = _make_mcp(skills, {"ok_skill": "done"}) - mcp._rpc_calls["fail_skill"] = MagicMock(side_effect=RuntimeError("boom")) - - # All skills listed - response = asyncio.run(mcp._handle_request({"method": "tools/list", "id": 1})) - tool_names = {tool["name"] for tool in response["result"]["tools"]} - assert "ok_skill" in tool_names - assert "fail_skill" in tool_names - - # Error skill returns error text - response = asyncio.run( - mcp._handle_request( - {"method": "tools/call", "id": 2, "params": {"name": "fail_skill", "arguments": {}}} - ) - ) - assert "Error:" in response["result"]["content"][0]["text"] - assert "boom" in response["result"]["content"][0]["text"] - - # Unknown skill returns not found - response = asyncio.run( - mcp._handle_request( - {"method": "tools/call", "id": 3, "params": {"name": "no_such", "arguments": {}}} - ) - ) - assert "not found" in response["result"]["content"][0]["text"].lower() - - -def test_mcp_module_initialize_and_unknown() -> None: - mcp = _make_mcp([], {}) - - response = asyncio.run(mcp._handle_request({"method": "initialize", "id": 1})) - assert response["result"]["serverInfo"]["name"] == "dimensional" - - response = asyncio.run(mcp._handle_request({"method": "unknown/method", "id": 2})) - assert response["error"]["code"] == -32601 - - -def test_mcp_module_invalid_tool_name() -> None: - mcp = _make_mcp([], {}) - - response = asyncio.run( - mcp._handle_request( - {"method": "tools/call", "id": 1, "params": {"name": 123, "arguments": {}}} - ) - ) - assert response["error"]["code"] == -32602 diff --git a/dimos/protocol/pubsub/__init__.py b/dimos/protocol/pubsub/__init__.py deleted file mode 100644 index 94a58b60de..0000000000 --- a/dimos/protocol/pubsub/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -import dimos.protocol.pubsub.impl.lcmpubsub as lcm -from dimos.protocol.pubsub.impl.memory import Memory -from dimos.protocol.pubsub.spec import PubSub - -__all__ = [ - "Memory", - "PubSub", - "lcm", -] diff --git a/dimos/protocol/pubsub/benchmark/test_benchmark.py b/dimos/protocol/pubsub/benchmark/test_benchmark.py deleted file mode 100644 index 39a4421c35..0000000000 --- a/dimos/protocol/pubsub/benchmark/test_benchmark.py +++ /dev/null @@ -1,176 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -import threading -import time -from typing import Any - -import pytest - -from dimos.protocol.pubsub.benchmark.testdata import testcases -from dimos.protocol.pubsub.benchmark.type import ( - BenchmarkResult, - BenchmarkResults, - Case, - MsgGen, - PubSubContext, -) - -# Message sizes for throughput benchmarking (powers of 2 from 64B to 10MB) -MSG_SIZES = [ - 64, - 256, - 1024, - 4096, - 16384, - 65536, - 262144, - 524288, - 1048576, - 1048576 * 2, - 1048576 * 5, - 1048576 * 10, -] - -# Benchmark duration in seconds -BENCH_DURATION = 1.0 - -# Max messages to send per test (prevents overwhelming slower transports) -MAX_MESSAGES = 5000 - -# Max time to wait for in-flight messages after publishing stops -RECEIVE_TIMEOUT = 1.0 - - -def size_id(size: int) -> str: - """Convert byte size to human-readable string for test IDs.""" - if size >= 1048576: - return f"{size // 1048576}MB" - if size >= 1024: - return f"{size // 1024}KB" - return f"{size}B" - - -def pubsub_id(testcase: Case[Any, Any]) -> str: - """Extract pubsub implementation name from context manager function name.""" - name: str = testcase.pubsub_context.__name__ - # Convert e.g. "lcm_pubsub_channel" -> "LCM", "memory_pubsub_channel" -> "Memory" - prefix = name.replace("_pubsub_channel", "").replace("_", " ") - return prefix.upper() if len(prefix) <= 3 else prefix.title().replace(" ", "") - - -@pytest.fixture(scope="module") -def benchmark_results() -> Generator[BenchmarkResults, None, None]: - """Module-scoped fixture to collect benchmark results.""" - results = BenchmarkResults() - yield results - results.print_summary() - results.print_heatmap() - results.print_bandwidth_heatmap() - results.print_latency_heatmap() - results.print_loss_heatmap() - - -@pytest.mark.tool -@pytest.mark.parametrize("msg_size", MSG_SIZES, ids=[size_id(s) for s in MSG_SIZES]) -@pytest.mark.parametrize("pubsub_context, msggen", testcases, ids=[pubsub_id(t) for t in testcases]) -def test_throughput( - pubsub_context: PubSubContext[Any, Any], - msggen: MsgGen[Any, Any], - msg_size: int, - benchmark_results: BenchmarkResults, -) -> None: - """Measure throughput for publishing and receiving messages over a fixed duration.""" - with pubsub_context() as pubsub: - topic, msg = msggen(msg_size) - received_count = 0 - target_count = [0] # Use list to allow modification after publish loop - lock = threading.Lock() - all_received = threading.Event() - - def callback(message: Any, _topic: Any) -> None: - nonlocal received_count - with lock: - received_count += 1 - if target_count[0] > 0 and received_count >= target_count[0]: - all_received.set() - - # Subscribe - pubsub.subscribe(topic, callback) - - # Warmup: give DDS/ROS time to establish connection - time.sleep(0.1) - - # Set target so callback can signal when all received - target_count[0] = MAX_MESSAGES - - # Publish messages until time limit, max messages, or all received - msgs_sent = 0 - start = time.perf_counter() - end_time = start + BENCH_DURATION - - while time.perf_counter() < end_time and msgs_sent < MAX_MESSAGES: - pubsub.publish(topic, msg) - msgs_sent += 1 - # Check if all already received (fast transports) - if all_received.is_set(): - break - - publish_end = time.perf_counter() - target_count[0] = msgs_sent # Update to actual sent count - - # Check if already done, otherwise wait up to RECEIVE_TIMEOUT - with lock: - if received_count >= msgs_sent: - all_received.set() - - if not all_received.is_set(): - all_received.wait(timeout=RECEIVE_TIMEOUT) - latency_end = time.perf_counter() - - with lock: - final_received = received_count - - # Latency: how long we waited after publishing for messages to arrive - # 0 = all arrived during publishing, 1000ms = hit timeout (loss occurred) - latency = latency_end - publish_end - - # Record result (duration is publish time only for throughput calculation) - # Extract transport name from context manager function name - ctx_name = pubsub_context.__name__ - prefix = ctx_name.replace("_pubsub_channel", "").replace("_", " ") - transport_name = prefix.upper() if len(prefix) <= 3 else prefix.title().replace(" ", "") - result = BenchmarkResult( - transport=transport_name, - duration=publish_end - start, - msgs_sent=msgs_sent, - msgs_received=final_received, - msg_size_bytes=msg_size, - receive_time=latency, - ) - benchmark_results.add(result) - - # Warn if significant message loss (but don't fail - benchmark records the data) - loss_pct = (1 - final_received / msgs_sent) * 100 if msgs_sent > 0 else 0 - if loss_pct > 10: - import warnings - - warnings.warn( - f"{transport_name} {msg_size}B: {loss_pct:.1f}% message loss " - f"({final_received}/{msgs_sent})", - stacklevel=2, - ) diff --git a/dimos/protocol/pubsub/benchmark/testdata.py b/dimos/protocol/pubsub/benchmark/testdata.py deleted file mode 100644 index ad604131e0..0000000000 --- a/dimos/protocol/pubsub/benchmark/testdata.py +++ /dev/null @@ -1,402 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -from contextlib import contextmanager -from dataclasses import dataclass -from typing import TYPE_CHECKING, Any - -import numpy as np - -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat -from dimos.protocol.pubsub.benchmark.type import Case - -try: - import cyclonedds as _cyclonedds # noqa: F401 - - DDS_AVAILABLE = True -except ImportError: - DDS_AVAILABLE = False -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, LCMPubSubBase, Topic as LCMTopic -from dimos.protocol.pubsub.impl.memory import Memory -from dimos.protocol.pubsub.impl.shmpubsub import ( - BytesSharedMemory, - LCMSharedMemory, - PickleSharedMemory, -) - - -def make_data_bytes(size: int) -> bytes: - """Generate random bytes of given size.""" - return bytes(i % 256 for i in range(size)) - - -def make_data_image(size: int) -> Image: - """Generate an RGB Image with approximately `size` bytes of data.""" - raw_data = np.frombuffer(make_data_bytes(size), dtype=np.uint8).reshape(-1) - # Pad to make it divisible by 3 for RGB - padded_size = ((len(raw_data) + 2) // 3) * 3 - padded_data = np.pad(raw_data, (0, padded_size - len(raw_data))) - pixels = len(padded_data) // 3 - # Find reasonable dimensions - height = max(1, int(pixels**0.5)) - width = pixels // height - data = padded_data[: height * width * 3].reshape(height, width, 3) - return Image(data=data, format=ImageFormat.RGB) - - -testcases: list[Case[Any, Any]] = [] - - -@contextmanager -def lcm_pubsub_channel() -> Generator[LCM, None, None]: - lcm_pubsub = LCM(autoconf=True) - lcm_pubsub.start() - yield lcm_pubsub - lcm_pubsub.stop() - - -def lcm_msggen(size: int) -> tuple[LCMTopic, Image]: - topic = LCMTopic(topic="benchmark/lcm", lcm_type=Image) - return (topic, make_data_image(size)) - - -testcases.append( - Case( - pubsub_context=lcm_pubsub_channel, - msg_gen=lcm_msggen, - ) -) - - -@contextmanager -def udp_bytes_pubsub_channel() -> Generator[LCMPubSubBase, None, None]: - """LCM with raw bytes - no encoding overhead.""" - lcm_pubsub = LCMPubSubBase(autoconf=True) - lcm_pubsub.start() - yield lcm_pubsub - lcm_pubsub.stop() - - -def udp_bytes_msggen(size: int) -> tuple[LCMTopic, bytes]: - """Generate raw bytes for LCM transport benchmark.""" - topic = LCMTopic(topic="benchmark/lcm_raw") - return (topic, make_data_bytes(size)) - - -testcases.append( - Case( - pubsub_context=udp_bytes_pubsub_channel, - msg_gen=udp_bytes_msggen, - ) -) - - -@contextmanager -def memory_pubsub_channel() -> Generator[Memory, None, None]: - """Context manager for Memory PubSub implementation.""" - yield Memory() - - -def memory_msggen(size: int) -> tuple[str, Any]: - return ("benchmark/memory", make_data_image(size)) - - -# testcases.append( -# Case( -# pubsub_context=memory_pubsub_channel, -# msg_gen=memory_msggen, -# ) -# ) - - -@contextmanager -def shm_pickle_pubsub_channel() -> Generator[PickleSharedMemory, None, None]: - # 12MB capacity to handle benchmark sizes up to 10MB - shm_pubsub = PickleSharedMemory(prefer="cpu", default_capacity=12 * 1024 * 1024) - shm_pubsub.start() - yield shm_pubsub - shm_pubsub.stop() - - -def shm_msggen(size: int) -> tuple[str, Any]: - """Generate message for SharedMemory pubsub benchmark.""" - return ("benchmark/shm", make_data_image(size)) - - -testcases.append( - Case( - pubsub_context=shm_pickle_pubsub_channel, - msg_gen=shm_msggen, - ) -) - - -@contextmanager -def shm_bytes_pubsub_channel() -> Generator[BytesSharedMemory, None, None]: - """SharedMemory with raw bytes - no pickle overhead.""" - shm_pubsub = BytesSharedMemory(prefer="cpu", default_capacity=12 * 1024 * 1024) - shm_pubsub.start() - yield shm_pubsub - shm_pubsub.stop() - - -def shm_bytes_msggen(size: int) -> tuple[str, bytes]: - """Generate raw bytes for SharedMemory transport benchmark.""" - return ("benchmark/shm_bytes", make_data_bytes(size)) - - -testcases.append( - Case( - pubsub_context=shm_bytes_pubsub_channel, - msg_gen=shm_bytes_msggen, - ) -) - - -@contextmanager -def shm_lcm_pubsub_channel() -> Generator[LCMSharedMemory, None, None]: - """SharedMemory with LCM binary encoding - no pickle overhead.""" - shm_pubsub = LCMSharedMemory(prefer="cpu", default_capacity=12 * 1024 * 1024) - shm_pubsub.start() - yield shm_pubsub - shm_pubsub.stop() - - -testcases.append( - Case( - pubsub_context=shm_lcm_pubsub_channel, - msg_gen=lcm_msggen, # Reuse the LCM message generator - ) -) - -if DDS_AVAILABLE: - from cyclonedds.idl import IdlStruct - from cyclonedds.idl.types import sequence, uint8 - from cyclonedds.qos import Policy, Qos - - from dimos.protocol.pubsub.impl.ddspubsub import ( - DDS, - Topic as DDSTopic, - ) - - @dataclass - class DDSBenchmarkData(IdlStruct): # type: ignore[misc] - """DDS message type for benchmarking with variable-size byte payload.""" - - data: sequence[uint8] # type: ignore[type-arg] - - @contextmanager - def dds_high_throughput_pubsub_channel() -> Generator[DDS, None, None]: - """DDS with high-throughput QoS preset.""" - HIGH_THROUGHPUT_QOS = Qos( - Policy.Reliability.BestEffort, - Policy.History.KeepLast(depth=1), - Policy.Durability.Volatile, - ) - dds_pubsub = DDS(qos=HIGH_THROUGHPUT_QOS) - dds_pubsub.start() - yield dds_pubsub - dds_pubsub.stop() - - @contextmanager - def dds_reliable_pubsub_channel() -> Generator[DDS, None, None]: - """DDS with reliable QoS preset.""" - RELIABLE_QOS = Qos( - Policy.Reliability.Reliable(max_blocking_time=0), - Policy.History.KeepLast(depth=5000), - Policy.Durability.Volatile, - ) - dds_pubsub = DDS(qos=RELIABLE_QOS) - dds_pubsub.start() - yield dds_pubsub - dds_pubsub.stop() - - def dds_msggen(size: int) -> tuple[DDSTopic, DDSBenchmarkData]: - """Generate DDS message for benchmark.""" - topic = DDSTopic(name="benchmark/dds", data_type=DDSBenchmarkData) - return (topic, DDSBenchmarkData(data=list(make_data_bytes(size)))) # type: ignore[arg-type] - - testcases.append( - Case( - pubsub_context=dds_high_throughput_pubsub_channel, - msg_gen=dds_msggen, - ) - ) - - testcases.append( - Case( - pubsub_context=dds_reliable_pubsub_channel, - msg_gen=dds_msggen, - ) - ) - - -try: - from dimos.protocol.pubsub.impl.redispubsub import Redis - - @contextmanager - def redis_pubsub_channel() -> Generator[Redis, None, None]: - redis_pubsub = Redis() - redis_pubsub.start() - yield redis_pubsub - redis_pubsub.stop() - - def redis_msggen(size: int) -> tuple[str, Any]: - # Redis uses JSON serialization, so use a simple dict with base64-encoded data - import base64 - - data = base64.b64encode(make_data_bytes(size)).decode("ascii") - return ("benchmark/redis", {"data": data, "size": size}) - - testcases.append( - Case( - pubsub_context=redis_pubsub_channel, - msg_gen=redis_msggen, - ) - ) - -except (ConnectionError, ImportError): - # either redis is not installed or the server is not running - print("Redis not available") - - -from dimos.protocol.pubsub.impl.rospubsub import ( - ROS_AVAILABLE, - DimosROS, - RawROS, - RawROSTopic, - ROSTopic, -) - -if TYPE_CHECKING: - from numpy.typing import NDArray - -if ROS_AVAILABLE: - from rclpy.qos import ( # type: ignore[no-untyped-call] - QoSDurabilityPolicy, - QoSHistoryPolicy, - QoSProfile, - QoSReliabilityPolicy, - ) - from sensor_msgs.msg import Image as ROSImage # type: ignore[attr-defined,no-untyped-call] - - @contextmanager - def ros_best_effort_pubsub_channel() -> Generator[RawROS, None, None]: - qos = QoSProfile( # type: ignore[no-untyped-call] - reliability=QoSReliabilityPolicy.BEST_EFFORT, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - ros_pubsub = RawROS(node_name="benchmark_ros_best_effort", qos=qos) - ros_pubsub.start() - yield ros_pubsub - ros_pubsub.stop() - - @contextmanager - def ros_reliable_pubsub_channel() -> Generator[RawROS, None, None]: - qos = QoSProfile( # type: ignore[no-untyped-call] - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - ros_pubsub = RawROS(node_name="benchmark_ros_reliable", qos=qos) - ros_pubsub.start() - yield ros_pubsub - ros_pubsub.stop() - - def ros_msggen(size: int) -> tuple[RawROSTopic, ROSImage]: - import numpy as np - - # Create image data - raw_data: NDArray[np.uint8] = np.frombuffer(make_data_bytes(size), dtype=np.uint8) - padded_size = ((len(raw_data) + 2) // 3) * 3 - padded_data: NDArray[np.uint8] = np.pad(raw_data, (0, padded_size - len(raw_data))) - pixels = len(padded_data) // 3 - height = max(1, int(pixels**0.5)) - width = pixels // height - final_data: NDArray[np.uint8] = padded_data[: height * width * 3] - - # Create ROS Image message - msg = ROSImage() # type: ignore[no-untyped-call] - msg.height = height - msg.width = width - msg.encoding = "rgb8" - msg.step = width * 3 - msg.data = bytes(final_data) - - topic = RawROSTopic(topic="/benchmark/ros", ros_type=ROSImage) - return (topic, msg) - - testcases.append( - Case( - pubsub_context=ros_best_effort_pubsub_channel, - msg_gen=ros_msggen, - ) - ) - - testcases.append( - Case( - pubsub_context=ros_reliable_pubsub_channel, - msg_gen=ros_msggen, - ) - ) - - @contextmanager - def dimos_ros_best_effort_pubsub_channel() -> Generator[DimosROS, None, None]: - qos = QoSProfile( # type: ignore[no-untyped-call] - reliability=QoSReliabilityPolicy.BEST_EFFORT, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - ros_pubsub = DimosROS(node_name="benchmark_dimos_ros_best_effort", qos=qos) - ros_pubsub.start() - yield ros_pubsub - ros_pubsub.stop() - - @contextmanager - def dimos_ros_reliable_pubsub_channel() -> Generator[DimosROS, None, None]: - qos = QoSProfile( # type: ignore[no-untyped-call] - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - ros_pubsub = DimosROS(node_name="benchmark_dimos_ros_reliable", qos=qos) - ros_pubsub.start() - yield ros_pubsub - ros_pubsub.stop() - - def dimos_ros_msggen(size: int) -> tuple[ROSTopic, Image]: - topic = ROSTopic(topic="/benchmark/dimos_ros", msg_type=Image) - return (topic, make_data_image(size)) - - # commented to save benchmarking time, - # since reliable and best effort are very similar in performance for local pubsub - # testcases.append( - # Case( - # pubsub_context=dimos_ros_best_effort_pubsub_channel, - # msg_gen=dimos_ros_msggen, - # ) - # ) - - testcases.append( - Case( - pubsub_context=dimos_ros_reliable_pubsub_channel, - msg_gen=dimos_ros_msggen, - ) - ) diff --git a/dimos/protocol/pubsub/benchmark/type.py b/dimos/protocol/pubsub/benchmark/type.py deleted file mode 100644 index a9ef80fe7a..0000000000 --- a/dimos/protocol/pubsub/benchmark/type.py +++ /dev/null @@ -1,273 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Iterator, Sequence -from contextlib import AbstractContextManager -from dataclasses import dataclass, field -from typing import Any, Generic - -from dimos.protocol.pubsub.spec import MsgT, PubSub, TopicT - -MsgGen = Callable[[int], tuple[TopicT, MsgT]] - -PubSubContext = Callable[[], AbstractContextManager[PubSub[TopicT, MsgT]]] - - -@dataclass -class Case(Generic[TopicT, MsgT]): - pubsub_context: PubSubContext[TopicT, MsgT] - msg_gen: MsgGen[TopicT, MsgT] - - def __iter__(self) -> Iterator[PubSubContext[TopicT, MsgT] | MsgGen[TopicT, MsgT]]: - return iter((self.pubsub_context, self.msg_gen)) - - def __len__(self) -> int: - return 2 - - -TestData = Sequence[Case[Any, Any]] - - -def _format_mib(value: float) -> str: - """Format bytes as MiB with intelligent rounding. - - >= 10 MiB: integer (e.g., "42") - 1-10 MiB: 1 decimal (e.g., "2.5") - < 1 MiB: 2 decimals (e.g., "0.07") - """ - mib = value / (1024**2) - if mib >= 10: - return f"{mib:.0f}" - if mib >= 1: - return f"{mib:.1f}" - return f"{mib:.2f}" - - -def _format_iec(value: float, concise: bool = False, decimals: int = 2) -> str: - """Format bytes with IEC units (Ki/Mi/Gi = 1024^1/2/3)""" - k = 1024.0 - units = ["B", "K", "M", "G", "T"] if concise else ["B", "KiB", "MiB", "GiB", "TiB"] - - for unit in units[:-1]: - if abs(value) < k: - return f"{value:.{decimals}f}{unit}" if concise else f"{value:.{decimals}f} {unit}" - value /= k - return f"{value:.{decimals}f}{units[-1]}" if concise else f"{value:.{decimals}f} {units[-1]}" - - -@dataclass -class BenchmarkResult: - transport: str - duration: float # Time spent publishing - msgs_sent: int - msgs_received: int - msg_size_bytes: int - receive_time: float = 0.0 # Time after publishing until all messages received - - @property - def total_time(self) -> float: - """Total time including latency.""" - return self.duration + self.receive_time - - @property - def throughput_msgs(self) -> float: - """Messages per second (including latency).""" - return self.msgs_received / self.total_time if self.total_time > 0 else 0 - - @property - def throughput_bytes(self) -> float: - """Bytes per second (including latency).""" - return ( - (self.msgs_received * self.msg_size_bytes) / self.total_time - if self.total_time > 0 - else 0 - ) - - @property - def loss_pct(self) -> float: - """Message loss percentage.""" - return (1 - self.msgs_received / self.msgs_sent) * 100 if self.msgs_sent > 0 else 0 - - -@dataclass -class BenchmarkResults: - results: list[BenchmarkResult] = field(default_factory=list) - - def add(self, result: BenchmarkResult) -> None: - self.results.append(result) - - def print_summary(self) -> None: - if not self.results: - return - - from rich.console import Console - from rich.table import Table - - console = Console() - - table = Table(title="Benchmark Results") - table.add_column("Transport", style="cyan") - table.add_column("Msg Size", justify="right") - table.add_column("Sent", justify="right") - table.add_column("Recv", justify="right") - table.add_column("Msgs/s", justify="right", style="green") - table.add_column("MiB/s", justify="right", style="green") - table.add_column("Latency", justify="right") - table.add_column("Loss", justify="right") - - for r in sorted(self.results, key=lambda x: (x.transport, x.msg_size_bytes)): - loss_style = "red" if r.loss_pct > 0 else "dim" - recv_style = "yellow" if r.receive_time > 0.1 else "dim" - table.add_row( - r.transport, - _format_iec(r.msg_size_bytes, decimals=0), - f"{r.msgs_sent:,}", - f"{r.msgs_received:,}", - f"{r.throughput_msgs:,.0f}", - _format_mib(r.throughput_bytes), - f"[{recv_style}]{r.receive_time * 1000:.0f}ms[/{recv_style}]", - f"[{loss_style}]{r.loss_pct:.1f}%[/{loss_style}]", - ) - - console.print() - console.print(table) - - def _print_heatmap( - self, - title: str, - value_fn: Callable[[BenchmarkResult], float], - format_fn: Callable[[float], str], - high_is_good: bool = True, - ) -> None: - """Generic heatmap printer.""" - if not self.results: - return - - transports = sorted(set(r.transport for r in self.results)) - sizes = sorted(set(r.msg_size_bytes for r in self.results)) - - # Build matrix - matrix: list[list[float]] = [] - for transport in transports: - row = [] - for size in sizes: - result = next( - ( - r - for r in self.results - if r.transport == transport and r.msg_size_bytes == size - ), - None, - ) - row.append(value_fn(result) if result else 0) - matrix.append(row) - - all_vals = [v for row in matrix for v in row if v > 0] - if not all_vals: - return - min_val, max_val = min(all_vals), max(all_vals) - - # ANSI 256 gradient: red -> orange -> yellow -> green - gradient = [ - 52, - 88, - 124, - 160, - 196, - 202, - 208, - 214, - 220, - 226, - 190, - 154, - 148, - 118, - 82, - 46, - 40, - 34, - ] - if not high_is_good: - gradient = gradient[::-1] - - def val_to_color(v: float) -> int: - if v <= 0 or max_val == min_val: - return 236 - t = (v - min_val) / (max_val - min_val) - return gradient[int(t * (len(gradient) - 1))] - - reset = "\033[0m" - size_labels = [_format_iec(s, concise=True, decimals=0) for s in sizes] - col_w = max(8, max(len(s) for s in size_labels) + 1) - transport_w = max(len(t) for t in transports) + 1 - - print() - print(f"{title:^{transport_w + col_w * len(sizes)}}") - print() - print(" " * transport_w + "".join(f"{s:^{col_w}}" for s in size_labels)) - - # Dark colors that need white text (dark reds) - dark_colors = {52, 88, 124, 160, 236} - - for i, transport in enumerate(transports): - row_str = f"{transport:<{transport_w}}" - for val in matrix[i]: - color = val_to_color(val) - fg = 255 if color in dark_colors else 16 # white on dark, black on bright - cell = format_fn(val) if val > 0 else "-" - row_str += f"\033[48;5;{color}m\033[38;5;{fg}m{cell:^{col_w}}{reset}" - print(row_str) - print() - - def print_heatmap(self) -> None: - """Print msgs/sec heatmap.""" - - def fmt(v: float) -> str: - return f"{v / 1000:.1f}k" if v >= 1000 else f"{v:.0f}" - - self._print_heatmap("Msgs/sec", lambda r: r.throughput_msgs, fmt) - - def print_bandwidth_heatmap(self) -> None: - """Print bandwidth heatmap.""" - - def fmt(v: float) -> str: - return _format_iec(v, concise=True, decimals=1) - - self._print_heatmap("Bandwidth (IEC)", lambda r: r.throughput_bytes, fmt) - - def print_latency_heatmap(self) -> None: - """Print latency heatmap (time waiting for messages after publishing).""" - - def fmt(v: float) -> str: - if v >= 1: - return f"{v:.1f}s" - return f"{v * 1000:.0f}ms" - - self._print_heatmap( - "Latency", - lambda r: r.receive_time, - fmt, - high_is_good=False, - ) - - def print_loss_heatmap(self) -> None: - """Print message loss percentage heatmap.""" - - def fmt(v: float) -> str: - return f"{v:.1f}%" - - self._print_heatmap("Loss %", lambda r: r.loss_pct, fmt, high_is_good=False) diff --git a/dimos/protocol/pubsub/bridge.py b/dimos/protocol/pubsub/bridge.py deleted file mode 100644 index f312caed7b..0000000000 --- a/dimos/protocol/pubsub/bridge.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Bridge utilities for connecting pubsub systems.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING, Generic, Protocol, TypeVar - -from dimos.protocol.service.spec import Service - -if TYPE_CHECKING: - from collections.abc import Callable - - from dimos.protocol.pubsub.spec import AllPubSub, PubSub - - -TopicT = TypeVar("TopicT") -MsgT = TypeVar("MsgT") -TopicFrom = TypeVar("TopicFrom") -TopicTo = TypeVar("TopicTo") -MsgFrom = TypeVar("MsgFrom") -MsgTo = TypeVar("MsgTo") - - -class Translator(Protocol[TopicFrom, TopicTo, MsgFrom, MsgTo]): # type: ignore[misc] - """Protocol for translating topics and messages between pubsub systems.""" - - def topic(self, topic: TopicFrom) -> TopicTo: - """Translate a topic from source to destination format.""" - ... - - def msg(self, msg: MsgFrom) -> MsgTo: - """Translate a message from source to destination format.""" - ... - - -def bridge( - pubsub1: AllPubSub[TopicFrom, MsgFrom], - pubsub2: PubSub[TopicTo, MsgTo], - translator: Translator[TopicFrom, TopicTo, MsgFrom, MsgTo], - # optionally we can override subscribe_all - # and only bridge a specific part of the pubsub tree - topic_from: TopicFrom | None = None, -) -> Callable[[], None]: - def pass_msg(msg: MsgFrom, topic: TopicFrom) -> None: - pubsub2.publish(translator.topic(topic), translator.msg(msg)) - - # Bridge only specific messages from pubsub1 to pubsub2 - if topic_from: - return pubsub1.subscribe(topic_from, pass_msg) - - # Bridge all messages from pubsub1 to pubsub2 - return pubsub1.subscribe_all(pass_msg) - - -@dataclass -class BridgeConfig(Generic[TopicFrom, TopicTo, MsgFrom, MsgTo]): - """Configuration for a one-way bridge.""" - - source: AllPubSub[TopicFrom, MsgFrom] - destination: PubSub[TopicTo, MsgTo] - translator: Translator[TopicFrom, TopicTo, MsgFrom, MsgTo] - subscribe_topic: TopicFrom | None = None - - -class Bridge(Service[BridgeConfig[TopicFrom, TopicTo, MsgFrom, MsgTo]]): - """Service that bridges messages from one pubsub to another.""" - - _unsubscribe: Callable[[], None] | None = None - - def start(self) -> None: - super().start() - self._unsubscribe = bridge( - self.config.source, - self.config.destination, - self.config.translator, - self.config.subscribe_topic, - ) - - def stop(self) -> None: - if self._unsubscribe: - self._unsubscribe() - self._unsubscribe = None - super().stop() diff --git a/dimos/protocol/pubsub/encoders.py b/dimos/protocol/pubsub/encoders.py deleted file mode 100644 index 6b2056fa8b..0000000000 --- a/dimos/protocol/pubsub/encoders.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Encoder mixins for PubSub implementations.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -import pickle -from typing import TYPE_CHECKING, Generic, Protocol, TypeVar, cast - -from dimos.msgs import DimosMsg -from dimos.msgs.sensor_msgs import Image - -if TYPE_CHECKING: - from collections.abc import Callable - -TopicT = TypeVar("TopicT") -MsgT = TypeVar("MsgT") -EncodingT = TypeVar("EncodingT") - - -class DecodingError(Exception): - """Raised by decode() to skip a message without calling the callback.""" - - pass - - -class PubSubEncoderMixin(Generic[TopicT, MsgT, EncodingT], ABC): - """Mixin that encodes messages before publishing and decodes them after receiving. - - This will override publish and subscribe methods to add encoding/decoding. - Must be mixed with a class implementing PubSubProtocol[TopicT, EncodingT]. - - Usage: Just specify encoder and decoder as a subclass: - - class MyPubSubWithJSON(PubSubEncoderMixin, MyPubSub): - def encoder(msg, topic): - json.dumps(msg).encode('utf-8') - def decoder(msg, topic): - data: json.loads(data.decode('utf-8')) - """ - - # Declare expected methods from PubSubProtocol for type checking - if TYPE_CHECKING: - _base_publish: Callable[[TopicT, EncodingT], None] - _base_subscribe: Callable[[TopicT, Callable[[EncodingT, TopicT], None]], Callable[[], None]] - - @abstractmethod - def encode(self, msg: MsgT, topic: TopicT) -> EncodingT: ... - - @abstractmethod - def decode(self, msg: EncodingT, topic: TopicT) -> MsgT: ... - - def publish(self, topic: TopicT, message: MsgT) -> None: - """Encode the message and publish it.""" - encoded_message = self.encode(message, topic) - if encoded_message is None: - return - super().publish(topic, encoded_message) # type: ignore[misc] - - def subscribe( - self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] - ) -> Callable[[], None]: - """Subscribe with automatic decoding.""" - - def wrapper_cb(encoded_data: EncodingT, topic: TopicT) -> None: - try: - decoded_message = self.decode(encoded_data, topic) - except DecodingError: - return - callback(decoded_message, topic) - - return cast("Callable[[], None]", super().subscribe(topic, wrapper_cb)) # type: ignore[misc] - - -class PickleEncoderMixin(PubSubEncoderMixin[TopicT, MsgT, bytes]): - """Encoder mixin that uses pickle for serialization. Works with any Python object.""" - - def encode(self, msg: MsgT, _: TopicT) -> bytes: - return pickle.dumps(msg) - - def decode(self, msg: bytes, _: TopicT) -> MsgT: - return cast("MsgT", pickle.loads(msg)) - - -class LCMTopicProto(Protocol): - """Protocol for topics usable with LCM encoders.""" - - topic: str # At decode time, always concrete string - lcm_type: type[DimosMsg] | None - - -class LCMEncoderMixin(PubSubEncoderMixin[LCMTopicProto, DimosMsg, bytes]): - """Encoder mixin for DimosMsg using LCM binary encoding.""" - - def encode(self, msg: DimosMsg | bytes, _: LCMTopicProto) -> bytes: - if isinstance(msg, bytes): - return msg - return msg.lcm_encode() - - def decode(self, msg: bytes, topic: LCMTopicProto) -> DimosMsg: - if topic.lcm_type is None: - raise DecodingError(f"Cannot decode: topic {topic.topic!r} has no lcm_type") - return topic.lcm_type.lcm_decode(msg) - - -class JpegEncoderMixin(PubSubEncoderMixin[LCMTopicProto, Image, bytes]): - """Encoder mixin for DimosMsg using JPEG encoding (for images).""" - - def encode(self, msg: Image, _: LCMTopicProto) -> bytes: - return msg.lcm_jpeg_encode() - - def decode(self, msg: bytes, topic: LCMTopicProto) -> Image: - if topic.topic == "LCM_SELF_TEST": - raise DecodingError("Ignoring LCM_SELF_TEST topic") - if topic.lcm_type is None: - raise DecodingError(f"Cannot decode: topic {topic.topic!r} has no lcm_type") - return cast("type[Image]", topic.lcm_type).lcm_jpeg_decode(msg) diff --git a/dimos/protocol/pubsub/impl/__init__.py b/dimos/protocol/pubsub/impl/__init__.py deleted file mode 100644 index 63a5bfa6d6..0000000000 --- a/dimos/protocol/pubsub/impl/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from dimos.protocol.pubsub.impl.lcmpubsub import ( - LCM as LCM, - LCMPubSubBase as LCMPubSubBase, - PickleLCM as PickleLCM, -) -from dimos.protocol.pubsub.impl.memory import Memory as Memory diff --git a/dimos/protocol/pubsub/impl/ddspubsub.py b/dimos/protocol/pubsub/impl/ddspubsub.py deleted file mode 100644 index 1e6dc36296..0000000000 --- a/dimos/protocol/pubsub/impl/ddspubsub.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections.abc import Callable -from dataclasses import dataclass -import threading -from typing import TYPE_CHECKING, Any, TypeAlias - -from cyclonedds.core import Listener -from cyclonedds.pub import DataWriter as DDSDataWriter -from cyclonedds.qos import Policy, Qos -from cyclonedds.sub import DataReader as DDSDataReader -from cyclonedds.topic import Topic as DDSTopic - -from dimos.protocol.pubsub.spec import PubSub -from dimos.protocol.service.ddsservice import DDSService -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from cyclonedds.idl import IdlStruct - -logger = setup_logger() - - -@dataclass(frozen=True) -class Topic: - """Represents a DDS topic.""" - - name: str - data_type: type[IdlStruct] - - def __str__(self) -> str: - return f"{self.name}#{self.data_type.__name__}" - - -MessageCallback: TypeAlias = Callable[[Any, Topic], None] - - -class _DDSMessageListener(Listener): # type: ignore[misc] - """Listener for DataReader that dispatches messages to callbacks.""" - - __slots__ = ("_callbacks", "_lock", "_topic") - - def __init__(self, topic: Topic) -> None: - super().__init__() # type: ignore[no-untyped-call] - self._topic = topic - self._callbacks: tuple[MessageCallback, ...] = () - self._lock = threading.Lock() - - def add_callback(self, callback: MessageCallback) -> None: - """Add a callback to the listener.""" - with self._lock: - self._callbacks = (*self._callbacks, callback) - - def remove_callback(self, callback: MessageCallback) -> None: - """Remove a callback from the listener.""" - with self._lock: - self._callbacks = tuple(cb for cb in self._callbacks if cb is not callback) - - def on_data_available(self, reader: DDSDataReader[Any]) -> None: - """Called when data is available on the reader.""" - try: - samples = reader.take() - except Exception as e: - logger.error(f"Error reading from topic {self._topic}: {e}", exc_info=True) - return - for sample in samples: - if sample is not None: - for callback in self._callbacks: - try: - callback(sample, self._topic) - except Exception as e: - logger.error(f"Callback error on topic {self._topic}: {e}", exc_info=True) - - -class DDS(DDSService, PubSub[Topic, Any]): - def __init__(self, qos: Qos | None = None, **kwargs: Any) -> None: - super().__init__(**kwargs) - self._qos = qos - self._writers: dict[Topic, DDSDataWriter[Any]] = {} - self._writer_lock = threading.Lock() - self._readers: dict[Topic, DDSDataReader[Any]] = {} - self._reader_lock = threading.Lock() - self._listeners: dict[Topic, _DDSMessageListener] = {} - - @property - def qos(self) -> Qos | None: - """Get the QoS settings.""" - return self._qos - - def _get_writer(self, topic: Topic) -> DDSDataWriter[Any]: - """Get or create a DataWriter for the given topic.""" - with self._writer_lock: - if topic not in self._writers: - dds_topic = DDSTopic(self.participant, topic.name, topic.data_type) - self._writers[topic] = DDSDataWriter(self.participant, dds_topic, qos=self._qos) - return self._writers[topic] - - def publish(self, topic: Topic, message: Any) -> None: - """Publish a message to a DDS topic.""" - writer = self._get_writer(topic) - try: - writer.write(message) - except Exception as e: - logger.error(f"Error publishing to topic {topic}: {e}", exc_info=True) - - def _get_listener(self, topic: Topic) -> _DDSMessageListener: - """Get or create a listener and reader for the given topic.""" - with self._reader_lock: - if topic not in self._readers: - dds_topic = DDSTopic(self.participant, topic.name, topic.data_type) - listener = _DDSMessageListener(topic) - self._readers[topic] = DDSDataReader( - self.participant, dds_topic, qos=self._qos, listener=listener - ) - self._listeners[topic] = listener - return self._listeners[topic] - - def subscribe(self, topic: Topic, callback: MessageCallback) -> Callable[[], None]: - """Subscribe to a DDS topic with a callback.""" - listener = self._get_listener(topic) - listener.add_callback(callback) - return lambda: self._unsubscribe_callback(topic, callback) - - def _unsubscribe_callback(self, topic: Topic, callback: MessageCallback) -> None: - """Unsubscribe a callback from a topic.""" - with self._reader_lock: - listener = self._listeners.get(topic) - if listener: - listener.remove_callback(callback) - - def stop(self) -> None: - """Stop the DDS service and clean up resources.""" - with self._reader_lock: - self._readers.clear() - self._listeners.clear() - with self._writer_lock: - self._writers.clear() - super().stop() - - -__all__ = [ - "DDS", - "MessageCallback", - "Policy", - "Qos", - "Topic", -] diff --git a/dimos/protocol/pubsub/impl/jpeg_shm.py b/dimos/protocol/pubsub/impl/jpeg_shm.py deleted file mode 100644 index 074f9fb76d..0000000000 --- a/dimos/protocol/pubsub/impl/jpeg_shm.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -from turbojpeg import TurboJPEG # type: ignore[import-untyped] - -from dimos.msgs.sensor_msgs.Image import Image, ImageFormat -from dimos.protocol.pubsub.encoders import PubSubEncoderMixin -from dimos.protocol.pubsub.impl.shmpubsub import SharedMemoryPubSubBase - - -class JpegSharedMemoryEncoderMixin(PubSubEncoderMixin[str, Image, bytes]): - def __init__(self, quality: int = 75, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - self.jpeg = TurboJPEG() - self.quality = quality - - def encode(self, msg: Any, _topic: str) -> bytes: - if not isinstance(msg, Image): - raise ValueError("Can only encode images.") - - bgr_image = msg.to_bgr().to_opencv() - return self.jpeg.encode(bgr_image, quality=self.quality) # type: ignore[no-any-return] - - def decode(self, msg: bytes, _topic: str) -> Image: - bgr_array = self.jpeg.decode(msg) - return Image(data=bgr_array, format=ImageFormat.BGR) - - -class JpegSharedMemory(JpegSharedMemoryEncoderMixin, SharedMemoryPubSubBase): # type: ignore[misc] - pass diff --git a/dimos/protocol/pubsub/impl/lcmpubsub.py b/dimos/protocol/pubsub/impl/lcmpubsub.py deleted file mode 100644 index bf6bbd0dec..0000000000 --- a/dimos/protocol/pubsub/impl/lcmpubsub.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -import re -from typing import TYPE_CHECKING, Any - -from dimos.protocol.pubsub.encoders import ( - JpegEncoderMixin, - LCMEncoderMixin, - PickleEncoderMixin, -) -from dimos.protocol.pubsub.patterns import Glob -from dimos.protocol.pubsub.spec import AllPubSub -from dimos.protocol.service.lcmservice import LCMConfig, LCMService, autoconf -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - import threading - - from dimos.msgs import DimosMsg - -logger = setup_logger() - - -@dataclass -class Topic: - topic: str | re.Pattern[str] | Glob - lcm_type: type[DimosMsg] | None = None - - @property - def is_pattern(self) -> bool: - return isinstance(self.topic, re.Pattern | Glob) - - @property - def pattern(self) -> str: - if isinstance(self.topic, re.Pattern): - return self.topic.pattern - if isinstance(self.topic, Glob): - return self.topic.pattern - return self.topic - - def __str__(self) -> str: - if self.lcm_type is None: - return self.pattern - return f"{self.pattern}#{self.lcm_type.msg_name}" - - @staticmethod - def from_channel_str(channel: str, default_lcm_type: type[DimosMsg] | None = None) -> Topic: - """Create Topic from channel string. - - Channel format: /topic#module.ClassName - Falls back to default_lcm_type if type cannot be parsed. - """ - from dimos.msgs import resolve_msg_type - - if "#" not in channel: - return Topic(topic=channel, lcm_type=default_lcm_type) - - topic_str, type_name = channel.rsplit("#", 1) - lcm_type = resolve_msg_type(type_name) - return Topic(topic=topic_str, lcm_type=lcm_type or default_lcm_type) - - -class LCMPubSubBase(LCMService, AllPubSub[Topic, Any]): - """LCM-based PubSub with native regex subscription support. - - LCM natively supports regex patterns in subscribe(), so we implement - RegexSubscribable directly without needing discovery-based fallback. - """ - - default_config = LCMConfig - _stop_event: threading.Event - _thread: threading.Thread | None - - def publish(self, topic: Topic | str, message: bytes) -> None: - """Publish a message to the specified channel.""" - if self.l is None: - logger.error("Tried to publish after LCM was closed") - return - - topic_str = str(topic) if isinstance(topic, Topic) else topic - self.l.publish(topic_str, message) - - def subscribe_all(self, callback: Callable[[bytes, Topic], Any]) -> Callable[[], None]: - return self.subscribe(Topic(re.compile(".*")), callback) # type: ignore[arg-type] - - def subscribe( - self, topic: Topic, callback: Callable[[bytes, Topic], None] - ) -> Callable[[], None]: - if self.l is None: - logger.error("Tried to subscribe after LCM was closed") - - def noop() -> None: - pass - - return noop - - if topic.is_pattern: - - def handler(channel: str, msg: bytes) -> None: - if channel == "LCM_SELF_TEST": - return - callback(msg, Topic.from_channel_str(channel, topic.lcm_type)) - - pattern_str = str(topic) - if not pattern_str.endswith("*"): - pattern_str = f"{pattern_str}(#.*)?" - - lcm_subscription = self.l.subscribe(pattern_str, handler) - else: - topic_str = str(topic) - lcm_subscription = self.l.subscribe(topic_str, lambda _, msg: callback(msg, topic)) - - # Set queue capacity to 10000 to handle high-volume bursts - lcm_subscription.set_queue_capacity(10000) - - def unsubscribe() -> None: - if self.l is None: - return - self.l.unsubscribe(lcm_subscription) - - return unsubscribe - - -# these ignoress might be unsolvable -# and should use composition not inheritance for encoding/decoding - - -class LCM( # type: ignore[misc] - LCMEncoderMixin, # type: ignore[type-arg] - LCMPubSubBase, -): ... - - -class PickleLCM( # type: ignore[misc] - PickleEncoderMixin, # type: ignore[type-arg] - LCMPubSubBase, -): ... - - -class JpegLCM( # type: ignore[misc] - JpegEncoderMixin, # type: ignore[type-arg] - LCMPubSubBase, -): ... - - -__all__ = [ - "LCM", - "Glob", - "JpegLCM", - "LCMEncoderMixin", - "LCMPubSubBase", - "PickleLCM", - "Topic", - "autoconf", -] diff --git a/dimos/protocol/pubsub/impl/memory.py b/dimos/protocol/pubsub/impl/memory.py deleted file mode 100644 index 3425a5ee3d..0000000000 --- a/dimos/protocol/pubsub/impl/memory.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import defaultdict -from collections.abc import Callable -from typing import Any - -from dimos.protocol import encode -from dimos.protocol.pubsub.encoders import PubSubEncoderMixin -from dimos.protocol.pubsub.spec import PubSub - - -class Memory(PubSub[str, Any]): - def __init__(self) -> None: - self._map: defaultdict[str, list[Callable[[Any, str], None]]] = defaultdict(list) - - def publish(self, topic: str, message: Any) -> None: - for cb in self._map[topic]: - cb(message, topic) - - def subscribe(self, topic: str, callback: Callable[[Any, str], None]) -> Callable[[], None]: - self._map[topic].append(callback) - - def unsubscribe() -> None: - try: - self._map[topic].remove(callback) - if not self._map[topic]: - del self._map[topic] - except (KeyError, ValueError): - pass - - return unsubscribe - - def unsubscribe(self, topic: str, callback: Callable[[Any, str], None]) -> None: - try: - self._map[topic].remove(callback) - if not self._map[topic]: - del self._map[topic] - except (KeyError, ValueError): - pass - - -class MemoryWithJSONEncoder(PubSubEncoderMixin, Memory): # type: ignore[type-arg] - """Memory PubSub with JSON encoding/decoding.""" - - def encode(self, msg: Any, topic: str) -> bytes: - return encode.JSON.encode(msg) - - def decode(self, msg: bytes, topic: str) -> Any: - return encode.JSON.decode(msg) diff --git a/dimos/protocol/pubsub/impl/redispubsub.py b/dimos/protocol/pubsub/impl/redispubsub.py deleted file mode 100644 index 6cc089e953..0000000000 --- a/dimos/protocol/pubsub/impl/redispubsub.py +++ /dev/null @@ -1,198 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import defaultdict -from collections.abc import Callable -from dataclasses import dataclass, field -import json -import threading -import time -from types import TracebackType -from typing import Any - -import redis # type: ignore[import-not-found] - -from dimos.protocol.pubsub.spec import PubSub -from dimos.protocol.service.spec import Service - - -@dataclass -class RedisConfig: - host: str = "localhost" - port: int = 6379 - db: int = 0 - kwargs: dict[str, Any] = field(default_factory=dict) - - -class Redis(PubSub[str, Any], Service[RedisConfig]): - """Redis-based pub/sub implementation.""" - - default_config = RedisConfig - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - # Redis connections - self._client = None - self._pubsub = None - - # Subscription management - self._callbacks: dict[str, list[Callable[[Any, str], None]]] = defaultdict(list) - self._listener_thread = None - self._running = False - - def start(self) -> None: - """Start the Redis pub/sub service.""" - if self._running: - return - self._connect() # type: ignore[no-untyped-call] - - def stop(self) -> None: - """Stop the Redis pub/sub service.""" - self.close() - - def _connect(self): # type: ignore[no-untyped-def] - """Connect to Redis and set up pub/sub.""" - try: - self._client = redis.Redis( - host=self.config.host, - port=self.config.port, - db=self.config.db, - decode_responses=True, - **self.config.kwargs, - ) - # Test connection - self._client.ping() # type: ignore[attr-defined] - - self._pubsub = self._client.pubsub() # type: ignore[attr-defined] - self._running = True - - # Start listener thread - self._listener_thread = threading.Thread(target=self._listen_loop, daemon=True) # type: ignore[assignment] - self._listener_thread.start() # type: ignore[attr-defined] - - except Exception as e: - raise ConnectionError( - f"Failed to connect to Redis at {self.config.host}:{self.config.port}: {e}" - ) - - def _listen_loop(self) -> None: - """Listen for messages from Redis and dispatch to callbacks.""" - while self._running: - try: - if not self._pubsub: - break - message = self._pubsub.get_message(timeout=0.1) - if message and message["type"] == "message": - topic = message["channel"] - data = message["data"] - - # Try to deserialize JSON, fall back to raw data - try: - data = json.loads(data) - except (json.JSONDecodeError, TypeError): - pass - - # Call all callbacks for this topic - for callback in self._callbacks.get(topic, []): - try: - callback(data, topic) - except Exception as e: - # Log error but continue processing other callbacks - print(f"Error in callback for topic {topic}: {e}") - - except Exception as e: - if self._running: # Only log if we're still supposed to be running - print(f"Error in Redis listener loop: {e}") - time.sleep(0.1) # Brief pause before retrying - - def publish(self, topic: str, message: Any) -> None: - """Publish a message to a topic.""" - if not self._client: - raise RuntimeError("Redis client not connected") - - # Serialize message as JSON if it's not a string - if isinstance(message, str): - data = message - else: - data = json.dumps(message) - - self._client.publish(topic, data) - - def subscribe(self, topic: str, callback: Callable[[Any, str], None]) -> Callable[[], None]: - """Subscribe to a topic with a callback.""" - if not self._pubsub: - raise RuntimeError("Redis pubsub not initialized") - - # If this is the first callback for this topic, subscribe to Redis channel - if topic not in self._callbacks or not self._callbacks[topic]: - self._pubsub.subscribe(topic) - - # Add callback to our list - self._callbacks[topic].append(callback) - - # Return unsubscribe function - def unsubscribe() -> None: - self.unsubscribe(topic, callback) - - return unsubscribe - - def unsubscribe(self, topic: str, callback: Callable[[Any, str], None]) -> None: - """Unsubscribe a callback from a topic.""" - if topic in self._callbacks: - try: - self._callbacks[topic].remove(callback) - - # If no more callbacks for this topic, unsubscribe from Redis channel - if not self._callbacks[topic]: - if self._pubsub: - self._pubsub.unsubscribe(topic) - del self._callbacks[topic] - - except ValueError: - pass # Callback wasn't in the list - - def close(self) -> None: - """Close Redis connections and stop listener thread.""" - self._running = False - - if self._listener_thread and self._listener_thread.is_alive(): - self._listener_thread.join(timeout=1.0) - - if self._pubsub: - try: - self._pubsub.close() - except Exception: - pass - self._pubsub = None - - if self._client: - try: - self._client.close() - except Exception: - pass - self._client = None - - self._callbacks.clear() - - def __enter__(self): # type: ignore[no-untyped-def] - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - self.close() diff --git a/dimos/protocol/pubsub/impl/rospubsub.py b/dimos/protocol/pubsub/impl/rospubsub.py deleted file mode 100644 index 1a3c989a4d..0000000000 --- a/dimos/protocol/pubsub/impl/rospubsub.py +++ /dev/null @@ -1,311 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from dataclasses import dataclass -import threading -from typing import Any, Protocol, runtime_checkable - -try: - import rclpy - from rclpy.executors import SingleThreadedExecutor - from rclpy.node import Node - from rclpy.qos import ( - QoSDurabilityPolicy, - QoSHistoryPolicy, - QoSProfile, - QoSReliabilityPolicy, - ) - - ROS_AVAILABLE = True -except ImportError: - ROS_AVAILABLE = False - rclpy = None # type: ignore[assignment] - SingleThreadedExecutor = None # type: ignore[assignment, misc] - Node = None # type: ignore[assignment, misc] - -import uuid - -from dimos.msgs import DimosMsg -from dimos.protocol.pubsub.impl.rospubsub_conversion import ( - derive_ros_type, - dimos_to_ros, - ros_to_dimos, -) -from dimos.protocol.pubsub.spec import PubSub - - -@runtime_checkable -class ROSMessage(Protocol): - """Protocol for ROS message types.""" - - def get_fields_and_field_types(self) -> dict[str, str]: ... - - -@dataclass -class RawROSTopic: - """Topic descriptor for raw ROS pubsub (uses ROS types directly).""" - - topic: str - ros_type: type - qos: "QoSProfile | None" = None - - -@dataclass -class ROSTopic: - """Topic descriptor for DimosROS pubsub (uses dimos message types).""" - - topic: str - msg_type: type[DimosMsg] - qos: "QoSProfile | None" = None - - -class RawROS(PubSub[RawROSTopic, Any]): - """ROS 2 PubSub implementation following the PubSub spec. - - This allows direct comparison of ROS messaging performance against - native LCM and other pubsub implementations. - """ - - def __init__(self, node_name: str | None = None, qos: "QoSProfile | None" = None) -> None: - """Initialize the ROS pubsub. - - Args: - node_name: Name for the ROS node (auto-generated if None) - qos: Optional QoS profile (defaults to BEST_EFFORT for throughput) - """ - if not ROS_AVAILABLE: - raise ImportError("rclpy is not installed. ROS pubsub requires ROS 2.") - - # Use unique node name to avoid conflicts in tests - self._node_name = node_name or f"dimos_ros_{uuid.uuid4().hex[:8]}" - self._node: Node | None = None - self._executor: SingleThreadedExecutor | None = None - self._spin_thread: threading.Thread | None = None - self._stop_event = threading.Event() - - # Track publishers and subscriptions - self._publishers: dict[str, Any] = {} - self._subscriptions: dict[str, list[tuple[Any, Callable[[Any, RawROSTopic], None]]]] = {} - self._lock = threading.Lock() - - # QoS profile - use provided or default to best-effort for throughput - if qos is not None: - self._qos = qos - else: - self._qos = QoSProfile( # type: ignore[no-untyped-call] - # Haven't noticed any difference between BEST_EFFORT and RELIABLE for local comms in our tests - # ./bin/dev python -m pytest -svm tool -k ros dimos/protocol/pubsub/benchmark/test_benchmark.py - # - # but RELIABLE seems to have marginally higher throughput - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_LAST, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - - def start(self) -> None: - """Start the ROS node and executor.""" - if self._spin_thread is not None: - return - - if not rclpy.ok(): # type: ignore[attr-defined] - rclpy.init() - - self._stop_event.clear() - self._node = Node(self._node_name) - self._executor = SingleThreadedExecutor() - self._executor.add_node(self._node) - - self._spin_thread = threading.Thread(target=self._spin, name="ros_pubsub_spin") - self._spin_thread.start() - - def stop(self) -> None: - """Stop the ROS node and clean up.""" - if self._spin_thread is None: - return - - # Signal spin thread to stop and shutdown executor - self._stop_event.set() - if self._executor: - self._executor.shutdown() # This stops spin_once from blocking - - # Wait for spin thread to exit - self._spin_thread.join(timeout=1.0) - - # Grab references while holding lock, then destroy without lock - with self._lock: - subs_to_destroy = [ - sub for topic_subs in self._subscriptions.values() for sub, _ in topic_subs - ] - pubs_to_destroy = list(self._publishers.values()) - self._subscriptions.clear() - self._publishers.clear() - - if self._node: - for subscription in subs_to_destroy: - self._node.destroy_subscription(subscription) - for publisher in pubs_to_destroy: - self._node.destroy_publisher(publisher) - - if self._node: - self._node.destroy_node() # type: ignore[no-untyped-call] - self._node = None - - self._executor = None - self._spin_thread = None - - def _spin(self) -> None: - """Background thread for spinning the ROS executor.""" - while not self._stop_event.is_set(): - executor = self._executor - if executor is None: - break - try: - executor.spin_once(timeout_sec=0.01) - except Exception: - break - - def _get_or_create_publisher(self, topic: RawROSTopic) -> Any: - """Get existing publisher or create a new one.""" - with self._lock: - if topic.topic not in self._publishers: - node = self._node - if node is None: - raise RuntimeError("Pubsub must be started before publishing") - qos = topic.qos if topic.qos is not None else self._qos - self._publishers[topic.topic] = node.create_publisher( - topic.ros_type, topic.topic, qos - ) - return self._publishers[topic.topic] - - def publish(self, topic: RawROSTopic, message: Any) -> None: - """Publish a message to a ROS topic. - - Args: - topic: RawROSTopic descriptor with topic name and message type - message: ROS message to publish - """ - if self._node is None: - return - - publisher = self._get_or_create_publisher(topic) - publisher.publish(message) - - def subscribe( - self, topic: RawROSTopic, callback: Callable[[Any, RawROSTopic], None] - ) -> Callable[[], None]: - """Subscribe to a ROS topic with a callback. - - Args: - topic: RawROSTopic descriptor with topic name and message type - callback: Function called with (message, topic) when message received - - Returns: - Unsubscribe function - """ - if self._node is None: - raise RuntimeError("ROS pubsub not started") - - with self._lock: - - def ros_callback(msg: Any) -> None: - callback(msg, topic) - - qos = topic.qos if topic.qos is not None else self._qos - subscription = self._node.create_subscription( - topic.ros_type, topic.topic, ros_callback, qos - ) - - if topic.topic not in self._subscriptions: - self._subscriptions[topic.topic] = [] - self._subscriptions[topic.topic].append((subscription, callback)) - - def unsubscribe() -> None: - with self._lock: - if topic.topic in self._subscriptions: - self._subscriptions[topic.topic] = [ - (sub, cb) - for sub, cb in self._subscriptions[topic.topic] - if cb is not callback - ] - if self._node: - self._node.destroy_subscription(subscription) - - return unsubscribe - - -class DimosROS(PubSub[ROSTopic, DimosMsg]): - """ROS PubSub with automatic dimos.msgs ↔ ROS message conversion. - - Uses ROSTopic (with dimos msg_type) instead of RawROSTopic (with ros_type). - Automatically converts between dimos and ROS message formats. - Uses composition with RawROS internally. - """ - - def __init__(self, node_name: str | None = None, qos: "QoSProfile | None" = None) -> None: - """Initialize the DimosROS pubsub. - - Args: - node_name: Name for the ROS node (auto-generated if None) - qos: Optional QoS profile (defaults to BEST_EFFORT for throughput) - """ - self._raw = RawROS(node_name, qos) - - def start(self) -> None: - """Start the ROS node and executor.""" - self._raw.start() - - def stop(self) -> None: - """Stop the ROS node and clean up.""" - self._raw.stop() - - def _to_raw_topic(self, topic: ROSTopic) -> RawROSTopic: - """Convert a ROSTopic to a RawROSTopic by deriving the ROS type.""" - ros_type = derive_ros_type(topic.msg_type) - return RawROSTopic(topic=topic.topic, ros_type=ros_type, qos=topic.qos) - - def publish(self, topic: ROSTopic, message: DimosMsg) -> None: - """Publish a dimos message to a ROS topic. - - Args: - topic: ROSTopic with dimos msg_type - message: Dimos message to publish - """ - raw_topic = self._to_raw_topic(topic) - ros_message = dimos_to_ros(message, raw_topic.ros_type) - self._raw.publish(raw_topic, ros_message) - - def subscribe( - self, topic: ROSTopic, callback: Callable[[DimosMsg, ROSTopic], None] - ) -> Callable[[], None]: - """Subscribe to a ROS topic with automatic dimos message conversion. - - Args: - topic: ROSTopic with dimos msg_type - callback: Function called with (dimos_message, topic) - - Returns: - Unsubscribe function - """ - raw_topic = self._to_raw_topic(topic) - - def wrapped_callback(ros_msg: Any, _raw_topic: RawROSTopic) -> None: - dimos_msg = ros_to_dimos(ros_msg, topic.msg_type) - callback(dimos_msg, topic) - - return self._raw.subscribe(raw_topic, wrapped_callback) - - -ROS = DimosROS diff --git a/dimos/protocol/pubsub/impl/rospubsub_conversion.py b/dimos/protocol/pubsub/impl/rospubsub_conversion.py deleted file mode 100644 index 275033a5ac..0000000000 --- a/dimos/protocol/pubsub/impl/rospubsub_conversion.py +++ /dev/null @@ -1,365 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Conversion functions between dimos messages and ROS messages. - -This module provides conversion functions between dimos message types and ROS messages. -It handles three categories of types: - -1. Complex types (different internal representation) - use LCM roundtrip -2. Simple types (field structures match) - use direct field copy -3. No dimos.msgs equivalent - return dimos_lcm type -""" - -from __future__ import annotations - -import importlib -import re -from typing import TYPE_CHECKING, Any, cast - -if TYPE_CHECKING: - from dimos.msgs import DimosMsg - from dimos.protocol.pubsub.impl.rospubsub import ROSMessage - - -# Complex types that need LCM roundtrip (explicit list) -# These types have different internal representations in dimos vs ROS/LCM -COMPLEX_TYPES: set[str] = { - "sensor_msgs.PointCloud2", - "sensor_msgs.Image", - "sensor_msgs.CameraInfo", - "geometry_msgs.PoseStamped", -} - -# Cache for dynamic imports of dimos types -_dimos_type_cache: dict[str, type[DimosMsg] | None] = {} - -# Cache for LCM type derivation -_lcm_type_cache: dict[str, type[Any]] = {} - -# Field name mappings between ROS and LCM (ROS name -> LCM name) -# This is some mixup in dimos_lcm having ROS1 and ROS2 message definitions? -# Would be good to clarify later, but this works for now -_ROS_TO_LCM_FIELD_MAP: dict[str, str] = { - "nanosec": "nsec", # ROS2 Time.nanosec -> LCM Time.nsec -} - -# Reverse mapping (LCM name -> ROS name) -_LCM_TO_ROS_FIELD_MAP: dict[str, str] = {v: k for k, v in _ROS_TO_LCM_FIELD_MAP.items()} - - -def get_dimos_type(msg_name: str) -> type[DimosMsg] | None: - """Try to import dimos.msgs type, return None if not found. Cached. - - Args: - msg_name: Message name in format "package.MessageName" (e.g., "geometry_msgs.Vector3") - - Returns: - The dimos message type, or None if not found - """ - if msg_name in _dimos_type_cache: - return _dimos_type_cache[msg_name] - - try: - package, name = msg_name.split(".") - module = importlib.import_module(f"dimos.msgs.{package}.{name}") - dimos_type = cast("type[DimosMsg]", getattr(module, name)) - _dimos_type_cache[msg_name] = dimos_type - return dimos_type - except (ImportError, AttributeError, ValueError): - _dimos_type_cache[msg_name] = None - return None - - -def derive_lcm_type(dimos_type: type[DimosMsg]) -> type[Any]: - """Derive the LCM message type from a dimos message type. - - Args: - dimos_type: A dimos message type (e.g., dimos.msgs.sensor_msgs.PointCloud2) - - Returns: - The corresponding LCM message type (e.g., dimos_lcm.sensor_msgs.PointCloud2) - """ - msg_name = dimos_type.msg_name # e.g., "sensor_msgs.PointCloud2" - - if msg_name in _lcm_type_cache: - return _lcm_type_cache[msg_name] - - parts = msg_name.split(".") - if len(parts) != 2: - raise ValueError(f"Invalid msg_name format: {msg_name}, expected 'package.MessageName'") - - package, message_name = parts - lcm_module = importlib.import_module(f"dimos_lcm.{package}.{message_name}") - lcm_type: type[Any] = getattr(lcm_module, message_name) - _lcm_type_cache[msg_name] = lcm_type - return lcm_type - - -def derive_ros_type(dimos_type: type[DimosMsg]) -> type[ROSMessage]: - """Derive the ROS message type from a dimos message type. - - Args: - dimos_type: A dimos message type (e.g., dimos.msgs.geometry_msgs.Vector3) - - Returns: - The corresponding ROS message type (e.g., geometry_msgs.msg.Vector3) - - Example: - msg_name = "geometry_msgs.Vector3" -> geometry_msgs.msg.Vector3 - """ - msg_name = dimos_type.msg_name # e.g., "geometry_msgs.Vector3" - parts = msg_name.split(".") - if len(parts) != 2: - raise ValueError(f"Invalid msg_name format: {msg_name}, expected 'package.MessageName'") - - package, message_name = parts - ros_module = importlib.import_module(f"{package}.msg") - return cast("type[ROSMessage]", getattr(ros_module, message_name)) - - -def _copy_ros_to_lcm_recursive(ros_msg: Any, lcm_msg: Any) -> None: - """Recursively copy fields from ROS message to LCM message. - - Handles nested messages, arrays, and primitive types. - - Args: - ros_msg: Source ROS message - lcm_msg: Target LCM message (modified in place) - """ - if not hasattr(ros_msg, "get_fields_and_field_types"): - raise TypeError(f"Expected ROS message, got {type(ros_msg).__name__}") - - field_types = ros_msg.get_fields_and_field_types() - for ros_field_name in field_types: - # Map ROS field name to LCM field name - lcm_field_name = _ROS_TO_LCM_FIELD_MAP.get(ros_field_name, ros_field_name) - - if not hasattr(lcm_msg, lcm_field_name): - continue - - ros_value = getattr(ros_msg, ros_field_name) - lcm_value = getattr(lcm_msg, lcm_field_name) - - # Handle nested messages - if hasattr(ros_value, "get_fields_and_field_types"): - _copy_ros_to_lcm_recursive(ros_value, lcm_value) - # Handle arrays of messages - elif isinstance(ros_value, (list, tuple)) and len(ros_value) > 0: - if hasattr(ros_value[0], "get_fields_and_field_types"): - # Array of nested messages - create LCM instances - lcm_array = [] - for ros_item in ros_value: - # Get the LCM element type from the first lcm_value element if available - # Otherwise try to derive from ros item - if isinstance(lcm_value, list) and len(lcm_value) > 0: - lcm_item = type(lcm_value[0])() - else: - # Try to create matching LCM type - lcm_item = _create_lcm_instance_for_ros_msg(ros_item) - _copy_ros_to_lcm_recursive(ros_item, lcm_item) - lcm_array.append(lcm_item) - setattr(lcm_msg, lcm_field_name, lcm_array) - else: - # Array of primitives - direct copy - setattr(lcm_msg, lcm_field_name, list(ros_value)) - # Handle bytes/data fields - elif isinstance(ros_value, (bytes, bytearray)): - setattr(lcm_msg, lcm_field_name, bytes(ros_value)) - # Handle array.array (ROS uses this for data fields) - elif hasattr(ros_value, "tobytes"): - setattr(lcm_msg, lcm_field_name, ros_value.tobytes()) - else: - # Primitive type - direct copy - setattr(lcm_msg, lcm_field_name, ros_value) - - # Update length fields if present (LCM convention: field_name_length) - length_field = f"{lcm_field_name}_length" - if hasattr(lcm_msg, length_field): - value = getattr(lcm_msg, lcm_field_name) - if isinstance(value, (list, tuple, bytes, bytearray)): - setattr(lcm_msg, length_field, len(value)) - - -def _copy_lcm_to_ros_recursive(lcm_msg: Any, ros_msg: Any) -> None: - """Recursively copy fields from LCM message to ROS message. - - Handles nested messages, arrays, and primitive types. - - Args: - lcm_msg: Source LCM message - ros_msg: Target ROS message (modified in place) - """ - if not hasattr(ros_msg, "get_fields_and_field_types"): - raise TypeError(f"Expected ROS message, got {type(ros_msg).__name__}") - - field_types = ros_msg.get_fields_and_field_types() - for ros_field_name in field_types: - # Map ROS field name to LCM field name - lcm_field_name = _ROS_TO_LCM_FIELD_MAP.get(ros_field_name, ros_field_name) - - if not hasattr(lcm_msg, lcm_field_name): - continue - - lcm_value = getattr(lcm_msg, lcm_field_name) - ros_value = getattr(ros_msg, ros_field_name) - - # Handle nested messages - if hasattr(ros_value, "get_fields_and_field_types"): - _copy_lcm_to_ros_recursive(lcm_value, ros_value) - # Handle arrays of messages - elif isinstance(lcm_value, (list, tuple)) and len(lcm_value) > 0: - if hasattr(lcm_value[0], "lcm_encode"): - # Array of nested LCM messages - ros_array = [] - for lcm_item in lcm_value: - ros_item = _create_ros_instance_for_lcm_msg( - lcm_item, field_types[ros_field_name] - ) - _copy_lcm_to_ros_recursive(lcm_item, ros_item) - ros_array.append(ros_item) - setattr(ros_msg, ros_field_name, ros_array) - else: - # Array of primitives - direct copy - setattr(ros_msg, ros_field_name, list(lcm_value)) - # Handle bytes/data fields - elif isinstance(lcm_value, (bytes, bytearray)): - # ROS data fields might expect array.array - if hasattr(ros_value, "frombytes"): - import array - - arr = array.array("B") - arr.frombytes(lcm_value) - setattr(ros_msg, ros_field_name, arr) - else: - setattr(ros_msg, ros_field_name, bytes(lcm_value)) - else: - # Primitive type - direct copy - setattr(ros_msg, ros_field_name, lcm_value) - - -def _create_lcm_instance_for_ros_msg(ros_msg: Any) -> Any: - """Create an LCM message instance that matches the ROS message type. - - Args: - ros_msg: ROS message to match - - Returns: - New LCM message instance - """ - # Get the ROS type name (e.g., "std_msgs.msg.Header" -> "std_msgs.Header") - ros_type = type(ros_msg) - module_name = ros_type.__module__ # e.g., "std_msgs.msg" - class_name = ros_type.__name__ # e.g., "Header" - - # Convert to LCM module path (std_msgs.msg.Header -> dimos_lcm.std_msgs.Header) - package = module_name.split(".")[0] # e.g., "std_msgs" - lcm_module = importlib.import_module(f"dimos_lcm.{package}.{class_name}") - lcm_type = getattr(lcm_module, class_name) - return lcm_type() - - -def _create_ros_instance_for_lcm_msg(lcm_msg: Any, ros_type_hint: str) -> Any: - """Create a ROS message instance that matches the LCM message type. - - Args: - lcm_msg: LCM message to match - ros_type_hint: ROS type hint string (e.g., "sequence") - - Returns: - New ROS message instance - """ - # Parse the type hint to get the message type - # e.g., "sequence" -> "sensor_msgs", "PointField" - # e.g., "sensor_msgs/PointField" -> "sensor_msgs", "PointField" - - match = re.search(r"(\w+)/(\w+)", ros_type_hint) - if match: - package, class_name = match.groups() - ros_module = importlib.import_module(f"{package}.msg") - ros_type = getattr(ros_module, class_name) - return ros_type() - - # Fallback: try to derive from LCM type - lcm_type = type(lcm_msg) - module_name = lcm_type.__module__ # e.g., "dimos_lcm.std_msgs.Header" - class_name = lcm_type.__name__ - parts = module_name.split(".") - if len(parts) >= 2: - package = parts[1] # e.g., "std_msgs" - ros_module = importlib.import_module(f"{package}.msg") - ros_type = getattr(ros_module, class_name) - return ros_type() - - raise ValueError(f"Cannot determine ROS type for LCM message: {lcm_type}") - - -def dimos_to_ros(msg: DimosMsg, ros_type: type[ROSMessage]) -> ROSMessage: - """Convert a dimos message to a ROS message. - - For complex types (PointCloud2, Image, CameraInfo), uses LCM roundtrip - to properly convert internal representations. For simple types, uses - direct field copy. - - Args: - msg: Dimos message instance - ros_type: Target ROS message type - - Returns: - ROS message instance - """ - msg_name = type(msg).msg_name - - if msg_name in COMPLEX_TYPES: - # Complex: dimos → encode → decode LCM → copy to ROS - lcm_type = derive_lcm_type(type(msg)) - lcm_bytes = msg.lcm_encode() - lcm_msg = lcm_type.lcm_decode(lcm_bytes) - ros_msg = ros_type() - _copy_lcm_to_ros_recursive(lcm_msg, ros_msg) - return ros_msg - - # Simple: recursive field copy (handles nested messages) - ros_msg = ros_type() - _copy_lcm_to_ros_recursive(msg, ros_msg) - return ros_msg - - -def ros_to_dimos(msg: Any, dimos_type: type[DimosMsg]) -> DimosMsg: - """Convert a ROS message to a dimos message. - - For complex types (PointCloud2, Image, CameraInfo), uses LCM roundtrip - to properly build the dimos internal representation. For simple types, - uses direct field copy. - - Args: - msg: ROS message instance - dimos_type: Target dimos message type - - Returns: - Dimos message instance - """ - msg_name = dimos_type.msg_name - - if msg_name in COMPLEX_TYPES: - # Complex: ROS → LCM → encode → decode → dimos - lcm_type = derive_lcm_type(dimos_type) - lcm_msg = lcm_type() - _copy_ros_to_lcm_recursive(msg, lcm_msg) - return dimos_type.lcm_decode(lcm_msg.lcm_encode()) - - # Simple type: recursive field copy (handles nested messages) - dimos_msg = dimos_type() - _copy_ros_to_lcm_recursive(msg, dimos_msg) - return dimos_msg diff --git a/dimos/protocol/pubsub/impl/shmpubsub.py b/dimos/protocol/pubsub/impl/shmpubsub.py deleted file mode 100644 index db0a91e579..0000000000 --- a/dimos/protocol/pubsub/impl/shmpubsub.py +++ /dev/null @@ -1,345 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# --------------------------------------------------------------------------- -# SharedMemory Pub/Sub over unified IPC channels (CPU/CUDA) -# --------------------------------------------------------------------------- - -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass -import hashlib -import os -import struct -import threading -import time -from typing import TYPE_CHECKING, Any -import uuid - -import numpy as np -import numpy.typing as npt - -from dimos.protocol.pubsub.encoders import LCMEncoderMixin, PickleEncoderMixin -from dimos.protocol.pubsub.impl.lcmpubsub import Topic -from dimos.protocol.pubsub.shm.ipc_factory import CpuShmChannel -from dimos.protocol.pubsub.spec import PubSub -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from collections.abc import Callable - -logger = setup_logger() - - -@dataclass -class SharedMemoryConfig: - prefer: str = "auto" # "auto" | "cpu" (DIMOS_IPC_BACKEND overrides), TODO: "cuda" - default_capacity: int = 3686400 # payload bytes (excludes 4-byte header) - close_channels_on_stop: bool = True - - -class SharedMemoryPubSubBase(PubSub[str, Any]): - """ - Pub/Sub over SharedMemory/CUDA-IPC, modeled after LCMPubSubBase but self-contained. - Wire format per topic/frame: [len:uint32_le] + payload bytes (padded to fixed capacity). - Features ported from Service: - - start()/stop() lifecycle - - one frame channel per topic - - per-topic fanout thread (reads from channel, invokes subscribers) - - CPU/CUDA backend selection (auto + env override) - - reconfigure(topic, capacity=...) - - drop initial empty frame; synchronous local delivery; echo suppression - """ - - # Per-topic state - # TODO: implement "is_cuda" below capacity, above cp - class _TopicState: - __slots__ = ( - "capacity", - "channel", - "cp", - "dtype", - "last_local_payload", - "last_seq", - "publish_buffer", - "publish_lock", - "shape", - "stop", - "subs", - "suppress_counts", - "thread", - ) - - def __init__(self, channel, capacity: int, cp_mod) -> None: # type: ignore[no-untyped-def] - self.channel = channel - self.capacity = int(capacity) - self.shape = (self.capacity + 20,) # +20 for header: length(4) + uuid(16) - self.dtype = np.uint8 - self.subs: list[Callable[[bytes, str], None]] = [] - self.stop = threading.Event() - self.thread: threading.Thread | None = None - self.last_seq = 0 # start at 0 to avoid b"" on first poll - # TODO: implement an initializer variable for is_cuda once CUDA IPC is in - self.cp = cp_mod - self.last_local_payload: bytes | None = None - self.suppress_counts: dict[bytes, int] = defaultdict(int) # UUID bytes as key - # Pre-allocated buffer to avoid allocation on every publish - self.publish_buffer: npt.NDArray[np.uint8] = np.zeros(self.shape, dtype=self.dtype) - # Lock for thread-safe publish buffer access - self.publish_lock = threading.Lock() - - # ----- init / lifecycle ------------------------------------------------- - - def __init__( - self, - *, - prefer: str = "auto", - default_capacity: int = 3686400, - close_channels_on_stop: bool = True, - **_: Any, - ) -> None: - super().__init__() - self.config = SharedMemoryConfig( - prefer=prefer, - default_capacity=default_capacity, - close_channels_on_stop=close_channels_on_stop, - ) - self._topics: dict[str, SharedMemoryPubSubBase._TopicState] = {} - self._lock = threading.Lock() - - def start(self) -> None: - pref = (self.config.prefer or "auto").lower() - backend = os.getenv("DIMOS_IPC_BACKEND", pref).lower() - logger.debug(f"SharedMemory PubSub starting (backend={backend})") - # No global thread needed; per-topic fanout starts on first subscribe. - - def stop(self) -> None: - with self._lock: - for _topic, st in list(self._topics.items()): - # stop fanout - try: - if st.thread: - st.stop.set() - st.thread.join(timeout=0.5) - st.thread = None - except Exception: - pass - # close/unlink channels if configured - if self.config.close_channels_on_stop: - try: - st.channel.close() - except Exception: - pass - self._topics.clear() - logger.debug("SharedMemory PubSub stopped.") - - # ----- PubSub API (bytes on the wire) ---------------------------------- - - def publish(self, topic: str, message: bytes) -> None: - if not isinstance(message, bytes | bytearray | memoryview): - raise TypeError(f"publish expects bytes-like, got {type(message)!r}") - - st = self._ensure_topic(topic) - - # Normalize once - payload_bytes = bytes(message) - L = len(payload_bytes) - if L > st.capacity: - logger.error(f"Payload too large: {L} > capacity {st.capacity}") - raise ValueError(f"Payload too large: {L} > capacity {st.capacity}") - - # Create a unique identifier using UUID4 - message_id = uuid.uuid4().bytes # 16 bytes - - # Mark this message to suppress its echo - st.suppress_counts[message_id] += 1 - - # Synchronous local delivery first (zero extra copies) - for cb in list(st.subs): - try: - cb(payload_bytes, topic) - except Exception: - logger.warn(f"Payload couldn't be pushed to topic: {topic}") - pass - - # Build host frame [len:4] + [uuid:16] + payload and publish - # We embed the message UUID in the frame for echo suppression - # Reuse pre-allocated buffer to avoid allocation overhead - # Lock to prevent concurrent threads from corrupting the shared buffer - with st.publish_lock: - host = st.publish_buffer - # Pack: length(4) + uuid(16) + payload - header = struct.pack(" Callable[[], None]: - """Subscribe a callback(message: bytes, topic). Returns unsubscribe.""" - st = self._ensure_topic(topic) - st.subs.append(callback) - if st.thread is None: - st.thread = threading.Thread(target=self._fanout_loop, args=(topic, st), daemon=True) - st.thread.start() - - def _unsub() -> None: - try: - st.subs.remove(callback) - except ValueError: - pass - if not st.subs and st.thread: - st.stop.set() - st.thread.join(timeout=0.5) - st.thread = None - st.stop.clear() - - return _unsub - - # ----- Capacity mgmt ---------------------------------------------------- - - def reconfigure(self, topic: str, *, capacity: int) -> dict: # type: ignore[type-arg] - """Change payload capacity (bytes) for a topic; returns new descriptor.""" - st = self._ensure_topic(topic) - new_cap = int(capacity) - new_shape = (new_cap + 20,) # +20 for header: length(4) + uuid(16) - # Lock to ensure no publish is using the buffer while we replace it - with st.publish_lock: - desc = st.channel.reconfigure(new_shape, np.uint8) - st.capacity = new_cap - st.shape = new_shape - st.dtype = np.uint8 - st.last_seq = -1 - st.publish_buffer = np.zeros(new_shape, dtype=np.uint8) - return desc # type: ignore[no-any-return] - - # ----- Internals -------------------------------------------------------- - - def _ensure_topic(self, topic: str) -> _TopicState: - with self._lock: - st = self._topics.get(topic) - if st is not None: - return st - cap = int(self.config.default_capacity) - - def _names_for_topic(topic: str, capacity: int) -> tuple[str, str]: - # Python's SharedMemory requires names without a leading '/' - # Use shorter digest to avoid macOS shared memory name length limits - h = hashlib.blake2b(f"{topic}:{capacity}".encode(), digest_size=8).hexdigest() - return f"psm_{h}_data", f"psm_{h}_ctrl" - - data_name, ctrl_name = _names_for_topic(topic, cap) - ch = CpuShmChannel((cap + 20,), np.uint8, data_name=data_name, ctrl_name=ctrl_name) - st = SharedMemoryPubSubBase._TopicState(ch, cap, None) - self._topics[topic] = st - return st - - def _fanout_loop(self, topic: str, st: _TopicState) -> None: - while not st.stop.is_set(): - seq, _ts_ns, view = st.channel.read(last_seq=st.last_seq, require_new=True) - if view is None: - time.sleep(0.001) - continue - st.last_seq = seq - - host = np.array(view, copy=True) - - try: - # Read header: length(4) + uuid(16) - L = struct.unpack(" st.capacity + 16: - continue - - # Extract UUID - message_id = host[4:20].tobytes() - - # Extract actual payload (after removing the 16 bytes for uuid) - payload_len = L - 16 - if payload_len > 0: - payload = host[20 : 20 + payload_len].tobytes() - else: - continue - - # Drop exactly the number of local echoes we created - cnt = st.suppress_counts.get(message_id, 0) - if cnt > 0: - if cnt == 1: - del st.suppress_counts[message_id] - else: - st.suppress_counts[message_id] = cnt - 1 - continue # suppressed - - except Exception: - continue - - for cb in list(st.subs): - try: - cb(payload, topic) - except Exception: - pass - - -BytesSharedMemory = SharedMemoryPubSubBase - - -class PickleSharedMemory( - PickleEncoderMixin[str, Any], - SharedMemoryPubSubBase, -): - """SharedMemory pubsub that transports arbitrary Python objects via pickle.""" - - ... - - -class LCMSharedMemoryPubSubBase(PubSub[Topic, Any]): - """SharedMemory pubsub that uses LCM Topic type, delegating to SharedMemoryPubSubBase.""" - - def __init__(self, **kwargs: Any) -> None: - super().__init__() - self._shm = SharedMemoryPubSubBase(**kwargs) - - def start(self) -> None: - self._shm.start() - - def stop(self) -> None: - self._shm.stop() - - def publish(self, topic: Topic, message: bytes) -> None: - self._shm.publish(str(topic), message) - - def subscribe( - self, topic: Topic, callback: Callable[[bytes, Topic], Any] - ) -> Callable[[], None]: - def wrapper(msg: bytes, _: str) -> None: - callback(msg, topic) - - return self._shm.subscribe(str(topic), wrapper) - - def reconfigure(self, topic: Topic, *, capacity: int) -> dict: # type: ignore[type-arg] - return self._shm.reconfigure(str(topic), capacity=capacity) - - -class LCMSharedMemory( # type: ignore[misc] - LCMEncoderMixin, - LCMSharedMemoryPubSubBase, -): - """SharedMemory pubsub that uses LCM binary encoding (no pickle overhead).""" - - ... diff --git a/dimos/protocol/pubsub/impl/test_lcmpubsub.py b/dimos/protocol/pubsub/impl/test_lcmpubsub.py deleted file mode 100644 index 9467e6a4cc..0000000000 --- a/dimos/protocol/pubsub/impl/test_lcmpubsub.py +++ /dev/null @@ -1,196 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -import time -from typing import Any - -import pytest - -from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 -from dimos.protocol.pubsub.impl.lcmpubsub import ( - LCM, - LCMPubSubBase, - PickleLCM, - Topic, -) - - -@pytest.fixture -def lcm_pub_sub_base() -> Generator[LCMPubSubBase, None, None]: - lcm = LCMPubSubBase(autoconf=True) - lcm.start() - yield lcm - lcm.stop() - - -@pytest.fixture -def pickle_lcm() -> Generator[PickleLCM, None, None]: - lcm = PickleLCM(autoconf=True) - lcm.start() - yield lcm - lcm.stop() - - -@pytest.fixture -def lcm() -> Generator[LCM, None, None]: - lcm = LCM(autoconf=True) - lcm.start() - yield lcm - lcm.stop() - - -class MockLCMMessage: - """Mock LCM message for testing""" - - msg_name = "geometry_msgs.Mock" - - def __init__(self, data: Any) -> None: - self.data = data - - def lcm_encode(self) -> bytes: - return str(self.data).encode("utf-8") - - @classmethod - def lcm_decode(cls, data: bytes) -> "MockLCMMessage": - return cls(data.decode("utf-8")) - - def __eq__(self, other: object) -> bool: - return isinstance(other, MockLCMMessage) and self.data == other.data - - -def test_LCMPubSubBase_pubsub(lcm_pub_sub_base: LCMPubSubBase) -> None: - lcm = lcm_pub_sub_base - - received_messages: list[tuple[Any, Any]] = [] - - topic = Topic(topic="/test_topic", lcm_type=MockLCMMessage) - test_message = MockLCMMessage("test_data") - - def callback(msg: Any, topic: Any) -> None: - received_messages.append((msg, topic)) - - lcm.subscribe(topic, callback) - lcm.publish(topic, test_message.lcm_encode()) - time.sleep(0.1) - - assert len(received_messages) == 1 - - received_data = received_messages[0][0] - received_topic = received_messages[0][1] - - print(f"Received data: {received_data}, Topic: {received_topic}") - - assert isinstance(received_data, bytes) - assert received_data.decode() == "test_data" - - assert isinstance(received_topic, Topic) - assert received_topic == topic - - -def test_lcm_autodecoder_pubsub(lcm: LCM) -> None: - received_messages: list[tuple[Any, Any]] = [] - - topic = Topic(topic="/test_topic", lcm_type=MockLCMMessage) - test_message = MockLCMMessage("test_data") - - def callback(msg: Any, topic: Any) -> None: - received_messages.append((msg, topic)) - - lcm.subscribe(topic, callback) - lcm.publish(topic, test_message) - time.sleep(0.1) - - assert len(received_messages) == 1 - - received_data = received_messages[0][0] - received_topic = received_messages[0][1] - - print(f"Received data: {received_data}, Topic: {received_topic}") - - assert isinstance(received_data, MockLCMMessage) - assert received_data == test_message - - assert isinstance(received_topic, Topic) - assert received_topic == topic - - -test_msgs = [ - (Vector3(1, 2, 3)), - (Quaternion(1, 2, 3, 4)), - (Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1))), -] - - -# passes some geometry types through LCM -@pytest.mark.parametrize("test_message", test_msgs) -def test_lcm_geometry_msgs_pubsub(test_message: Any, lcm: LCM) -> None: - received_messages: list[tuple[Any, Any]] = [] - - topic = Topic(topic="/test_topic", lcm_type=test_message.__class__) - - def callback(msg: Any, topic: Any) -> None: - received_messages.append((msg, topic)) - - lcm.subscribe(topic, callback) - lcm.publish(topic, test_message) - - time.sleep(0.1) - - assert len(received_messages) == 1 - - received_data = received_messages[0][0] - received_topic = received_messages[0][1] - - print(f"Received data: {received_data}, Topic: {received_topic}") - - assert isinstance(received_data, test_message.__class__) - assert received_data == test_message - - assert isinstance(received_topic, Topic) - assert received_topic == topic - - print(test_message, topic) - - -# passes some geometry types through pickle LCM -@pytest.mark.parametrize("test_message", test_msgs) -def test_lcm_geometry_msgs_autopickle_pubsub(test_message: Any, pickle_lcm: PickleLCM) -> None: - lcm = pickle_lcm - received_messages: list[tuple[Any, Any]] = [] - - topic = Topic(topic="/test_topic") - - def callback(msg: Any, topic: Any) -> None: - received_messages.append((msg, topic)) - - lcm.subscribe(topic, callback) - lcm.publish(topic, test_message) - - time.sleep(0.1) - - assert len(received_messages) == 1 - - received_data = received_messages[0][0] - received_topic = received_messages[0][1] - - print(f"Received data: {received_data}, Topic: {received_topic}") - - assert isinstance(received_data, test_message.__class__) - assert received_data == test_message - - assert isinstance(received_topic, Topic) - assert received_topic == topic - - print(test_message, topic) diff --git a/dimos/protocol/pubsub/impl/test_rospubsub.py b/dimos/protocol/pubsub/impl/test_rospubsub.py deleted file mode 100644 index 6cf49c37b2..0000000000 --- a/dimos/protocol/pubsub/impl/test_rospubsub.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator -import threading - -from dimos_lcm.geometry_msgs import PointStamped -import numpy as np -import pytest - -from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped -from dimos.msgs.geometry_msgs.Twist import Twist -from dimos.msgs.geometry_msgs.Vector3 import Vector3 -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.protocol.pubsub.impl.rospubsub import DimosROS, ROSTopic - -# Add msg_name to LCM PointStamped for testing nested message conversion -PointStamped.msg_name = "geometry_msgs.PointStamped" -from dimos.utils.data import get_data -from dimos.utils.testing import TimedSensorReplay - - -def ros_node(): - ros = DimosROS() - ros.start() - try: - yield ros - finally: - ros.stop() - - -@pytest.fixture() -def publisher() -> Generator[DimosROS, None, None]: - yield from ros_node() - - -@pytest.fixture() -def subscriber() -> Generator[DimosROS, None, None]: - yield from ros_node() - - -@pytest.mark.ros -def test_basic_conversion(publisher, subscriber): - """Test Vector3 publish/subscribe through ROS. - - Simple flat dimos.msgs type with no nesting (just x/y/z floats). - """ - topic = ROSTopic("/test_ros_topic", Vector3) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, Vector3(1.0, 2.0, 3.0)) - - assert event.wait(timeout=2.0), "No message received" - assert len(received) == 1 - msg = received[0] - assert msg.x == 1.0 - assert msg.y == 2.0 - assert msg.z == 3.0 - - -@pytest.mark.ros -def test_pointcloud2_pubsub(publisher, subscriber): - """Test PointCloud2 publish/subscribe through ROS. - - COMPLEX_TYPE - has non-standard attributes (numpy arrays, custom accessors) - that can't be treated like a standard message with direct field copy. - Uses LCM encode/decode roundtrip to properly convert internal representation. - """ - dir_name = get_data("unitree_go2_bigoffice") - - # Load real lidar data from replay (5 seconds in) - replay = TimedSensorReplay(f"{dir_name}/lidar") - original = replay.find_closest_seek(5.0) - - assert original is not None, "Failed to load lidar data from replay" - assert len(original) > 0, "Loaded empty pointcloud" - - topic = ROSTopic("/test_pointcloud2", PointCloud2) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, original) - - assert event.wait(timeout=5.0), "No PointCloud2 message received" - assert len(received) == 1 - - converted = received[0] - - # Verify point cloud data is preserved - original_points, _ = original.as_numpy() - converted_points, _ = converted.as_numpy() - - assert len(original_points) == len(converted_points), ( - f"Point count mismatch: {len(original_points)} vs {len(converted_points)}" - ) - - np.testing.assert_allclose( - original_points, - converted_points, - rtol=1e-5, - atol=1e-5, - err_msg="Points don't match after ROS pubsub roundtrip", - ) - - # Verify frame_id is preserved - assert converted.frame_id == original.frame_id - - # Verify timestamp is preserved (within 1ms tolerance) - assert abs(original.ts - converted.ts) < 0.001 - - -@pytest.mark.ros -def test_pointcloud2_empty_pubsub(publisher, subscriber): - """Test empty PointCloud2 publish/subscribe. - - Edge case for COMPLEX_TYPE with zero points. - """ - original = PointCloud2.from_numpy( - np.array([]).reshape(0, 3), - frame_id="empty_frame", - timestamp=1234567890.0, - ) - - topic = ROSTopic("/test_empty_pointcloud", PointCloud2) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, original) - - assert event.wait(timeout=2.0), "No empty PointCloud2 message received" - assert len(received) == 1 - assert len(received[0]) == 0 - - -@pytest.mark.ros -def test_posestamped_pubsub(publisher, subscriber): - """Test PoseStamped publish/subscribe through ROS. - - COMPLEX_TYPE with custom dimos.msgs implementation and nested messages - (Header, Pose containing Point and Quaternion). Uses LCM roundtrip. - """ - original = PoseStamped( - ts=1234567890.123456, - frame_id="base_link", - position=[1.0, 2.0, 3.0], - orientation=[0.0, 0.0, 0.7071068, 0.7071068], # 90 degree yaw - ) - - topic = ROSTopic("/test_posestamped", PoseStamped) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, original) - - assert event.wait(timeout=2.0), "No PoseStamped message received" - assert len(received) == 1 - - converted = received[0] - - # Verify all fields preserved - assert converted.frame_id == original.frame_id - assert abs(converted.ts - original.ts) < 0.001 # 1ms tolerance - assert converted.x == original.x - assert converted.y == original.y - assert converted.z == original.z - np.testing.assert_allclose(converted.orientation.z, original.orientation.z, rtol=1e-5) - np.testing.assert_allclose(converted.orientation.w, original.orientation.w, rtol=1e-5) - - -@pytest.mark.ros -def test_pointstamped_pubsub(publisher, subscriber): - """Test PointStamped publish/subscribe through ROS. - - Raw LCM type with nested messages (Header, Point) but NO custom dimos.msgs - implementation. Tests recursive field copy for non-COMPLEX_TYPES. - """ - original = PointStamped() - original.header.stamp.sec = 1234567890 - original.header.stamp.nsec = 123456000 - original.header.frame_id = "map" - original.point.x = 1.5 - original.point.y = 2.5 - original.point.z = 3.5 - - topic = ROSTopic("/test_pointstamped", PointStamped) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, original) - - assert event.wait(timeout=2.0), "No PointStamped message received" - assert len(received) == 1 - - converted = received[0] - - # Verify nested header fields are preserved - assert converted.header.frame_id == original.header.frame_id - assert converted.header.stamp.sec == original.header.stamp.sec - assert converted.header.stamp.nsec == original.header.stamp.nsec - - # Verify point coordinates are preserved - assert converted.point.x == original.point.x - assert converted.point.y == original.point.y - assert converted.point.z == original.point.z - - -@pytest.mark.ros -def test_twist_pubsub(publisher, subscriber): - """Test Twist publish/subscribe through ROS. - - dimos.msgs type with nested Vector3 messages (linear, angular). - Tests recursive field copy with custom dimos.msgs nested types. - """ - original = Twist( - linear=[1.0, 2.0, 3.0], - angular=[0.1, 0.2, 0.3], - ) - - topic = ROSTopic("/test_twist", Twist) - - received = [] - event = threading.Event() - - def callback(msg, t): - received.append(msg) - event.set() - - subscriber.subscribe(topic, callback) - publisher.publish(topic, original) - - assert event.wait(timeout=2.0), "No Twist message received" - assert len(received) == 1 - - converted = received[0] - - # Verify linear velocity preserved - assert converted.linear.x == original.linear.x - assert converted.linear.y == original.linear.y - assert converted.linear.z == original.linear.z - - # Verify angular velocity preserved - assert converted.angular.x == original.angular.x - assert converted.angular.y == original.angular.y - assert converted.angular.z == original.angular.z diff --git a/dimos/protocol/pubsub/patterns.py b/dimos/protocol/pubsub/patterns.py deleted file mode 100644 index b7c24f4b02..0000000000 --- a/dimos/protocol/pubsub/patterns.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import re -from typing import TypeVar - -TopicT = TypeVar("TopicT") -MsgT = TypeVar("MsgT") - - -class Glob: - """Glob pattern that compiles to regex - - Supports: - * - matches any characters except / - ** - matches any characters including / - ? - matches single character - - Example: - Topic(topic=Glob("/sensor/*")) # matches /sensor/temp, /sensor/humidity - Topic(topic=Glob("/robot/**")) # matches /robot/arm/joint1, /robot/leg/motor - """ - - def __init__(self, pattern: str) -> None: - self._glob = pattern - self._regex = self._compile(pattern) - - @staticmethod - def _compile(pattern: str) -> str: - """Convert glob pattern to regex.""" - result = [] - i = 0 - while i < len(pattern): - c = pattern[i] - if c == "*": - if i + 1 < len(pattern) and pattern[i + 1] == "*": - result.append(".*") - i += 2 - else: - result.append("[^/]*") - i += 1 - elif c == "?": - result.append(".") - i += 1 - elif c in r"\^$.|+[]{}()": - result.append("\\" + c) - i += 1 - else: - result.append(c) - i += 1 - return "".join(result) - - @property - def pattern(self) -> str: - """Return the regex pattern string.""" - return self._regex - - @property - def glob(self) -> str: - """Return the original glob pattern.""" - return self._glob - - def __repr__(self) -> str: - return f"Glob({self._glob!r})" - - -Pattern = str | re.Pattern[str] | Glob - - -def pattern_matches(pattern: Pattern, topic_str: str) -> bool: - """Check if a topic string matches a pattern. - - Args: - pattern: A string (exact match), compiled regex, or Glob pattern. - topic_str: The topic string to match against. - - Returns: - True if the topic matches the pattern. - """ - if isinstance(pattern, str): - return pattern == topic_str - elif isinstance(pattern, Glob): - return bool(re.fullmatch(pattern.pattern, topic_str)) - else: - return bool(pattern.fullmatch(topic_str)) diff --git a/dimos/protocol/pubsub/shm/ipc_factory.py b/dimos/protocol/pubsub/shm/ipc_factory.py deleted file mode 100644 index 5f0b20165e..0000000000 --- a/dimos/protocol/pubsub/shm/ipc_factory.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# frame_ipc.py -# Python 3.9+ -from abc import ABC, abstractmethod -from multiprocessing.shared_memory import SharedMemory -import os -import time - -import numpy as np - -_UNLINK_ON_GC = os.getenv("DIMOS_IPC_UNLINK_ON_GC", "0").lower() not in ("0", "false", "no") - - -def _open_shm_with_retry(name: str) -> SharedMemory: - tries = int(os.getenv("DIMOS_IPC_ATTACH_RETRIES", "40")) # ~40 tries - base_ms = float(os.getenv("DIMOS_IPC_ATTACH_BACKOFF_MS", "5")) # 5 ms - cap_ms = float(os.getenv("DIMOS_IPC_ATTACH_BACKOFF_CAP_MS", "200")) # 200 ms - last = None - for i in range(tries): - try: - return SharedMemory(name=name) - except FileNotFoundError as e: - last = e - # exponential backoff, capped - time.sleep(min((base_ms * (2**i)), cap_ms) / 1000.0) - raise FileNotFoundError(f"SHM not found after {tries} retries: {name}") from last - - -def _sanitize_shm_name(name: str) -> str: - # Python's SharedMemory expects names like 'psm_abc', without leading '/' - return name.lstrip("/") if isinstance(name, str) else name - - -# --------------------------- -# 1) Abstract interface -# --------------------------- - - -class FrameChannel(ABC): - """Single-slot 'freshest frame' IPC channel with a tiny control block. - - Double-buffered to avoid torn reads. - - Descriptor is JSON-safe; attach() reconstructs in another process. - """ - - @property - @abstractmethod - def device(self) -> str: # "cpu" or "cuda" - ... - - @property - @abstractmethod - def shape(self) -> tuple: ... # type: ignore[type-arg] - - @property - @abstractmethod - def dtype(self) -> np.dtype: ... # type: ignore[type-arg] - - @abstractmethod - def publish(self, frame, length: int | None = None) -> None: # type: ignore[no-untyped-def] - """Write into inactive buffer, then flip visible index (write control last). - - Args: - frame: The numpy array to publish - length: Optional length to copy (for variable-size messages). If None, copies full frame. - """ - ... - - @abstractmethod - def read(self, last_seq: int = -1, require_new: bool = True): # type: ignore[no-untyped-def] - """Return (seq:int, ts_ns:int, view-or-None).""" - ... - - @abstractmethod - def descriptor(self) -> dict: # type: ignore[type-arg] - """Tiny JSON-safe descriptor (names/handles/shape/dtype/device).""" - ... - - @classmethod - @abstractmethod - def attach(cls, desc: dict) -> "FrameChannel": # type: ignore[type-arg] - """Attach in another process.""" - ... - - @abstractmethod - def close(self) -> None: - """Detach resources (owner also unlinks manager if applicable).""" - ... - - -from multiprocessing.shared_memory import SharedMemory -import os -import weakref - - -def _safe_unlink(name: str) -> None: - try: - shm = SharedMemory(name=name) - shm.unlink() - except FileNotFoundError: - pass - except Exception: - pass - - -# --------------------------- -# 2) CPU shared-memory backend -# --------------------------- - - -class CpuShmChannel(FrameChannel): - def __init__( # type: ignore[no-untyped-def] - self, - shape, - dtype=np.uint8, - *, - data_name: str | None = None, - ctrl_name: str | None = None, - ) -> None: - self._shape = tuple(shape) - self._dtype = np.dtype(dtype) - self._nbytes = int(self._dtype.itemsize * np.prod(self._shape)) - - def _create_or_open(name: str, size: int): # type: ignore[no-untyped-def] - try: - shm = SharedMemory(create=True, size=size, name=name) - owner = True - except FileExistsError: - shm = SharedMemory(name=name) # attach existing - owner = False - return shm, owner - - if data_name is None or ctrl_name is None: - # fallback: random names (old behavior) - self._shm_data = SharedMemory(create=True, size=2 * self._nbytes) - self._shm_ctrl = SharedMemory(create=True, size=24) - self._is_owner = True - else: - self._shm_data, own_d = _create_or_open(data_name, 2 * self._nbytes) - self._shm_ctrl, own_c = _create_or_open(ctrl_name, 24) - self._is_owner = own_d and own_c - - self._ctrl = np.ndarray((3,), dtype=np.int64, buffer=self._shm_ctrl.buf) # type: ignore[var-annotated] - if self._is_owner: - self._ctrl[:] = 0 # initialize only once - - # only owners set unlink finalizers (beware cross-process timing) - self._finalizer_data = ( - weakref.finalize(self, _safe_unlink, self._shm_data.name) - if (_UNLINK_ON_GC and self._is_owner) - else None - ) - self._finalizer_ctrl = ( - weakref.finalize(self, _safe_unlink, self._shm_ctrl.name) - if (_UNLINK_ON_GC and self._is_owner) - else None - ) - - def descriptor(self): # type: ignore[no-untyped-def] - return { - "kind": "cpu", - "shape": self._shape, - "dtype": self._dtype.str, - "nbytes": self._nbytes, - "data_name": self._shm_data.name, - "ctrl_name": self._shm_ctrl.name, - } - - @property - def device(self) -> str: - return "cpu" - - @property - def shape(self): # type: ignore[no-untyped-def] - return self._shape - - @property - def dtype(self): # type: ignore[no-untyped-def] - return self._dtype - - def publish(self, frame, length: int | None = None) -> None: # type: ignore[no-untyped-def] - assert isinstance(frame, np.ndarray) - assert frame.shape == self._shape and frame.dtype == self._dtype - active = int(self._ctrl[2]) - inactive = 1 - active - view = np.ndarray( # type: ignore[var-annotated] - self._shape, - dtype=self._dtype, - buffer=self._shm_data.buf, - offset=inactive * self._nbytes, - ) - # Only copy actual payload length if specified, otherwise copy full frame - if length is not None and length < len(frame): - np.copyto(view[:length], frame[:length], casting="no") - else: - np.copyto(view, frame, casting="no") - ts = np.int64(time.time_ns()) - # Publish order: ts -> idx -> seq - self._ctrl[1] = ts - self._ctrl[2] = inactive - self._ctrl[0] += 1 - - def read(self, last_seq: int = -1, require_new: bool = True): # type: ignore[no-untyped-def] - for _ in range(3): - seq1 = int(self._ctrl[0]) - idx = int(self._ctrl[2]) - ts = int(self._ctrl[1]) - view = np.ndarray( # type: ignore[var-annotated] - self._shape, dtype=self._dtype, buffer=self._shm_data.buf, offset=idx * self._nbytes - ) - if seq1 == int(self._ctrl[0]): - if require_new and seq1 == last_seq: - return seq1, ts, None - return seq1, ts, view - return last_seq, 0, None - - def descriptor(self): # type: ignore[no-redef, no-untyped-def] - return { - "kind": "cpu", - "shape": self._shape, - "dtype": self._dtype.str, - "nbytes": self._nbytes, - "data_name": self._shm_data.name, - "ctrl_name": self._shm_ctrl.name, - } - - @classmethod - def attach(cls, desc: str): # type: ignore[no-untyped-def, override] - obj = object.__new__(cls) - obj._shape = tuple(desc["shape"]) # type: ignore[index] - obj._dtype = np.dtype(desc["dtype"]) # type: ignore[index] - obj._nbytes = int(desc["nbytes"]) # type: ignore[index] - data_name = desc["data_name"] # type: ignore[index] - ctrl_name = desc["ctrl_name"] # type: ignore[index] - try: - obj._shm_data = _open_shm_with_retry(data_name) - obj._shm_ctrl = _open_shm_with_retry(ctrl_name) - except FileNotFoundError as e: - raise FileNotFoundError( - f"CPU IPC attach failed: control/data SHM not found " - f"(ctrl='{ctrl_name}', data='{data_name}'). " - f"Ensure the writer is running on the same host and the channel is alive." - ) from e - obj._ctrl = np.ndarray((3,), dtype=np.int64, buffer=obj._shm_ctrl.buf) - # attachments don’t own/unlink - obj._finalizer_data = obj._finalizer_ctrl = None - return obj - - def close(self) -> None: - if getattr(self, "_is_owner", False): - try: - self._shm_ctrl.close() - finally: - try: - _safe_unlink(self._shm_ctrl.name) - except: - pass - if hasattr(self, "_shm_data"): - try: - self._shm_data.close() - finally: - try: - _safe_unlink(self._shm_data.name) - except: - pass - return - # readers: just close handles - try: - self._shm_ctrl.close() - except: - pass - try: - self._shm_data.close() - except: - pass - - -# --------------------------- -# 3) Factories -# --------------------------- - - -class CPU_IPC_Factory: - """Creates/attaches CPU shared-memory channels.""" - - @staticmethod - def create(shape, dtype=np.uint8) -> CpuShmChannel: # type: ignore[no-untyped-def] - return CpuShmChannel(shape, dtype=dtype) - - @staticmethod - def attach(desc: dict) -> CpuShmChannel: # type: ignore[type-arg] - assert desc.get("kind") == "cpu", "Descriptor kind mismatch" - return CpuShmChannel.attach(desc) # type: ignore[arg-type, no-any-return] - - -# --------------------------- -# 4) Runtime selector -# --------------------------- - - -def make_frame_channel( # type: ignore[no-untyped-def] - shape, dtype=np.uint8, prefer: str = "auto", device: int = 0 -) -> FrameChannel: - """Choose CUDA IPC if available (or requested), otherwise CPU SHM.""" - # TODO: Implement the CUDA version of creating this factory - return CPU_IPC_Factory.create(shape, dtype=dtype) diff --git a/dimos/protocol/pubsub/spec.py b/dimos/protocol/pubsub/spec.py deleted file mode 100644 index fe979fce82..0000000000 --- a/dimos/protocol/pubsub/spec.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -import asyncio -from collections.abc import AsyncIterator, Callable -from contextlib import asynccontextmanager -from dataclasses import dataclass -from typing import Any, Generic, Protocol, TypeVar, runtime_checkable - -MsgT = TypeVar("MsgT") -TopicT = TypeVar("TopicT") -MsgT_co = TypeVar("MsgT_co", covariant=True) -TopicT_co = TypeVar("TopicT_co", covariant=True) - - -class PubSubBaseMixin(Generic[TopicT, MsgT]): - """Mixin class providing sugar methods for PubSub implementations. - Depends on the basic publish and subscribe methods being implemented. - """ - - def subscribe( - self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] - ) -> Callable[[], None]: - """Subscribe to a topic. Implemented by subclasses.""" - raise NotImplementedError - - @dataclass(slots=True) - class _Subscription: - _bus: "PubSubBaseMixin[Any, Any]" - _topic: Any - _cb: Callable[[Any, Any], None] - _unsubscribe_fn: Callable[[], None] - - def unsubscribe(self) -> None: - self._unsubscribe_fn() - - def __enter__(self) -> "PubSubBaseMixin._Subscription": - return self - - def __exit__(self, *exc: Any) -> None: - self.unsubscribe() - - def sub(self, topic: TopicT, cb: Callable[[MsgT, TopicT], None]) -> "_Subscription": - unsubscribe_fn = self.subscribe(topic, cb) - return self._Subscription(self, topic, cb, unsubscribe_fn) - - async def aiter(self, topic: TopicT, *, max_pending: int | None = None) -> AsyncIterator[MsgT]: - q: asyncio.Queue[MsgT] = asyncio.Queue(maxsize=max_pending or 0) - - def _cb(msg: MsgT, topic: TopicT) -> None: - q.put_nowait(msg) - - unsubscribe_fn = self.subscribe(topic, _cb) - try: - while True: - yield await q.get() - finally: - unsubscribe_fn() - - @asynccontextmanager - async def queue( - self, topic: TopicT, *, max_pending: int | None = None - ) -> AsyncIterator[asyncio.Queue[MsgT]]: - q: asyncio.Queue[MsgT] = asyncio.Queue(maxsize=max_pending or 0) - - def _queue_cb(msg: MsgT, topic: TopicT) -> None: - q.put_nowait(msg) - - unsubscribe_fn = self.subscribe(topic, _queue_cb) - try: - yield q - finally: - unsubscribe_fn() - - -class PubSub(PubSubBaseMixin[TopicT, MsgT], ABC): - """Abstract base class for pub/sub implementations with sugar methods.""" - - @abstractmethod - def publish(self, topic: TopicT, message: MsgT) -> None: - """Publish a message to a topic.""" - ... - - @abstractmethod - def subscribe( - self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] - ) -> Callable[[], None]: - """Subscribe to a topic with a callback. returns unsubscribe function""" - ... - - -# AllPubSub and DiscoveryPubSub are complementary mixins: -# -# AllPubsub supports subscribing to all topics (Redis, LCM, MQTT) -# DiscoveryPubSub supports discovering new topics (ROS) -# -# These capabilities are orthogonal but they can implement one another. -# Implementations should subclass whichever matches their native capability. -# The other method will be synthesized automatically. -# -# - AllPubSub: Native support for subscribing to all topics at once. -# Provides a default subscribe_new_topics() by tracking seen topics. -# -# - DiscoveryPubSub: Native support for discovering new topics as they appear. -# Provides a default subscribe_all() by subscribing to each discovered topic. -class AllPubSub(PubSub[TopicT, MsgT], ABC): - """Mixin for PubSub that supports subscribing to all topics. - - Subclass from this if you support native subscribe-all (e.g. MQTT #, Redis *). - Provides a default subscribe_new_topics() implementation. - """ - - @abstractmethod - def subscribe_all(self, callback: Callable[[MsgT, TopicT], Any]) -> Callable[[], None]: - """Subscribe to all topics.""" - ... - - def subscribe_new_topics(self, callback: Callable[[TopicT], Any]) -> Callable[[], None]: - """Discover new topics by tracking seen topics from subscribe_all.""" - import threading - - seen: set[TopicT] = set() - lock = threading.Lock() - - def on_msg(msg: MsgT, topic: TopicT) -> None: - with lock: - if topic not in seen: - seen.add(topic) - callback(topic) - - return self.subscribe_all(on_msg) - - -# This is for ros for now -class DiscoveryPubSub(PubSub[TopicT, MsgT], ABC): - """Mixin for PubSub that supports discovery of topics. - - Subclass from this if you support topic discovery (e.g. MQTT, Redis, NATS, RabbitMQ). - """ - - @abstractmethod - def subscribe_new_topics(self, callback: Callable[[TopicT], Any]) -> Callable[[], None]: - """Get notified when new topics are discovered.""" - ... - - def subscribe_all(self, callback: Callable[[MsgT, TopicT], Any]) -> Callable[[], None]: - """Subscribe to all topics by subscribing to each discovered topic.""" - import threading - - subscriptions: list[Callable[[], None]] = [] - lock = threading.Lock() - - def on_new_topic(topic: TopicT) -> None: - unsub = self.subscribe(topic, callback) - with lock: - subscriptions.append(unsub) - - discovery_unsub = self.subscribe_new_topics(on_new_topic) - - def unsubscribe_all() -> None: - discovery_unsub() - with lock: - subs = subscriptions.copy() - for unsub in subs: - unsub() - - return unsubscribe_all - - -@runtime_checkable -class SubscribeAllCapable(Protocol[MsgT_co, TopicT_co]): - """Protocol for pubsubs that support subscribe_all. - - Both AllPubSub (native) and DiscoveryPubSub (synthesized) satisfy this. - """ - - def subscribe_all(self, callback: Callable[[Any, Any], Any]) -> Callable[[], None]: - """Subscribe to all messages on all topics.""" - ... diff --git a/dimos/protocol/pubsub/test_encoder.py b/dimos/protocol/pubsub/test_encoder.py deleted file mode 100644 index dec1e42972..0000000000 --- a/dimos/protocol/pubsub/test_encoder.py +++ /dev/null @@ -1,171 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -from typing import Any - -from dimos.protocol.pubsub.impl.memory import Memory, MemoryWithJSONEncoder - - -def test_json_encoded_pubsub() -> None: - """Test memory pubsub with JSON encoding.""" - pubsub = MemoryWithJSONEncoder() - received_messages = [] - - def callback(message: Any, topic: str) -> None: - received_messages.append(message) - - # Subscribe to a topic - pubsub.subscribe("json_topic", callback) - - # Publish various types of messages - test_messages = [ - "hello world", - 42, - 3.14, - True, - None, - {"name": "Alice", "age": 30, "active": True}, - [1, 2, 3, "four", {"five": 5}], - {"nested": {"data": [1, 2, {"deep": True}]}}, - ] - - for msg in test_messages: - pubsub.publish("json_topic", msg) - - # Verify all messages were received and properly decoded - assert len(received_messages) == len(test_messages) - for original, received in zip(test_messages, received_messages, strict=False): - assert original == received - - -def test_json_encoding_edge_cases() -> None: - """Test edge cases for JSON encoding.""" - pubsub = MemoryWithJSONEncoder() - received_messages = [] - - def callback(message: Any, topic: str) -> None: - received_messages.append(message) - - pubsub.subscribe("edge_cases", callback) - - # Test edge cases - edge_cases = [ - "", # empty string - [], # empty list - {}, # empty dict - 0, # zero - False, # False boolean - [None, None, None], # list with None values - {"": "empty_key", "null": None, "empty_list": [], "empty_dict": {}}, - ] - - for case in edge_cases: - pubsub.publish("edge_cases", case) - - assert received_messages == edge_cases - - -def test_multiple_subscribers_with_encoding() -> None: - """Test that multiple subscribers work with encoding.""" - pubsub = MemoryWithJSONEncoder() - received_messages_1 = [] - received_messages_2 = [] - - def callback_1(message: Any, topic: str) -> None: - received_messages_1.append(message) - - def callback_2(message: Any, topic: str) -> None: - received_messages_2.append(f"callback_2: {message}") - - pubsub.subscribe("json_topic", callback_1) - pubsub.subscribe("json_topic", callback_2) - pubsub.publish("json_topic", {"multi": "subscriber test"}) - - # Both callbacks should receive the message - assert received_messages_1[-1] == {"multi": "subscriber test"} - assert received_messages_2[-1] == "callback_2: {'multi': 'subscriber test'}" - - -# def test_unsubscribe_with_encoding(): -# """Test unsubscribe works correctly with encoded callbacks.""" -# pubsub = MemoryWithJSONEncoder() -# received_messages_1 = [] -# received_messages_2 = [] - -# def callback_1(message): -# received_messages_1.append(message) - -# def callback_2(message): -# received_messages_2.append(message) - -# pubsub.subscribe("json_topic", callback_1) -# pubsub.subscribe("json_topic", callback_2) - -# # Unsubscribe first callback -# pubsub.unsubscribe("json_topic", callback_1) -# pubsub.publish("json_topic", "only callback_2 should get this") - -# # Only callback_2 should receive the message -# assert len(received_messages_1) == 0 -# assert received_messages_2 == ["only callback_2 should get this"] - - -def test_data_actually_encoded_in_transit() -> None: - """Validate that data is actually encoded in transit by intercepting raw bytes.""" - - # Create a spy memory that captures what actually gets published - class SpyMemory(Memory): - def __init__(self) -> None: - super().__init__() - self.raw_messages_received: list[tuple[str, Any, type]] = [] - - def publish(self, topic: str, message: Any) -> None: - # Capture what actually gets published - self.raw_messages_received.append((topic, message, type(message))) - super().publish(topic, message) - - # Create encoder that uses our spy memory - class SpyMemoryWithJSON(MemoryWithJSONEncoder, SpyMemory): - pass - - pubsub = SpyMemoryWithJSON() - received_decoded: list[Any] = [] - - def callback(message: Any, topic: str) -> None: - received_decoded.append(message) - - pubsub.subscribe("test_topic", callback) - - # Publish a complex object - original_message = {"name": "Alice", "age": 30, "items": [1, 2, 3]} - pubsub.publish("test_topic", original_message) - - # Verify the message was received and decoded correctly - assert len(received_decoded) == 1 - assert received_decoded[0] == original_message - - # Verify the underlying transport actually received JSON bytes, not the original object - assert len(pubsub.raw_messages_received) == 1 - topic, raw_message, raw_type = pubsub.raw_messages_received[0] - - assert topic == "test_topic" - assert raw_type == bytes # Should be bytes, not dict - assert isinstance(raw_message, bytes) - - # Verify it's actually JSON - decoded_raw = json.loads(raw_message.decode("utf-8")) - assert decoded_raw == original_message diff --git a/dimos/protocol/pubsub/test_pattern_sub.py b/dimos/protocol/pubsub/test_pattern_sub.py deleted file mode 100644 index 99aea49b05..0000000000 --- a/dimos/protocol/pubsub/test_pattern_sub.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Grid tests for subscribe_all pattern subscriptions.""" - -from collections.abc import Callable, Generator -from contextlib import AbstractContextManager, contextmanager -from dataclasses import dataclass, field -import re -import time -from typing import Any, Generic, TypeVar - -import pytest - -from dimos.msgs.geometry_msgs import Pose, Quaternion, Vector3 -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, LCMPubSubBase, Topic -from dimos.protocol.pubsub.patterns import Glob -from dimos.protocol.pubsub.spec import AllPubSub, PubSub - -TopicT = TypeVar("TopicT") -MsgT = TypeVar("MsgT") - -# Type alias for (publisher, subscriber) tuple -PubSubPair = tuple[PubSub[TopicT, MsgT], AllPubSub[TopicT, MsgT]] - - -@dataclass -class Case(Generic[TopicT, MsgT]): - """Test case for grid testing pubsub implementations.""" - - name: str - pubsub_context: Callable[[], AbstractContextManager[PubSubPair[TopicT, MsgT]]] - topic_values: list[tuple[TopicT, MsgT]] - tags: set[str] = field(default_factory=set) - # Pattern tests: (pattern_topic, {indices of topic_values that should match}) - glob_patterns: list[tuple[TopicT, set[int]]] = field(default_factory=list) - regex_patterns: list[tuple[TopicT, set[int]]] = field(default_factory=list) - - -@contextmanager -def lcm_typed_context() -> Generator[tuple[LCM, LCM], None, None]: - pub = LCM(autoconf=True) - sub = LCM(autoconf=False) - pub.start() - sub.start() - try: - yield pub, sub - finally: - pub.stop() - sub.stop() - - -@contextmanager -def lcm_bytes_context() -> Generator[tuple[LCMPubSubBase, LCMPubSubBase], None, None]: - pub = LCMPubSubBase(autoconf=True) - sub = LCMPubSubBase(autoconf=False) - pub.start() - sub.start() - try: - yield pub, sub - finally: - pub.stop() - sub.stop() - - -testcases: list[Case[Any, Any]] = [ - Case( - name="lcm_typed", - pubsub_context=lcm_typed_context, - topic_values=[ - (Topic("/sensor/position", Vector3), Vector3(1, 2, 3)), - (Topic("/sensor/orientation", Quaternion), Quaternion(0, 0, 0, 1)), - (Topic("/robot/arm", Pose), Pose(Vector3(4, 5, 6), Quaternion(0, 0, 0, 1))), - ], - tags={"all", "glob", "regex"}, - glob_patterns=[ - (Topic(topic=Glob("/sensor/*")), {0, 1}), - (Topic(topic=Glob("/**/arm")), {2}), - (Topic(topic=Glob("/**")), {0, 1, 2}), - ], - regex_patterns=[ - (Topic(re.compile(r"/sensor/.*")), {0, 1}), - (Topic(re.compile(r".*/arm"), Pose), {2}), - (Topic(re.compile(r".*/arm")), {2}), - (Topic(re.compile(r".*/arm#geometry.*")), {2}), - ], - ), - Case( - name="lcm_bytes", - pubsub_context=lcm_bytes_context, - topic_values=[ - (Topic("/sensor/temp"), b"temp"), - (Topic("/sensor/humidity"), b"humidity"), - (Topic("/robot/arm"), b"arm"), - ], - tags={"all", "glob", "regex"}, - glob_patterns=[ - (Topic(topic=Glob("/sensor/*")), {0, 1}), - (Topic(topic=Glob("/**/arm")), {2}), - (Topic(topic=Glob("/**")), {0, 1, 2}), - ], - regex_patterns=[ - (Topic(re.compile(r"/sensor/.*")), {0, 1}), - (Topic(re.compile(r".*/arm")), {2}), - ], - ), -] - -# Build filtered lists for parametrize -all_cases = [c for c in testcases if "all" in c.tags] -glob_cases = [c for c in testcases if "glob" in c.tags] -regex_cases = [c for c in testcases if "regex" in c.tags] - - -def _topic_matches_prefix(topic: Any, prefix: str = "/") -> bool: - """Check if topic string starts with prefix. - - LCM uses UDP multicast, so messages from other tests running in parallel - can leak into subscribe_all callbacks. We filter to only our test topics. - """ - topic_str = str(topic.topic if hasattr(topic, "topic") else topic) - return topic_str.startswith(prefix) - - -@pytest.mark.parametrize("tc", all_cases, ids=lambda c: c.name) -def test_subscribe_all_receives_all_topics(tc: Case[Any, Any]) -> None: - """Test that subscribe_all receives messages from all topics.""" - received: list[tuple[Any, Any]] = [] - - with tc.pubsub_context() as (pub, sub): - # Filter to only our test topics (LCM multicast can leak from parallel tests) - sub.subscribe_all(lambda msg, topic: received.append((msg, topic))) - time.sleep(0.01) # Allow subscription to be ready - - for topic, value in tc.topic_values: - pub.publish(topic, value) - - time.sleep(0.01) - - assert len(received) == len(tc.topic_values) - - # Verify all messages were received - received_msgs = [r[0] for r in received] - expected_msgs = [v for _, v in tc.topic_values] - for expected in expected_msgs: - assert expected in received_msgs - - -@pytest.mark.parametrize("tc", all_cases, ids=lambda c: c.name) -def test_subscribe_all_unsubscribe(tc: Case[Any, Any]) -> None: - """Test that unsubscribe stops receiving messages.""" - received: list[tuple[Any, Any]] = [] - topic, value = tc.topic_values[0] - - with tc.pubsub_context() as (pub, sub): - unsub = sub.subscribe_all(lambda msg, topic: received.append((msg, topic))) - time.sleep(0.01) # Allow subscription to be ready - - pub.publish(topic, value) - time.sleep(0.01) - assert len(received) == 1 - - unsub() - - pub.publish(topic, value) - time.sleep(0.01) - assert len(received) == 1 # No new messages - - -@pytest.mark.parametrize("tc", all_cases, ids=lambda c: c.name) -def test_subscribe_all_with_regular_subscribe(tc: Case[Any, Any]) -> None: - """Test that subscribe_all coexists with regular subscriptions.""" - all_received: list[tuple[Any, Any]] = [] - specific_received: list[tuple[Any, Any]] = [] - topic1, value1 = tc.topic_values[0] - topic2, value2 = tc.topic_values[1] - - with tc.pubsub_context() as (pub, sub): - sub.subscribe_all( - lambda msg, topic: all_received.append((msg, topic)) - if _topic_matches_prefix(topic) - else None - ) - sub.subscribe(topic1, lambda msg, topic: specific_received.append((msg, topic))) - time.sleep(0.01) # Allow subscriptions to be ready - - pub.publish(topic1, value1) - pub.publish(topic2, value2) - time.sleep(0.01) - - # subscribe_all gets both - assert len(all_received) == 2 - - # specific subscription gets only topic1 - assert len(specific_received) == 1 - assert specific_received[0][0] == value1 - - -@pytest.mark.parametrize("tc", glob_cases, ids=lambda c: c.name) -def test_subscribe_glob(tc: Case[Any, Any]) -> None: - """Test that glob pattern subscriptions receive only matching topics.""" - for pattern_topic, expected_indices in tc.glob_patterns: - received: list[tuple[Any, Any]] = [] - - with tc.pubsub_context() as (pub, sub): - sub.subscribe(pattern_topic, lambda msg, topic, r=received: r.append((msg, topic))) - time.sleep(0.01) # Allow subscription to be ready - - for topic, value in tc.topic_values: - pub.publish(topic, value) - - time.sleep(0.01) - - assert len(received) == len(expected_indices), ( - f"Expected {len(expected_indices)} messages for pattern {pattern_topic}, " - f"got {len(received)}" - ) - - # Verify we received the expected messages - expected_msgs = [tc.topic_values[i][1] for i in expected_indices] - received_msgs = [r[0] for r in received] - for expected in expected_msgs: - assert expected in received_msgs - - -@pytest.mark.parametrize("tc", regex_cases, ids=lambda c: c.name) -def test_subscribe_regex(tc: Case[Any, Any]) -> None: - """Test that regex pattern subscriptions receive only matching topics.""" - for pattern_topic, expected_indices in tc.regex_patterns: - received: list[tuple[Any, Any]] = [] - - with tc.pubsub_context() as (pub, sub): - sub.subscribe(pattern_topic, lambda msg, topic, r=received: r.append((msg, topic))) - - time.sleep(0.01) - - for topic, value in tc.topic_values: - pub.publish(topic, value) - - time.sleep(0.01) - - assert len(received) == len(expected_indices), ( - f"Expected {len(expected_indices)} messages for pattern {pattern_topic}, " - f"got {len(received)}" - ) - - # Verify we received the expected messages - expected_msgs = [tc.topic_values[i][1] for i in expected_indices] - received_msgs = [r[0] for r in received] - for expected in expected_msgs: - assert expected in received_msgs diff --git a/dimos/protocol/pubsub/test_patterns.py b/dimos/protocol/pubsub/test_patterns.py deleted file mode 100644 index 6d0ce35016..0000000000 --- a/dimos/protocol/pubsub/test_patterns.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for pattern matching utilities.""" - -import re - -from dimos.protocol.pubsub.patterns import Glob, pattern_matches - - -class TestPatternMatchesString: - """Tests for exact string matching.""" - - def test_exact_match(self) -> None: - assert pattern_matches("/sensor/temp", "/sensor/temp") is True - - def test_no_match(self) -> None: - assert pattern_matches("/sensor/temp", "/sensor/humidity") is False - - def test_empty_string(self) -> None: - assert pattern_matches("", "") is True - assert pattern_matches("", "/sensor") is False - - def test_partial_match_fails(self) -> None: - assert pattern_matches("/sensor", "/sensor/temp") is False - assert pattern_matches("/sensor/temp", "/sensor") is False - - -class TestPatternMatchesGlob: - """Tests for Glob pattern matching.""" - - def test_single_wildcard(self) -> None: - glob = Glob("/sensor/*") - assert pattern_matches(glob, "/sensor/temp") is True - assert pattern_matches(glob, "/sensor/humidity") is True - assert pattern_matches(glob, "/sensor/") is True - - def test_single_wildcard_no_slash(self) -> None: - glob = Glob("/sensor/*") - assert pattern_matches(glob, "/sensor/nested/path") is False - - def test_double_wildcard(self) -> None: - glob = Glob("/robot/**") - assert pattern_matches(glob, "/robot/arm") is True - assert pattern_matches(glob, "/robot/arm/joint1") is True - assert pattern_matches(glob, "/robot/leg/motor/encoder") is True - - def test_question_mark(self) -> None: - glob = Glob("/sensor/?") - assert pattern_matches(glob, "/sensor/a") is True - assert pattern_matches(glob, "/sensor/1") is True - assert pattern_matches(glob, "/sensor/ab") is False - - def test_mixed_patterns(self) -> None: - glob = Glob("/robot/*/joint?") - assert pattern_matches(glob, "/robot/arm/joint1") is True - assert pattern_matches(glob, "/robot/leg/joint2") is True - assert pattern_matches(glob, "/robot/arm/joint12") is False - assert pattern_matches(glob, "/robot/arm/nested/joint1") is False - - def test_no_wildcards(self) -> None: - glob = Glob("/exact/path") - assert pattern_matches(glob, "/exact/path") is True - assert pattern_matches(glob, "/exact/other") is False - - def test_double_wildcard_middle(self) -> None: - glob = Glob("/start/**/end") - # Note: ** becomes .* so /start/**/end requires a / before end - assert pattern_matches(glob, "/start//end") is True - assert pattern_matches(glob, "/start/middle/end") is True - assert pattern_matches(glob, "/start/a/b/c/end") is True - - -class TestPatternMatchesRegex: - """Tests for compiled regex pattern matching.""" - - def test_simple_regex(self) -> None: - pattern = re.compile(r"/sensor/\w+") - assert pattern_matches(pattern, "/sensor/temp") is True - assert pattern_matches(pattern, "/sensor/123") is True - - def test_regex_anchored(self) -> None: - pattern = re.compile(r"/sensor/temp") - assert pattern_matches(pattern, "/sensor/temp") is True - assert pattern_matches(pattern, "/sensor/temperature") is False - - def test_regex_groups(self) -> None: - pattern = re.compile(r"/robot/(arm|leg)/joint(\d+)") - assert pattern_matches(pattern, "/robot/arm/joint1") is True - assert pattern_matches(pattern, "/robot/leg/joint42") is True - assert pattern_matches(pattern, "/robot/head/joint1") is False - - def test_regex_optional(self) -> None: - pattern = re.compile(r"/sensor/temp/?") - assert pattern_matches(pattern, "/sensor/temp") is True - assert pattern_matches(pattern, "/sensor/temp/") is True - - -class TestGlobClass: - """Tests for the Glob class itself.""" - - def test_pattern_property(self) -> None: - glob = Glob("/sensor/*") - assert glob.pattern == "/sensor/[^/]*" - - def test_glob_property(self) -> None: - glob = Glob("/sensor/*") - assert glob.glob == "/sensor/*" - - def test_repr(self) -> None: - glob = Glob("/sensor/*") - assert repr(glob) == "Glob('/sensor/*')" - - def test_double_star_regex(self) -> None: - glob = Glob("/robot/**") - assert glob.pattern == "/robot/.*" - - def test_special_chars_escaped(self) -> None: - glob = Glob("/path.with.dots") - assert pattern_matches(glob, "/path.with.dots") is True - assert pattern_matches(glob, "/pathXwithXdots") is False diff --git a/dimos/protocol/pubsub/test_spec.py b/dimos/protocol/pubsub/test_spec.py deleted file mode 100644 index 0bdfa62628..0000000000 --- a/dimos/protocol/pubsub/test_spec.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -from collections.abc import Callable, Generator -from contextlib import contextmanager -import time -from typing import Any - -import pytest - -from dimos.msgs.geometry_msgs import Vector3 -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, Topic -from dimos.protocol.pubsub.impl.memory import Memory - - -@contextmanager -def memory_context() -> Generator[Memory, None, None]: - """Context manager for Memory PubSub implementation.""" - memory = Memory() - try: - yield memory - finally: - # Cleanup logic can be added here if needed - pass - - -# Use Any for context manager type to accommodate both Memory and Redis -testdata: list[tuple[Callable[[], Any], Any, list[Any]]] = [ - (memory_context, "topic", ["value1", "value2", "value3"]), -] - -try: - from dimos.protocol.pubsub.impl.redispubsub import Redis - - @contextmanager - def redis_context() -> Generator[Redis, None, None]: - redis_pubsub = Redis() - redis_pubsub.start() - yield redis_pubsub - redis_pubsub.stop() - - testdata.append( - (redis_context, "redis_topic", ["redis_value1", "redis_value2", "redis_value3"]) - ) - -except (ConnectionError, ImportError): - # either redis is not installed or the server is not running - print("Redis not available") - -try: - from geometry_msgs.msg import Vector3 as ROSVector3 - from rclpy.qos import ( - QoSDurabilityPolicy, - QoSHistoryPolicy, - QoSProfile, - QoSReliabilityPolicy, - ) - - from dimos.protocol.pubsub.impl.rospubsub import RawROS, RawROSTopic - - # Use RELIABLE QoS with larger depth for testing - _test_qos = QoSProfile( - reliability=QoSReliabilityPolicy.RELIABLE, - history=QoSHistoryPolicy.KEEP_ALL, - durability=QoSDurabilityPolicy.VOLATILE, - depth=5000, - ) - - @contextmanager - def ros_context() -> Generator[RawROS, None, None]: - ros_pubsub = RawROS(qos=_test_qos) - ros_pubsub.start() - time.sleep(0.1) - try: - yield ros_pubsub - finally: - ros_pubsub.stop() - - testdata.append( - ( - ros_context, - RawROSTopic(topic="/test_ros_topic", ros_type=ROSVector3, qos=_test_qos), - [ - ROSVector3(x=1.0, y=2.0, z=3.0), - ROSVector3(x=4.0, y=5.0, z=6.0), - ROSVector3(x=7.0, y=8.0, z=9.0), - ], - ) - ) - -except ImportError: - # ROS 2 not available - print("ROS 2 not available") - - -@contextmanager -def lcm_context() -> Generator[LCM, None, None]: - lcm_pubsub = LCM(autoconf=True) - lcm_pubsub.start() - yield lcm_pubsub - lcm_pubsub.stop() - - -testdata.append( - ( - lcm_context, - Topic(topic="/test_topic", lcm_type=Vector3), - [Vector3(1, 2, 3), Vector3(4, 5, 6), Vector3(7, 8, 9)], # Using Vector3 as mock data, - ) -) - - -from dimos.protocol.pubsub.impl.shmpubsub import PickleSharedMemory - - -@contextmanager -def shared_memory_cpu_context() -> Generator[PickleSharedMemory, None, None]: - shared_mem_pubsub = PickleSharedMemory(prefer="cpu") - shared_mem_pubsub.start() - yield shared_mem_pubsub - shared_mem_pubsub.stop() - - -testdata.append( - ( - shared_memory_cpu_context, - "/shared_mem_topic_cpu", - [b"shared_mem_value1", b"shared_mem_value2", b"shared_mem_value3"], - ) -) - - -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -def test_store(pubsub_context: Callable[[], Any], topic: Any, values: list[Any]) -> None: - with pubsub_context() as x: - # Create a list to capture received messages - received_messages: list[Any] = [] - - # Define callback function that stores received messages - def callback(message: Any, _: Any) -> None: - received_messages.append(message) - - # Subscribe to the topic with our callback - x.subscribe(topic, callback) - - # Publish the first value to the topic - x.publish(topic, values[0]) - - # Give Redis time to process the message if needed - time.sleep(0.1) - - print("RECEIVED", received_messages) - # Verify the callback was called with the correct value - assert len(received_messages) == 1 - assert received_messages[0] == values[0] - - -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -def test_multiple_subscribers( - pubsub_context: Callable[[], Any], topic: Any, values: list[Any] -) -> None: - """Test that multiple subscribers receive the same message.""" - with pubsub_context() as x: - # Create lists to capture received messages for each subscriber - received_messages_1: list[Any] = [] - received_messages_2: list[Any] = [] - - # Define callback functions - def callback_1(message: Any, topic: Any) -> None: - received_messages_1.append(message) - - def callback_2(message: Any, topic: Any) -> None: - received_messages_2.append(message) - - # Subscribe both callbacks to the same topic - x.subscribe(topic, callback_1) - x.subscribe(topic, callback_2) - - # Publish the first value - x.publish(topic, values[0]) - - # Give Redis time to process the message if needed - time.sleep(0.1) - - # Verify both callbacks received the message - assert len(received_messages_1) == 1 - assert received_messages_1[0] == values[0] - assert len(received_messages_2) == 1 - assert received_messages_2[0] == values[0] - - -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -def test_unsubscribe(pubsub_context: Callable[[], Any], topic: Any, values: list[Any]) -> None: - """Test that unsubscribed callbacks don't receive messages.""" - with pubsub_context() as x: - # Create a list to capture received messages - received_messages: list[Any] = [] - - # Define callback function - def callback(message: Any, topic: Any) -> None: - received_messages.append(message) - - # Subscribe and get unsubscribe function - unsubscribe = x.subscribe(topic, callback) - - # Unsubscribe using the returned function - unsubscribe() - - # Publish the first value - x.publish(topic, values[0]) - - # Give time to process the message if needed - time.sleep(0.1) - - # Verify the callback was not called after unsubscribing - assert len(received_messages) == 0 - - -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -def test_multiple_messages( - pubsub_context: Callable[[], Any], topic: Any, values: list[Any] -) -> None: - """Test that subscribers receive multiple messages in order.""" - with pubsub_context() as x: - # Create a list to capture received messages - received_messages: list[Any] = [] - - # Define callback function - def callback(message: Any, topic: Any) -> None: - received_messages.append(message) - - # Subscribe to the topic - x.subscribe(topic, callback) - - # Publish the rest of the values (after the first one used in basic tests) - messages_to_send = values[1:] if len(values) > 1 else values - for msg in messages_to_send: - x.publish(topic, msg) - - # Give Redis time to process the messages if needed - time.sleep(0.2) - - # Verify all messages were received in order - assert len(received_messages) == len(messages_to_send) - assert received_messages == messages_to_send - - -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -@pytest.mark.asyncio -async def test_async_iterator( - pubsub_context: Callable[[], Any], topic: Any, values: list[Any] -) -> None: - """Test that async iterator receives messages correctly.""" - with pubsub_context() as x: - # Get the messages to send (using the rest of the values) - messages_to_send = values[1:] if len(values) > 1 else values - received_messages = [] - - # Create the async iterator - async_iter = x.aiter(topic) - - # Create a task to consume messages from the async iterator - async def consume_messages() -> None: - try: - async for message in async_iter: - received_messages.append(message) - # Stop after receiving all expected messages - if len(received_messages) >= len(messages_to_send): - break - except asyncio.CancelledError: - pass - - # Start the consumer task - consumer_task = asyncio.create_task(consume_messages()) - - # Give the consumer a moment to set up - await asyncio.sleep(0.1) - - # Publish messages - for msg in messages_to_send: - x.publish(topic, msg) - # Small delay to ensure message is processed - await asyncio.sleep(0.1) - - # Wait for the consumer to finish or timeout - try: - await asyncio.wait_for(consumer_task, timeout=1.0) # Longer timeout for Redis - except asyncio.TimeoutError: - consumer_task.cancel() - try: - await consumer_task - except asyncio.CancelledError: - pass - - # Verify all messages were received in order - assert len(received_messages) == len(messages_to_send) - assert received_messages == messages_to_send - - -@pytest.mark.integration -@pytest.mark.parametrize("pubsub_context, topic, values", testdata) -def test_high_volume_messages( - pubsub_context: Callable[[], Any], topic: Any, values: list[Any] -) -> None: - """Test that all 5k messages are received correctly. - Limited to 5k because ros transport cannot handle more. - Might want to have separate expectations per transport later - """ - with pubsub_context() as x: - # Create a list to capture received messages - received_messages: list[Any] = [] - last_message_time = [time.time()] # Use list to allow modification in callback - - # Define callback function - def callback(message: Any, topic: Any) -> None: - received_messages.append(message) - last_message_time[0] = time.time() - - # Subscribe to the topic - x.subscribe(topic, callback) - - # Publish 5000 messages - num_messages = 5000 - for _ in range(num_messages): - x.publish(topic, values[0]) - - # Wait until no messages received for 0.5 seconds - timeout = 2.0 # Maximum time to wait - stable_duration = 0.1 # Time without new messages to consider done - start_time = time.time() - - while time.time() - start_time < timeout: - if time.time() - last_message_time[0] >= stable_duration: - break - time.sleep(0.1) - - # Capture count and clear list to avoid printing huge list on failure - received_len = len(received_messages) - received_messages.clear() - assert received_len == num_messages, f"Expected {num_messages} messages, got {received_len}" diff --git a/dimos/protocol/rpc/__init__.py b/dimos/protocol/rpc/__init__.py deleted file mode 100644 index 1eb892d956..0000000000 --- a/dimos/protocol/rpc/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.protocol.rpc.pubsubrpc import LCMRPC, ShmRPC -from dimos.protocol.rpc.spec import RPCClient, RPCServer, RPCSpec - -__all__ = ["LCMRPC", "RPCClient", "RPCServer", "RPCSpec", "ShmRPC"] diff --git a/dimos/protocol/rpc/pubsubrpc.py b/dimos/protocol/rpc/pubsubrpc.py deleted file mode 100644 index 3b77227218..0000000000 --- a/dimos/protocol/rpc/pubsubrpc.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import abstractmethod -from collections.abc import Callable -from concurrent.futures import ThreadPoolExecutor -import threading -import time -from typing import ( - TYPE_CHECKING, - Any, - Generic, - TypedDict, - TypeVar, -) - -from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH -from dimos.protocol.pubsub.impl.lcmpubsub import PickleLCM, Topic -from dimos.protocol.pubsub.impl.shmpubsub import PickleSharedMemory -from dimos.protocol.pubsub.spec import PubSub -from dimos.protocol.rpc.rpc_utils import deserialize_exception, serialize_exception -from dimos.protocol.rpc.spec import Args, RPCSpec -from dimos.utils.generic import short_id -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from types import FunctionType - -logger = setup_logger() - -MsgT = TypeVar("MsgT") -TopicT = TypeVar("TopicT") - -# (name, true_if_response_topic) -> TopicT -TopicGen = Callable[[str, bool], TopicT] -MsgGen = Callable[[str, list], MsgT] # type: ignore[type-arg] - - -class RPCReq(TypedDict): - id: float | None - name: str - args: Args - - -class RPCRes(TypedDict, total=False): - id: float - res: Any - exception: dict[str, Any] | None # Contains exception info: type, message, traceback - - -class PubSubRPCMixin(RPCSpec, PubSub[TopicT, MsgT], Generic[TopicT, MsgT]): - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - # Thread pool for RPC handler execution (prevents deadlock in nested calls) - self._call_thread_pool: ThreadPoolExecutor | None = None - self._call_thread_pool_lock = threading.RLock() - self._call_thread_pool_max_workers = 50 - - # Shared response subscriptions: one per RPC name instead of one per call - # Maps str(topic_res) -> (subscription, {msg_id -> callback}) - self._response_subs: dict[str, tuple[Any, dict[float, Callable[..., Any]]]] = {} - self._response_subs_lock = threading.RLock() - - # Message ID counter for unique IDs even with concurrent calls - self._msg_id_counter = 0 - self._msg_id_lock = threading.Lock() - - def __getstate__(self) -> dict[str, Any]: - state: dict[str, Any] - if hasattr(super(), "__getstate__"): - state = super().__getstate__() # type: ignore[assignment, misc] - else: - state = self.__dict__.copy() - - # Exclude unpicklable attributes when serializing. - state.pop("_call_thread_pool", None) - state.pop("_call_thread_pool_lock", None) - state.pop("_response_subs", None) - state.pop("_response_subs_lock", None) - state.pop("_msg_id_lock", None) - - return state - - def __setstate__(self, state: dict[str, Any]) -> None: - if hasattr(super(), "__setstate__"): - super().__setstate__(state) # type: ignore[misc] - else: - self.__dict__.update(state) - - # Restore unserializable attributes. - self._call_thread_pool = None - self._call_thread_pool_lock = threading.RLock() - self._response_subs = {} - self._response_subs_lock = threading.RLock() - self._msg_id_lock = threading.Lock() - - @abstractmethod - def topicgen(self, name: str, req_or_res: bool) -> TopicT: ... - - def _encodeRPCReq(self, req: RPCReq) -> dict[str, Any]: - return dict(req) - - def _decodeRPCRes(self, msg: dict[Any, Any]) -> RPCRes: - return msg # type: ignore[return-value] - - def _encodeRPCRes(self, res: RPCRes) -> dict[str, Any]: - return dict(res) - - def _decodeRPCReq(self, msg: dict[Any, Any]) -> RPCReq: - return msg # type: ignore[return-value] - - def _get_call_thread_pool(self) -> ThreadPoolExecutor: - """Get or create the thread pool for RPC handler execution (lazy initialization).""" - with self._call_thread_pool_lock: - if self._call_thread_pool is None: - self._call_thread_pool = ThreadPoolExecutor( - max_workers=self._call_thread_pool_max_workers - ) - return self._call_thread_pool - - def _shutdown_thread_pool(self) -> None: - """Safely shutdown the thread pool with deadlock prevention.""" - with self._call_thread_pool_lock: - if self._call_thread_pool: - # Check if we're being called from within the thread pool - # to avoid "cannot join current thread" error - current_thread = threading.current_thread() - is_pool_thread = False - - # Check if current thread is one of the pool's threads - if hasattr(self._call_thread_pool, "_threads"): - is_pool_thread = current_thread in self._call_thread_pool._threads - elif "ThreadPoolExecutor" in current_thread.name: - # Fallback: check thread name pattern - is_pool_thread = True - - # Don't wait if we're in a pool thread to avoid deadlock - self._call_thread_pool.shutdown(wait=not is_pool_thread) - self._call_thread_pool = None - - def stop(self) -> None: - """Stop the RPC service and cleanup thread pool. - - Subclasses that override this method should call super().stop() - to ensure the thread pool is properly shutdown. - """ - self._shutdown_thread_pool() - - # Cleanup shared response subscriptions - with self._response_subs_lock: - for unsub, _ in self._response_subs.values(): - unsub() - self._response_subs.clear() - - # Call parent stop if it exists - if hasattr(super(), "stop"): - super().stop() # type: ignore[misc] - - def call(self, name: str, arguments: Args, cb: Callable | None): # type: ignore[no-untyped-def, type-arg] - if cb is None: - return self.call_nowait(name, arguments) - - return self.call_cb(name, arguments, cb) - - def call_cb(self, name: str, arguments: Args, cb: Callable[..., Any]) -> Any: - topic_req = self.topicgen(name, False) - topic_res = self.topicgen(name, True) - - # Generate unique msg_id: timestamp + counter for concurrent calls - with self._msg_id_lock: - self._msg_id_counter += 1 - msg_id = time.time() + (self._msg_id_counter / 1_000_000) - - req: RPCReq = {"name": name, "args": arguments, "id": msg_id} - - # Get or create shared subscription for this RPC's response topic - topic_res_key = str(topic_res) - with self._response_subs_lock: - if topic_res_key not in self._response_subs: - # Create shared handler that routes to callbacks by msg_id - callbacks_dict: dict[float, Callable[..., Any]] = {} - - def shared_response_handler(msg: MsgT, _: TopicT) -> None: - res = self._decodeRPCRes(msg) # type: ignore[arg-type] - res_id = res.get("id") - if res_id is None: - return - - # Look up callback for this msg_id - with self._response_subs_lock: - callback = callbacks_dict.pop(res_id, None) - - if callback is None: - return # No callback registered (already handled or timed out) - - # Check if response contains an exception - exc_data = res.get("exception") - if exc_data: - # Reconstruct the exception and pass it to the callback - from typing import cast - - from dimos.protocol.rpc.rpc_utils import SerializedException - - exc = deserialize_exception(cast("SerializedException", exc_data)) - callback(exc) - else: - # Normal response - pass the result - callback(res.get("res")) - - # Create single shared subscription - unsub = self.subscribe(topic_res, shared_response_handler) - self._response_subs[topic_res_key] = (unsub, callbacks_dict) - - # Register this call's callback - _, callbacks_dict = self._response_subs[topic_res_key] - callbacks_dict[msg_id] = cb - - # Publish request - self.publish(topic_req, self._encodeRPCReq(req)) # type: ignore[arg-type] - - # Return unsubscribe function that removes this callback from the dict - def unsubscribe_callback() -> None: - with self._response_subs_lock: - if topic_res_key in self._response_subs: - _, callbacks_dict = self._response_subs[topic_res_key] - callbacks_dict.pop(msg_id, None) - - return unsubscribe_callback - - def call_nowait(self, name: str, arguments: Args) -> None: - topic_req = self.topicgen(name, False) - req: RPCReq = {"name": name, "args": arguments, "id": None} - self.publish(topic_req, self._encodeRPCReq(req)) # type: ignore[arg-type] - - def serve_rpc(self, f: FunctionType, name: str | None = None): # type: ignore[no-untyped-def, override] - if not name: - name = f.__name__ - - topic_req = self.topicgen(name, False) - topic_res = self.topicgen(name, True) - - def receive_call(msg: MsgT, _: TopicT) -> None: - req = self._decodeRPCReq(msg) # type: ignore[arg-type] - - if req.get("name") != name: - return - - args = req.get("args") - if args is None: - return - - # Execute RPC handler in a separate thread to avoid deadlock when - # the handler makes nested RPC calls. - def execute_and_respond() -> None: - try: - response = f(*args[0], **args[1]) - req_id = req.get("id") - if req_id is not None: - self.publish(topic_res, self._encodeRPCRes({"id": req_id, "res": response})) # type: ignore[arg-type] - - except Exception as e: - logger.exception(f"Exception in RPC handler for {name}: {e}", exc_info=e) - # Send exception data to client if this was a request with an ID - req_id = req.get("id") - if req_id is not None: - exc_data = serialize_exception(e) - # Type ignore: SerializedException is compatible with dict[str, Any] - self.publish( - topic_res, - self._encodeRPCRes({"id": req_id, "exception": exc_data}), # type: ignore[arg-type, typeddict-item] - ) - - # Always use thread pool to execute RPC handlers (prevents deadlock) - self._get_call_thread_pool().submit(execute_and_respond) - - return self.subscribe(topic_req, receive_call) - - -class LCMRPC(PubSubRPCMixin[Topic, Any], PickleLCM): - def __init__(self, **kwargs: Any) -> None: - # Need to ensure PickleLCM gets initialized properly - # This is due to the diamond inheritance pattern with multiple base classes - PickleLCM.__init__(self, **kwargs) - # Initialize PubSubRPCMixin's thread pool - PubSubRPCMixin.__init__(self, **kwargs) - - def topicgen(self, name: str, req_or_res: bool) -> Topic: - suffix = "res" if req_or_res else "req" - topic = f"/rpc/{name}/{suffix}" - if len(topic) > LCM_MAX_CHANNEL_NAME_LENGTH: - topic = f"/rpc/{short_id(name)}/{suffix}" - return Topic(topic=topic) - - -class ShmRPC(PubSubRPCMixin[str, Any], PickleSharedMemory): - def __init__(self, prefer: str = "cpu", **kwargs: Any) -> None: - # Need to ensure SharedMemory gets initialized properly - # This is due to the diamond inheritance pattern with multiple base classes - PickleSharedMemory.__init__(self, prefer=prefer, **kwargs) - # Initialize PubSubRPCMixin's thread pool - PubSubRPCMixin.__init__(self, **kwargs) - - def topicgen(self, name: str, req_or_res: bool) -> str: - suffix = "res" if req_or_res else "req" - return f"/rpc/{name}/{suffix}" diff --git a/dimos/protocol/rpc/redisrpc.py b/dimos/protocol/rpc/redisrpc.py deleted file mode 100644 index 32c3794bf4..0000000000 --- a/dimos/protocol/rpc/redisrpc.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.protocol.pubsub.impl.redispubsub import Redis -from dimos.protocol.rpc.pubsubrpc import PubSubRPCMixin - - -class RedisRPC(PubSubRPCMixin, Redis): # type: ignore[type-arg] - def topicgen(self, name: str, req_or_res: bool) -> str: - return f"/rpc/{name}/{'res' if req_or_res else 'req'}" diff --git a/dimos/protocol/rpc/rpc_utils.py b/dimos/protocol/rpc/rpc_utils.py deleted file mode 100644 index 26ab281e45..0000000000 --- a/dimos/protocol/rpc/rpc_utils.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Utilities for serializing and deserializing exceptions for RPC transport.""" - -from __future__ import annotations - -import traceback -from typing import Any, TypedDict - - -class SerializedException(TypedDict): - """Type for serialized exception data.""" - - type_name: str - type_module: str - args: tuple[Any, ...] - traceback: str - - -class RemoteError(Exception): - """Exception that was raised on a remote RPC server. - - Preserves the original exception type and full stack trace from the remote side. - """ - - def __init__( - self, type_name: str, type_module: str, args: tuple[Any, ...], traceback: str - ) -> None: - super().__init__(*args if args else (f"Remote exception: {type_name}",)) - self.remote_type = f"{type_module}.{type_name}" - self.remote_traceback = traceback - - def __str__(self) -> str: - base_msg = super().__str__() - return ( - f"[Remote {self.remote_type}] {base_msg}\n\nRemote traceback:\n{self.remote_traceback}" - ) - - -def serialize_exception(exc: Exception) -> SerializedException: - """Convert an exception to a transferable format. - - Args: - exc: The exception to serialize - - Returns: - A dictionary containing the exception data that can be transferred - """ - # Get the full traceback as a string - tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) - - return SerializedException( - type_name=type(exc).__name__, - type_module=type(exc).__module__, - args=exc.args, - traceback=tb_str, - ) - - -def deserialize_exception(exc_data: SerializedException) -> Exception: - """Reconstruct an exception from serialized data. - - For builtin exceptions, instantiates the actual type. - For custom exceptions, returns a RemoteError. - - Args: - exc_data: The serialized exception data - - Returns: - An exception that can be raised with full type and traceback info - """ - type_name = exc_data.get("type_name", "Exception") - type_module = exc_data.get("type_module", "builtins") - args: tuple[Any, ...] = exc_data.get("args", ()) - tb_str = exc_data.get("traceback", "") - - # Only reconstruct builtin exceptions - if type_module == "builtins": - try: - import builtins - - exc_class = getattr(builtins, type_name, None) - if exc_class and issubclass(exc_class, BaseException): - exc = exc_class(*args) - # Add remote traceback as __cause__ for context - exc.__cause__ = RemoteError(type_name, type_module, args, tb_str) - return exc # type: ignore[no-any-return] - except (AttributeError, TypeError): - pass - - # Use RemoteError for non-builtin or if reconstruction failed - return RemoteError(type_name, type_module, args, tb_str) diff --git a/dimos/protocol/rpc/spec.py b/dimos/protocol/rpc/spec.py deleted file mode 100644 index 47ad77e825..0000000000 --- a/dimos/protocol/rpc/spec.py +++ /dev/null @@ -1,104 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -from collections.abc import Callable -import threading -from typing import Any, Protocol, overload - - -class Empty: ... - - -Args = tuple[list, dict[str, Any]] # type: ignore[type-arg] - - -# module that we can inspect for RPCs -class RPCInspectable(Protocol): - @property - def rpcs(self) -> dict[str, Callable]: ... # type: ignore[type-arg] - - -class RPCClient(Protocol): - # if we don't provide callback, we don't get a return unsub f - @overload - def call(self, name: str, arguments: Args, cb: None) -> None: ... - - # if we provide callback, we do get return unsub f - @overload - def call(self, name: str, arguments: Args, cb: Callable[[Any], None]) -> Callable[[], Any]: ... - - def call(self, name: str, arguments: Args, cb: Callable | None) -> Callable[[], Any] | None: ... # type: ignore[type-arg] - - def call_nowait(self, name: str, arguments: Args) -> None: ... - - # we expect to crash if we don't get a return value after 10 seconds - # but callers can override this timeout for extra long functions - def call_sync( - self, name: str, arguments: Args, rpc_timeout: float | None = 120.0 - ) -> tuple[Any, Callable[[], None]]: - if name == "start": - rpc_timeout = 1200.0 # starting modules can take longer - event = threading.Event() - - def receive_value(val) -> None: # type: ignore[no-untyped-def] - event.result = val # type: ignore[attr-defined] # attach to event - event.set() - - unsub_fn = self.call(name, arguments, receive_value) - if not event.wait(rpc_timeout): - raise TimeoutError(f"RPC call to '{name}' timed out after {rpc_timeout} seconds") - - # Check if the result is an exception and raise it - result = event.result # type: ignore[attr-defined] - if isinstance(result, BaseException): - raise result - - return result, unsub_fn - - async def call_async(self, name: str, arguments: Args) -> Any: - loop = asyncio.get_event_loop() - future = loop.create_future() - - def receive_value(val) -> None: # type: ignore[no-untyped-def] - try: - # Check if the value is an exception - if isinstance(val, BaseException): - loop.call_soon_threadsafe(future.set_exception, val) - else: - loop.call_soon_threadsafe(future.set_result, val) - except Exception as e: - loop.call_soon_threadsafe(future.set_exception, e) - - self.call(name, arguments, receive_value) - - return await future - - -class RPCServer(Protocol): - def serve_rpc(self, f: Callable, name: str) -> Callable[[], None]: ... # type: ignore[type-arg] - - def serve_module_rpc(self, module: RPCInspectable, name: str | None = None) -> None: - for fname in module.rpcs.keys(): - if not name: - name = module.__class__.__name__ - - def override_f(*args, fname=fname, **kwargs): # type: ignore[no-untyped-def] - return getattr(module, fname)(*args, **kwargs) - - topic = name + "/" + fname - self.serve_rpc(override_f, topic) - - -class RPCSpec(RPCServer, RPCClient): ... diff --git a/dimos/protocol/rpc/test_lcmrpc.py b/dimos/protocol/rpc/test_lcmrpc.py deleted file mode 100644 index f31d20cf19..0000000000 --- a/dimos/protocol/rpc/test_lcmrpc.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Generator - -import pytest - -from dimos.constants import LCM_MAX_CHANNEL_NAME_LENGTH -from dimos.protocol.rpc import LCMRPC - - -@pytest.fixture -def lcmrpc() -> Generator[LCMRPC, None, None]: - ret = LCMRPC() - ret.start() - yield ret - ret.stop() - - -def test_short_name(lcmrpc) -> None: - actual = lcmrpc.topicgen("Hello/say", req_or_res=True) - assert actual.topic == "/rpc/Hello/say/res" - - -def test_long_name(lcmrpc) -> None: - long = "GreatyLongComplexExampleClassNameForTestingStuff/create" - long_topic = lcmrpc.topicgen(long, req_or_res=True).topic - assert long_topic == "/rpc/2cudPuFGMJdWxM5KZb/res" - - less_long = long[:-1] - less_long_topic = lcmrpc.topicgen(less_long, req_or_res=True).topic - assert less_long_topic == "/rpc/GreatyLongComplexExampleClassNameForTestingStuff/creat/res" - - assert len(less_long_topic) == LCM_MAX_CHANNEL_NAME_LENGTH diff --git a/dimos/protocol/rpc/test_rpc_utils.py b/dimos/protocol/rpc/test_rpc_utils.py deleted file mode 100644 index b5e6253aaf..0000000000 --- a/dimos/protocol/rpc/test_rpc_utils.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for RPC exception serialization utilities.""" - -from dimos.protocol.rpc.rpc_utils import ( - RemoteError, - deserialize_exception, - serialize_exception, -) - - -def test_exception_builtin_serialization(): - """Test serialization and deserialization of exceptions.""" - - # Test with a builtin exception - try: - raise ValueError("test error", 42) - except ValueError as e: - serialized = serialize_exception(e) - - # Check serialized format - assert serialized["type_name"] == "ValueError" - assert serialized["type_module"] == "builtins" - assert serialized["args"] == ("test error", 42) - assert "Traceback" in serialized["traceback"] - assert "test error" in serialized["traceback"] - - # Deserialize and check we get a real ValueError back - deserialized = deserialize_exception(serialized) - assert isinstance(deserialized, ValueError) - assert deserialized.args == ("test error", 42) - # Check that remote traceback is attached as cause - assert isinstance(deserialized.__cause__, RemoteError) - assert "test error" in deserialized.__cause__.remote_traceback - - -def test_exception_custom_serialization(): - # Test with a custom exception - class CustomError(Exception): - pass - - try: - raise CustomError("custom message") - except CustomError as e: - serialized = serialize_exception(e) - - # Check serialized format - assert serialized["type_name"] == "CustomError" - # Module name varies when running under pytest vs directly - assert serialized["type_module"] in ("__main__", "dimos.protocol.rpc.test_rpc_utils") - assert serialized["args"] == ("custom message",) - - # Deserialize - should get RemoteError since it's not builtin - deserialized = deserialize_exception(serialized) - assert isinstance(deserialized, RemoteError) - assert "CustomError" in deserialized.remote_type - assert "custom message" in str(deserialized) - assert "custom message" in deserialized.remote_traceback diff --git a/dimos/protocol/rpc/test_spec.py b/dimos/protocol/rpc/test_spec.py deleted file mode 100644 index c29db13703..0000000000 --- a/dimos/protocol/rpc/test_spec.py +++ /dev/null @@ -1,399 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Grid tests for RPC implementations to ensure spec compliance.""" - -import asyncio -from collections.abc import Callable -from contextlib import contextmanager -import threading -import time -from typing import Any - -import pytest - -from dimos.protocol.rpc.pubsubrpc import LCMRPC, ShmRPC -from dimos.protocol.rpc.rpc_utils import RemoteError - - -class CustomTestError(Exception): - """Custom exception for testing.""" - - pass - - -# Build testdata list with available implementations -testdata: list[tuple[Callable[[], Any], str]] = [] - - -# Context managers for different RPC implementations -@contextmanager -def lcm_rpc_context(): - """Context manager for LCMRPC implementation.""" - from dimos.protocol.service.lcmservice import autoconf - - autoconf() - server = LCMRPC() - client = LCMRPC() - server.start() - client.start() - - try: - yield server, client - finally: - server.stop() - client.stop() - - -testdata.append((lcm_rpc_context, "lcm")) - - -@contextmanager -def shm_rpc_context(): - """Context manager for Shared Memory RPC implementation.""" - # Create two separate instances that communicate through shared memory segments - server = ShmRPC(prefer="cpu") - client = ShmRPC(prefer="cpu") - server.start() - client.start() - - try: - yield server, client - finally: - server.stop() - client.stop() - - -testdata.append((shm_rpc_context, "shm")) - -# Try to add RedisRPC if available -try: - from dimos.protocol.rpc.redisrpc import RedisRPC - - @contextmanager - def redis_rpc_context(): - """Context manager for RedisRPC implementation.""" - server = RedisRPC() - client = RedisRPC() - server.start() - client.start() - - try: - yield server, client - finally: - server.stop() - client.stop() - - testdata.append((redis_rpc_context, "redis")) -except (ImportError, ConnectionError): - print("RedisRPC not available") - - -# Test functions that will be served -def add_function(a: int, b: int) -> int: - """Simple addition function for testing.""" - return a + b - - -def failing_function(msg: str) -> str: - """Function that raises exceptions for testing.""" - if msg == "fail": - raise ValueError("Test error message") - elif msg == "custom": - raise CustomTestError("Custom error") - return f"Success: {msg}" - - -def slow_function(delay: float) -> str: - """Function that takes time to execute.""" - time.sleep(delay) - return f"Completed after {delay} seconds" - - -# Grid tests - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_basic_sync_call(rpc_context, impl_name: str) -> None: - """Test basic synchronous RPC calls.""" - with rpc_context() as (server, client): - # Serve the function - unsub = server.serve_rpc(add_function, "add") - - try: - # Make sync call - result, _ = client.call_sync("add", ([5, 3], {}), rpc_timeout=2.0) - assert result == 8 - - # Test with different arguments - result, _ = client.call_sync("add", ([10, -2], {}), rpc_timeout=2.0) - assert result == 8 - - finally: - unsub() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -@pytest.mark.asyncio -@pytest.mark.skip( - reason="Async RPC calls have a deadlock issue when run in the full test suite (works in isolation)" -) -async def test_async_call(rpc_context, impl_name: str) -> None: - """Test asynchronous RPC calls.""" - with rpc_context() as (server, client): - # Serve the function - unsub = server.serve_rpc(add_function, "add_async") - - try: - # Make async call - result = await client.call_async("add_async", ([7, 4], {})) - assert result == 11 - - # Test multiple async calls - results = await asyncio.gather( - client.call_async("add_async", ([1, 2], {})), - client.call_async("add_async", ([3, 4], {})), - client.call_async("add_async", ([5, 6], {})), - ) - assert results == [3, 7, 11] - - finally: - unsub() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_callback_call(rpc_context, impl_name: str) -> None: - """Test callback-based RPC calls.""" - with rpc_context() as (server, client): - # Serve the function - unsub_server = server.serve_rpc(add_function, "add_callback") - - try: - # Test with callback - event = threading.Event() - received_value = None - - def callback(val) -> None: - nonlocal received_value - received_value = val - event.set() - - client.call("add_callback", ([20, 22], {}), callback) - assert event.wait(2.0) - assert received_value == 42 - - finally: - unsub_server() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_exception_handling_sync(rpc_context, impl_name: str) -> None: - """Test that exceptions are properly passed through sync RPC calls.""" - with rpc_context() as (server, client): - # Serve the function that can raise exceptions - unsub = server.serve_rpc(failing_function, "test_exc") - - try: - # Test successful call - result, _ = client.call_sync("test_exc", (["ok"], {}), rpc_timeout=2.0) - assert result == "Success: ok" - - # Test builtin exception - should raise actual ValueError - with pytest.raises(ValueError) as exc_info: - client.call_sync("test_exc", (["fail"], {}), rpc_timeout=2.0) - assert "Test error message" in str(exc_info.value) - # Check that the cause contains the remote traceback - assert isinstance(exc_info.value.__cause__, RemoteError) - assert "failing_function" in exc_info.value.__cause__.remote_traceback - - # Test custom exception - should raise RemoteError - with pytest.raises(RemoteError) as exc_info: - client.call_sync("test_exc", (["custom"], {}), rpc_timeout=2.0) - assert "Custom error" in str(exc_info.value) - assert "CustomTestError" in exc_info.value.remote_type - assert "failing_function" in exc_info.value.remote_traceback - - finally: - unsub() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -@pytest.mark.asyncio -async def test_exception_handling_async(rpc_context, impl_name: str) -> None: - """Test that exceptions are properly passed through async RPC calls.""" - with rpc_context() as (server, client): - # Serve the function that can raise exceptions - unsub = server.serve_rpc(failing_function, "test_exc_async") - - try: - # Test successful call - result = await client.call_async("test_exc_async", (["ok"], {})) - assert result == "Success: ok" - - # Test builtin exception - with pytest.raises(ValueError) as exc_info: - await client.call_async("test_exc_async", (["fail"], {})) - assert "Test error message" in str(exc_info.value) - assert isinstance(exc_info.value.__cause__, RemoteError) - - # Test custom exception - with pytest.raises(RemoteError) as exc_info: - await client.call_async("test_exc_async", (["custom"], {})) - assert "Custom error" in str(exc_info.value) - assert "CustomTestError" in exc_info.value.remote_type - - finally: - unsub() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_exception_handling_callback(rpc_context, impl_name: str) -> None: - """Test that exceptions are properly passed through callback-based RPC calls.""" - with rpc_context() as (server, client): - # Serve the function that can raise exceptions - unsub_server = server.serve_rpc(failing_function, "test_exc_cb") - - try: - # Test with callback - exception should be passed to callback - event = threading.Event() - received_value = None - - def callback(val) -> None: - nonlocal received_value - received_value = val - event.set() - - # Test successful call - client.call("test_exc_cb", (["ok"], {}), callback) - assert event.wait(2.0) - assert received_value == "Success: ok" - event.clear() - - # Test failed call - exception should be passed to callback - client.call("test_exc_cb", (["fail"], {}), callback) - assert event.wait(2.0) - assert isinstance(received_value, ValueError) - assert "Test error message" in str(received_value) - assert isinstance(received_value.__cause__, RemoteError) - - finally: - unsub_server() - - -@pytest.mark.integration -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_timeout(rpc_context, impl_name: str) -> None: - """Test that RPC calls properly timeout.""" - with rpc_context() as (server, client): - # Serve a slow function - unsub = server.serve_rpc(slow_function, "slow") - - try: - # Call with short timeout should fail - # Using 10 seconds sleep to ensure it would definitely timeout - with pytest.raises(TimeoutError) as exc_info: - client.call_sync("slow", ([2.0], {}), rpc_timeout=0.1) - assert "timed out" in str(exc_info.value) - - # Call with sufficient timeout should succeed - result, _ = client.call_sync("slow", ([0.01], {}), rpc_timeout=1.0) - assert "Completed after 0.01 seconds" in result - - finally: - unsub() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_nonexistent_service(rpc_context, impl_name: str) -> None: - """Test calling a service that doesn't exist.""" - with rpc_context() as (_server, client): - # Don't serve any function, just try to call - with pytest.raises(TimeoutError) as exc_info: - client.call_sync("nonexistent", ([1, 2], {}), rpc_timeout=0.1) - assert "nonexistent" in str(exc_info.value) - assert "timed out" in str(exc_info.value) - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_multiple_services(rpc_context, impl_name: str) -> None: - """Test serving multiple RPC functions simultaneously.""" - with rpc_context() as (server, client): - # Serve multiple functions - unsub1 = server.serve_rpc(add_function, "service1") - unsub2 = server.serve_rpc(lambda x: x * 2, "service2") - unsub3 = server.serve_rpc(lambda s: s.upper(), "service3") - - try: - # Call all services - result1, _ = client.call_sync("service1", ([3, 4], {}), rpc_timeout=1.0) - assert result1 == 7 - - result2, _ = client.call_sync("service2", ([21], {}), rpc_timeout=1.0) - assert result2 == 42 - - result3, _ = client.call_sync("service3", (["hello"], {}), rpc_timeout=1.0) - assert result3 == "HELLO" - - finally: - unsub1() - unsub2() - unsub3() - - -@pytest.mark.parametrize("rpc_context, impl_name", testdata) -def test_concurrent_calls(rpc_context, impl_name: str) -> None: - """Test making multiple concurrent RPC calls.""" - # Skip for SharedMemory - double-buffered architecture can't handle concurrent bursts - # The channel only holds 2 frames, so 1000 rapid concurrent responses overwrite each other - if impl_name == "shm": - pytest.skip("SharedMemory uses double-buffering; can't handle 1000 concurrent responses") - - with rpc_context() as (server, client): - # Serve a function that we'll call concurrently - unsub = server.serve_rpc(add_function, "concurrent_add") - - try: - # Make multiple concurrent calls using threads - results = [] - threads = [] - - def make_call(a, b) -> None: - result, _ = client.call_sync("concurrent_add", ([a, b], {}), rpc_timeout=2.0) - results.append(result) - - # Start 1000 concurrent calls - for i in range(1000): - t = threading.Thread(target=make_call, args=(i, i + 1)) - threads.append(t) - t.start() - - # Wait for all threads to complete - for t in threads: - t.join(timeout=10.0) - - # Verify all calls succeeded - assert len(results) == 1000 - # Results should be [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] but may be in any order - expected = [i + (i + 1) for i in range(1000)] - assert sorted(results) == sorted(expected) - - finally: - unsub() - - -if __name__ == "__main__": - # Run tests for debugging - pytest.main([__file__, "-v"]) diff --git a/dimos/protocol/service/__init__.py b/dimos/protocol/service/__init__.py deleted file mode 100644 index fb9df08ca9..0000000000 --- a/dimos/protocol/service/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from dimos.protocol.service.lcmservice import LCMService -from dimos.protocol.service.spec import Configurable as Configurable, Service as Service - -__all__ = [ - "Configurable", - "LCMService", - "Service", -] diff --git a/dimos/protocol/service/ddsservice.py b/dimos/protocol/service/ddsservice.py deleted file mode 100644 index 6ed04c07ad..0000000000 --- a/dimos/protocol/service/ddsservice.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from dataclasses import dataclass -import threading -from typing import TYPE_CHECKING, Any - -try: - from cyclonedds.domain import DomainParticipant - - DDS_AVAILABLE = True -except ImportError: - DDS_AVAILABLE = False - DomainParticipant = None # type: ignore[assignment, misc] - -from dimos.protocol.service.spec import Service -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from cyclonedds.qos import Qos - -logger = setup_logger() - -_participants: dict[int, DomainParticipant] = {} -_participants_lock = threading.Lock() - - -@dataclass -class DDSConfig: - """Configuration for DDS service.""" - - domain_id: int = 0 - qos: Qos | None = None - - -class DDSService(Service[DDSConfig]): - default_config = DDSConfig - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - def start(self) -> None: - """Start the DDS service.""" - domain_id = self.config.domain_id - with _participants_lock: - if domain_id not in _participants: - _participants[domain_id] = DomainParticipant(domain_id) - logger.info(f"DDS service started with Cyclone DDS domain {domain_id}") - super().start() - - def stop(self) -> None: - """Stop the DDS service.""" - super().stop() - - @property - def participant(self) -> DomainParticipant: - """Get the DomainParticipant instance for this service's domain.""" - domain_id = self.config.domain_id - if domain_id not in _participants: - raise RuntimeError(f"DomainParticipant not initialized for domain {domain_id}") - return _participants[domain_id] - - -__all__ = [ - "DDSConfig", - "DDSService", -] diff --git a/dimos/protocol/service/lcmservice.py b/dimos/protocol/service/lcmservice.py deleted file mode 100644 index 4655780fb3..0000000000 --- a/dimos/protocol/service/lcmservice.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from concurrent.futures import ThreadPoolExecutor -from dataclasses import dataclass -import os -import platform -import threading -import traceback - -import lcm - -from dimos.protocol.service.spec import Service -from dimos.protocol.service.system_configurator import ( - BufferConfiguratorLinux, - BufferConfiguratorMacOS, - MaxFileConfiguratorMacOS, - MulticastConfiguratorLinux, - MulticastConfiguratorMacOS, - SystemConfigurator, - configure_system, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -_DEFAULT_LCM_HOST = "239.255.76.67" -_DEFAULT_LCM_PORT = "7667" -# LCM_DEFAULT_URL is used by LCM (we didn't pick that env var name) -_DEFAULT_LCM_URL = os.getenv( - "LCM_DEFAULT_URL", f"udpm://{_DEFAULT_LCM_HOST}:{_DEFAULT_LCM_PORT}?ttl=0" -) - - -def autoconf(check_only: bool = False) -> None: - # check multicast and buffer sizes - system = platform.system() - checks: list[SystemConfigurator] = [] - if system == "Linux": - checks = [ - MulticastConfiguratorLinux(loopback_interface="lo"), - BufferConfiguratorLinux(), - ] - elif system == "Darwin": - checks = [ - MulticastConfiguratorMacOS(loopback_interface="lo0"), - BufferConfiguratorMacOS(), - MaxFileConfiguratorMacOS(), - ] - else: - logger.error(f"System configuration not supported on {system}") - return - configure_system(checks, check_only=check_only) - - -@dataclass -class LCMConfig: - ttl: int = 0 - url: str | None = None - autoconf: bool = True - lcm: lcm.LCM | None = None - - def __post_init__(self) -> None: - if self.url is None: - self.url = _DEFAULT_LCM_URL - - -_LCM_LOOP_TIMEOUT = 50 - - -# this class just sets up cpp LCM instance -# and runs its handle loop in a thread -# higher order stuff is done by pubsub/impl/lcmpubsub.py -class LCMService(Service[LCMConfig]): - default_config = LCMConfig - l: lcm.LCM | None - _stop_event: threading.Event - _l_lock: threading.Lock - _thread: threading.Thread | None - _call_thread_pool: ThreadPoolExecutor | None = None - _call_thread_pool_lock: threading.RLock = threading.RLock() - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - # we support passing an existing LCM instance - if self.config.lcm: - self.l = self.config.lcm - else: - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() - - self._l_lock = threading.Lock() - self._stop_event = threading.Event() - self._thread = None - - def __getstate__(self): # type: ignore[no-untyped-def] - """Exclude unpicklable runtime attributes when serializing.""" - state = self.__dict__.copy() - # Remove unpicklable attributes - state.pop("l", None) - state.pop("_stop_event", None) - state.pop("_thread", None) - state.pop("_l_lock", None) - state.pop("_call_thread_pool", None) - state.pop("_call_thread_pool_lock", None) - return state - - def __setstate__(self, state) -> None: # type: ignore[no-untyped-def] - """Restore object from pickled state.""" - self.__dict__.update(state) - # Reinitialize runtime attributes - self.l = None - self._stop_event = threading.Event() - self._thread = None - self._l_lock = threading.Lock() - self._call_thread_pool = None - self._call_thread_pool_lock = threading.RLock() - - def start(self) -> None: - # Reinitialize LCM if it's None (e.g., after unpickling) - if self.l is None: - if self.config.lcm: - self.l = self.config.lcm - else: - self.l = lcm.LCM(self.config.url) if self.config.url else lcm.LCM() - - try: - autoconf(check_only=not self.config.autoconf) - except Exception as e: - print(f"Error checking system configuration: {e}") - - self._stop_event.clear() - self._thread = threading.Thread(target=self._lcm_loop) - self._thread.daemon = True - self._thread.start() - - def _lcm_loop(self) -> None: - """LCM message handling loop.""" - while not self._stop_event.is_set(): - try: - with self._l_lock: - if self.l is None: - break - self.l.handle_timeout(_LCM_LOOP_TIMEOUT) - except Exception as e: - stack_trace = traceback.format_exc() - print(f"Error in LCM handling: {e}\n{stack_trace}") - - def stop(self) -> None: - """Stop the LCM loop.""" - self._stop_event.set() - if self._thread is not None: - # Only join if we're not the LCM thread (avoid "cannot join current thread") - if threading.current_thread() != self._thread: - self._thread.join(timeout=1.0) - if self._thread.is_alive(): - logger.warning("LCM thread did not stop cleanly within timeout") - - # Clean up LCM instance if we created it - if not self.config.lcm: - with self._l_lock: - if self.l is not None: - del self.l - self.l = None - - with self._call_thread_pool_lock: - if self._call_thread_pool: - # Check if we're being called from within the thread pool - # If so, we can't wait for shutdown (would cause "cannot join current thread") - current_thread = threading.current_thread() - is_pool_thread = False - - # Check if current thread is one of the pool's threads - # ThreadPoolExecutor threads have names like "ThreadPoolExecutor-N_M" - if hasattr(self._call_thread_pool, "_threads"): - is_pool_thread = current_thread in self._call_thread_pool._threads - elif "ThreadPoolExecutor" in current_thread.name: - # Fallback: check thread name pattern - is_pool_thread = True - - # Don't wait if we're in a pool thread to avoid deadlock - self._call_thread_pool.shutdown(wait=not is_pool_thread) - self._call_thread_pool = None - - def _get_call_thread_pool(self) -> ThreadPoolExecutor: - with self._call_thread_pool_lock: - if self._call_thread_pool is None: - self._call_thread_pool = ThreadPoolExecutor(max_workers=4) - return self._call_thread_pool diff --git a/dimos/protocol/service/spec.py b/dimos/protocol/service/spec.py deleted file mode 100644 index c4e6758614..0000000000 --- a/dimos/protocol/service/spec.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC -from typing import Generic, TypeVar - -# Generic type for service configuration -ConfigT = TypeVar("ConfigT") - - -class Configurable(Generic[ConfigT]): - default_config: type[ConfigT] - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - self.config: ConfigT = self.default_config(**kwargs) - - -class Service(Configurable[ConfigT], ABC): - def start(self) -> None: - # Only call super().start() if it exists - if hasattr(super(), "start"): - super().start() # type: ignore[misc] - - def stop(self) -> None: - # Only call super().stop() if it exists - if hasattr(super(), "stop"): - super().stop() # type: ignore[misc] diff --git a/dimos/protocol/service/system_configurator.py b/dimos/protocol/service/system_configurator.py deleted file mode 100644 index 44b8c45276..0000000000 --- a/dimos/protocol/service/system_configurator.py +++ /dev/null @@ -1,436 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from functools import cache -import os -import re -import resource -import subprocess -from typing import Any - -# ----------------------------- sudo helpers ----------------------------- - - -@cache -def _is_root_user() -> bool: - try: - return os.geteuid() == 0 - except AttributeError: - return False - - -def sudo_run(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[str]: - if _is_root_user(): - return subprocess.run(list(args), **kwargs) - return subprocess.run(["sudo", *args], **kwargs) - - -def _read_sysctl_int(name: str) -> int | None: - try: - result = subprocess.run(["sysctl", name], capture_output=True, text=True) - if result.returncode != 0: - print( - f"[sysctl] ERROR: `sysctl {name}` rc={result.returncode} stderr={result.stderr!r}" - ) - return None - - text = result.stdout.strip().replace(":", "=") - if "=" not in text: - print(f"[sysctl] ERROR: unexpected output for {name}: {text!r}") - return None - - return int(text.split("=", 1)[1].strip()) - except Exception as error: - print(f"[sysctl] ERROR: reading {name}: {error}") - return None - - -def _write_sysctl_int(name: str, value: int) -> None: - sudo_run("sysctl", "-w", f"{name}={value}", check=True, text=True, capture_output=False) - - -# -------------------------- base class for system config checks/requirements -------------------------- - - -class SystemConfigurator(ABC): - critical: bool = False - - @abstractmethod - def check(self) -> bool: - """Return True if configured. Log errors and return False on uncertainty.""" - raise NotImplementedError - - @abstractmethod - def explanation(self) -> str | None: - """ - Return a human-readable summary of what would be done (sudo commands) if not configured. - Return None when no changes are needed. - """ - raise NotImplementedError - - @abstractmethod - def fix(self) -> None: - """Apply fixes (may attempt sudo, catch, and apply fallback measures if needed).""" - raise NotImplementedError - - -# ----------------------------- generic enforcement of system configs ----------------------------- - - -def configure_system(checks: list[SystemConfigurator], check_only: bool = False) -> None: - if os.environ.get("CI"): - print("CI environment detected: skipping system configuration.") - return - - # run checks - failing = [check for check in checks if not check.check()] - if not failing: - return - - # ask for permission to modify system - explanations: list[str] = [msg for check in failing if (msg := check.explanation()) is not None] - - if explanations: - print("System configuration changes are recommended/required:\n") - print("\n\n".join(explanations)) - print() - - if check_only: - return - - try: - answer = input("Apply these changes now? [y/N]: ").strip().lower() - except (EOFError, KeyboardInterrupt): - answer = "" - - if answer not in ("y", "yes"): - if any(check.critical for check in failing): - raise SystemExit(1) - return - - for check in failing: - try: - check.fix() - except subprocess.CalledProcessError as error: - if check.critical: - print(f"Critical fix failed rc={error.returncode}") - print(f"stdout: {error.stdout}") - print(f"stderr: {error.stderr}") - raise - print(f"Optional improvement failed: rc={error.returncode}") - print(f"stdout: {error.stdout}") - print(f"stderr: {error.stderr}") - - print("System configuration completed.") - - -# ------------------------------ specific checks: multicast ------------------------------ - - -class MulticastConfiguratorLinux(SystemConfigurator): - critical = True - MULTICAST_PREFIX = "224.0.0.0/4" - - def __init__(self, loopback_interface: str = "lo"): - self.loopback_interface = loopback_interface - - self.loopback_ok: bool | None = None - self.route_ok: bool | None = None - - self.enable_multicast_cmd = [ - "ip", - "link", - "set", - self.loopback_interface, - "multicast", - "on", - ] - self.add_route_cmd = [ - "ip", - "route", - "add", - self.MULTICAST_PREFIX, - "dev", - self.loopback_interface, - ] - - def check(self) -> bool: - # Verify `ip` exists (iproute2) - try: - subprocess.run(["ip", "-V"], capture_output=True, text=True, check=False) - except FileNotFoundError as error: - print( - f"ERROR: `ip` not found (iproute2 missing, did you install system requirements?): {error}" - ) - self.loopback_ok = self.route_ok = False - return False - except Exception as error: - print(f"ERROR: failed probing `ip`: {error}") - self.loopback_ok = self.route_ok = False - return False - - # check MULTICAST on loopback - try: - result = subprocess.run( - ["ip", "-o", "link", "show", self.loopback_interface], - capture_output=True, - text=True, - ) - if result.returncode != 0: - print( - f"ERROR: `ip link show {self.loopback_interface}` rc={result.returncode} " - f"stderr={result.stderr!r}" - ) - self.loopback_ok = False - else: - match = re.search(r"<([^>]*)>", result.stdout) - flags = { - flag.strip().upper() - for flag in (match.group(1).split(",") if match else []) - if flag.strip() - } - self.loopback_ok = "MULTICAST" in flags - except Exception as error: - print(f"ERROR: failed checking loopback multicast: {error}") - self.loopback_ok = False - - # Check if multicast route exists - try: - result = subprocess.run( - ["ip", "-o", "route", "show", self.MULTICAST_PREFIX], - capture_output=True, - text=True, - ) - if result.returncode != 0: - print( - f"ERROR: `ip route show {self.MULTICAST_PREFIX}` rc={result.returncode} " - f"stderr={result.stderr!r}" - ) - self.route_ok = False - else: - self.route_ok = bool(result.stdout.strip()) - except Exception as error: - print(f"ERROR: failed checking multicast route: {error}") - self.route_ok = False - - return bool(self.loopback_ok and self.route_ok) - - def explanation(self) -> str | None: - output = "" - if not self.loopback_ok: - output += f"- Multicast: sudo {' '.join(self.enable_multicast_cmd)}\n" - if not self.route_ok: - output += f"- Multicast: sudo {' '.join(self.add_route_cmd)}\n" - return output - - def fix(self) -> None: - if not self.loopback_ok: - sudo_run(*self.enable_multicast_cmd, check=True, text=True, capture_output=True) - if not self.route_ok: - sudo_run(*self.add_route_cmd, check=True, text=True, capture_output=True) - - -class MulticastConfiguratorMacOS(SystemConfigurator): - critical = True - - def __init__(self, loopback_interface: str = "lo0"): - self.loopback_interface = loopback_interface - self.add_route_cmd = [ - "route", - "add", - "-net", - "224.0.0.0/4", - "-interface", - self.loopback_interface, - ] - - def check(self) -> bool: - # `netstat -nr` shows the routing table. We search for a 224/4 route entry. - try: - result = subprocess.run(["netstat", "-nr"], capture_output=True, text=True) - if result.returncode != 0: - print(f"ERROR: `netstat -nr` rc={result.returncode} stderr={result.stderr!r}") - return False - - route_ok = ("224.0.0.0/4" in result.stdout) or ("224.0.0/4" in result.stdout) - return bool(route_ok) - except Exception as error: - print(f"ERROR: failed checking multicast route via netstat: {error}") - return False - - def explanation(self) -> str | None: - return f"Multicast: - sudo {' '.join(self.add_route_cmd)}" - - def fix(self) -> None: - sudo_run(*self.add_route_cmd, check=True, text=True, capture_output=True) - - -# ------------------------------ specific checks: buffers ------------------------------ - -IDEAL_RMEM_SIZE = 67_108_864 # 64MB - - -class BufferConfiguratorLinux(SystemConfigurator): - critical = False - - TARGET_RMEM_SIZE = IDEAL_RMEM_SIZE - - def __init__(self) -> None: - self.needs: list[tuple[str, int]] = [] # (key, target_value) - - def check(self) -> bool: - self.needs.clear() - for key, target in [ - ("net.core.rmem_max", self.TARGET_RMEM_SIZE), - ("net.core.rmem_default", self.TARGET_RMEM_SIZE), - ]: - current = _read_sysctl_int(key) - if current is None or current < target: - self.needs.append((key, target)) - return not self.needs - - def explanation(self) -> str | None: - lines = [] - for key, target in self.needs: - lines.append(f"- socket buffer optimization: sudo sysctl -w {key}={target}") - return "\n".join(lines) - - def fix(self) -> None: - for key, target in self.needs: - _write_sysctl_int(key, target) - - -class BufferConfiguratorMacOS(SystemConfigurator): - critical = False - MAX_POSSIBLE_RECVSPACE = 2_097_152 - MAX_POSSIBLE_BUFFER_SIZE = 8_388_608 - MAX_POSSIBLE_DGRAM_SIZE = 65_535 - # these values are based on macos 26 - - TARGET_BUFFER_SIZE = MAX_POSSIBLE_BUFFER_SIZE - TARGET_RECVSPACE = MAX_POSSIBLE_RECVSPACE # we want this to be IDEAL_RMEM_SIZE but MacOS 26 (and probably in general) doesn't support it - TARGET_DGRAM_SIZE = MAX_POSSIBLE_DGRAM_SIZE - - def __init__(self) -> None: - self.needs: list[tuple[str, int]] = [] - - def check(self) -> bool: - self.needs.clear() - for key, target in [ - ("kern.ipc.maxsockbuf", self.TARGET_BUFFER_SIZE), - ("net.inet.udp.recvspace", self.TARGET_RECVSPACE), - ("net.inet.udp.maxdgram", self.TARGET_DGRAM_SIZE), - ]: - current = _read_sysctl_int(key) - if current is None or current < target: - self.needs.append((key, target)) - return not self.needs - - def explanation(self) -> str | None: - lines = [] - for key, target in self.needs: - lines.append(f"- sudo sysctl -w {key}={target}") - return "\n".join(lines) - - def fix(self) -> None: - for key, target in self.needs: - _write_sysctl_int(key, target) - - -# ------------------------------ specific checks: ulimit ------------------------------ - - -class MaxFileConfiguratorMacOS(SystemConfigurator): - """Ensure the open file descriptor limit (ulimit -n) is at least TARGET_FILE_COUNT_LIMIT.""" - - critical = False - TARGET_FILE_COUNT_LIMIT = 65536 - - def __init__(self, target: int = TARGET_FILE_COUNT_LIMIT): - self.target = target - self.current_soft: int = 0 - self.current_hard: int = 0 - self.can_fix_without_sudo: bool = False - - def check(self) -> bool: - try: - self.current_soft, self.current_hard = resource.getrlimit(resource.RLIMIT_NOFILE) - except Exception as error: - print(f"[ulimit] ERROR: failed to get RLIMIT_NOFILE: {error}") - return False - - if self.current_soft >= self.target: - return True - - # Check if we can raise to target without sudo (hard limit is high enough) - self.can_fix_without_sudo = self.current_hard >= self.target - return False - - def explanation(self) -> str | None: - lines = [] - if self.can_fix_without_sudo: - lines.append(f"- Raise soft file count limit to {self.target} (no sudo required)") - else: - lines.append(f"- Raise soft file count limit to {min(self.target, self.current_hard)}") - lines.append( - f"- Raise hard limit via: sudo launchctl limit maxfiles {self.target} {self.target}" - ) - return "\n".join(lines) - - def fix(self) -> None: - if self.current_soft >= self.target: - return - - if self.can_fix_without_sudo: - # Hard limit is sufficient, just raise the soft limit - try: - resource.setrlimit(resource.RLIMIT_NOFILE, (self.target, self.current_hard)) - except Exception as error: - print(f"[ulimit] ERROR: failed to set soft limit: {error}") - raise - else: - # Need to raise both soft and hard limits via launchctl - try: - sudo_run( - "launchctl", - "limit", - "maxfiles", - str(self.target), - str(self.target), - check=True, - text=True, - capture_output=True, - ) - except subprocess.CalledProcessError as error: - print(f"[ulimit] WARNING: launchctl failed: {error.stderr}") - # Fallback: raise soft limit as high as the current hard limit allows - if self.current_hard > self.current_soft: - try: - resource.setrlimit( - resource.RLIMIT_NOFILE, (self.current_hard, self.current_hard) - ) - except Exception as fallback_error: - print(f"[ulimit] ERROR: fallback also failed: {fallback_error}") - raise - - # After launchctl, try to apply the new limit to the current process - try: - resource.setrlimit(resource.RLIMIT_NOFILE, (self.target, self.target)) - except Exception as error: - print( - f"[ulimit] WARNING: could not apply to current process (restart may be required): {error}" - ) diff --git a/dimos/protocol/service/test_lcmservice.py b/dimos/protocol/service/test_lcmservice.py deleted file mode 100644 index 4231302426..0000000000 --- a/dimos/protocol/service/test_lcmservice.py +++ /dev/null @@ -1,314 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time -from unittest.mock import MagicMock, patch - -from dimos.protocol.pubsub.impl.lcmpubsub import Topic -from dimos.protocol.service.lcmservice import ( - _DEFAULT_LCM_URL, - LCMConfig, - LCMService, - autoconf, -) -from dimos.protocol.service.system_configurator import ( - BufferConfiguratorLinux, - BufferConfiguratorMacOS, - MaxFileConfiguratorMacOS, - MulticastConfiguratorLinux, - MulticastConfiguratorMacOS, -) - -# ----------------------------- autoconf tests ----------------------------- - - -class TestConfigureSystemForLcm: - def test_creates_linux_checks_on_linux(self) -> None: - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): - with patch("dimos.protocol.service.lcmservice.configure_system") as mock_configure: - autoconf() - mock_configure.assert_called_once() - checks = mock_configure.call_args[0][0] - assert len(checks) == 2 - assert isinstance(checks[0], MulticastConfiguratorLinux) - assert isinstance(checks[1], BufferConfiguratorLinux) - assert checks[0].loopback_interface == "lo" - - def test_creates_macos_checks_on_darwin(self) -> None: - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Darwin"): - with patch("dimos.protocol.service.lcmservice.configure_system") as mock_configure: - autoconf() - mock_configure.assert_called_once() - checks = mock_configure.call_args[0][0] - assert len(checks) == 3 - assert isinstance(checks[0], MulticastConfiguratorMacOS) - assert isinstance(checks[1], BufferConfiguratorMacOS) - assert isinstance(checks[2], MaxFileConfiguratorMacOS) - assert checks[0].loopback_interface == "lo0" - - def test_passes_check_only_flag(self) -> None: - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Linux"): - with patch("dimos.protocol.service.lcmservice.configure_system") as mock_configure: - autoconf(check_only=True) - mock_configure.assert_called_once() - assert mock_configure.call_args[1]["check_only"] is True - - def test_logs_error_on_unsupported_system(self) -> None: - with patch("dimos.protocol.service.lcmservice.platform.system", return_value="Windows"): - with patch("dimos.protocol.service.lcmservice.configure_system") as mock_configure: - with patch("dimos.protocol.service.lcmservice.logger") as mock_logger: - autoconf() - mock_configure.assert_not_called() - mock_logger.error.assert_called_once() - assert "Windows" in mock_logger.error.call_args[0][0] - - -# ----------------------------- LCMConfig tests ----------------------------- - - -class TestLCMConfig: - def test_default_values(self) -> None: - config = LCMConfig() - assert config.ttl == 0 - assert config.url == _DEFAULT_LCM_URL - assert config.autoconf is True - assert config.lcm is None - - def test_custom_url(self) -> None: - custom_url = "udpm://192.168.1.1:7777?ttl=1" - config = LCMConfig(url=custom_url) - assert config.url == custom_url - - def test_post_init_sets_default_url_when_none(self) -> None: - config = LCMConfig(url=None) - assert config.url == _DEFAULT_LCM_URL - - def test_autoconf_can_be_disabled(self) -> None: - config = LCMConfig(autoconf=False) - assert config.autoconf is False - - -# ----------------------------- Topic tests ----------------------------- - - -class TestTopic: - def test_str_without_lcm_type(self) -> None: - topic = Topic(topic="my_topic") - assert str(topic) == "my_topic" - - def test_str_with_lcm_type(self) -> None: - mock_type = MagicMock() - mock_type.msg_name = "TestMessage" - topic = Topic(topic="my_topic", lcm_type=mock_type) - assert str(topic) == "my_topic#TestMessage" - - -# ----------------------------- LCMService tests ----------------------------- - - -class TestLCMService: - def test_init_with_default_config(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - service = LCMService() - assert service.config.url == _DEFAULT_LCM_URL - assert service.l == mock_lcm_instance - mock_lcm_class.assert_called_once_with(_DEFAULT_LCM_URL) - - def test_init_with_custom_url(self) -> None: - custom_url = "udpm://192.168.1.1:7777?ttl=1" - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - # Pass url as kwarg, not config= - LCMService(url=custom_url) - mock_lcm_class.assert_called_once_with(custom_url) - - def test_init_with_existing_lcm_instance(self) -> None: - mock_lcm_instance = MagicMock() - - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - # Pass lcm as kwarg - service = LCMService(lcm=mock_lcm_instance) - mock_lcm_class.assert_not_called() - assert service.l == mock_lcm_instance - - def test_start_and_stop(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf"): - service = LCMService(autoconf=False) - service.start() - - # Verify thread is running - assert service._thread is not None - assert service._thread.is_alive() - - service.stop() - - # Give the thread a moment to stop - time.sleep(0.1) - assert not service._thread.is_alive() - - def test_start_calls_configure_system(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf") as mock_configure: - service = LCMService(autoconf=True) - service.start() - - # With autoconf=True, check_only should be False - mock_configure.assert_called_once_with(check_only=False) - - service.stop() - - def test_start_with_autoconf_disabled(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf") as mock_configure: - service = LCMService(autoconf=False) - service.start() - - # With autoconf=False, check_only should be True - mock_configure.assert_called_once_with(check_only=True) - - service.stop() - - def test_getstate_excludes_unpicklable_attrs(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - service = LCMService() - state = service.__getstate__() - - assert "l" not in state - assert "_stop_event" not in state - assert "_thread" not in state - assert "_l_lock" not in state - assert "_call_thread_pool" not in state - assert "_call_thread_pool_lock" not in state - - def test_setstate_reinitializes_runtime_attrs(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - service = LCMService() - state = service.__getstate__() - - # Simulate unpickling - new_service = object.__new__(LCMService) - new_service.__setstate__(state) - - assert new_service.l is None - assert isinstance(new_service._stop_event, threading.Event) - assert new_service._thread is None - # threading.Lock is a factory function, not a type - # Just check that the lock exists and has acquire/release methods - assert hasattr(new_service._l_lock, "acquire") - assert hasattr(new_service._l_lock, "release") - - def test_start_reinitializes_lcm_after_unpickling(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf"): - service = LCMService() - state = service.__getstate__() - - # Simulate unpickling - new_service = object.__new__(LCMService) - new_service.__setstate__(state) - - # Start should reinitialize LCM - new_service.start() - - # LCM should be created again - assert mock_lcm_class.call_count == 2 - - new_service.stop() - - def test_stop_cleans_up_lcm_instance(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf"): - service = LCMService() - service.start() - service.stop() - - # LCM instance should be cleaned up when we created it - assert service.l is None - - def test_stop_preserves_external_lcm_instance(self) -> None: - mock_lcm_instance = MagicMock() - - with patch("dimos.protocol.service.lcmservice.autoconf"): - # Pass lcm as kwarg - service = LCMService(lcm=mock_lcm_instance) - service.start() - service.stop() - - # External LCM instance should not be cleaned up - assert service.l == mock_lcm_instance - - def test_get_call_thread_pool_creates_pool(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - service = LCMService() - assert service._call_thread_pool is None - - pool = service._get_call_thread_pool() - assert pool is not None - assert service._call_thread_pool == pool - - # Should return same pool on subsequent calls - pool2 = service._get_call_thread_pool() - assert pool2 is pool - - # Clean up - pool.shutdown(wait=False) - - def test_stop_shuts_down_thread_pool(self) -> None: - with patch("dimos.protocol.service.lcmservice.lcm.LCM") as mock_lcm_class: - mock_lcm_instance = MagicMock() - mock_lcm_class.return_value = mock_lcm_instance - - with patch("dimos.protocol.service.lcmservice.autoconf"): - service = LCMService() - service.start() - - # Create thread pool - pool = service._get_call_thread_pool() - assert pool is not None - - service.stop() - - # Pool should be cleaned up - assert service._call_thread_pool is None diff --git a/dimos/protocol/service/test_spec.py b/dimos/protocol/service/test_spec.py deleted file mode 100644 index efb24d7e38..0000000000 --- a/dimos/protocol/service/test_spec.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass - -from dimos.protocol.service.spec import Service - - -@dataclass -class DatabaseConfig: - host: str = "localhost" - port: int = 5432 - database_name: str = "test_db" - timeout: float = 30.0 - max_connections: int = 10 - ssl_enabled: bool = False - - -class DatabaseService(Service[DatabaseConfig]): - default_config = DatabaseConfig - - def start(self) -> None: ... - def stop(self) -> None: ... - - -def test_default_configuration() -> None: - """Test that default configuration is applied correctly.""" - service = DatabaseService() - - # Check that all default values are set - assert service.config.host == "localhost" - assert service.config.port == 5432 - assert service.config.database_name == "test_db" - assert service.config.timeout == 30.0 - assert service.config.max_connections == 10 - assert service.config.ssl_enabled is False - - -def test_partial_configuration_override() -> None: - """Test that partial configuration correctly overrides defaults.""" - service = DatabaseService(host="production-db", port=3306, ssl_enabled=True) - - # Check overridden values - assert service.config.host == "production-db" - assert service.config.port == 3306 - assert service.config.ssl_enabled is True - - # Check that defaults are preserved for non-overridden values - assert service.config.database_name == "test_db" - assert service.config.timeout == 30.0 - assert service.config.max_connections == 10 - - -def test_complete_configuration_override() -> None: - """Test that all configuration values can be overridden.""" - service = DatabaseService( - host="custom-host", - port=9999, - database_name="custom_db", - timeout=60.0, - max_connections=50, - ssl_enabled=True, - ) - - # Check that all values match the custom config - assert service.config.host == "custom-host" - assert service.config.port == 9999 - assert service.config.database_name == "custom_db" - assert service.config.timeout == 60.0 - assert service.config.max_connections == 50 - assert service.config.ssl_enabled is True - - -def test_service_subclassing() -> None: - @dataclass - class ExtraConfig(DatabaseConfig): - extra_param: str = "default_value" - - class ExtraDatabaseService(DatabaseService): - default_config = ExtraConfig - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - - bla = ExtraDatabaseService(host="custom-host2", extra_param="extra_value") - - assert bla.config.host == "custom-host2" - assert bla.config.extra_param == "extra_value" - assert bla.config.port == 5432 # Default value from DatabaseConfig diff --git a/dimos/protocol/service/test_system_configurator.py b/dimos/protocol/service/test_system_configurator.py deleted file mode 100644 index 07f8ede64c..0000000000 --- a/dimos/protocol/service/test_system_configurator.py +++ /dev/null @@ -1,482 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import resource -from unittest.mock import MagicMock, patch - -import pytest - -from dimos.protocol.service.system_configurator import ( - IDEAL_RMEM_SIZE, - BufferConfiguratorLinux, - BufferConfiguratorMacOS, - MaxFileConfiguratorMacOS, - MulticastConfiguratorLinux, - MulticastConfiguratorMacOS, - SystemConfigurator, - _is_root_user, - _read_sysctl_int, - _write_sysctl_int, - configure_system, - sudo_run, -) - -# ----------------------------- Helper function tests ----------------------------- - - -class TestIsRootUser: - def test_is_root_when_euid_is_zero(self) -> None: - # Clear the cache before testing - _is_root_user.cache_clear() - with patch("os.geteuid", return_value=0): - assert _is_root_user() is True - - def test_is_not_root_when_euid_is_nonzero(self) -> None: - _is_root_user.cache_clear() - with patch("os.geteuid", return_value=1000): - assert _is_root_user() is False - - def test_returns_false_when_geteuid_not_available(self) -> None: - _is_root_user.cache_clear() - with patch("os.geteuid", side_effect=AttributeError): - assert _is_root_user() is False - - -class TestSudoRun: - def test_runs_without_sudo_when_root(self) -> None: - _is_root_user.cache_clear() - with patch("os.geteuid", return_value=0): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - sudo_run("echo", "hello", check=True) - mock_run.assert_called_once_with(["echo", "hello"], check=True) - - def test_runs_with_sudo_when_not_root(self) -> None: - _is_root_user.cache_clear() - with patch("os.geteuid", return_value=1000): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - sudo_run("echo", "hello", check=True) - mock_run.assert_called_once_with(["sudo", "echo", "hello"], check=True) - - -class TestReadSysctlInt: - def test_reads_value_with_equals_sign(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0, stdout="net.core.rmem_max = 67108864") - result = _read_sysctl_int("net.core.rmem_max") - assert result == 67108864 - - def test_reads_value_with_colon(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0, stdout="kern.ipc.maxsockbuf: 8388608") - result = _read_sysctl_int("kern.ipc.maxsockbuf") - assert result == 8388608 - - def test_returns_none_on_nonzero_returncode(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=1, stderr="error") - result = _read_sysctl_int("net.core.rmem_max") - assert result is None - - def test_returns_none_on_malformed_output(self) -> None: - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0, stdout="invalid output") - result = _read_sysctl_int("net.core.rmem_max") - assert result is None - - def test_returns_none_on_exception(self) -> None: - with patch("subprocess.run", side_effect=Exception("Command failed")): - result = _read_sysctl_int("net.core.rmem_max") - assert result is None - - -class TestWriteSysctlInt: - def test_calls_sudo_run_with_correct_args(self) -> None: - _is_root_user.cache_clear() - with patch("os.geteuid", return_value=1000): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - _write_sysctl_int("net.core.rmem_max", 67108864) - mock_run.assert_called_once_with( - ["sudo", "sysctl", "-w", "net.core.rmem_max=67108864"], - check=True, - text=True, - capture_output=False, - ) - - -# ----------------------------- configure_system tests ----------------------------- - - -class MockConfigurator(SystemConfigurator): - """A mock configurator for testing configure_system.""" - - def __init__(self, passes: bool = True, is_critical: bool = False) -> None: - self._passes = passes - self.critical = is_critical - self.fix_called = False - - def check(self) -> bool: - return self._passes - - def explanation(self) -> str | None: - if self._passes: - return None - return "Mock explanation" - - def fix(self) -> None: - self.fix_called = True - - -class TestConfigureSystem: - def test_skips_in_ci_environment(self) -> None: - with patch.dict(os.environ, {"CI": "true"}): - mock_check = MockConfigurator(passes=False) - configure_system([mock_check]) - assert not mock_check.fix_called - - def test_does_nothing_when_all_checks_pass(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=True) - configure_system([mock_check]) - assert not mock_check.fix_called - - def test_check_only_mode_does_not_fix(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=False) - configure_system([mock_check], check_only=True) - assert not mock_check.fix_called - - def test_prompts_user_and_fixes_on_yes(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=False) - with patch("builtins.input", return_value="y"): - configure_system([mock_check]) - assert mock_check.fix_called - - def test_does_not_fix_on_no(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=False) - with patch("builtins.input", return_value="n"): - configure_system([mock_check]) - assert not mock_check.fix_called - - def test_exits_on_no_with_critical_check(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=False, is_critical=True) - with patch("builtins.input", return_value="n"): - with pytest.raises(SystemExit) as exc_info: - configure_system([mock_check]) - assert exc_info.value.code == 1 - - def test_handles_eof_error_on_input(self) -> None: - with patch.dict(os.environ, {"CI": ""}, clear=False): - mock_check = MockConfigurator(passes=False) - with patch("builtins.input", side_effect=EOFError): - configure_system([mock_check]) - assert not mock_check.fix_called - - -# ----------------------------- MulticastConfiguratorLinux tests ----------------------------- - - -class TestMulticastConfiguratorLinux: - def test_check_returns_true_when_fully_configured(self) -> None: - configurator = MulticastConfiguratorLinux() - with patch("subprocess.run") as mock_run: - mock_run.side_effect = [ - MagicMock(returncode=0), # ip -V - MagicMock( - returncode=0, - stdout="1: lo: mtu 65536", - ), - MagicMock(returncode=0, stdout="224.0.0.0/4 dev lo scope link"), - ] - assert configurator.check() is True - assert configurator.loopback_ok is True - assert configurator.route_ok is True - - def test_check_returns_false_when_multicast_flag_missing(self) -> None: - configurator = MulticastConfiguratorLinux() - with patch("subprocess.run") as mock_run: - mock_run.side_effect = [ - MagicMock(returncode=0), # ip -V - MagicMock(returncode=0, stdout="1: lo: mtu 65536"), - MagicMock(returncode=0, stdout="224.0.0.0/4 dev lo scope link"), - ] - assert configurator.check() is False - assert configurator.loopback_ok is False - assert configurator.route_ok is True - - def test_check_returns_false_when_route_missing(self) -> None: - configurator = MulticastConfiguratorLinux() - with patch("subprocess.run") as mock_run: - mock_run.side_effect = [ - MagicMock(returncode=0), # ip -V - MagicMock( - returncode=0, - stdout="1: lo: mtu 65536", - ), - MagicMock(returncode=0, stdout=""), # Empty - no route - ] - assert configurator.check() is False - assert configurator.loopback_ok is True - assert configurator.route_ok is False - - def test_check_returns_false_when_ip_not_found(self) -> None: - configurator = MulticastConfiguratorLinux() - with patch("subprocess.run", side_effect=FileNotFoundError): - assert configurator.check() is False - assert configurator.loopback_ok is False - assert configurator.route_ok is False - - def test_explanation_includes_needed_commands(self) -> None: - configurator = MulticastConfiguratorLinux() - configurator.loopback_ok = False - configurator.route_ok = False - explanation = configurator.explanation() - assert "ip link set lo multicast on" in explanation - assert "ip route add 224.0.0.0/4 dev lo" in explanation - - def test_fix_runs_needed_commands(self) -> None: - _is_root_user.cache_clear() - configurator = MulticastConfiguratorLinux() - configurator.loopback_ok = False - configurator.route_ok = False - with patch("os.geteuid", return_value=0): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - configurator.fix() - assert mock_run.call_count == 2 - - -# ----------------------------- MulticastConfiguratorMacOS tests ----------------------------- - - -class TestMulticastConfiguratorMacOS: - def test_check_returns_true_when_route_exists(self) -> None: - configurator = MulticastConfiguratorMacOS() - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock( - returncode=0, - stdout="224.0.0.0/4 link#1 UCS lo0", - ) - assert configurator.check() is True - - def test_check_returns_false_when_route_missing(self) -> None: - configurator = MulticastConfiguratorMacOS() - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock( - returncode=0, stdout="default 192.168.1.1 UGScg en0" - ) - assert configurator.check() is False - - def test_check_returns_false_on_netstat_error(self) -> None: - configurator = MulticastConfiguratorMacOS() - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=1, stderr="error") - assert configurator.check() is False - - def test_explanation_includes_route_command(self) -> None: - configurator = MulticastConfiguratorMacOS() - explanation = configurator.explanation() - assert "route add -net 224.0.0.0/4 -interface lo0" in explanation - - def test_fix_runs_route_command(self) -> None: - _is_root_user.cache_clear() - configurator = MulticastConfiguratorMacOS() - with patch("os.geteuid", return_value=0): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - configurator.fix() - mock_run.assert_called_once() - args = mock_run.call_args[0][0] - assert "route" in args - assert "224.0.0.0/4" in args - - -# ----------------------------- BufferConfiguratorLinux tests ----------------------------- - - -class TestBufferConfiguratorLinux: - def test_check_returns_true_when_buffers_sufficient(self) -> None: - configurator = BufferConfiguratorLinux() - with patch("dimos.protocol.service.system_configurator._read_sysctl_int") as mock_read: - mock_read.return_value = IDEAL_RMEM_SIZE - assert configurator.check() is True - assert configurator.needs == [] - - def test_check_returns_false_when_rmem_max_low(self) -> None: - configurator = BufferConfiguratorLinux() - with patch("dimos.protocol.service.system_configurator._read_sysctl_int") as mock_read: - mock_read.side_effect = [1048576, IDEAL_RMEM_SIZE] # rmem_max low - assert configurator.check() is False - assert len(configurator.needs) == 1 - assert configurator.needs[0][0] == "net.core.rmem_max" - - def test_check_returns_false_when_both_low(self) -> None: - configurator = BufferConfiguratorLinux() - with patch("dimos.protocol.service.system_configurator._read_sysctl_int") as mock_read: - mock_read.return_value = 1048576 # Both low - assert configurator.check() is False - assert len(configurator.needs) == 2 - - def test_explanation_lists_needed_changes(self) -> None: - configurator = BufferConfiguratorLinux() - configurator.needs = [("net.core.rmem_max", IDEAL_RMEM_SIZE)] - explanation = configurator.explanation() - assert "net.core.rmem_max" in explanation - assert str(IDEAL_RMEM_SIZE) in explanation - - def test_fix_writes_needed_values(self) -> None: - configurator = BufferConfiguratorLinux() - configurator.needs = [("net.core.rmem_max", IDEAL_RMEM_SIZE)] - with patch("dimos.protocol.service.system_configurator._write_sysctl_int") as mock_write: - configurator.fix() - mock_write.assert_called_once_with("net.core.rmem_max", IDEAL_RMEM_SIZE) - - -# ----------------------------- BufferConfiguratorMacOS tests ----------------------------- - - -class TestBufferConfiguratorMacOS: - def test_check_returns_true_when_buffers_sufficient(self) -> None: - configurator = BufferConfiguratorMacOS() - with patch("dimos.protocol.service.system_configurator._read_sysctl_int") as mock_read: - mock_read.side_effect = [ - BufferConfiguratorMacOS.TARGET_BUFFER_SIZE, - BufferConfiguratorMacOS.TARGET_RECVSPACE, - BufferConfiguratorMacOS.TARGET_DGRAM_SIZE, - ] - assert configurator.check() is True - assert configurator.needs == [] - - def test_check_returns_false_when_values_low(self) -> None: - configurator = BufferConfiguratorMacOS() - with patch("dimos.protocol.service.system_configurator._read_sysctl_int") as mock_read: - mock_read.return_value = 1024 # All low - assert configurator.check() is False - assert len(configurator.needs) == 3 - - def test_explanation_lists_needed_changes(self) -> None: - configurator = BufferConfiguratorMacOS() - configurator.needs = [ - ("kern.ipc.maxsockbuf", BufferConfiguratorMacOS.TARGET_BUFFER_SIZE), - ] - explanation = configurator.explanation() - assert "kern.ipc.maxsockbuf" in explanation - - def test_fix_writes_needed_values(self) -> None: - configurator = BufferConfiguratorMacOS() - configurator.needs = [ - ("kern.ipc.maxsockbuf", BufferConfiguratorMacOS.TARGET_BUFFER_SIZE), - ] - with patch("dimos.protocol.service.system_configurator._write_sysctl_int") as mock_write: - configurator.fix() - mock_write.assert_called_once_with( - "kern.ipc.maxsockbuf", BufferConfiguratorMacOS.TARGET_BUFFER_SIZE - ) - - -# ----------------------------- MaxFileConfiguratorMacOS tests ----------------------------- - - -class TestMaxFileConfiguratorMacOS: - def test_check_returns_true_when_soft_limit_sufficient(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - with patch("resource.getrlimit") as mock_getrlimit: - mock_getrlimit.return_value = (65536, 1048576) - assert configurator.check() is True - assert configurator.current_soft == 65536 - assert configurator.current_hard == 1048576 - - def test_check_returns_false_when_soft_limit_low(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - with patch("resource.getrlimit") as mock_getrlimit: - mock_getrlimit.return_value = (256, 1048576) - assert configurator.check() is False - assert configurator.can_fix_without_sudo is True - - def test_check_returns_false_when_both_limits_low(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - with patch("resource.getrlimit") as mock_getrlimit: - mock_getrlimit.return_value = (256, 10240) - assert configurator.check() is False - assert configurator.can_fix_without_sudo is False - - def test_check_returns_false_on_exception(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - with patch("resource.getrlimit", side_effect=Exception("error")): - assert configurator.check() is False - - def test_explanation_when_sudo_not_needed(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 256 - configurator.current_hard = 1048576 - configurator.can_fix_without_sudo = True - explanation = configurator.explanation() - assert "65536" in explanation - assert "no sudo" in explanation.lower() or "Raise soft" in explanation - - def test_explanation_when_sudo_needed(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 256 - configurator.current_hard = 10240 - configurator.can_fix_without_sudo = False - explanation = configurator.explanation() - assert "launchctl" in explanation - - def test_fix_raises_soft_limit_without_sudo(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 256 - configurator.current_hard = 1048576 - configurator.can_fix_without_sudo = True - with patch("resource.setrlimit") as mock_setrlimit: - configurator.fix() - mock_setrlimit.assert_called_once_with(resource.RLIMIT_NOFILE, (65536, 1048576)) - - def test_fix_does_nothing_when_already_sufficient(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 65536 - configurator.current_hard = 1048576 - with patch("resource.setrlimit") as mock_setrlimit: - configurator.fix() - mock_setrlimit.assert_not_called() - - def test_fix_uses_launchctl_when_hard_limit_low(self) -> None: - _is_root_user.cache_clear() - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 256 - configurator.current_hard = 10240 - configurator.can_fix_without_sudo = False - with patch("os.geteuid", return_value=0): - with patch("subprocess.run") as mock_run: - mock_run.return_value = MagicMock(returncode=0) - with patch("resource.setrlimit"): - configurator.fix() - # Check launchctl was called - args = mock_run.call_args[0][0] - assert "launchctl" in args - assert "maxfiles" in args - - def test_fix_raises_on_setrlimit_error(self) -> None: - configurator = MaxFileConfiguratorMacOS(target=65536) - configurator.current_soft = 256 - configurator.current_hard = 1048576 - configurator.can_fix_without_sudo = True - with patch("resource.setrlimit", side_effect=ValueError("test error")): - with pytest.raises(ValueError): - configurator.fix() diff --git a/dimos/protocol/tf/__init__.py b/dimos/protocol/tf/__init__.py deleted file mode 100644 index cb00dbde3c..0000000000 --- a/dimos/protocol/tf/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.protocol.tf.tf import LCMTF, TF, MultiTBuffer, PubSubTF, TBuffer, TFConfig, TFSpec - -__all__ = ["LCMTF", "TF", "MultiTBuffer", "PubSubTF", "TBuffer", "TFConfig", "TFSpec"] diff --git a/dimos/protocol/tf/test_tf.py b/dimos/protocol/tf/test_tf.py deleted file mode 100644 index bdbd808cbb..0000000000 --- a/dimos/protocol/tf/test_tf.py +++ /dev/null @@ -1,685 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -import time - -import pytest - -from dimos.core import TF -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Vector3 -from dimos.protocol.tf import MultiTBuffer, TBuffer - - -# from https://foxglove.dev/blog/understanding-ros-transforms -def test_tf_ros_example() -> None: - tf = TF() - - base_link_to_arm = Transform( - translation=Vector3(1.0, -1.0, 0.0), - rotation=Quaternion.from_euler(Vector3(0, 0, math.pi / 6)), - frame_id="base_link", - child_frame_id="arm", - ts=time.time(), - ) - - arm_to_end = Transform( - translation=Vector3(1.0, 1.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation - frame_id="arm", - child_frame_id="end_effector", - ts=time.time(), - ) - - tf.publish(base_link_to_arm, arm_to_end) - time.sleep(0.2) - - end_effector_global_pose = tf.get("base_link", "end_effector") - assert end_effector_global_pose is not None - - assert end_effector_global_pose.translation.x == pytest.approx(1.366, abs=1e-3) - assert end_effector_global_pose.translation.y == pytest.approx(0.366, abs=1e-3) - - tf.stop() - - -def test_tf_main() -> None: - """Test TF broadcasting and querying between two TF instances. - If you run foxglove-bridge this will show up in the UI""" - - # here we create broadcasting and receiving TF instance. - # this is to verify that comms work multiprocess, normally - # you'd use only one instance in your module - broadcaster = TF() - querier = TF() - - # Create a transform from world to robot - current_time = time.time() - - world_to_charger = Transform( - translation=Vector3(2.0, -2.0, 0.0), - rotation=Quaternion.from_euler(Vector3(0, 0, 2)), - frame_id="world", - child_frame_id="charger", - ts=current_time, - ) - - world_to_robot = Transform( - translation=Vector3(1.0, 2.0, 3.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation - frame_id="world", - child_frame_id="robot", - ts=current_time, - ) - - # Broadcast the transform - broadcaster.publish(world_to_robot) - broadcaster.publish(world_to_charger) - # Give time for the message to propagate - time.sleep(0.05) - - # Verify frames are available - frames = querier.get_frames() - assert "world" in frames - assert "robot" in frames - - # Add another transform in the chain - robot_to_sensor = Transform( - translation=Vector3(0.5, 0.0, 0.2), - rotation=Quaternion(0.0, 0.0, 0.707107, 0.707107), # 90 degrees around Z - frame_id="robot", - child_frame_id="sensor", - ts=current_time, - ) - - broadcaster.publish(robot_to_sensor) - - time.sleep(0.05) - - # we can now query (from a separate process given we use querier) the transform tree - chain_transform = querier.get("world", "sensor") - - # broadcaster will agree with us - assert broadcaster.get("world", "sensor") == chain_transform - - # The chain should compose: world->robot (1,2,3) + robot->sensor (0.5,0,0.2) - # Expected translation: (1.5, 2.0, 3.2) - assert chain_transform is not None - assert abs(chain_transform.translation.x - 1.5) < 0.001 - assert abs(chain_transform.translation.y - 2.0) < 0.001 - assert abs(chain_transform.translation.z - 3.2) < 0.001 - - # we see something on camera - random_object_in_view = PoseStamped( - frame_id="random_object", - position=Vector3(1, 0, 0), - ) - - print("Random obj", random_object_in_view) - - # random_object is perceived by the sensor - # we create a transform pointing from sensor to object - random_t = random_object_in_view.new_transform_from("sensor") - - # we could have also done - assert random_t == random_object_in_view.new_transform_to("sensor").inverse() - - print("randm t", random_t) - - # we broadcast our object location - broadcaster.publish(random_t) - - ## we could also publish world -> random_object if we wanted to - # broadcaster.publish( - # broadcaster.get("world", "sensor") + random_object_in_view.new_transform("sensor").inverse() - # ) - ## (this would mess with the transform system because it expects trees not graphs) - ## and our random_object would get re-connected to world from sensor - - print(broadcaster) - - # Give time for the message to propagate - time.sleep(0.05) - - # we know where the object is in the world frame now - world_object = broadcaster.get("world", "random_object") - - # both instances agree - assert querier.get("world", "random_object") == world_object - - print("world object", world_object) - - # if you have "diagon" https://diagon.arthursonzogni.com/ installed you can draw a graph - print(broadcaster.graph()) - - assert world_object is not None - assert abs(world_object.translation.x - 1.5) < 0.001 - assert abs(world_object.translation.y - 3.0) < 0.001 - assert abs(world_object.translation.z - 3.2) < 0.001 - - # this doesn't work atm - robot_to_charger = broadcaster.get("robot", "charger") - assert robot_to_charger is not None - - # Expected: robot->world->charger - print(f"robot_to_charger translation: {robot_to_charger.translation}") - print(f"robot_to_charger rotation: {robot_to_charger.rotation}") - - assert abs(robot_to_charger.translation.x - 1.0) < 0.001 - assert abs(robot_to_charger.translation.y - (-4.0)) < 0.001 - assert abs(robot_to_charger.translation.z - (-3.0)) < 0.001 - - # Stop services (they were autostarted but don't know how to autostop) - broadcaster.stop() - querier.stop() - - -class TestTBuffer: - def test_add_transform(self) -> None: - buffer = TBuffer(buffer_size=10.0) - transform = Transform( - translation=Vector3(1.0, 2.0, 3.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="world", - child_frame_id="robot", - ts=time.time(), - ) - - buffer.add(transform) - assert len(buffer) == 1 - assert buffer.first() == transform - - def test_get(self) -> None: - buffer = TBuffer() - base_time = time.time() - - # Add transforms at different times - for i in range(3): - transform = Transform( - translation=Vector3(float(i), 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time + i * 0.5, - ) - buffer.add(transform) - - # Test getting latest transform - latest = buffer.get() - assert latest is not None - assert latest.translation.x == 2.0 - - # Test getting transform at specific time - middle = buffer.get(time_point=base_time + 0.75) - assert middle is not None - assert middle.translation.x == 2.0 # Closest to i=1 - - # Test time tolerance - result = buffer.get(time_point=base_time + 10.0, time_tolerance=0.1) - assert result is None # Outside tolerance - - def test_buffer_pruning(self) -> None: - buffer = TBuffer(buffer_size=1.0) # 1 second buffer - - # Add old transform - old_time = time.time() - 2.0 - old_transform = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=old_time, - ) - buffer.add(old_transform) - - # Add recent transform - recent_transform = Transform( - translation=Vector3(2.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=time.time(), - ) - buffer.add(recent_transform) - - # Old transform should be pruned - assert len(buffer) == 1 - first = buffer.first() - assert first is not None - assert first.translation.x == 2.0 - - -class TestMultiTBuffer: - def test_multiple_frame_pairs(self) -> None: - ttbuffer = MultiTBuffer(buffer_size=10.0) - - # Add transforms for different frame pairs - transform1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot1", - ts=time.time(), - ) - - transform2 = Transform( - translation=Vector3(2.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot2", - ts=time.time(), - ) - - ttbuffer.receive_transform(transform1, transform2) - - # Should have two separate buffers - assert len(ttbuffer.buffers) == 2 - assert ("world", "robot1") in ttbuffer.buffers - assert ("world", "robot2") in ttbuffer.buffers - - def test_graph(self) -> None: - ttbuffer = MultiTBuffer(buffer_size=10.0) - - # Add transforms for different frame pairs - transform1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot1", - ts=time.time(), - ) - - transform2 = Transform( - translation=Vector3(2.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot2", - ts=time.time(), - ) - - ttbuffer.receive_transform(transform1, transform2) - - print(ttbuffer.graph()) - - def test_get_latest_transform(self) -> None: - ttbuffer = MultiTBuffer() - - # Add multiple transforms - for i in range(3): - transform = Transform( - translation=Vector3(float(i), 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=time.time() + i * 0.1, - ) - ttbuffer.receive_transform(transform) - time.sleep(0.01) - - # Get latest transform - latest = ttbuffer.get("world", "robot") - assert latest is not None - assert latest.translation.x == 2.0 - - def test_get_transform_at_time(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Add transforms at known times - for i in range(5): - transform = Transform( - translation=Vector3(float(i), 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time + i * 0.5, - ) - ttbuffer.receive_transform(transform) - - # Get transform closest to middle time - middle_time = base_time + 1.25 # Should be closest to i=2 (t=1.0) or i=3 (t=1.5) - result = ttbuffer.get("world", "robot", time_point=middle_time) - assert result is not None - # At t=1.25, it's equidistant from i=2 (t=1.0) and i=3 (t=1.5) - # The implementation picks the later one when equidistant - assert result.translation.x == 3.0 - - def test_time_tolerance(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Add single transform - transform = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time, - ) - ttbuffer.receive_transform(transform) - - # Within tolerance - result = ttbuffer.get("world", "robot", time_point=base_time + 0.1, time_tolerance=0.2) - assert result is not None - - # Outside tolerance - result = ttbuffer.get("world", "robot", time_point=base_time + 0.5, time_tolerance=0.1) - assert result is None - - def test_nonexistent_frame_pair(self) -> None: - ttbuffer = MultiTBuffer() - - # Try to get transform for non-existent frame pair - result = ttbuffer.get("foo", "bar") - assert result is None - - def test_get_transform_search_direct(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Add direct transform - transform = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time, - ) - ttbuffer.receive_transform(transform) - - # Search should return single transform - result = ttbuffer.get_transform_search("world", "robot") - assert result is not None - assert len(result) == 1 - assert result[0].translation.x == 1.0 - - def test_get_transform_search_chain(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create transform chain: world -> robot -> sensor - transform1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time, - ) - transform2 = Transform( - translation=Vector3(0.0, 2.0, 0.0), - frame_id="robot", - child_frame_id="sensor", - ts=base_time, - ) - ttbuffer.receive_transform(transform1, transform2) - - # Search should find chain - result = ttbuffer.get_transform_search("world", "sensor") - assert result is not None - assert len(result) == 2 - assert result[0].translation.x == 1.0 # world -> robot - assert result[1].translation.y == 2.0 # robot -> sensor - - def test_get_transform_search_complex_chain(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create more complex graph: - # world -> base -> arm -> hand - # \-> robot -> sensor - transforms = [ - Transform( - frame_id="world", - child_frame_id="base", - translation=Vector3(1.0, 0.0, 0.0), - ts=base_time, - ), - Transform( - frame_id="base", - child_frame_id="arm", - translation=Vector3(0.0, 1.0, 0.0), - ts=base_time, - ), - Transform( - frame_id="arm", - child_frame_id="hand", - translation=Vector3(0.0, 0.0, 1.0), - ts=base_time, - ), - Transform( - frame_id="world", - child_frame_id="robot", - translation=Vector3(2.0, 0.0, 0.0), - ts=base_time, - ), - Transform( - frame_id="robot", - child_frame_id="sensor", - translation=Vector3(0.0, 2.0, 0.0), - ts=base_time, - ), - ] - - for t in transforms: - ttbuffer.receive_transform(t) - - # Find path world -> hand (should go through base -> arm) - result = ttbuffer.get_transform_search("world", "hand") - assert result is not None - assert len(result) == 3 - assert result[0].child_frame_id == "base" - assert result[1].child_frame_id == "arm" - assert result[2].child_frame_id == "hand" - - def test_get_transform_search_no_path(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create disconnected transforms - transform1 = Transform(frame_id="world", child_frame_id="robot", ts=base_time) - transform2 = Transform(frame_id="base", child_frame_id="sensor", ts=base_time) - ttbuffer.receive_transform(transform1, transform2) - - # No path exists - result = ttbuffer.get_transform_search("world", "sensor") - assert result is None - - def test_get_transform_search_with_time(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Add transforms at different times - old_transform = Transform( - frame_id="world", - child_frame_id="robot", - translation=Vector3(1.0, 0.0, 0.0), - ts=base_time - 10.0, - ) - new_transform = Transform( - frame_id="world", - child_frame_id="robot", - translation=Vector3(2.0, 0.0, 0.0), - ts=base_time, - ) - ttbuffer.receive_transform(old_transform, new_transform) - - # Search at specific time - result = ttbuffer.get_transform_search("world", "robot", time_point=base_time) - assert result is not None - assert result[0].translation.x == 2.0 - - # Search with time tolerance - result = ttbuffer.get_transform_search( - "world", "robot", time_point=base_time + 1.0, time_tolerance=0.1 - ) - assert result is None # Outside tolerance - - def test_get_transform_search_shortest_path(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create graph with multiple paths: - # world -> A -> B -> target (3 hops) - # world -> target (direct, 1 hop) - transforms = [ - Transform(frame_id="world", child_frame_id="A", ts=base_time), - Transform(frame_id="A", child_frame_id="B", ts=base_time), - Transform(frame_id="B", child_frame_id="target", ts=base_time), - Transform(frame_id="world", child_frame_id="target", ts=base_time), - ] - - for t in transforms: - ttbuffer.receive_transform(t) - - # BFS should find the direct path (shortest) - result = ttbuffer.get_transform_search("world", "target") - assert result is not None - assert len(result) == 1 # Direct path, not the 3-hop path - assert result[0].child_frame_id == "target" - - def test_string_representations(self) -> None: - # Test empty buffers - empty_buffer = TBuffer() - assert str(empty_buffer) == "TBuffer(empty)" - - empty_ttbuffer = MultiTBuffer() - assert str(empty_ttbuffer) == "MultiTBuffer(empty)" - - # Test TBuffer with data - buffer = TBuffer() - base_time = time.time() - for i in range(3): - transform = Transform( - translation=Vector3(float(i), 0.0, 0.0), - frame_id="world", - child_frame_id="robot", - ts=base_time + i * 0.1, - ) - buffer.add(transform) - - buffer_str = str(buffer) - assert "3 msgs" in buffer_str - assert "world -> robot" in buffer_str - assert "0.20s" in buffer_str # duration - - # Test MultiTBuffer with multiple frame pairs - ttbuffer = MultiTBuffer() - transforms = [ - Transform(frame_id="world", child_frame_id="robot1", ts=base_time), - Transform(frame_id="world", child_frame_id="robot2", ts=base_time + 0.5), - Transform(frame_id="robot1", child_frame_id="sensor", ts=base_time + 1.0), - ] - - for t in transforms: - ttbuffer.receive_transform(t) - - ttbuffer_str = str(ttbuffer) - print("\nMultiTBuffer string representation:") - print(ttbuffer_str) - - assert "MultiTBuffer(3 buffers):" in ttbuffer_str - assert "TBuffer(world -> robot1, 1 msgs" in ttbuffer_str - assert "TBuffer(world -> robot2, 1 msgs" in ttbuffer_str - assert "TBuffer(robot1 -> sensor, 1 msgs" in ttbuffer_str - - def test_get_with_transform_chain_composition(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create transform chain: world -> robot -> sensor - # world -> robot: translate by (1, 0, 0) - transform1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity - frame_id="world", - child_frame_id="robot", - ts=base_time, - ) - - # robot -> sensor: translate by (0, 2, 0) and rotate 90 degrees around Z - import math - - # 90 degrees around Z: quaternion (0, 0, sin(45°), cos(45°)) - transform2 = Transform( - translation=Vector3(0.0, 2.0, 0.0), - rotation=Quaternion(0.0, 0.0, math.sin(math.pi / 4), math.cos(math.pi / 4)), - frame_id="robot", - child_frame_id="sensor", - ts=base_time, - ) - - ttbuffer.receive_transform(transform1, transform2) - - # Get composed transform from world to sensor - result = ttbuffer.get("world", "sensor") - assert result is not None - - # The composed transform should: - # 1. Apply world->robot translation: (1, 0, 0) - # 2. Apply robot->sensor translation in robot frame: (0, 2, 0) - # Total translation: (1, 2, 0) - assert abs(result.translation.x - 1.0) < 1e-6 - assert abs(result.translation.y - 2.0) < 1e-6 - assert abs(result.translation.z - 0.0) < 1e-6 - - # Rotation should be 90 degrees around Z (same as transform2) - assert abs(result.rotation.x - 0.0) < 1e-6 - assert abs(result.rotation.y - 0.0) < 1e-6 - assert abs(result.rotation.z - math.sin(math.pi / 4)) < 1e-6 - assert abs(result.rotation.w - math.cos(math.pi / 4)) < 1e-6 - - # Frame IDs should be correct - assert result.frame_id == "world" - assert result.child_frame_id == "sensor" - - def test_get_with_longer_transform_chain(self) -> None: - ttbuffer = MultiTBuffer() - base_time = time.time() - - # Create longer chain: world -> base -> arm -> hand - # Each adds a translation along different axes - transforms = [ - Transform( - translation=Vector3(1.0, 0.0, 0.0), # Move 1 along X - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="world", - child_frame_id="base", - ts=base_time, - ), - Transform( - translation=Vector3(0.0, 2.0, 0.0), # Move 2 along Y - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base", - child_frame_id="arm", - ts=base_time, - ), - Transform( - translation=Vector3(0.0, 0.0, 3.0), # Move 3 along Z - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="arm", - child_frame_id="hand", - ts=base_time, - ), - ] - - for t in transforms: - ttbuffer.receive_transform(t) - - # Get composed transform from world to hand - result = ttbuffer.get("world", "hand") - assert result is not None - - # Total translation should be sum of all: (1, 2, 3) - assert abs(result.translation.x - 1.0) < 1e-6 - assert abs(result.translation.y - 2.0) < 1e-6 - assert abs(result.translation.z - 3.0) < 1e-6 - - # Rotation should still be identity (all rotations were identity) - assert abs(result.rotation.x - 0.0) < 1e-6 - assert abs(result.rotation.y - 0.0) < 1e-6 - assert abs(result.rotation.z - 0.0) < 1e-6 - assert abs(result.rotation.w - 1.0) < 1e-6 - - assert result.frame_id == "world" - assert result.child_frame_id == "hand" diff --git a/dimos/protocol/tf/tf.py b/dimos/protocol/tf/tf.py deleted file mode 100644 index 825e89fc8c..0000000000 --- a/dimos/protocol/tf/tf.py +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import abstractmethod -from collections import deque -from dataclasses import dataclass, field -from functools import reduce -from typing import TypeVar - -from dimos.memory.timeseries.inmemory import InMemoryStore -from dimos.msgs.geometry_msgs import PoseStamped, Transform -from dimos.msgs.tf2_msgs import TFMessage -from dimos.protocol.pubsub.impl.lcmpubsub import LCM, Topic -from dimos.protocol.pubsub.spec import PubSub -from dimos.protocol.service.lcmservice import Service # type: ignore[attr-defined] - -CONFIG = TypeVar("CONFIG") - - -# generic configuration for transform service -@dataclass -class TFConfig: - buffer_size: float = 10.0 # seconds - rate_limit: float = 10.0 # Hz - - -# generic specification for transform service -class TFSpec(Service[TFConfig]): - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - @abstractmethod - def publish(self, *args: Transform) -> None: ... - - @abstractmethod - def publish_static(self, *args: Transform) -> None: ... - - def get_frames(self) -> set[str]: - return set() - - @abstractmethod - def get( - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ) -> Transform | None: ... - - def receive_transform(self, *args: Transform) -> None: ... - - def receive_tfmessage(self, msg: TFMessage) -> None: - for transform in msg.transforms: - self.receive_transform(transform) - - -MsgT = TypeVar("MsgT") -TopicT = TypeVar("TopicT") - - -class TBuffer(InMemoryStore[Transform]): - def __init__(self, buffer_size: float = 10.0) -> None: - super().__init__() - self.buffer_size = buffer_size - - def add(self, transform: Transform) -> None: - self.save(transform) - self.prune_old(transform.ts - self.buffer_size) - - def get(self, time_point: float | None = None, time_tolerance: float = 1.0) -> Transform | None: - """Get transform at specified time or latest if no time given.""" - if time_point is None: - return self.last() - return self.find_closest(time_point, time_tolerance) - - def __str__(self) -> str: - if len(self) == 0: - return "TBuffer(empty)" - - first_item = self.first() - time_range = self.time_range() - if time_range and first_item: - from dimos.types.timestamped import to_human_readable - - start_time = to_human_readable(time_range[0]) - end_time = to_human_readable(time_range[1]) - duration = time_range[1] - time_range[0] - - frame_str = f"{first_item.frame_id} -> {first_item.child_frame_id}" - - return ( - f"TBuffer(" - f"{frame_str}, " - f"{len(self)} msgs, " - f"{duration:.2f}s [{start_time} - {end_time}])" - ) - - return f"TBuffer({len(self)} msgs)" - - -# stores multiple transform buffers -# creates a new buffer on demand when new transform is detected -class MultiTBuffer: - def __init__(self, buffer_size: float = 10.0) -> None: - self.buffers: dict[tuple[str, str], TBuffer] = {} - self.buffer_size = buffer_size - - def receive_transform(self, *args: Transform) -> None: - for transform in args: - key = (transform.frame_id, transform.child_frame_id) - if key not in self.buffers: - self.buffers[key] = TBuffer(self.buffer_size) - self.buffers[key].add(transform) - - def get_frames(self) -> set[str]: - frames = set() - for parent, child in self.buffers: - frames.add(parent) - frames.add(child) - return frames - - def get_connections(self, frame_id: str) -> set[str]: - """Get all frames connected to the given frame (both as parent and child).""" - connections = set() - for parent, child in self.buffers: - if parent == frame_id: - connections.add(child) - if child == frame_id: - connections.add(parent) - return connections - - def get_transform( - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ) -> Transform | None: - # Check forward direction - key = (parent_frame, child_frame) - if key in self.buffers: - return self.buffers[key].get(time_point, time_tolerance) # type: ignore[arg-type] - - # Check reverse direction and return inverse - reverse_key = (child_frame, parent_frame) - if reverse_key in self.buffers: - transform = self.buffers[reverse_key].get(time_point, time_tolerance) # type: ignore[arg-type] - return transform.inverse() if transform else None - - return None - - def get(self, *args, **kwargs) -> Transform | None: # type: ignore[no-untyped-def] - simple = self.get_transform(*args, **kwargs) - if simple is not None: - return simple - - complex = self.get_transform_search(*args, **kwargs) - - if complex is None: - return None - - return reduce(lambda t1, t2: t1 + t2, complex) - - def get_transform_search( - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ) -> list[Transform] | None: - """Search for shortest transform chain between parent and child frames using BFS.""" - # Check if direct transform exists (already checked in get_transform, but for clarity) - direct = self.get_transform(parent_frame, child_frame, time_point, time_tolerance) - if direct is not None: - return [direct] - - # BFS to find shortest path - queue: deque[tuple[str, list[Transform]]] = deque([(parent_frame, [])]) - visited = {parent_frame} - - while queue: - current_frame, path = queue.popleft() - - if current_frame == child_frame: - return path - - # Get all connections for current frame - connections = self.get_connections(current_frame) - - for next_frame in connections: - if next_frame not in visited: - visited.add(next_frame) - - # Get the transform between current and next frame - transform = self.get_transform( - current_frame, next_frame, time_point, time_tolerance - ) - if transform: - queue.append((next_frame, [*path, transform])) - - return None - - def graph(self) -> str: - import subprocess - - def connection_str(connection: tuple[str, str]) -> str: - (frame_from, frame_to) = connection - return f"{frame_from} -> {frame_to}" - - graph_str = "\n".join(map(connection_str, self.buffers.keys())) - - try: - result = subprocess.run( - ["diagon", "GraphDAG", "-style=Unicode"], - input=graph_str, - capture_output=True, - text=True, - ) - return result.stdout if result.returncode == 0 else graph_str - except Exception: - return "no diagon installed" - - def __str__(self) -> str: - if not self.buffers: - return f"{self.__class__.__name__}(empty)" - - lines = [f"{self.__class__.__name__}({len(self.buffers)} buffers):"] - for buffer in self.buffers.values(): - lines.append(f" {buffer}") - - return "\n".join(lines) - - -@dataclass -class PubSubTFConfig(TFConfig): - topic: Topic | None = None # Required field but needs default for dataclass inheritance - pubsub: type[PubSub] | PubSub | None = None # type: ignore[type-arg] - autostart: bool = True - - -class PubSubTF(MultiTBuffer, TFSpec): - default_config: type[PubSubTFConfig] = PubSubTFConfig - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - TFSpec.__init__(self, **kwargs) - MultiTBuffer.__init__(self, self.config.buffer_size) - - pubsub_config = getattr(self.config, "pubsub", None) - if pubsub_config is not None: - if callable(pubsub_config): - self.pubsub = pubsub_config() - else: - self.pubsub = pubsub_config - else: - raise ValueError("PubSub configuration is missing") - - if self.config.autostart: # type: ignore[attr-defined] - self.start() - - def start(self, sub: bool = True) -> None: - self.pubsub.start() - if sub: - topic = getattr(self.config, "topic", None) - if topic: - self.pubsub.subscribe(topic, self.receive_msg) - - def stop(self) -> None: - self.pubsub.stop() - - def publish(self, *args: Transform) -> None: - """Send transforms using the configured PubSub.""" - if not self.pubsub: - raise ValueError("PubSub is not configured.") - - self.receive_transform(*args) - topic = getattr(self.config, "topic", None) - if topic: - self.pubsub.publish(topic, TFMessage(*args)) - - def publish_static(self, *args: Transform) -> None: - raise NotImplementedError("Static transforms not implemented in PubSubTF.") - - def publish_all(self) -> None: - """Publish all transforms currently stored in all buffers.""" - all_transforms = [] - for buffer in self.buffers.values(): - # Get the latest transform from each buffer - latest = buffer.get() # get() with no args returns latest - if latest: - all_transforms.append(latest) - - if all_transforms: - self.publish(*all_transforms) - - def get( - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ) -> Transform | None: - return super().get(parent_frame, child_frame, time_point, time_tolerance) - - def get_pose( - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ) -> PoseStamped | None: - tf = self.get(parent_frame, child_frame, time_point, time_tolerance) - if not tf: - return None - return tf.to_pose() - - def receive_msg(self, msg: TFMessage, topic: Topic) -> None: - self.receive_tfmessage(msg) - - -@dataclass -class LCMPubsubConfig(PubSubTFConfig): - topic: Topic = field(default_factory=lambda: Topic("/tf", TFMessage)) - pubsub: type[PubSub] | PubSub | None = LCM # type: ignore[type-arg] - autostart: bool = True - - -class LCMTF(PubSubTF): - default_config: type[LCMPubsubConfig] = LCMPubsubConfig - - -TF = LCMTF diff --git a/dimos/protocol/tf/tflcmcpp.py b/dimos/protocol/tf/tflcmcpp.py deleted file mode 100644 index 158a68d3d8..0000000000 --- a/dimos/protocol/tf/tflcmcpp.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime -from typing import Union - -from dimos.msgs.geometry_msgs import Transform -from dimos.protocol.service.lcmservice import LCMConfig, LCMService -from dimos.protocol.tf.tf import TFConfig, TFSpec - - -# this doesn't work due to tf_lcm_py package -class TFLCM(TFSpec, LCMService): - """A service for managing and broadcasting transforms using LCM. - This is not a separete module, You can include this in your module - if you need to access transforms. - - Ideally we would have a generic pubsub for transforms so we are - transport agnostic (TODO) - - For now we are not doing this because we want to use cpp buffer/lcm - implementation. We also don't want to manually hook up tf stream - for each module. - """ - - default_config = Union[TFConfig, LCMConfig] - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - - import tf_lcm_py as tf # type: ignore[import-not-found] - - self.l = tf.LCM() - self.buffer = tf.Buffer(self.config.buffer_size) - self.listener = tf.TransformListener(self.l, self.buffer) - self.broadcaster = tf.TransformBroadcaster() - self.static_broadcaster = tf.StaticTransformBroadcaster() - - # will call the underlying LCMService.start - self.start() - - def send(self, *args: Transform) -> None: - for t in args: - self.broadcaster.send_transform(t.lcm_transform()) - - def send_static(self, *args: Transform) -> None: - for t in args: - self.static_broadcaster.send_static_transform(t) - - def lookup( # type: ignore[no-untyped-def] - self, - parent_frame: str, - child_frame: str, - time_point: float | None = None, - time_tolerance: float | None = None, - ): - return self.buffer.lookup_transform( - parent_frame, - child_frame, - datetime.now(), - lcm_module=self.l, - ) - - def can_transform( - self, parent_frame: str, child_frame: str, time_point: float | datetime | None = None - ) -> bool: - if not time_point: - time_point = datetime.now() - - if isinstance(time_point, float): - time_point = datetime.fromtimestamp(time_point) - - return self.buffer.can_transform(parent_frame, child_frame, time_point) # type: ignore[no-any-return] - - def get_frames(self) -> set[str]: - return set(self.buffer.get_all_frame_names()) - - def start(self) -> None: - super().start() - ... - - def stop(self) -> None: ... diff --git a/dimos/robot/__init__.py b/dimos/robot/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/all_blueprints.py b/dimos/robot/all_blueprints.py deleted file mode 100644 index 19d7e7db29..0000000000 --- a/dimos/robot/all_blueprints.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This file is auto-generated. Do not edit manually. -# Run `pytest dimos/robot/test_all_blueprints_generation.py` to regenerate. - -all_blueprints = { - "arm-teleop": "dimos.teleop.quest.blueprints:arm_teleop", - "arm-teleop-dual": "dimos.teleop.quest.blueprints:arm_teleop_dual", - "arm-teleop-piper": "dimos.teleop.quest.blueprints:arm_teleop_piper", - "arm-teleop-visualizing": "dimos.teleop.quest.blueprints:arm_teleop_visualizing", - "arm-teleop-xarm6": "dimos.teleop.quest.blueprints:arm_teleop_xarm6", - "coordinator-basic": "dimos.control.blueprints:coordinator_basic", - "coordinator-cartesian-ik-mock": "dimos.control.blueprints:coordinator_cartesian_ik_mock", - "coordinator-cartesian-ik-piper": "dimos.control.blueprints:coordinator_cartesian_ik_piper", - "coordinator-combined-xarm6": "dimos.control.blueprints:coordinator_combined_xarm6", - "coordinator-dual-mock": "dimos.control.blueprints:coordinator_dual_mock", - "coordinator-dual-xarm": "dimos.control.blueprints:coordinator_dual_xarm", - "coordinator-mock": "dimos.control.blueprints:coordinator_mock", - "coordinator-piper": "dimos.control.blueprints:coordinator_piper", - "coordinator-piper-xarm": "dimos.control.blueprints:coordinator_piper_xarm", - "coordinator-teleop-dual": "dimos.control.blueprints:coordinator_teleop_dual", - "coordinator-teleop-piper": "dimos.control.blueprints:coordinator_teleop_piper", - "coordinator-teleop-xarm6": "dimos.control.blueprints:coordinator_teleop_xarm6", - "coordinator-velocity-xarm6": "dimos.control.blueprints:coordinator_velocity_xarm6", - "coordinator-xarm6": "dimos.control.blueprints:coordinator_xarm6", - "coordinator-xarm7": "dimos.control.blueprints:coordinator_xarm7", - "demo-agent": "dimos.agents.demo_agent:demo_agent", - "demo-agent-camera": "dimos.agents.demo_agent:demo_agent_camera", - "demo-camera": "dimos.hardware.sensors.camera.module:demo_camera", - "demo-error-on-name-conflicts": "dimos.robot.unitree.demo_error_on_name_conflicts:demo_error_on_name_conflicts", - "demo-google-maps-skill": "dimos.agents.skills.demo_google_maps_skill:demo_google_maps_skill", - "demo-gps-nav": "dimos.agents.skills.demo_gps_nav:demo_gps_nav", - "demo-grasping": "dimos.manipulation.grasping.demo_grasping:demo_grasping", - "demo-object-scene-registration": "dimos.perception.demo_object_scene_registration:demo_object_scene_registration", - "demo-osm": "dimos.mapping.osm.demo_osm:demo_osm", - "demo-skill": "dimos.agents.skills.demo_skill:demo_skill", - "dual-xarm6-planner": "dimos.manipulation.manipulation_blueprints:dual_xarm6_planner", - "keyboard-teleop-piper": "dimos.robot.manipulators.piper.blueprints:keyboard_teleop_piper", - "keyboard-teleop-xarm6": "dimos.robot.manipulators.xarm.blueprints:keyboard_teleop_xarm6", - "keyboard-teleop-xarm7": "dimos.robot.manipulators.xarm.blueprints:keyboard_teleop_xarm7", - "mid360": "dimos.hardware.sensors.lidar.livox.livox_blueprints:mid360", - "mid360-fastlio": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio", - "mid360-fastlio-voxels": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio_voxels", - "mid360-fastlio-voxels-native": "dimos.hardware.sensors.lidar.fastlio2.fastlio_blueprints:mid360_fastlio_voxels_native", - "phone-go2-teleop": "dimos.teleop.phone.blueprints:phone_go2_teleop", - "simple-phone-teleop": "dimos.teleop.phone.blueprints:simple_phone_teleop", - "uintree-g1-primitive-no-nav": "dimos.robot.unitree.g1.blueprints.primitive.uintree_g1_primitive_no_nav:uintree_g1_primitive_no_nav", - "unitree-g1": "dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1:unitree_g1", - "unitree-g1-agentic": "dimos.robot.unitree.g1.blueprints.agentic.unitree_g1_agentic:unitree_g1_agentic", - "unitree-g1-agentic-sim": "dimos.robot.unitree.g1.blueprints.agentic.unitree_g1_agentic_sim:unitree_g1_agentic_sim", - "unitree-g1-basic": "dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic:unitree_g1_basic", - "unitree-g1-basic-sim": "dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic_sim:unitree_g1_basic_sim", - "unitree-g1-detection": "dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1_detection:unitree_g1_detection", - "unitree-g1-full": "dimos.robot.unitree.g1.blueprints.agentic.unitree_g1_full:unitree_g1_full", - "unitree-g1-joystick": "dimos.robot.unitree.g1.blueprints.basic.unitree_g1_joystick:unitree_g1_joystick", - "unitree-g1-shm": "dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1_shm:unitree_g1_shm", - "unitree-g1-sim": "dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1_sim:unitree_g1_sim", - "unitree-go2": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2:unitree_go2", - "unitree-go2-agentic": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic:unitree_go2_agentic", - "unitree-go2-agentic-huggingface": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_huggingface:unitree_go2_agentic_huggingface", - "unitree-go2-agentic-mcp": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_mcp:unitree_go2_agentic_mcp", - "unitree-go2-agentic-ollama": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic_ollama:unitree_go2_agentic_ollama", - "unitree-go2-basic": "dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic:unitree_go2_basic", - "unitree-go2-detection": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_detection:unitree_go2_detection", - "unitree-go2-ros": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_ros:unitree_go2_ros", - "unitree-go2-spatial": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial:unitree_go2_spatial", - "unitree-go2-temporal-memory": "dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_temporal_memory:unitree_go2_temporal_memory", - "unitree-go2-vlm-stream-test": "dimos.robot.unitree.go2.blueprints.smart.unitree_go2_vlm_stream_test:unitree_go2_vlm_stream_test", - "xarm-perception": "dimos.manipulation.manipulation_blueprints:xarm_perception", - "xarm-perception-agent": "dimos.manipulation.manipulation_blueprints:xarm_perception_agent", - "xarm6-planner-only": "dimos.manipulation.manipulation_blueprints:xarm6_planner_only", - "xarm7-planner-coordinator": "dimos.manipulation.manipulation_blueprints:xarm7_planner_coordinator", - "xarm7-trajectory-sim": "dimos.simulation.sim_blueprints:xarm7_trajectory_sim", -} - - -all_modules = { - "agent": "dimos.agents.agent", - "arm_teleop_module": "dimos.teleop.quest.quest_extensions", - "camera_module": "dimos.hardware.sensors.camera.module", - "cartesian_motion_controller": "dimos.manipulation.control.servo_control.cartesian_motion_controller", - "control_coordinator": "dimos.control.coordinator", - "cost_mapper": "dimos.mapping.costmapper", - "demo_calculator_skill": "dimos.agents.skills.demo_calculator_skill", - "demo_robot": "dimos.agents.skills.demo_robot", - "depth_module": "dimos.robot.unitree.depth_module", - "detection3d_module": "dimos.perception.detection.module3D", - "detection_db_module": "dimos.perception.detection.moduleDB", - "fastlio2_module": "dimos.hardware.sensors.lidar.fastlio2.module", - "foxglove_bridge": "dimos.robot.foxglove_bridge", - "g1_connection": "dimos.robot.unitree.g1.connection", - "g1_sim_connection": "dimos.robot.unitree.g1.sim", - "g1_skills": "dimos.robot.unitree.g1.skill_container", - "go2_connection": "dimos.robot.unitree.go2.connection", - "google_maps_skill": "dimos.agents.skills.google_maps_skill_container", - "gps_nav_skill": "dimos.agents.skills.gps_nav_skill", - "grasping_module": "dimos.manipulation.grasping.grasping", - "joint_trajectory_controller": "dimos.manipulation.control.trajectory_controller.joint_trajectory_controller", - "keyboard_teleop": "dimos.robot.unitree.keyboard_teleop", - "keyboard_teleop_module": "dimos.teleop.keyboard.keyboard_teleop_module", - "manipulation_module": "dimos.manipulation.manipulation_module", - "mapper": "dimos.robot.unitree.type.map", - "mid360_module": "dimos.hardware.sensors.lidar.livox.module", - "navigation_skill": "dimos.agents.skills.navigation", - "object_scene_registration_module": "dimos.perception.object_scene_registration", - "object_tracking": "dimos.perception.object_tracker", - "osm_skill": "dimos.agents.skills.osm", - "person_follow_skill": "dimos.agents.skills.person_follow", - "person_tracker_module": "dimos.perception.detection.person_tracker", - "phone_teleop_module": "dimos.teleop.phone.phone_teleop_module", - "quest_teleop_module": "dimos.teleop.quest.quest_teleop_module", - "realsense_camera": "dimos.hardware.sensors.camera.realsense.camera", - "replanning_a_star_planner": "dimos.navigation.replanning_a_star.module", - "rerun_bridge": "dimos.visualization.rerun.bridge", - "ros_nav": "dimos.navigation.rosnav", - "simple_phone_teleop_module": "dimos.teleop.phone.phone_extensions", - "simulation": "dimos.simulation.manipulators.sim_module", - "spatial_memory": "dimos.perception.spatial_perception", - "speak_skill": "dimos.agents.skills.speak_skill", - "temporal_memory": "dimos.perception.experimental.temporal_memory.temporal_memory", - "twist_teleop_module": "dimos.teleop.quest.quest_extensions", - "unitree_skills": "dimos.robot.unitree.unitree_skill_container", - "utilization": "dimos.utils.monitoring", - "visualizing_teleop_module": "dimos.teleop.quest.quest_extensions", - "vlm_agent": "dimos.agents.vlm_agent", - "vlm_stream_tester": "dimos.agents.vlm_stream_tester", - "voxel_mapper": "dimos.mapping.voxels", - "wavefront_frontier_explorer": "dimos.navigation.frontier_exploration.wavefront_frontier_goal_selector", - "web_input": "dimos.agents.web_human_input", - "websocket_vis": "dimos.web.websocket_vis.websocket_vis_module", - "zed_camera": "dimos.hardware.sensors.camera.zed.camera", -} diff --git a/dimos/robot/cli/dimos.py b/dimos/robot/cli/dimos.py deleted file mode 100644 index c390d3b76c..0000000000 --- a/dimos/robot/cli/dimos.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from enum import Enum -import inspect -import sys -from typing import Any, get_args, get_origin - -from dotenv import load_dotenv -import typer - -from dimos.core.global_config import GlobalConfig, global_config -from dimos.robot.all_blueprints import all_blueprints - -RobotType = Enum("RobotType", {key.replace("-", "_").upper(): key for key in all_blueprints.keys()}) # type: ignore[misc] - -main = typer.Typer( - help="Dimensional CLI", - no_args_is_help=True, -) - -load_dotenv() - - -def create_dynamic_callback(): # type: ignore[no-untyped-def] - fields = GlobalConfig.model_fields - - # Build the function signature dynamically - params = [ - inspect.Parameter("ctx", inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=typer.Context), - ] - - # Create parameters for each field in GlobalConfig - for field_name, field_info in fields.items(): - field_type = field_info.annotation - - # Handle Optional types - # Check for Optional/Union with None - if get_origin(field_type) is type(str | None): - inner_types = get_args(field_type) - if len(inner_types) == 2 and type(None) in inner_types: - # It's Optional[T], get the actual type T - actual_type = next(t for t in inner_types if t != type(None)) - else: - actual_type = field_type - else: - actual_type = field_type - - # Convert field name from snake_case to kebab-case for CLI - cli_option_name = field_name.replace("_", "-") - - # Special handling for boolean fields - if actual_type is bool: - # For boolean fields, create --flag/--no-flag pattern - param = inspect.Parameter( - field_name, - inspect.Parameter.KEYWORD_ONLY, - default=typer.Option( - None, # None means use the model's default if not provided - f"--{cli_option_name}/--no-{cli_option_name}", - help=f"Override {field_name} in GlobalConfig", - ), - annotation=bool | None, - ) - else: - # For non-boolean fields, use regular option - param = inspect.Parameter( - field_name, - inspect.Parameter.KEYWORD_ONLY, - default=typer.Option( - None, # None means use the model's default if not provided - f"--{cli_option_name}", - help=f"Override {field_name} in GlobalConfig", - ), - annotation=actual_type | None, - ) - params.append(param) - - def callback(**kwargs) -> None: # type: ignore[no-untyped-def] - ctx = kwargs.pop("ctx") - ctx.obj = {k: v for k, v in kwargs.items() if v is not None} - - callback.__signature__ = inspect.Signature(params) # type: ignore[attr-defined] - - return callback - - -main.callback()(create_dynamic_callback()) # type: ignore[no-untyped-call] - - -@main.command() -def run( - ctx: typer.Context, - robot_type: RobotType = typer.Argument(..., help="Type of robot to run"), - extra_modules: list[str] = typer.Option( # type: ignore[valid-type] - [], "--extra-module", help="Extra modules to add to the blueprint" - ), -) -> None: - """Start a robot blueprint""" - from dimos.core.blueprints import autoconnect - from dimos.protocol import pubsub - from dimos.robot.get_all_blueprints import get_blueprint_by_name, get_module_by_name - from dimos.utils.logging_config import setup_exception_handler - - setup_exception_handler() - - cli_config_overrides: dict[str, Any] = ctx.obj - global_config.update(**cli_config_overrides) - pubsub.lcm.autoconf() # type: ignore[attr-defined] - blueprint = get_blueprint_by_name(robot_type.value) - - if extra_modules: - loaded_modules = [get_module_by_name(mod_name) for mod_name in extra_modules] # type: ignore[attr-defined] - blueprint = autoconnect(blueprint, *loaded_modules) - - dimos = blueprint.build(cli_config_overrides=cli_config_overrides) - dimos.loop() - - -@main.command() -def show_config(ctx: typer.Context) -> None: - """Show current config settings and their values.""" - - cli_config_overrides: dict[str, Any] = ctx.obj - global_config.update(**cli_config_overrides) - - for field_name, value in global_config.model_dump().items(): - typer.echo(f"{field_name}: {value}") - - -@main.command() -def list() -> None: - """List all available blueprints.""" - from dimos.robot.all_blueprints import all_blueprints - - blueprints = [name for name in all_blueprints.keys() if not name.startswith("demo-")] - for blueprint_name in sorted(blueprints): - typer.echo(blueprint_name) - - -@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) -def lcmspy(ctx: typer.Context) -> None: - """LCM spy tool for monitoring LCM messages.""" - from dimos.utils.cli.lcmspy.run_lcmspy import main as lcmspy_main - - sys.argv = ["lcmspy", *ctx.args] - lcmspy_main() - - -@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) -def agentspy(ctx: typer.Context) -> None: - """Agent spy tool for monitoring agents.""" - from dimos.utils.cli.agentspy.agentspy import main as agentspy_main - - sys.argv = ["agentspy", *ctx.args] - agentspy_main() - - -@main.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) -def humancli(ctx: typer.Context) -> None: - """Interface interacting with agents.""" - from dimos.utils.cli.human.humanclianim import main as humancli_main - - sys.argv = ["humancli", *ctx.args] - humancli_main() - - -topic_app = typer.Typer(help="Topic commands for pub/sub") -main.add_typer(topic_app, name="topic") - - -@topic_app.command() -def echo( - topic: str = typer.Argument(..., help="Topic name to listen on (e.g., /goal_request)"), - type_name: str | None = typer.Argument( - None, - help="Optional message type (e.g., PoseStamped). If omitted, infer from '/topic#pkg.Msg'.", - ), -) -> None: - from dimos.robot.cli.topic import topic_echo - - topic_echo(topic, type_name) - - -@topic_app.command() -def send( - topic: str = typer.Argument(..., help="Topic name to send to (e.g., /goal_request)"), - message_expr: str = typer.Argument(..., help="Python expression for the message"), -) -> None: - from dimos.robot.cli.topic import topic_send - - topic_send(topic, message_expr) - - -@main.command(name="rerun-bridge") -def rerun_bridge_cmd( - viewer_mode: str = typer.Option( - "native", help="Viewer mode: native (desktop), web (browser), none (headless)" - ), - memory_limit: str = typer.Option( - "25%", help="Memory limit for Rerun viewer (e.g., '4GB', '16GB', '25%')" - ), -) -> None: - """Launch the Rerun visualization bridge.""" - from dimos.visualization.rerun.bridge import run_bridge - - run_bridge(viewer_mode=viewer_mode, memory_limit=memory_limit) - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/cli/topic.py b/dimos/robot/cli/topic.py deleted file mode 100644 index 1f7ada4f28..0000000000 --- a/dimos/robot/cli/topic.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import importlib -import re -import time - -import typer - -from dimos.core.transport import LCMTransport, pLCMTransport -from dimos.protocol.pubsub.impl.lcmpubsub import LCMPubSubBase - -_modules_to_try = [ - "dimos.msgs.geometry_msgs", - "dimos.msgs.nav_msgs", - "dimos.msgs.sensor_msgs", - "dimos.msgs.std_msgs", - "dimos.msgs.vision_msgs", - "dimos.msgs.foxglove_msgs", - "dimos.msgs.tf2_msgs", -] - - -def _resolve_type(type_name: str) -> type: - for module_name in _modules_to_try: - try: - module = importlib.import_module(module_name) - if hasattr(module, type_name): - return getattr(module, type_name) # type: ignore[no-any-return] - except ImportError: - continue - - raise ValueError(f"Could not find type '{type_name}' in any known message modules") - - -def topic_echo(topic: str, type_name: str | None) -> None: - # Explicit mode (legacy): unchanged. - if type_name is not None: - msg_type = _resolve_type(type_name) - use_pickled = getattr(msg_type, "lcm_encode", None) is None - transport: pLCMTransport[object] | LCMTransport[object] = ( - pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) - ) - - def _on_message(msg: object) -> None: - print(msg) - - transport.subscribe(_on_message) - typer.echo(f"Listening on {topic} for {type_name} messages... (Ctrl+C to stop)") - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - typer.echo("\nStopped.") - return - - # Inferred typed mode: listen on /topic#pkg.Msg and decode from the msg_name suffix. - bus = LCMPubSubBase(autoconf=True) - bus.start() # starts threaded handle loop - - typed_pattern = rf"^{re.escape(topic)}#.*" - - def on_msg(channel: str, data: bytes) -> None: - _, msg_name = channel.split("#", 1) # e.g. "nav_msgs.Odometry" - pkg, cls_name = msg_name.split(".", 1) # "nav_msgs", "Odometry" - module = importlib.import_module(f"dimos.msgs.{pkg}") - cls = getattr(module, cls_name) - print(cls.lcm_decode(data)) - - assert bus.l is not None - bus.l.subscribe(typed_pattern, on_msg) - - typer.echo( - f"Listening on {topic} (inferring from typed LCM channels like '{topic}#pkg.Msg')... " - "(Ctrl+C to stop)" - ) - - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - bus.stop() - typer.echo("\nStopped.") - - -def topic_send(topic: str, message_expr: str) -> None: - eval_context: dict[str, object] = {} - modules_to_import = [ - "dimos.msgs.geometry_msgs", - "dimos.msgs.nav_msgs", - "dimos.msgs.sensor_msgs", - "dimos.msgs.std_msgs", - "dimos.msgs.vision_msgs", - "dimos.msgs.foxglove_msgs", - "dimos.msgs.tf2_msgs", - ] - - for module_name in modules_to_import: - try: - module = importlib.import_module(module_name) - for name in getattr(module, "__all__", dir(module)): - if not name.startswith("_"): - obj = getattr(module, name, None) - if obj is not None: - eval_context[name] = obj - except ImportError: - continue - - try: - message = eval(message_expr, eval_context) - except Exception as e: - typer.echo(f"Error parsing message: {e}", err=True) - raise typer.Exit(1) - - msg_type = type(message) - use_pickled = getattr(msg_type, "lcm_encode", None) is None - transport: pLCMTransport[object] | LCMTransport[object] = ( - pLCMTransport(topic) if use_pickled else LCMTransport(topic, msg_type) - ) - - transport.broadcast(None, message) - typer.echo(f"Sent to {topic}: {message}") diff --git a/dimos/robot/drone/README.md b/dimos/robot/drone/README.md deleted file mode 100644 index 6e8ceb4d63..0000000000 --- a/dimos/robot/drone/README.md +++ /dev/null @@ -1,289 +0,0 @@ -# DimOS Drone Module - -Complete integration for DJI drones via RosettaDrone MAVLink bridge with visual servoing and autonomous tracking capabilities. - -## Quick Start - -### Test the System -```bash -# Test with replay mode (no hardware needed) -python dimos/robot/drone/drone.py --replay - -# Real drone - indoor (IMU odometry) -python dimos/robot/drone/drone.py - -# Real drone - outdoor (GPS odometry) -python dimos/robot/drone/drone.py --outdoor -``` - -### Python API Usage -```python -from dimos.robot.drone.drone import Drone - -# Connect to drone -drone = Drone(connection_string='udp:0.0.0.0:14550', outdoor=True) # Use outdoor=True for GPS -drone.start() - -# Basic operations -drone.arm() -drone.takeoff(altitude=5.0) -drone.move(Vector3(1.0, 0, 0), duration=2.0) # Forward 1m/s for 2s - -# Visual tracking -drone.tracking.track_object("person", duration=120) # Track for 2 minutes - -# Land and cleanup -drone.land() -drone.stop() -``` - -## Installation - -### Python Package -```bash -# Install DimOS with drone support -pip install -e .[drone] -``` - -### System Dependencies -```bash -# GStreamer for video streaming -sudo apt-get install -y gstreamer1.0-tools gstreamer1.0-plugins-base \ - gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ - gstreamer1.0-libav python3-gi python3-gi-cairo - -# LCM for communication -sudo apt-get install liblcm-dev -``` - -### Environment Setup -```bash -export DRONE_IP=0.0.0.0 # Listen on all interfaces -export DRONE_VIDEO_PORT=5600 -export DRONE_MAVLINK_PORT=14550 -``` - -## RosettaDrone Setup (Critical) - -RosettaDrone is an Android app that bridges DJI SDK to MAVLink protocol. Without it, the drone cannot communicate with DimOS. - -### Option 1: Pre-built APK -1. Download latest release: https://github.com/RosettaDrone/rosettadrone/releases -2. Install on Android device connected to DJI controller -3. Configure in app: - - MAVLink Target IP: Your computer's IP - - MAVLink Port: 14550 - - Video Port: 5600 - - Enable video streaming - -### Option 2: Build from Source - -#### Prerequisites -- Android Studio -- DJI Developer Account: https://developer.dji.com/ -- Git - -#### Build Steps -```bash -# Clone repository -git clone https://github.com/RosettaDrone/rosettadrone.git -cd rosettadrone - -# Build with Gradle -./gradlew assembleRelease - -# APK will be in: app/build/outputs/apk/release/ -``` - -#### Configure DJI API Key -1. Register app at https://developer.dji.com/user/apps - - Package name: `sq.rogue.rosettadrone` -2. Add key to `app/src/main/AndroidManifest.xml`: -```xml - -``` - -#### Install APK -```bash -adb install -r app/build/outputs/apk/release/rosettadrone-release.apk -``` - -### Hardware Connection -``` -DJI Drone ← Wireless → DJI Controller ← USB → Android Device ← WiFi → DimOS Computer -``` - -1. Connect Android to DJI controller via USB -2. Start RosettaDrone app -3. Wait for "DJI Connected" status -4. Verify "MAVLink Active" shows in app - -## Architecture - -### Module Structure -``` -drone.py # Main orchestrator -├── connection_module.py # MAVLink communication & skills -├── camera_module.py # Video processing & depth estimation -├── tracking_module.py # Visual servoing & object tracking -├── mavlink_connection.py # Low-level MAVLink protocol -└── dji_video_stream.py # GStreamer video capture -``` - -### Communication Flow -``` -DJI Drone → RosettaDrone → MAVLink UDP → connection_module → LCM Topics - → Video UDP → dji_video_stream → tracking_module -``` - -### LCM Topics -- `/drone/odom` - Position and orientation -- `/drone/status` - Armed state, battery -- `/drone/video` - Camera frames -- `/drone/tracking/cmd_vel` - Tracking velocity commands -- `/drone/tracking/overlay` - Visualization with tracking box - -## Visual Servoing & Tracking - -### Object Tracking -```python -# Track specific object -result = drone.tracking.track_object("red flag", duration=60) - -# Track nearest/most prominent object -result = drone.tracking.track_object(None, duration=60) - -# Stop tracking -drone.tracking.stop_tracking() -``` - -### PID Tuning -Configure in `drone.py` initialization: -```python -# Indoor (gentle, precise) -x_pid_params=(0.001, 0.0, 0.0001, (-0.5, 0.5), None, 30) - -# Outdoor (aggressive, wind-resistant) -x_pid_params=(0.003, 0.0001, 0.0002, (-1.0, 1.0), None, 10) -``` - -Parameters: `(Kp, Ki, Kd, (min_output, max_output), integral_limit, deadband_pixels)` - -### Visual Servoing Flow -1. Qwen model detects object → bounding box -2. CSRT tracker initialized on bbox -3. PID controller computes velocity from pixel error -4. Velocity commands sent via LCM stream -5. Connection module converts to MAVLink commands - -## Available Skills - -### Movement & Control -- `move(vector, duration)` - Move with velocity vector -- `takeoff(altitude)` - Takeoff to altitude -- `land()` - Land at current position -- `arm()/disarm()` - Arm/disarm motors -- `fly_to(lat, lon, alt)` - Fly to GPS coordinates - -### Perception -- `observe()` - Get current camera frame -- `follow_object(description, duration)` - Follow object with servoing - -### Tracking Module -- `track_object(name, duration)` - Track and follow object -- `stop_tracking()` - Stop current tracking -- `get_status()` - Get tracking status - -## Testing - -### Unit Tests -```bash -pytest -s dimos/robot/drone/ -``` - -### Replay Mode (No Hardware) -```python -# Use recorded data for testing -drone = Drone(connection_string='replay') -drone.start() -# All operations work with recorded data -``` - -## Troubleshooting - -### No MAVLink Connection -- Check Android and computer are on same network -- Verify IP address in RosettaDrone matches computer -- Test with: `nc -lu 14550` (should see data) -- Check firewall: `sudo ufw allow 14550/udp` - -### No Video Stream -- Enable video in RosettaDrone settings -- Test with: `nc -lu 5600` (should see data) -- Verify GStreamer installed: `gst-launch-1.0 --version` - -### Tracking Issues -- Increase lighting for better detection -- Adjust PID gains for environment -- Check `max_lost_frames` in tracking module -- Monitor with Foxglove on `ws://localhost:8765` - -### Wrong Movement Direction -- Don't modify coordinate conversions -- Verify with: `pytest test_drone.py::test_ned_to_ros_coordinate_conversion` -- Check camera orientation assumptions - -## Advanced Features - -### Coordinate Systems -- **MAVLink/NED**: X=North, Y=East, Z=Down -- **ROS/DimOS**: X=Forward, Y=Left, Z=Up -- Automatic conversion handled internally - -### Depth Estimation -Camera module can generate depth maps using Metric3D: -```python -# Depth published to /drone/depth and /drone/pointcloud -# Requires GPU with 8GB+ VRAM -``` - -### Foxglove Visualization -Connect Foxglove Studio to `ws://localhost:8765` to see: -- Live video with tracking overlay -- 3D drone position -- Telemetry plots -- Transform tree - -## Network Ports -- **14550**: MAVLink UDP -- **5600**: Video stream UDP -- **8765**: Foxglove WebSocket -- **7667**: LCM messaging - -## Development - -### Adding New Skills -Add to `connection_module.py` with `@skill` decorator: -```python -@skill -def my_skill(self, param: float) -> str: - """Skill description for LLM.""" - # Implementation - return "Result" -``` - -### Modifying PID Control -Edit gains in `drone.py` `_deploy_tracking()`: -- Increase Kp for faster response -- Add Ki for steady-state error -- Increase Kd for damping -- Adjust limits for max velocity - -## Safety Notes -- Always test in simulator or with propellers removed first -- Set conservative PID gains initially -- Implement geofencing for outdoor flights -- Monitor battery voltage continuously -- Have manual override ready diff --git a/dimos/robot/drone/__init__.py b/dimos/robot/drone/__init__.py deleted file mode 100644 index 1ed8521b8b..0000000000 --- a/dimos/robot/drone/__init__.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Generic drone module for MAVLink-based drones.""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "camera_module": ["DroneCameraModule"], - "connection_module": ["DroneConnectionModule"], - "drone": ["Drone"], - "mavlink_connection": ["MavlinkConnection"], - }, -) diff --git a/dimos/robot/drone/camera_module.py b/dimos/robot/drone/camera_module.py deleted file mode 100644 index 8ba88fd028..0000000000 --- a/dimos/robot/drone/camera_module.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Camera module for drone with depth estimation.""" - -import threading -import time -from typing import Any - -from dimos_lcm.sensor_msgs import CameraInfo - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.msgs.std_msgs import Header -from dimos.perception.common.utils import colorize_depth -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class DroneCameraModule(Module): - """ - Camera module for drone that processes RGB images to generate depth using Metric3D. - - Subscribes to: - - /video: RGB camera images from drone - - Publishes: - - /drone/color_image: RGB camera images - - /drone/depth_image: Depth images from Metric3D - - /drone/depth_colorized: Colorized depth - - /drone/camera_info: Camera calibration - - /drone/camera_pose: Camera pose from TF - """ - - # Inputs - video: In[Image] - - # Outputs - color_image: Out[Image] - depth_image: Out[Image] - depth_colorized: Out[Image] - camera_info: Out[CameraInfo] - camera_pose: Out[PoseStamped] - - def __init__( - self, - camera_intrinsics: list[float], - world_frame_id: str = "world", - camera_frame_id: str = "camera_link", - base_frame_id: str = "base_link", - gt_depth_scale: float = 2.0, - **kwargs: Any, - ) -> None: - """Initialize drone camera module. - - Args: - camera_intrinsics: [fx, fy, cx, cy] - camera_frame_id: TF frame for camera - base_frame_id: TF frame for drone base - gt_depth_scale: Depth scale factor - """ - super().__init__(**kwargs) - - if len(camera_intrinsics) != 4: - raise ValueError("Camera intrinsics must be [fx, fy, cx, cy]") - - self.camera_intrinsics = camera_intrinsics - self.camera_frame_id = camera_frame_id - self.base_frame_id = base_frame_id - self.world_frame_id = world_frame_id - self.gt_depth_scale = gt_depth_scale - - # Metric3D for depth - self.metric3d: Any = None # Lazy-loaded Metric3D model - - # Processing state - self._running = False - self._latest_frame: Image | None = None - self._processing_thread: threading.Thread | None = None - self._stop_processing = threading.Event() - - logger.info(f"DroneCameraModule initialized with intrinsics: {camera_intrinsics}") - - @rpc - def start(self) -> None: - """Start the camera module.""" - if self._running: - logger.warning("Camera module already running") - return - - # Start processing thread for depth (which will init Metric3D and handle video) - self._running = True - self._stop_processing.clear() - self._processing_thread = threading.Thread(target=self._processing_loop, daemon=True) - self._processing_thread.start() - - logger.info("Camera module started") - return - - def _on_video_frame(self, frame: Image) -> None: - """Handle incoming video frame.""" - if not self._running: - return - - # Publish color image immediately - self.color_image.publish(frame) - - # Store for depth processing - self._latest_frame = frame - - def _processing_loop(self) -> None: - """Process depth estimation in background.""" - # Initialize Metric3D in the background thread - if self.metric3d is None: - try: - from dimos.models.depth.metric3d import Metric3D - - self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) - logger.info("Metric3D initialized") - except Exception as e: - logger.warning(f"Metric3D not available: {e}") - self.metric3d = None - - # Subscribe to video once connection is available - subscribed = False - while not subscribed and not self._stop_processing.is_set(): - try: - if self.video.connection is not None: - self.video.subscribe(self._on_video_frame) - subscribed = True - logger.info("Subscribed to video input") - else: - time.sleep(0.1) - except Exception as e: - logger.debug(f"Waiting for video connection: {e}") - time.sleep(0.1) - - logger.info("Depth processing loop started") - - _reported_error = False - - while not self._stop_processing.is_set(): - if self._latest_frame is not None and self.metric3d is not None: - try: - frame = self._latest_frame - self._latest_frame = None - - # Get numpy array from Image - img_array = frame.data - - # Generate depth - depth_array = self.metric3d.infer_depth(img_array) / self.gt_depth_scale - - # Create header - header = Header(self.camera_frame_id) - - # Publish depth - depth_msg = Image( - data=depth_array, - format=ImageFormat.DEPTH, - frame_id=header.frame_id, - ts=header.ts, - ) - self.depth_image.publish(depth_msg) - - # Publish colorized depth - depth_colorized_array = colorize_depth( - depth_array, max_depth=10.0, overlay_stats=True - ) - if depth_colorized_array is not None: - depth_colorized_msg = Image( - data=depth_colorized_array, - format=ImageFormat.RGB, - frame_id=header.frame_id, - ts=header.ts, - ) - self.depth_colorized.publish(depth_colorized_msg) - - # Publish camera info - self._publish_camera_info(header, img_array.shape) - - # Publish camera pose - self._publish_camera_pose(header) - - except Exception as e: - if not _reported_error: - _reported_error = True - logger.error(f"Error processing depth: {e}") - else: - time.sleep(0.01) - - logger.info("Depth processing loop stopped") - - def _publish_camera_info(self, header: Header, shape: tuple[int, ...]) -> None: - """Publish camera calibration info.""" - try: - fx, fy, cx, cy = self.camera_intrinsics - height, width = shape[:2] - - # Camera matrix K (3x3) - K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] - - # No distortion for now - D = [0.0, 0.0, 0.0, 0.0, 0.0] - - # Identity rotation - R = [1, 0, 0, 0, 1, 0, 0, 0, 1] - - # Projection matrix P (3x4) - P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] - - msg = CameraInfo( - D_length=len(D), - header=header, - height=height, - width=width, - distortion_model="plumb_bob", - D=D, - K=K, - R=R, - P=P, - binning_x=0, - binning_y=0, - ) - - self.camera_info.publish(msg) - - except Exception as e: - logger.error(f"Error publishing camera info: {e}") - - def _publish_camera_pose(self, header: Header) -> None: - """Publish camera pose from TF.""" - try: - transform = self.tf.get( - parent_frame=self.world_frame_id, - child_frame=self.camera_frame_id, - time_point=header.ts, - time_tolerance=1.0, - ) - - if transform: - pose_msg = PoseStamped( - ts=header.ts, - frame_id=self.camera_frame_id, - position=transform.translation, - orientation=transform.rotation, - ) - self.camera_pose.publish(pose_msg) - - except Exception as e: - logger.error(f"Error publishing camera pose: {e}") - - @rpc - def stop(self) -> None: - """Stop the camera module.""" - if not self._running: - return - - self._running = False - self._stop_processing.set() - - # Wait for thread - if self._processing_thread and self._processing_thread.is_alive(): - self._processing_thread.join(timeout=2.0) - - # Cleanup Metric3D - if self.metric3d: - self.metric3d.cleanup() - - logger.info("Camera module stopped") diff --git a/dimos/robot/drone/connection_module.py b/dimos/robot/drone/connection_module.py deleted file mode 100644 index db5c4ca4cc..0000000000 --- a/dimos/robot/drone/connection_module.py +++ /dev/null @@ -1,488 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""DimOS module wrapper for drone connection.""" - -from collections.abc import Generator -import json -import threading -import time -from typing import Any - -from dimos_lcm.std_msgs import String -from reactivex.disposable import CompositeDisposable, Disposable - -from dimos.agents.annotation import skill -from dimos.core import In, Module, Out, rpc -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.robot.drone.dji_video_stream import DJIDroneVideoStream -from dimos.robot.drone.mavlink_connection import MavlinkConnection -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def _add_disposable(composite: CompositeDisposable, item: Disposable | Any) -> None: - if isinstance(item, Disposable): - composite.add(item) - elif callable(item): - composite.add(Disposable(item)) - - -class DroneConnectionModule(Module): - """Module that handles drone sensor data and movement commands.""" - - # Inputs - movecmd: In[Vector3] - movecmd_twist: In[Twist] # Twist commands from tracking/navigation - gps_goal: In[LatLon] - tracking_status: In[Any] - - # Outputs - odom: Out[PoseStamped] - gps_location: Out[LatLon] - status: Out[Any] # JSON status - telemetry: Out[Any] # Full telemetry JSON - video: Out[Image] - follow_object_cmd: Out[Any] - - # Parameters - connection_string: str - - # Internal state - _odom: PoseStamped | None = None - _status: dict[str, Any] = {} - _latest_video_frame: Image | None = None - _latest_telemetry: dict[str, Any] | None = None - _latest_status: dict[str, Any] | None = None - _latest_status_lock: threading.RLock - - def __init__( - self, - connection_string: str = "udp:0.0.0.0:14550", - video_port: int = 5600, - outdoor: bool = False, - *args: Any, - **kwargs: Any, - ) -> None: - """Initialize drone connection module. - - Args: - connection_string: MAVLink connection string - video_port: UDP port for video stream - outdoor: Use GPS only mode (no velocity integration) - """ - self.connection_string = connection_string - self.video_port = video_port - self.outdoor = outdoor - self.connection: MavlinkConnection | None = None - self.video_stream: DJIDroneVideoStream | None = None - self._latest_video_frame = None - self._latest_telemetry = None - self._latest_status = None - self._latest_status_lock = threading.RLock() - self._running = False - self._telemetry_thread: threading.Thread | None = None - Module.__init__(self, *args, **kwargs) - - @rpc - def start(self) -> None: - """Start the connection and subscribe to sensor streams.""" - # Check for replay mode - if self.connection_string == "replay": - from dimos.robot.drone.dji_video_stream import FakeDJIVideoStream - from dimos.robot.drone.mavlink_connection import FakeMavlinkConnection - - self.connection = FakeMavlinkConnection("replay") - self.video_stream = FakeDJIVideoStream(port=self.video_port) - else: - self.connection = MavlinkConnection(self.connection_string, outdoor=self.outdoor) - self.connection.connect() - - self.video_stream = DJIDroneVideoStream(port=self.video_port) - - if not self.connection.connected: - logger.error("Failed to connect to drone") - return - - # Start video stream (already created above) - if self.video_stream.start(): - logger.info("Video stream started") - # Subscribe to video, store latest frame and publish it - _add_disposable( - self._disposables, - self.video_stream.get_stream().subscribe(self._store_and_publish_frame), - ) - # # TEMPORARY - DELETE AFTER RECORDING - # from dimos.utils.testing import TimedSensorStorage - # self._video_storage = TimedSensorStorage("drone/video") - # self._video_subscription = self._video_storage.save_stream(self.video_stream.get_stream()).subscribe() - # logger.info("Recording video to data/drone/video/") - else: - logger.warning("Video stream failed to start") - - # Subscribe to drone streams - _add_disposable( - self._disposables, self.connection.odom_stream().subscribe(self._publish_tf) - ) - _add_disposable( - self._disposables, self.connection.status_stream().subscribe(self._publish_status) - ) - _add_disposable( - self._disposables, self.connection.telemetry_stream().subscribe(self._publish_telemetry) - ) - - # Subscribe to movement commands - _add_disposable(self._disposables, self.movecmd.subscribe(self.move)) - - # Subscribe to Twist movement commands - if self.movecmd_twist.transport: - _add_disposable(self._disposables, self.movecmd_twist.subscribe(self._on_move_twist)) - - if self.gps_goal.transport: - _add_disposable(self._disposables, self.gps_goal.subscribe(self._on_gps_goal)) - - if self.tracking_status.transport: - _add_disposable( - self._disposables, self.tracking_status.subscribe(self._on_tracking_status) - ) - - # Start telemetry update thread - import threading - - self._running = True - self._telemetry_thread = threading.Thread(target=self._telemetry_loop, daemon=True) - self._telemetry_thread.start() - - logger.info("Drone connection module started") - return - - def _store_and_publish_frame(self, frame: Image) -> None: - """Store the latest video frame and publish it.""" - self._latest_video_frame = frame - self.video.publish(frame) - - def _publish_tf(self, msg: PoseStamped) -> None: - """Publish odometry and TF transforms.""" - self._odom = msg - - # Publish odometry - self.odom.publish(msg) - - # Publish base_link transform - base_link = Transform( - translation=msg.position, - rotation=msg.orientation, - frame_id="world", - child_frame_id="base_link", - ts=msg.ts if hasattr(msg, "ts") else time.time(), - ) - self.tf.publish(base_link) - - # Publish camera_link transform (camera mounted on front of drone, no gimbal factored in yet) - camera_link = Transform( - translation=Vector3(0.1, 0.0, -0.05), # 10cm forward, 5cm down - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # No rotation relative to base - frame_id="base_link", - child_frame_id="camera_link", - ts=time.time(), - ) - self.tf.publish(camera_link) - - def _publish_status(self, status: dict[str, Any]) -> None: - """Publish drone status as JSON string.""" - self._status = status - - status_str = String(json.dumps(status)) - self.status.publish(status_str) - - def _publish_telemetry(self, telemetry: dict[str, Any]) -> None: - """Publish full telemetry as JSON string.""" - telemetry_str = String(json.dumps(telemetry)) - self.telemetry.publish(telemetry_str) - self._latest_telemetry = telemetry - - if "GLOBAL_POSITION_INT" in telemetry: - tel = telemetry["GLOBAL_POSITION_INT"] - self.gps_location.publish(LatLon(lat=tel["lat"], lon=tel["lon"])) - - def _telemetry_loop(self) -> None: - """Continuously update telemetry at 30Hz.""" - frame_count = 0 - while self._running: - try: - # Update telemetry from drone - if self.connection is not None: - self.connection.update_telemetry(timeout=0.01) - - # Publish default odometry if we don't have real data yet - if frame_count % 10 == 0: # Every ~3Hz - if self._odom is None: - # Publish default pose - default_pose = PoseStamped( - position=Vector3(0, 0, 0), - orientation=Quaternion(0, 0, 0, 1), - frame_id="world", - ts=time.time(), - ) - self._publish_tf(default_pose) - logger.debug("Publishing default odometry") - - frame_count += 1 - time.sleep(0.033) # ~30Hz - except Exception as e: - logger.debug(f"Telemetry update error: {e}") - time.sleep(0.1) - - @rpc - def get_odom(self) -> PoseStamped | None: - """Get current odometry. - - Returns: - Current pose or None - """ - return self._odom - - @rpc - def get_status(self) -> dict[str, Any]: - """Get current drone status. - - Returns: - Status dictionary - """ - return self._status.copy() - - @skill - def move(self, vector: Vector3, duration: float = 0.0) -> None: - """Send movement command to drone. - - Args: - vector: Velocity vector [x, y, z] in m/s - duration: How long to move (0 = continuous) - """ - if self.connection: - # Convert dict/list to Vector3 - if isinstance(vector, dict): - vector = Vector3(vector.get("x", 0), vector.get("y", 0), vector.get("z", 0)) - elif isinstance(vector, (list, tuple)): - vector = Vector3( - vector[0] if len(vector) > 0 else 0, - vector[1] if len(vector) > 1 else 0, - vector[2] if len(vector) > 2 else 0, - ) - self.connection.move(vector, duration) - - @skill - def takeoff(self, altitude: float = 3.0) -> bool: - """Takeoff to specified altitude. - - Args: - altitude: Target altitude in meters - - Returns: - True if takeoff initiated - """ - if self.connection: - return self.connection.takeoff(altitude) - return False - - @skill - def land(self) -> bool: - """Land the drone. - - Returns: - True if land command sent - """ - if self.connection: - return self.connection.land() - return False - - @skill - def arm(self) -> bool: - """Arm the drone. - - Returns: - True if armed successfully - """ - if self.connection: - return self.connection.arm() - return False - - @skill - def disarm(self) -> bool: - """Disarm the drone. - - Returns: - True if disarmed successfully - """ - if self.connection: - return self.connection.disarm() - return False - - @skill - def set_mode(self, mode: str) -> bool: - """Set flight mode. - - Args: - mode: Flight mode name - - Returns: - True if mode set successfully - """ - if self.connection: - return self.connection.set_mode(mode) - return False - - def move_twist(self, twist: Twist, duration: float = 0.0, lock_altitude: bool = True) -> bool: - """Move using ROS-style Twist commands. - - Args: - twist: Twist message with linear velocities - duration: How long to move (0 = single command) - lock_altitude: If True, ignore Z velocity - - Returns: - True if command sent successfully - """ - if self.connection: - return self.connection.move_twist(twist, duration, lock_altitude) - return False - - @skill - def is_flying_to_target(self) -> bool: - """Check if drone is currently flying to a GPS target. - - Returns: - True if flying to target, False otherwise - """ - if self.connection and hasattr(self.connection, "is_flying_to_target"): - return self.connection.is_flying_to_target - return False - - @skill - def fly_to(self, lat: float, lon: float, alt: float) -> str: - """Fly drone to GPS coordinates (blocking operation). - - Args: - lat: Latitude in degrees - lon: Longitude in degrees - alt: Altitude in meters (relative to home) - - Returns: - String message indicating success or failure reason - """ - if self.connection: - return self.connection.fly_to(lat, lon, alt) - return "Failed: No connection to drone" - - @skill - def follow_object( - self, object_description: str, duration: float = 120.0 - ) -> Generator[str, None, None]: - """Follow an object with visual servoing. - - Example: - - follow_object(object_description="red car", duration=120) - - Args: - object_description (str): A short and clear description of the object. - duration (float, optional): How long to track for. Defaults to 120.0. - """ - msg = {"object_description": object_description, "duration": duration} - self.follow_object_cmd.publish(String(json.dumps(msg))) - - yield "Started trying to track. First, trying to find the object." - - start_time = time.time() - - started_tracking = False - - while time.time() - start_time < duration: - time.sleep(0.01) - with self._latest_status_lock: - if not self._latest_status: - continue - match self._latest_status.get("status"): - case "not_found" | "failed": - yield f"The '{object_description}' object has not been found. Stopped tracking." - break - case "tracking": - # Only return tracking once. - if not started_tracking: - started_tracking = True - yield f"The '{object_description}' object is now being followed." - case "lost": - yield f"The '{object_description}' object has been lost. Stopped tracking." - break - case "stopped": - yield f"Tracking '{object_description}' has stopped." - break - else: - yield f"Stopped tracking '{object_description}'" - - def _on_move_twist(self, msg: Twist) -> None: - """Handle Twist movement commands from tracking/navigation. - - Args: - msg: Twist message with linear and angular velocities - """ - if self.connection: - # Use move_twist to properly handle Twist messages - self.connection.move_twist(msg, duration=0, lock_altitude=True) - - def _on_gps_goal(self, cmd: LatLon) -> None: - if self._latest_telemetry is None or self.connection is None: - return - current_alt = self._latest_telemetry.get("GLOBAL_POSITION_INT", {}).get("relative_alt", 0) - self.connection.fly_to(cmd.lat, cmd.lon, current_alt) - - def _on_tracking_status(self, status: String) -> None: - with self._latest_status_lock: - self._latest_status = json.loads(status.data) - - @rpc - def stop(self) -> None: - """Stop the module.""" - # Stop the telemetry loop - self._running = False - - # Wait for telemetry thread to finish - if self._telemetry_thread and self._telemetry_thread.is_alive(): - self._telemetry_thread.join(timeout=2.0) - - # Stop video stream - if self.video_stream: - self.video_stream.stop() - - # Disconnect from drone - if self.connection: - self.connection.disconnect() - - logger.info("Drone connection module stopped") - - # Call parent stop to clean up Module infrastructure (event loop, LCM, disposables, etc.) - super().stop() - - @skill - def observe(self) -> Image | None: - """Returns the latest video frame from the drone camera. Use this skill for any visual world queries. - - This skill provides the current camera view for perception tasks. - Returns None if no frame has been captured yet. - """ - return self._latest_video_frame diff --git a/dimos/robot/drone/dji_video_stream.py b/dimos/robot/drone/dji_video_stream.py deleted file mode 100644 index 2339eacca2..0000000000 --- a/dimos/robot/drone/dji_video_stream.py +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Video streaming using GStreamer appsink for proper frame extraction.""" - -import functools -import subprocess -import threading -import time -from typing import Any - -import numpy as np -from reactivex import Observable, Subject - -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class DJIDroneVideoStream: - """Capture drone video using GStreamer appsink.""" - - def __init__(self, port: int = 5600, width: int = 640, height: int = 360) -> None: - self.port = port - self.width = width - self.height = height - self._video_subject: Subject[Image] = Subject() - self._process: subprocess.Popen[bytes] | None = None - self._stop_event = threading.Event() - - def start(self) -> bool: - """Start video capture using gst-launch with appsink.""" - try: - # Use appsink to get properly formatted frames - # The ! at the end tells appsink to emit data on stdout in a parseable format - cmd = [ - "gst-launch-1.0", - "-q", - "udpsrc", - f"port={self.port}", - "!", - "application/x-rtp,encoding-name=H264,payload=96", - "!", - "rtph264depay", - "!", - "h264parse", - "!", - "avdec_h264", - "!", - "videoscale", - "!", - f"video/x-raw,width={self.width},height={self.height}", - "!", - "videoconvert", - "!", - "video/x-raw,format=RGB", - "!", - "filesink", - "location=/dev/stdout", - "buffer-mode=2", # Unbuffered output - ] - - logger.info(f"Starting video capture on UDP port {self.port}") - logger.debug(f"Pipeline: {' '.join(cmd)}") - - self._process = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0 - ) - - self._stop_event.clear() - - # Start capture thread - self._capture_thread = threading.Thread(target=self._capture_loop, daemon=True) - self._capture_thread.start() - - # Start error monitoring - self._error_thread = threading.Thread(target=self._error_monitor, daemon=True) - self._error_thread.start() - - logger.info("Video stream started") - return True - - except Exception as e: - logger.error(f"Failed to start video stream: {e}") - return False - - def _capture_loop(self) -> None: - """Read frames with fixed size.""" - channels = 3 - frame_size = self.width * self.height * channels - - logger.info( - f"Capturing frames: {self.width}x{self.height} RGB ({frame_size} bytes per frame)" - ) - - frame_count = 0 - total_bytes = 0 - - while not self._stop_event.is_set(): - try: - # Read exactly one frame worth of data - frame_data = b"" - bytes_needed = frame_size - - while bytes_needed > 0 and not self._stop_event.is_set(): - if self._process is None or self._process.stdout is None: - break - chunk = self._process.stdout.read(bytes_needed) - if not chunk: - logger.warning("No data from GStreamer") - time.sleep(0.1) - break - frame_data += chunk - bytes_needed -= len(chunk) - - if len(frame_data) == frame_size: - # We have a complete frame - total_bytes += frame_size - - # Convert to numpy array - frame = np.frombuffer(frame_data, dtype=np.uint8) - frame = frame.reshape((self.height, self.width, channels)) - - # Create Image message (RGB format - matches GStreamer pipeline output) - img_msg = Image.from_numpy(frame, format=ImageFormat.RGB) - - # Publish - self._video_subject.on_next(img_msg) - - frame_count += 1 - if frame_count == 1: - logger.debug(f"First frame captured! Shape: {frame.shape}") - elif frame_count % 100 == 0: - logger.debug( - f"Captured {frame_count} frames ({total_bytes / 1024 / 1024:.1f} MB)" - ) - - except Exception as e: - if not self._stop_event.is_set(): - logger.error(f"Error in capture loop: {e}") - time.sleep(0.1) - - def _error_monitor(self) -> None: - """Monitor GStreamer stderr.""" - while not self._stop_event.is_set() and self._process is not None: - if self._process.stderr is None: - break - line = self._process.stderr.readline() - if line: - msg = line.decode("utf-8").strip() - if "ERROR" in msg or "WARNING" in msg: - logger.warning(f"GStreamer: {msg}") - else: - logger.debug(f"GStreamer: {msg}") - - def stop(self) -> None: - """Stop video stream.""" - self._stop_event.set() - - if self._process: - self._process.terminate() - try: - self._process.wait(timeout=2) - except subprocess.TimeoutExpired: - self._process.kill() - self._process = None - - logger.info("Video stream stopped") - - def get_stream(self) -> Subject[Image]: - """Get the video stream observable.""" - return self._video_subject - - -class FakeDJIVideoStream(DJIDroneVideoStream): - """Replay video for testing.""" - - def __init__(self, port: int = 5600) -> None: - super().__init__(port) - from dimos.utils.data import get_data - - # Ensure data is available - get_data("drone") - - def start(self) -> bool: - """Start replay of recorded video.""" - self._stop_event.clear() - logger.info("Video replay started") - return True - - @functools.cache - def get_stream(self) -> Observable[Image]: # type: ignore[override] - """Get the replay stream directly.""" - from dimos.utils.testing import TimedSensorReplay - - logger.info("Creating video replay stream") - video_store: Any = TimedSensorReplay("drone/video") - stream: Observable[Image] = video_store.stream() - return stream - - def stop(self) -> None: - """Stop replay.""" - self._stop_event.set() - logger.info("Video replay stopped") diff --git a/dimos/robot/drone/drone.py b/dimos/robot/drone/drone.py deleted file mode 100644 index 8e72d56ed1..0000000000 --- a/dimos/robot/drone/drone.py +++ /dev/null @@ -1,495 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Main Drone robot class for DimOS.""" - -import functools -import logging -import os -from typing import Any - -from dimos_lcm.sensor_msgs import CameraInfo -from dimos_lcm.std_msgs import String -from reactivex import Observable - -from dimos import core -from dimos.agents.agent import agent -from dimos.agents.skills.google_maps_skill_container import GoogleMapsSkillContainer -from dimos.agents.skills.osm import OsmSkill -from dimos.agents.web_human_input import web_input -from dimos.core.blueprints import Blueprint, autoconnect -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import PoseStamped, Twist, Vector3 -from dimos.msgs.sensor_msgs import Image -from dimos.protocol import pubsub -from dimos.robot.drone.camera_module import DroneCameraModule -from dimos.robot.drone.connection_module import DroneConnectionModule -from dimos.robot.drone.drone_tracking_module import DroneTrackingModule -from dimos.robot.foxglove_bridge import FoxgloveBridge - -# LCM not needed in orchestrator - modules handle communication -from dimos.robot.robot import Robot -from dimos.types.robot_capabilities import RobotCapability -from dimos.utils.logging_config import setup_logger -from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule - -logger = setup_logger() - - -class Drone(Robot): - """Generic MAVLink-based drone with video and depth capabilities.""" - - def __init__( - self, - connection_string: str = "udp:0.0.0.0:14550", - video_port: int = 5600, - camera_intrinsics: list[float] | None = None, - output_dir: str | None = None, - outdoor: bool = False, - ) -> None: - """Initialize drone robot. - - Args: - connection_string: MAVLink connection string - video_port: UDP port for video stream - camera_intrinsics: Camera intrinsics [fx, fy, cx, cy] - output_dir: Directory for outputs - outdoor: Use GPS only mode (no velocity integration) - """ - super().__init__() - - self.connection_string = connection_string - self.video_port = video_port - self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") - self.outdoor = outdoor - - if camera_intrinsics is None: - # Assuming 1920x1080 with typical FOV - self.camera_intrinsics = [1000.0, 1000.0, 960.0, 540.0] - else: - self.camera_intrinsics = camera_intrinsics - - self.capabilities = [ - RobotCapability.LOCOMOTION, # Aerial locomotion - RobotCapability.VISION, - ] - - self.dimos: core.DimosCluster | None = None - self.connection: DroneConnectionModule | None = None - self.camera: DroneCameraModule | None = None - self.tracking: DroneTrackingModule | None = None - self.foxglove_bridge: FoxgloveBridge | None = None - self.websocket_vis: WebsocketVisModule | None = None - - self._setup_directories() - - def _setup_directories(self) -> None: - """Setup output directories.""" - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"Drone outputs will be saved to: {self.output_dir}") - - def start(self) -> None: - """Start the drone system with all modules.""" - logger.info("Starting Drone robot system...") - - # Start DimOS cluster - self.dimos = core.start(4) - - # Deploy modules - self._deploy_connection() - self._deploy_camera() - self._deploy_tracking() - self._deploy_visualization() - self._deploy_navigation() - - # Start modules - self._start_modules() - - logger.info("Drone system initialized and started") - logger.info("Foxglove visualization available at http://localhost:8765") - - def _deploy_connection(self) -> None: - """Deploy and configure connection module.""" - assert self.dimos is not None - logger.info("Deploying connection module...") - - self.connection = self.dimos.deploy( # type: ignore[attr-defined] - DroneConnectionModule, - # connection_string="replay", - connection_string=self.connection_string, - video_port=self.video_port, - outdoor=self.outdoor, - ) - - # Configure LCM transports - self.connection.odom.transport = core.LCMTransport("/drone/odom", PoseStamped) - self.connection.gps_location.transport = core.pLCMTransport("/gps_location") - self.connection.gps_goal.transport = core.pLCMTransport("/gps_goal") - self.connection.status.transport = core.LCMTransport("/drone/status", String) - self.connection.telemetry.transport = core.LCMTransport("/drone/telemetry", String) - self.connection.video.transport = core.LCMTransport("/drone/video", Image) - self.connection.follow_object_cmd.transport = core.LCMTransport( - "/drone/follow_object_cmd", String - ) - self.connection.movecmd.transport = core.LCMTransport("/drone/cmd_vel", Vector3) - self.connection.movecmd_twist.transport = core.LCMTransport( - "/drone/tracking/cmd_vel", Twist - ) - - logger.info("Connection module deployed") - - def _deploy_camera(self) -> None: - """Deploy and configure camera module.""" - assert self.dimos is not None - assert self.connection is not None - logger.info("Deploying camera module...") - - self.camera = self.dimos.deploy( # type: ignore[attr-defined] - DroneCameraModule, camera_intrinsics=self.camera_intrinsics - ) - - # Configure LCM transports - self.camera.color_image.transport = core.LCMTransport("/drone/color_image", Image) - self.camera.depth_image.transport = core.LCMTransport("/drone/depth_image", Image) - self.camera.depth_colorized.transport = core.LCMTransport("/drone/depth_colorized", Image) - self.camera.camera_info.transport = core.LCMTransport("/drone/camera_info", CameraInfo) - self.camera.camera_pose.transport = core.LCMTransport("/drone/camera_pose", PoseStamped) - - # Connect video from connection module to camera module - self.camera.video.connect(self.connection.video) - - logger.info("Camera module deployed") - - def _deploy_tracking(self) -> None: - """Deploy and configure tracking module.""" - assert self.dimos is not None - assert self.connection is not None - logger.info("Deploying tracking module...") - - self.tracking = self.dimos.deploy( # type: ignore[attr-defined] - DroneTrackingModule, - outdoor=self.outdoor, - ) - - self.tracking.tracking_overlay.transport = core.LCMTransport( - "/drone/tracking_overlay", Image - ) - self.tracking.tracking_status.transport = core.LCMTransport( - "/drone/tracking_status", String - ) - self.tracking.cmd_vel.transport = core.LCMTransport("/drone/tracking/cmd_vel", Twist) - - self.tracking.video_input.connect(self.connection.video) - self.tracking.follow_object_cmd.connect(self.connection.follow_object_cmd) - - self.connection.movecmd_twist.connect(self.tracking.cmd_vel) - self.connection.tracking_status.connect(self.tracking.tracking_status) - - logger.info("Tracking module deployed") - - def _deploy_visualization(self) -> None: - """Deploy and configure visualization modules.""" - assert self.dimos is not None - assert self.connection is not None - self.websocket_vis = self.dimos.deploy(WebsocketVisModule) # type: ignore[attr-defined] - # self.websocket_vis.click_goal.transport = core.LCMTransport("/goal_request", PoseStamped) - self.websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") - # self.websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) - # self.websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) - self.websocket_vis.cmd_vel.transport = core.LCMTransport("/cmd_vel", Twist) - - self.websocket_vis.odom.connect(self.connection.odom) - self.websocket_vis.gps_location.connect(self.connection.gps_location) - # self.websocket_vis.path.connect(self.global_planner.path) - # self.websocket_vis.global_costmap.connect(self.mapper.global_costmap) - - self.foxglove_bridge = FoxgloveBridge() - - def _deploy_navigation(self) -> None: - assert self.websocket_vis is not None - assert self.connection is not None - # Connect In (subscriber) to Out (publisher) - self.connection.gps_goal.connect(self.websocket_vis.gps_goal) - - def _start_modules(self) -> None: - """Start all deployed modules.""" - assert self.connection is not None - assert self.camera is not None - assert self.tracking is not None - assert self.websocket_vis is not None - assert self.foxglove_bridge is not None - logger.info("Starting modules...") - - # Start connection first - result = self.connection.start() - if not result: - logger.warning("Connection module failed to start (no drone connected?)") - - # Start camera - result = self.camera.start() - if not result: - logger.warning("Camera module failed to start") - - result = self.tracking.start() - if result: - logger.info("Tracking module started successfully") - else: - logger.warning("Tracking module failed to start") - - self.websocket_vis.start() - - # Start Foxglove - self.foxglove_bridge.start() - - logger.info("All modules started") - - # Robot control methods - - def get_odom(self) -> PoseStamped | None: - """Get current odometry. - - Returns: - Current pose or None - """ - if self.connection is None: - return None - result: PoseStamped | None = self.connection.get_odom() - return result - - @functools.cached_property - def gps_position_stream(self) -> Observable[LatLon]: - assert self.connection is not None - result: Observable[LatLon] = self.connection.gps_location.transport.pure_observable() - return result - - def get_status(self) -> dict[str, Any]: - """Get drone status. - - Returns: - Status dictionary - """ - if self.connection is None: - return {} - result: dict[str, Any] = self.connection.get_status() - return result - - def move(self, vector: Vector3, duration: float = 0.0) -> None: - """Send movement command. - - Args: - vector: Velocity vector [x, y, z] in m/s - duration: How long to move (0 = continuous) - """ - if self.connection is None: - return - self.connection.move(vector, duration) - - def takeoff(self, altitude: float = 3.0) -> bool: - """Takeoff to altitude. - - Args: - altitude: Target altitude in meters - - Returns: - True if takeoff initiated - """ - if self.connection is None: - return False - result: bool = self.connection.takeoff(altitude) - return result - - def land(self) -> bool: - """Land the drone. - - Returns: - True if land command sent - """ - if self.connection is None: - return False - result: bool = self.connection.land() - return result - - def arm(self) -> bool: - """Arm the drone. - - Returns: - True if armed successfully - """ - if self.connection is None: - return False - result: bool = self.connection.arm() - return result - - def disarm(self) -> bool: - """Disarm the drone. - - Returns: - True if disarmed successfully - """ - if self.connection is None: - return False - result: bool = self.connection.disarm() - return result - - def set_mode(self, mode: str) -> bool: - """Set flight mode. - - Args: - mode: Mode name (STABILIZE, GUIDED, LAND, RTL, etc.) - - Returns: - True if mode set successfully - """ - if self.connection is None: - return False - result: bool = self.connection.set_mode(mode) - return result - - def fly_to(self, lat: float, lon: float, alt: float) -> str: - """Fly to GPS coordinates. - - Args: - lat: Latitude in degrees - lon: Longitude in degrees - alt: Altitude in meters (relative to home) - - Returns: - String message indicating success or failure - """ - if self.connection is None: - return "Failed: No connection" - result: str = self.connection.fly_to(lat, lon, alt) - return result - - def cleanup(self) -> None: - self.stop() - - def stop(self) -> None: - """Stop the drone system.""" - logger.info("Stopping drone system...") - - if self.connection: - self.connection.stop() - - if self.camera: - self.camera.stop() - - if self.foxglove_bridge: - self.foxglove_bridge.stop() - - if self.dimos: - self.dimos.close_all() # type: ignore[attr-defined] - - logger.info("Drone system stopped") - - -DRONE_SYSTEM_PROMPT = """\ -You are controlling a DJI drone with MAVLink interface. -You have access to drone control skills you are already flying so only run move_twist, set_mode, and fly_to. -When the user gives commands, use the appropriate skills to control the drone. -Always confirm actions and report results. Send fly_to commands only at above 200 meters altitude to be safe. -Here are some GPS locations to remember -6th and Natoma intersection: 37.78019978319006, -122.40770815020853, -454 Natoma (Office): 37.780967465525244, -122.40688342010769 -5th and mission intersection: 37.782598539339695, -122.40649441875473 -6th and mission intersection: 37.781007204789354, -122.40868447123661""" - - -def drone_agentic( - connection_string: str = "udp:0.0.0.0:14550", - video_port: int = 5600, - outdoor: bool = False, - camera_intrinsics: list[float] | None = None, - system_prompt: str = DRONE_SYSTEM_PROMPT, - model: str = "gpt-4o", -) -> Blueprint: - if camera_intrinsics is None: - camera_intrinsics = [1000.0, 1000.0, 960.0, 540.0] - - return autoconnect( - DroneConnectionModule.blueprint( - connection_string=connection_string, - video_port=video_port, - outdoor=outdoor, - ), - DroneCameraModule.blueprint(camera_intrinsics=camera_intrinsics), - DroneTrackingModule.blueprint(outdoor=outdoor), - WebsocketVisModule.blueprint(), - FoxgloveBridge.blueprint(), - GoogleMapsSkillContainer.blueprint(), - OsmSkill.blueprint(), - agent(system_prompt=system_prompt, model=model), - web_input(), - ).remappings( - [ - (DroneTrackingModule, "video_input", "video"), - (DroneTrackingModule, "cmd_vel", "movecmd_twist"), - ] - ) - - -def main() -> None: - """Main entry point for drone system.""" - import argparse - - parser = argparse.ArgumentParser(description="DimOS Drone System") - parser.add_argument("--replay", action="store_true", help="Use recorded data for testing") - - parser.add_argument( - "--outdoor", - action="store_true", - help="Outdoor mode - use GPS only, no velocity integration", - ) - args = parser.parse_args() - - # Configure logging - setup_logger(level=logging.INFO) - - # Suppress verbose loggers - logging.getLogger("distributed").setLevel(logging.WARNING) - logging.getLogger("asyncio").setLevel(logging.WARNING) - - if args.replay: - connection = "replay" - print("\n🔄 REPLAY MODE - Using drone replay data") - else: - connection = os.getenv("DRONE_CONNECTION", "udp:0.0.0.0:14550") - video_port = int(os.getenv("DRONE_VIDEO_PORT", "5600")) - - print(f""" -╔══════════════════════════════════════════╗ -║ DimOS Mavlink Drone Runner ║ -╠══════════════════════════════════════════╣ -║ MAVLink: {connection:<30} ║ -║ Video: UDP port {video_port:<22}║ -║ Foxglove: http://localhost:8765 ║ -╚══════════════════════════════════════════╝ - """) - - pubsub.lcm.autoconf() # type: ignore[attr-defined] - - blueprint = drone_agentic( - connection_string=connection, - video_port=video_port, - outdoor=args.outdoor, - ) - - blueprint.build().loop() - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/drone/drone_tracking_module.py b/dimos/robot/drone/drone_tracking_module.py deleted file mode 100644 index e1b633a05b..0000000000 --- a/dimos/robot/drone/drone_tracking_module.py +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Drone tracking module with visual servoing for object following.""" - -import json -import threading -import time -from typing import Any - -import cv2 -from dimos_lcm.std_msgs import String -import numpy as np -from numpy.typing import NDArray - -from dimos.core import In, Module, Out, rpc -from dimos.models.qwen.video_query import get_bbox_from_qwen_frame -from dimos.msgs.geometry_msgs import Twist, Vector3 -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.robot.drone.drone_visual_servoing_controller import ( - DroneVisualServoingController, - PIDParams, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -INDOOR_PID_PARAMS: PIDParams = (0.001, 0.0, 0.0001, (-1.0, 1.0), None, 30) -OUTDOOR_PID_PARAMS: PIDParams = (0.05, 0.0, 0.0003, (-5.0, 5.0), None, 10) -INDOOR_MAX_VELOCITY = 1.0 # m/s safety cap for indoor mode - - -class DroneTrackingModule(Module): - """Module for drone object tracking with visual servoing control.""" - - # Inputs - video_input: In[Image] - follow_object_cmd: In[Any] - - # Outputs - tracking_overlay: Out[Image] # Visualization with bbox and crosshairs - tracking_status: Out[Any] # JSON status updates - cmd_vel: Out[Twist] # Velocity commands for drone control - - def __init__( - self, - outdoor: bool = False, - x_pid_params: PIDParams | None = None, - y_pid_params: PIDParams | None = None, - z_pid_params: PIDParams | None = None, - ) -> None: - """Initialize the drone tracking module. - - Args: - outdoor: If True, use aggressive outdoor PID params (5 m/s max). - If False (default), use conservative indoor params (1 m/s max). - x_pid_params: PID parameters for forward/backward control. - If None, uses preset based on outdoor flag. - y_pid_params: PID parameters for left/right strafe control. - If None, uses preset based on outdoor flag. - z_pid_params: Optional PID parameters for altitude control. - """ - super().__init__() - - default_params = OUTDOOR_PID_PARAMS if outdoor else INDOOR_PID_PARAMS - x_pid_params = x_pid_params if x_pid_params is not None else default_params - y_pid_params = y_pid_params if y_pid_params is not None else default_params - - self._outdoor = outdoor - self._max_velocity = None if outdoor else INDOOR_MAX_VELOCITY - - self.servoing_controller = DroneVisualServoingController( - x_pid_params=x_pid_params, y_pid_params=y_pid_params, z_pid_params=z_pid_params - ) - - # Tracking state - self._tracking_active = False - self._tracking_thread: threading.Thread | None = None - self._current_object: str | None = None - self._latest_frame: Image | None = None - self._frame_lock = threading.Lock() - - # Subscribe to video input when transport is set - # (will be done by connection module) - - def _on_new_frame(self, frame: Image) -> None: - """Handle new video frame.""" - with self._frame_lock: - self._latest_frame = frame - - def _on_follow_object_cmd(self, cmd: String) -> None: - msg = json.loads(cmd.data) - self.track_object(msg["object_description"], msg["duration"]) - - def _get_latest_frame(self) -> np.ndarray[Any, np.dtype[Any]] | None: - """Get the latest video frame as numpy array.""" - with self._frame_lock: - if self._latest_frame is None: - return None - # Convert Image to numpy array - data: np.ndarray[Any, np.dtype[Any]] = self._latest_frame.data - return data - - @rpc - def start(self) -> None: - """Start the tracking module and subscribe to video input.""" - if self.video_input.transport: - self.video_input.subscribe(self._on_new_frame) - logger.info("DroneTrackingModule started - subscribed to video input") - else: - logger.warning("DroneTrackingModule: No video input transport configured") - - if self.follow_object_cmd.transport: - self.follow_object_cmd.subscribe(self._on_follow_object_cmd) - - return - - @rpc - def stop(self) -> None: - self._stop_tracking() - super().stop() - - @rpc - def track_object(self, object_name: str | None = None, duration: float = 120.0) -> str: - """Track and follow an object using visual servoing. - - Args: - object_name: Name of object to track, or None for most prominent - duration: Maximum tracking duration in seconds - - Returns: - String status message - """ - if self._tracking_active: - return "Already tracking an object" - - # Get current frame - frame = self._get_latest_frame() - if frame is None: - return "Error: No video frame available" - - logger.info(f"Starting track_object for {object_name or 'any object'}") - - try: - # Detect object with Qwen - logger.info("Detecting object with Qwen...") - bbox = get_bbox_from_qwen_frame(frame, object_name) - - if bbox is None: - msg = f"No object detected{' for: ' + object_name if object_name else ''}" - logger.warning(msg) - self._publish_status({"status": "not_found", "object": self._current_object}) - return msg - - logger.info(f"Object detected at bbox: {bbox}") - - # Initialize CSRT tracker (use legacy for OpenCV 4) - try: - tracker = cv2.legacy.TrackerCSRT_create() # type: ignore[attr-defined] - except AttributeError: - tracker = cv2.TrackerCSRT_create() # type: ignore[attr-defined] - - # Convert bbox format from [x1, y1, x2, y2] to [x, y, w, h] - x1, y1, x2, y2 = bbox - x, y, w, h = x1, y1, x2 - x1, y2 - y1 - - # Initialize tracker - success = tracker.init(frame, (x, y, w, h)) - if not success: - self._publish_status({"status": "failed", "object": self._current_object}) - return "Failed to initialize tracker" - - self._current_object = object_name or "object" - self._tracking_active = True - - # Start tracking in thread (non-blocking - caller should poll get_status()) - self._tracking_thread = threading.Thread( - target=self._visual_servoing_loop, args=(tracker, duration), daemon=True - ) - self._tracking_thread.start() - - return f"Tracking started for {self._current_object}. Poll get_status() for updates." - - except Exception as e: - logger.error(f"Tracking error: {e}") - self._stop_tracking() - return f"Tracking failed: {e!s}" - - def _visual_servoing_loop(self, tracker: Any, duration: float) -> None: - """Main visual servoing control loop. - - Args: - tracker: OpenCV tracker instance - duration: Maximum duration in seconds - """ - start_time = time.time() - frame_count = 0 - lost_track_count = 0 - max_lost_frames = 100 - - logger.info("Starting visual servoing loop") - - try: - while self._tracking_active and (time.time() - start_time < duration): - # Get latest frame - frame = self._get_latest_frame() - if frame is None: - time.sleep(0.01) - continue - - frame_count += 1 - - # Update tracker - success, bbox = tracker.update(frame) - - if not success: - lost_track_count += 1 - logger.warning(f"Lost track (count: {lost_track_count})") - - if lost_track_count >= max_lost_frames: - logger.error("Lost track of object") - self._publish_status( - {"status": "lost", "object": self._current_object, "frame": frame_count} - ) - break - continue - else: - lost_track_count = 0 - - # Calculate object center - x, y, w, h = bbox - current_x = x + w / 2 - current_y = y + h / 2 - - # Get frame dimensions - frame_height, frame_width = frame.shape[:2] - center_x = frame_width / 2 - center_y = frame_height / 2 - - # Compute velocity commands - vx, vy, vz = self.servoing_controller.compute_velocity_control( - target_x=current_x, - target_y=current_y, - center_x=center_x, - center_y=center_y, - dt=0.033, # ~30Hz - lock_altitude=True, - ) - - # Clamp velocity for indoor safety - if self._max_velocity is not None: - vx = max(-self._max_velocity, min(self._max_velocity, vx)) - vy = max(-self._max_velocity, min(self._max_velocity, vy)) - - # Publish velocity command via LCM - if self.cmd_vel.transport: - twist = Twist() - twist.linear = Vector3(vx, vy, 0) - twist.angular = Vector3(0, 0, 0) # No rotation for now - self.cmd_vel.publish(twist) - - # Publish visualization if transport is set - if self.tracking_overlay.transport: - overlay = self._draw_tracking_overlay( - frame, (int(x), int(y), int(w), int(h)), (int(current_x), int(current_y)) - ) - overlay_msg = Image.from_numpy(overlay, format=ImageFormat.BGR) - self.tracking_overlay.publish(overlay_msg) - - # Publish status - self._publish_status( - { - "status": "tracking", - "object": self._current_object, - "bbox": [int(x), int(y), int(w), int(h)], - "center": [int(current_x), int(current_y)], - "error": [int(current_x - center_x), int(current_y - center_y)], - "velocity": [float(vx), float(vy), float(vz)], - "frame": frame_count, - } - ) - - # Control loop rate - time.sleep(0.033) # ~30Hz - - except Exception as e: - logger.error(f"Error in servoing loop: {e}") - finally: - # Stop movement by publishing zero velocity - if self.cmd_vel.transport: - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - self.cmd_vel.publish(stop_twist) - self._tracking_active = False - logger.info(f"Visual servoing loop ended after {frame_count} frames") - - def _draw_tracking_overlay( - self, - frame: NDArray[np.uint8], - bbox: tuple[int, int, int, int], - center: tuple[int, int], - ) -> NDArray[np.uint8]: # type: ignore[type-arg] - """Draw tracking visualization overlay. - - Args: - frame: Current video frame - bbox: Bounding box (x, y, w, h) - center: Object center (x, y) - - Returns: - Frame with overlay drawn - """ - overlay: NDArray[np.uint8] = frame.copy() # type: ignore[type-arg] - x, y, w, h = bbox - - # Draw tracking box (green) - cv2.rectangle(overlay, (x, y), (x + w, y + h), (0, 255, 0), 2) - - # Draw object center (red crosshair) - cv2.drawMarker(overlay, center, (0, 0, 255), cv2.MARKER_CROSS, 20, 2) - - # Draw desired center (blue crosshair) - frame_h, frame_w = frame.shape[:2] - frame_center = (frame_w // 2, frame_h // 2) - cv2.drawMarker(overlay, frame_center, (255, 0, 0), cv2.MARKER_CROSS, 20, 2) - - # Draw line from object to desired center - cv2.line(overlay, center, frame_center, (255, 255, 0), 1) - - # Add status text - status_text = f"Tracking: {self._current_object}" - cv2.putText(overlay, status_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) - - # Add error text - error_x = center[0] - frame_center[0] - error_y = center[1] - frame_center[1] - error_text = f"Error: ({error_x}, {error_y})" - cv2.putText( - overlay, error_text, (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 1 - ) - - return overlay - - def _publish_status(self, status: dict[str, Any]) -> None: - """Publish tracking status as JSON. - - Args: - status: Status dictionary - """ - if self.tracking_status.transport: - status_msg = String(json.dumps(status)) - self.tracking_status.publish(status_msg) - - def _stop_tracking(self) -> None: - """Stop tracking and clean up.""" - self._tracking_active = False - if self._tracking_thread and self._tracking_thread.is_alive(): - self._tracking_thread.join(timeout=1) - - # Send stop command via LCM - if self.cmd_vel.transport: - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - self.cmd_vel.publish(stop_twist) - - self._publish_status({"status": "stopped", "object": self._current_object}) - - self._current_object = None - logger.info("Tracking stopped") - - @rpc - def stop_tracking(self) -> str: - """Stop current tracking operation.""" - self._stop_tracking() - return "Tracking stopped" - - @rpc - def get_status(self) -> dict[str, Any]: - """Get current tracking status. - - Returns: - Status dictionary - """ - return { - "active": self._tracking_active, - "object": self._current_object, - "has_frame": self._latest_frame is not None, - } diff --git a/dimos/robot/drone/drone_visual_servoing_controller.py b/dimos/robot/drone/drone_visual_servoing_controller.py deleted file mode 100644 index 72e47331f7..0000000000 --- a/dimos/robot/drone/drone_visual_servoing_controller.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Minimal visual servoing controller for drone with downward-facing camera.""" - -from typing import TypeAlias - -from dimos.utils.simple_controller import PIDController - -# Type alias for PID parameters tuple -PIDParams: TypeAlias = tuple[float, float, float, tuple[float, float], float | None, int] - - -class DroneVisualServoingController: - """Minimal visual servoing for downward-facing drone camera using velocity-only control.""" - - def __init__( - self, - x_pid_params: PIDParams, - y_pid_params: PIDParams, - z_pid_params: PIDParams | None = None, - ) -> None: - """ - Initialize drone visual servoing controller. - - Args: - x_pid_params: (kp, ki, kd, output_limits, integral_limit, deadband) for forward/back - y_pid_params: (kp, ki, kd, output_limits, integral_limit, deadband) for left/right - z_pid_params: Optional params for altitude control - """ - self.x_pid = PIDController(*x_pid_params) - self.y_pid = PIDController(*y_pid_params) - self.z_pid = PIDController(*z_pid_params) if z_pid_params else None - - def compute_velocity_control( - self, - target_x: float, - target_y: float, # Target position in image (pixels or normalized) - center_x: float = 0.0, - center_y: float = 0.0, # Desired position (usually image center) - target_z: float | None = None, - desired_z: float | None = None, # Optional altitude control - dt: float = 0.1, - lock_altitude: bool = True, - ) -> tuple[float, float, float]: - """ - Compute velocity commands to center target in camera view. - - For downward camera: - - Image X error -> Drone Y velocity (left/right strafe) - - Image Y error -> Drone X velocity (forward/backward) - - Args: - target_x: Target X position in image - target_y: Target Y position in image - center_x: Desired X position (default 0) - center_y: Desired Y position (default 0) - target_z: Current altitude (optional) - desired_z: Desired altitude (optional) - dt: Time step - lock_altitude: If True, vz will always be 0 - - Returns: - tuple: (vx, vy, vz) velocities in m/s - """ - # Compute errors (positive = target is to the right/below center) - error_x = target_x - center_x # Lateral error in image - error_y = target_y - center_y # Forward error in image - - # PID control (swap axes for downward camera) - # For downward camera: object below center (positive error_y) = object is behind drone - # Need to negate: positive error_y should give negative vx (move backward) - vy = self.y_pid.update(error_x, dt) # type: ignore[no-untyped-call] # Image X -> Drone Y (strafe) - vx = -self.x_pid.update(error_y, dt) # type: ignore[no-untyped-call] # Image Y -> Drone X (NEGATED) - - # Optional altitude control - vz = 0.0 - if not lock_altitude and self.z_pid and target_z is not None and desired_z is not None: - error_z = target_z - desired_z - vz = self.z_pid.update(error_z, dt) # type: ignore[no-untyped-call] - - return vx, vy, vz - - def reset(self) -> None: - """Reset all PID controllers.""" - self.x_pid.integral = 0.0 - self.x_pid.prev_error = 0.0 - self.y_pid.integral = 0.0 - self.y_pid.prev_error = 0.0 - if self.z_pid: - self.z_pid.integral = 0.0 - self.z_pid.prev_error = 0.0 diff --git a/dimos/robot/drone/mavlink_connection.py b/dimos/robot/drone/mavlink_connection.py deleted file mode 100644 index d8a7c97c4a..0000000000 --- a/dimos/robot/drone/mavlink_connection.py +++ /dev/null @@ -1,1109 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""MAVLink-based drone connection for DimOS.""" - -import functools -import logging -import time -from typing import Any - -from pymavlink import mavutil # type: ignore[import-not-found, import-untyped] -from reactivex import Subject - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Twist, Vector3 -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.INFO) - - -class MavlinkConnection: - """MAVLink connection for drone control.""" - - def __init__( - self, - connection_string: str = "udp:0.0.0.0:14550", - outdoor: bool = False, - max_velocity: float = 5.0, - ) -> None: - """Initialize drone connection. - - Args: - connection_string: MAVLink connection string - outdoor: Use GPS only mode (no velocity integration) - max_velocity: Maximum velocity in m/s - """ - self.connection_string = connection_string - self.outdoor = outdoor - self.max_velocity = max_velocity - self.mavlink: Any = None # MAVLink connection object - self.connected = False - self.telemetry: dict[str, Any] = {} - - self._odom_subject: Subject[PoseStamped] = Subject() - self._status_subject: Subject[dict[str, Any]] = Subject() - self._telemetry_subject: Subject[dict[str, Any]] = Subject() - self._raw_mavlink_subject: Subject[dict[str, Any]] = Subject() - - # Velocity tracking for smoothing - self.prev_vx = 0.0 - self.prev_vy = 0.0 - self.prev_vz = 0.0 - - # Flag to prevent concurrent fly_to commands - self.flying_to_target = False - - def connect(self) -> bool: - """Connect to drone via MAVLink.""" - try: - logger.info(f"Connecting to {self.connection_string}") - self.mavlink = mavutil.mavlink_connection(self.connection_string) - self.mavlink.wait_heartbeat(timeout=30) - self.connected = True - logger.info(f"Connected to system {self.mavlink.target_system}") - - self.update_telemetry() - return True - except Exception as e: - logger.error(f"Connection failed: {e}") - return False - - def update_telemetry(self, timeout: float = 0.1) -> None: - """Update telemetry data from available messages.""" - if not self.connected: - return - - end_time = time.time() + timeout - while time.time() < end_time: - msg = self.mavlink.recv_match(blocking=False) - if not msg: - time.sleep(0.001) - continue - msg_type = msg.get_type() - msg_dict = msg.to_dict() - if msg_type == "HEARTBEAT": - bool(msg_dict.get("base_mode", 0) & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) - # print("HEARTBEAT:", msg_dict, "ARMED:", armed) - # print("MESSAGE", msg_dict) - # print("MESSAGE TYPE", msg_type) - # self._raw_mavlink_subject.on_next(msg_dict) - - self.telemetry[msg_type] = msg_dict - - # Apply unit conversions for known fields - if msg_type == "GLOBAL_POSITION_INT": - msg_dict["lat"] = msg_dict.get("lat", 0) / 1e7 - msg_dict["lon"] = msg_dict.get("lon", 0) / 1e7 - msg_dict["alt"] = msg_dict.get("alt", 0) / 1000.0 - msg_dict["relative_alt"] = msg_dict.get("relative_alt", 0) / 1000.0 - msg_dict["vx"] = msg_dict.get("vx", 0) / 100.0 # cm/s to m/s - msg_dict["vy"] = msg_dict.get("vy", 0) / 100.0 - msg_dict["vz"] = msg_dict.get("vz", 0) / 100.0 - msg_dict["hdg"] = msg_dict.get("hdg", 0) / 100.0 # centidegrees to degrees - self._publish_odom() - - elif msg_type == "GPS_RAW_INT": - msg_dict["lat"] = msg_dict.get("lat", 0) / 1e7 - msg_dict["lon"] = msg_dict.get("lon", 0) / 1e7 - msg_dict["alt"] = msg_dict.get("alt", 0) / 1000.0 - msg_dict["vel"] = msg_dict.get("vel", 0) / 100.0 - msg_dict["cog"] = msg_dict.get("cog", 0) / 100.0 - - elif msg_type == "SYS_STATUS": - msg_dict["voltage_battery"] = msg_dict.get("voltage_battery", 0) / 1000.0 - msg_dict["current_battery"] = msg_dict.get("current_battery", 0) / 100.0 - self._publish_status() - - elif msg_type == "POWER_STATUS": - msg_dict["Vcc"] = msg_dict.get("Vcc", 0) / 1000.0 - msg_dict["Vservo"] = msg_dict.get("Vservo", 0) / 1000.0 - - elif msg_type == "HEARTBEAT": - # Extract armed status - base_mode = msg_dict.get("base_mode", 0) - msg_dict["armed"] = bool(base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED) - self._publish_status() - - elif msg_type == "ATTITUDE": - self._publish_odom() - - self.telemetry[msg_type] = msg_dict - - self._publish_telemetry() - - def _publish_odom(self) -> None: - """Publish odometry data - GPS for outdoor mode, velocity integration for indoor mode.""" - attitude = self.telemetry.get("ATTITUDE", {}) - roll = attitude.get("roll", 0) - pitch = attitude.get("pitch", 0) - yaw = attitude.get("yaw", 0) - - # Use heading from GLOBAL_POSITION_INT if no ATTITUDE data - if "roll" not in attitude and "GLOBAL_POSITION_INT" in self.telemetry: - import math - - heading = self.telemetry["GLOBAL_POSITION_INT"].get("hdg", 0) - yaw = math.radians(heading) - - if "roll" not in attitude and "GLOBAL_POSITION_INT" not in self.telemetry: - logger.debug("No attitude or position data available") - return - - # MAVLink --> ROS conversion - # MAVLink: positive pitch = nose up, positive yaw = clockwise - # ROS: positive pitch = nose down, positive yaw = counter-clockwise - quaternion = Quaternion.from_euler(Vector3(roll, -pitch, -yaw)) - - if not hasattr(self, "_position"): - self._position = {"x": 0.0, "y": 0.0, "z": 0.0} - self._last_update = time.time() - if self.outdoor: - self._gps_origin = None - - current_time = time.time() - dt = current_time - self._last_update - - # Get position data from GLOBAL_POSITION_INT - pos_data = self.telemetry.get("GLOBAL_POSITION_INT", {}) - - # Outdoor mode: Use GPS coordinates - if self.outdoor and pos_data: - lat = pos_data.get("lat", 0) # Already in degrees from update_telemetry - lon = pos_data.get("lon", 0) # Already in degrees from update_telemetry - - if lat != 0 and lon != 0: # Valid GPS fix - if self._gps_origin is None: - self._gps_origin = {"lat": lat, "lon": lon} - logger.debug(f"GPS origin set: lat={lat:.7f}, lon={lon:.7f}") - - # Convert GPS to local X/Y coordinates - import math - - R = 6371000 # Earth radius in meters - dlat = math.radians(lat - self._gps_origin["lat"]) - dlon = math.radians(lon - self._gps_origin["lon"]) - - # X = North, Y = West (ROS convention) - self._position["x"] = dlat * R - self._position["y"] = -dlon * R * math.cos(math.radians(self._gps_origin["lat"])) - - # Indoor mode: Use velocity integration (ORIGINAL CODE - UNCHANGED) - elif pos_data and dt > 0: - vx = pos_data.get("vx", 0) # North velocity in m/s (already converted) - vy = pos_data.get("vy", 0) # East velocity in m/s (already converted) - - # +vx is North, +vy is East in NED mavlink frame - # ROS/Foxglove: X=forward(North), Y=left(West), Z=up - self._position["x"] += vx * dt # North → X (forward) - self._position["y"] += -vy * dt # East → -Y (right in ROS, Y points left/West) - - # Altitude handling (same for both modes) - if "ALTITUDE" in self.telemetry: - self._position["z"] = self.telemetry["ALTITUDE"].get("altitude_relative", 0) - elif pos_data: - self._position["z"] = pos_data.get( - "relative_alt", 0 - ) # Already in m from update_telemetry - - self._last_update = current_time - - # Debug logging - mode = "GPS" if self.outdoor else "VELOCITY" - logger.debug( - f"[{mode}] Position: x={self._position['x']:.2f}m, y={self._position['y']:.2f}m, z={self._position['z']:.2f}m" - ) - - pose = PoseStamped( - position=Vector3(self._position["x"], self._position["y"], self._position["z"]), - orientation=quaternion, - frame_id="world", - ts=current_time, - ) - - self._odom_subject.on_next(pose) - - def _publish_status(self) -> None: - """Publish drone status with key telemetry.""" - heartbeat = self.telemetry.get("HEARTBEAT", {}) - sys_status = self.telemetry.get("SYS_STATUS", {}) - gps_raw = self.telemetry.get("GPS_RAW_INT", {}) - global_pos = self.telemetry.get("GLOBAL_POSITION_INT", {}) - altitude = self.telemetry.get("ALTITUDE", {}) - - status = { - "armed": heartbeat.get("armed", False), - "mode": heartbeat.get("custom_mode", -1), - "battery_voltage": sys_status.get("voltage_battery", 0), - "battery_current": sys_status.get("current_battery", 0), - "battery_remaining": sys_status.get("battery_remaining", 0), - "satellites": gps_raw.get("satellites_visible", 0), - "altitude": altitude.get("altitude_relative", global_pos.get("relative_alt", 0)), - "heading": global_pos.get("hdg", 0), - "vx": global_pos.get("vx", 0), - "vy": global_pos.get("vy", 0), - "vz": global_pos.get("vz", 0), - "lat": global_pos.get("lat", 0), - "lon": global_pos.get("lon", 0), - "ts": time.time(), - } - self._status_subject.on_next(status) - - def _publish_telemetry(self) -> None: - """Publish full telemetry data.""" - telemetry_with_ts = self.telemetry.copy() - telemetry_with_ts["timestamp"] = time.time() - self._telemetry_subject.on_next(telemetry_with_ts) - - def move(self, velocity: Vector3, duration: float = 0.0) -> bool: - """Send movement command to drone. - - Args: - velocity: Velocity vector [x, y, z] in m/s - duration: How long to move (0 = continuous) - - Returns: - True if command sent successfully - """ - if not self.connected: - return False - - # MAVLink body frame velocities - forward = velocity.y # Forward/backward - right = velocity.x # Left/right - down = velocity.z # Up/down (negative for DOWN, positive for UP) - - logger.debug(f"Moving: forward={forward}, right={right}, down={down}") - - if duration > 0: - # Send velocity for duration - end_time = time.time() + duration - while time.time() < end_time: - self.mavlink.mav.set_position_target_local_ned_send( - 0, # time_boot_ms - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, - 0b0000111111000111, # type_mask (only velocities) - 0, - 0, - 0, # positions - forward, - right, - down, # velocities - 0, - 0, - 0, # accelerations - 0, - 0, # yaw, yaw_rate - ) - time.sleep(0.1) - self.stop() - else: - # Single velocity command - self.mavlink.mav.set_position_target_local_ned_send( - 0, - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, - 0b0000111111000111, - 0, - 0, - 0, - forward, - right, - down, - 0, - 0, - 0, - 0, - 0, - ) - - return True - - def move_twist(self, twist: Twist, duration: float = 0.0, lock_altitude: bool = True) -> bool: - """Move using ROS-style Twist commands. - - Args: - twist: Twist message with linear velocities (angular.z ignored for now) - duration: How long to move (0 = single command) - lock_altitude: If True, ignore Z velocity and maintain current altitude - - Returns: - True if command sent successfully - """ - if not self.connected: - return False - - # Extract velocities - forward = twist.linear.x # m/s forward (body frame) - right = twist.linear.y # m/s right (body frame) - down = 0.0 if lock_altitude else -twist.linear.z # Lock altitude by default - - if duration > 0: - # Send velocity for duration - end_time = time.time() + duration - while time.time() < end_time: - self.mavlink.mav.set_position_target_local_ned_send( - 0, # time_boot_ms - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for strafing - 0b0000111111000111, # type_mask - velocities only, no rotation - 0, - 0, - 0, # positions (ignored) - forward, - right, - down, # velocities in m/s - 0, - 0, - 0, # accelerations (ignored) - 0, - 0, # yaw, yaw_rate (ignored) - ) - time.sleep(0.05) # 20Hz - # Send stop command - self.stop() - else: - # Send single command for continuous movement - self.mavlink.mav.set_position_target_local_ned_send( - 0, # time_boot_ms - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for strafing - 0b0000111111000111, # type_mask - velocities only, no rotation - 0, - 0, - 0, # positions (ignored) - forward, - right, - down, # velocities in m/s - 0, - 0, - 0, # accelerations (ignored) - 0, - 0, # yaw, yaw_rate (ignored) - ) - - return True - - def stop(self) -> bool: - """Stop all movement.""" - if not self.connected: - return False - - self.mavlink.mav.set_position_target_local_ned_send( - 0, - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, - 0b0000111111000111, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ) - return True - - def rotate_to(self, target_heading_deg: float, timeout: float = 60.0) -> bool: - """Rotate drone to face a specific heading. - - Args: - target_heading_deg: Target heading in degrees (0-360, 0=North, 90=East) - timeout: Maximum time to spend rotating in seconds - - Returns: - True if rotation completed successfully - """ - if not self.connected: - return False - - logger.info(f"Rotating to heading {target_heading_deg:.1f}°") - - import math - import time - - start_time = time.time() - loop_count = 0 - - while time.time() - start_time < timeout: - loop_count += 1 - - # Don't call update_telemetry - let background thread handle it - # Just read the current telemetry which should be continuously updated - - if "GLOBAL_POSITION_INT" not in self.telemetry: - logger.warning("No GLOBAL_POSITION_INT in telemetry dict") - time.sleep(0.1) - continue - - # Debug: Log what's in telemetry - gps_telem = self.telemetry["GLOBAL_POSITION_INT"] - - # Get current heading - check if already converted or still in centidegrees - raw_hdg = gps_telem.get("hdg", 0) - - # Debug logging to figure out the issue - if loop_count % 5 == 0: # Log every 5th iteration - logger.info(f"DEBUG TELEMETRY: raw hdg={raw_hdg}, type={type(raw_hdg)}") - logger.info(f"DEBUG TELEMETRY keys: {list(gps_telem.keys())[:5]}") # First 5 keys - - # Check if hdg is already converted (should be < 360 if in degrees, > 360 if in centidegrees) - if raw_hdg > 360: - logger.info(f"HDG appears to be in centidegrees: {raw_hdg}") - current_heading_deg = raw_hdg / 100.0 - else: - logger.info(f"HDG appears to be in degrees already: {raw_hdg}") - current_heading_deg = raw_hdg - else: - # Normal conversion - if raw_hdg > 360: - current_heading_deg = raw_hdg / 100.0 - else: - current_heading_deg = raw_hdg - - # Normalize to 0-360 - if current_heading_deg > 360: - current_heading_deg = current_heading_deg % 360 - - # Calculate heading error (shortest angular distance) - heading_error = target_heading_deg - current_heading_deg - if heading_error > 180: - heading_error -= 360 - elif heading_error < -180: - heading_error += 360 - - logger.info( - f"ROTATION: current={current_heading_deg:.1f}° → target={target_heading_deg:.1f}° (error={heading_error:.1f}°)" - ) - - # Check if we're close enough - if abs(heading_error) < 10: # Complete within 10 degrees - logger.info( - f"ROTATION COMPLETE: current={current_heading_deg:.1f}° ≈ target={target_heading_deg:.1f}° (within {abs(heading_error):.1f}°)" - ) - # Don't stop - let fly_to immediately transition to forward movement - return True - - # Calculate yaw rate with minimum speed to avoid slow approach - yaw_rate = heading_error * 0.3 # Higher gain for faster rotation - # Ensure minimum rotation speed of 15 deg/s to avoid crawling near target - if abs(yaw_rate) < 15.0: - yaw_rate = 15.0 if heading_error > 0 else -15.0 - yaw_rate = max(-60.0, min(60.0, yaw_rate)) # Cap at 60 deg/s max - yaw_rate_rad = math.radians(yaw_rate) - - logger.info( - f"ROTATING: yaw_rate={yaw_rate:.1f} deg/s to go from {current_heading_deg:.1f}° → {target_heading_deg:.1f}°" - ) - - # Send rotation command - self.mavlink.mav.set_position_target_local_ned_send( - 0, # time_boot_ms - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_BODY_NED, # Body frame for rotation - 0b0000011111111111, # type_mask - ignore everything except yaw_rate - 0, - 0, - 0, # positions (ignored) - 0, - 0, - 0, # velocities (ignored) - 0, - 0, - 0, # accelerations (ignored) - 0, # yaw (ignored) - yaw_rate_rad, # yaw_rate in rad/s - ) - - time.sleep(0.1) # 10Hz control loop - - logger.warning("Rotation timeout") - self.stop() - return False - - def arm(self) -> bool: - """Arm the drone.""" - if not self.connected: - return False - - logger.info("Arming motors...") - self.update_telemetry() - - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - ) - - # Wait for ACK - ack = self.mavlink.recv_match(type="COMMAND_ACK", blocking=True, timeout=5) - if ack and ack.command == mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM: - if ack.result == mavutil.mavlink.MAV_RESULT_ACCEPTED: - logger.info("Arm command accepted") - - # Verify armed status - for _i in range(10): - msg = self.mavlink.recv_match(type="HEARTBEAT", blocking=True, timeout=1) - if msg: - armed = msg.base_mode & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED - if armed: - logger.info("Motors ARMED successfully!") - return True - time.sleep(0.5) - else: - logger.error(f"Arm failed with result: {ack.result}") - - return False - - def disarm(self) -> bool: - """Disarm the drone.""" - if not self.connected: - return False - - logger.info("Disarming motors...") - - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_COMPONENT_ARM_DISARM, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ) - - time.sleep(1) - return True - - def takeoff(self, altitude: float = 3.0) -> bool: - """Takeoff to specified altitude.""" - if not self.connected: - return False - - logger.info(f"Taking off to {altitude}m...") - - # Set GUIDED mode - if not self.set_mode("GUIDED"): - logger.error("Failed to set GUIDED mode for takeoff") - return False - - # Send takeoff command - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_NAV_TAKEOFF, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - altitude, - ) - - logger.info(f"Takeoff command sent for {altitude}m altitude") - return True - - def land(self) -> bool: - """Land the drone at current position.""" - if not self.connected: - return False - - logger.info("Landing...") - - # Send initial land command - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_NAV_LAND, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ) - - # Wait for disarm with confirmations - disarm_count = 0 - for _ in range(120): # 60 seconds max (120 * 0.5s) - # Keep sending land command - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_NAV_LAND, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ) - - # Check armed status - msg = self.mavlink.recv_match(type="HEARTBEAT", blocking=True, timeout=0.5) - if msg: - msg_dict = msg.to_dict() - armed = bool( - msg_dict.get("base_mode", 0) & mavutil.mavlink.MAV_MODE_FLAG_SAFETY_ARMED - ) - logger.debug(f"HEARTBEAT: {msg_dict} ARMED: {armed}") - - disarm_count = 0 if armed else disarm_count + 1 - - if disarm_count >= 5: # 2.5 seconds of continuous disarm - logger.info("Drone landed and disarmed") - return True - - time.sleep(0.5) - - logger.warning("Land timeout") - return self.set_mode("LAND") - - def fly_to(self, lat: float, lon: float, alt: float) -> str: - """Fly to GPS coordinates - sends commands continuously until reaching target. - - Args: - lat: Latitude in degrees - lon: Longitude in degrees - alt: Altitude in meters (relative to home) - - Returns: - String message indicating success or failure reason - """ - if not self.connected: - return "Failed: Not connected to drone" - - # Check if already flying to a target - if self.flying_to_target: - logger.warning( - "Already flying to target, ignoring new fly_to command. Wait until completed to send new fly_to command." - ) - return ( - "Already flying to target - wait for completion before sending new fly_to command" - ) - - self.flying_to_target = True - - # Ensure GUIDED mode for GPS navigation - if not self.set_mode("GUIDED"): - logger.error("Failed to set GUIDED mode for GPS navigation") - self.flying_to_target = False - return "Failed: Could not set GUIDED mode for GPS navigation" - - logger.info(f"Flying to GPS: lat={lat:.7f}, lon={lon:.7f}, alt={alt:.1f}m") - - # Reset velocity tracking for smooth start - self.prev_vx = 0.0 - self.prev_vy = 0.0 - self.prev_vz = 0.0 - - # Send velocity commands towards GPS target at 10Hz - acceptance_radius = 30.0 # meters - max_duration = 120 # seconds max flight time - start_time = time.time() - max_speed = self.max_velocity # m/s max speed - - import math - - loop_count = 0 - - try: - while time.time() - start_time < max_duration: - loop_start = time.time() - - # Don't update telemetry here - let background thread handle it - # self.update_telemetry(timeout=0.01) # Removed to prevent message conflicts - - # Check current position from telemetry - if "GLOBAL_POSITION_INT" in self.telemetry: - t1 = time.time() - - # Telemetry already has converted values (see update_telemetry lines 104-107) - current_lat = self.telemetry["GLOBAL_POSITION_INT"].get( - "lat", 0 - ) # Already in degrees - current_lon = self.telemetry["GLOBAL_POSITION_INT"].get( - "lon", 0 - ) # Already in degrees - current_alt = self.telemetry["GLOBAL_POSITION_INT"].get( - "relative_alt", 0 - ) # Already in meters - - t2 = time.time() - - logger.info( - f"DEBUG: Current GPS: lat={current_lat:.10f}, lon={current_lon:.10f}, alt={current_alt:.2f}m" - ) - logger.info( - f"DEBUG: Target GPS: lat={lat:.10f}, lon={lon:.10f}, alt={alt:.2f}m" - ) - - # Calculate vector to target with high precision - dlat = lat - current_lat - dlon = lon - current_lon - dalt = alt - current_alt - - logger.info( - f"DEBUG: Delta: dlat={dlat:.10f}, dlon={dlon:.10f}, dalt={dalt:.2f}m" - ) - - t3 = time.time() - - # Convert lat/lon difference to meters with high precision - # Using more accurate calculation - lat_rad = current_lat * math.pi / 180.0 - meters_per_degree_lat = ( - 111132.92 - 559.82 * math.cos(2 * lat_rad) + 1.175 * math.cos(4 * lat_rad) - ) - meters_per_degree_lon = 111412.84 * math.cos(lat_rad) - 93.5 * math.cos( - 3 * lat_rad - ) - - x_dist = dlat * meters_per_degree_lat # North distance in meters - y_dist = dlon * meters_per_degree_lon # East distance in meters - - logger.info( - f"DEBUG: Distance in meters: North={x_dist:.2f}m, East={y_dist:.2f}m, Up={dalt:.2f}m" - ) - - # Calculate total distance - distance = math.sqrt(x_dist**2 + y_dist**2 + dalt**2) - logger.info(f"DEBUG: Total distance to target: {distance:.2f}m") - - t4 = time.time() - - if distance < acceptance_radius: - logger.info(f"Reached GPS target (within {distance:.1f}m)") - self.stop() - # Return to manual control - self.set_mode("STABILIZE") - logger.info("Returned to STABILIZE mode for manual control") - self.flying_to_target = False - return f"Success: Reached target location (lat={lat:.7f}, lon={lon:.7f}, alt={alt:.1f}m)" - - # Only send velocity commands if we're far enough - if distance > 0.1: - # On first loop, rotate to face the target - if loop_count == 0: - # Calculate bearing to target - bearing_rad = math.atan2( - y_dist, x_dist - ) # East, North -> angle from North - target_heading_deg = math.degrees(bearing_rad) - if target_heading_deg < 0: - target_heading_deg += 360 - - logger.info( - f"Rotating to face target at heading {target_heading_deg:.1f}°" - ) - self.rotate_to(target_heading_deg, timeout=45.0) - logger.info("Rotation complete, starting movement") - - # Now just move towards target (no rotation) - t5 = time.time() - - # Calculate movement speed - maintain max speed until 20m from target - if distance > 20: - speed = max_speed # Full speed when far from target - else: - # Ramp down speed from 20m to target - speed = max( - 0.5, distance / 4.0 - ) # At 20m: 5m/s, at 10m: 2.5m/s, at 2m: 0.5m/s - - # Calculate target velocities - target_vx = (x_dist / distance) * speed # North velocity - target_vy = (y_dist / distance) * speed # East velocity - target_vz = (dalt / distance) * speed # Up velocity (positive = up) - - # Direct velocity assignment (no acceleration limiting) - vx = target_vx - vy = target_vy - vz = target_vz - - # Store for next iteration - self.prev_vx = vx - self.prev_vy = vy - self.prev_vz = vz - - logger.info( - f"MOVING: vx={vx:.3f} vy={vy:.3f} vz={vz:.3f} m/s, distance={distance:.1f}m" - ) - - # Send velocity command in LOCAL_NED frame - self.mavlink.mav.set_position_target_local_ned_send( - 0, # time_boot_ms - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_FRAME_LOCAL_NED, # Local NED for movement - 0b0000111111000111, # type_mask - use velocities only - 0, - 0, - 0, # positions (not used) - vx, - vy, - vz, # velocities in m/s - 0, - 0, - 0, # accelerations (not used) - 0, # yaw (not used) - 0, # yaw_rate (not used) - ) - - # Log if stuck - if loop_count > 20 and loop_count % 10 == 0: - logger.warning( - f"STUCK? Been sending commands for {loop_count} iterations but distance still {distance:.1f}m" - ) - - t6 = time.time() - - # Log timing every 10 loops - loop_count += 1 - if loop_count % 10 == 0: - logger.info( - f"TIMING: telemetry_read={t2 - t1:.4f}s, delta_calc={t3 - t2:.4f}s, " - f"distance_calc={t4 - t3:.4f}s, velocity_calc={t5 - t4:.4f}s, " - f"mavlink_send={t6 - t5:.4f}s, total_loop={t6 - loop_start:.4f}s" - ) - else: - logger.info("DEBUG: Too close to send velocity commands") - - else: - logger.warning("DEBUG: No GLOBAL_POSITION_INT in telemetry!") - - time.sleep(0.1) # Send at 10Hz - - except Exception as e: - logger.error(f"Error during fly_to: {e}") - self.flying_to_target = False # Clear flag immediately - raise # Re-raise the exception so caller sees the error - finally: - # Always clear the flag when exiting - if self.flying_to_target: - logger.info("Stopped sending GPS velocity commands (timeout)") - self.flying_to_target = False - self.set_mode("BRAKE") - time.sleep(0.5) - # Return to manual control - self.set_mode("STABILIZE") - logger.info("Returned to STABILIZE mode for manual control") - - return "Failed: Timeout - did not reach target within 120 seconds" - - def set_mode(self, mode: str) -> bool: - """Set flight mode.""" - if not self.connected: - return False - - mode_mapping = { - "STABILIZE": 0, - "GUIDED": 4, - "LOITER": 5, - "RTL": 6, - "LAND": 9, - "POSHOLD": 16, - "BRAKE": 17, - } - - if mode not in mode_mapping: - logger.error(f"Unknown mode: {mode}") - return False - - mode_id = mode_mapping[mode] - logger.info(f"Setting mode to {mode}") - - self.update_telemetry() - - self.mavlink.mav.command_long_send( - self.mavlink.target_system, - self.mavlink.target_component, - mavutil.mavlink.MAV_CMD_DO_SET_MODE, - 0, - mavutil.mavlink.MAV_MODE_FLAG_CUSTOM_MODE_ENABLED, - mode_id, - 0, - 0, - 0, - 0, - 0, - ) - - ack = self.mavlink.recv_match(type="COMMAND_ACK", blocking=True, timeout=3) - if ack and ack.result == mavutil.mavlink.MAV_RESULT_ACCEPTED: - logger.info(f"Mode changed to {mode}") - self.telemetry["mode"] = mode_id - return True - - return False - - @functools.cache - def odom_stream(self) -> Subject[PoseStamped]: - """Get odometry stream.""" - return self._odom_subject - - @functools.cache - def status_stream(self) -> Subject[dict[str, Any]]: - """Get status stream.""" - return self._status_subject - - @functools.cache - def telemetry_stream(self) -> Subject[dict[str, Any]]: - """Get full telemetry stream.""" - return self._telemetry_subject - - def get_telemetry(self) -> dict[str, Any]: - """Get current telemetry.""" - # Update telemetry multiple times to ensure we get data - for _ in range(5): - self.update_telemetry(timeout=0.2) - return self.telemetry.copy() - - def disconnect(self) -> None: - """Disconnect from drone.""" - if self.mavlink: - self.mavlink.close() - self.connected = False - logger.info("Disconnected") - - @property - def is_flying_to_target(self) -> bool: - """Check if drone is currently flying to a GPS target.""" - return self.flying_to_target - - def get_video_stream(self, fps: int = 30) -> None: - """Get video stream (to be implemented with GStreamer).""" - # Will be implemented in camera module - return None - - -class FakeMavlinkConnection(MavlinkConnection): - """Replay MAVLink for testing.""" - - def __init__(self, connection_string: str) -> None: - # Call parent init (which no longer calls connect()) - super().__init__(connection_string) - - # Create fake mavlink object - class FakeMavlink: - def __init__(self) -> None: - from dimos.utils.data import get_data - from dimos.utils.testing import TimedSensorReplay - - get_data("drone") - - self.replay: Any = TimedSensorReplay("drone/mavlink") - self.messages: list[dict[str, Any]] = [] - # The stream() method returns an Observable that emits messages with timing - self.replay.stream().subscribe(self.messages.append) - - # Properties that get accessed - self.target_system = 1 - self.target_component = 1 - self.mav = self # self.mavlink.mav is used in many places - - def recv_match( - self, blocking: bool = False, type: Any = None, timeout: Any = None - ) -> Any: - """Return next replay message as fake message object.""" - if not self.messages: - return None - - msg_dict = self.messages.pop(0) - - # Create message object with ALL attributes that might be accessed - class FakeMsg: - def __init__(self, d: dict[str, Any]) -> None: - self._dict = d - # Set any direct attributes that get accessed - self.base_mode = d.get("base_mode", 0) - self.command = d.get("command", 0) - self.result = d.get("result", 0) - - def get_type(self) -> Any: - return self._dict.get("mavpackettype", "") - - def to_dict(self) -> dict[str, Any]: - return self._dict - - # Filter by type if requested - if type and msg_dict.get("type") != type: - return None - - return FakeMsg(msg_dict) - - def wait_heartbeat(self, timeout: int = 30) -> None: - """Fake heartbeat received.""" - pass - - def close(self) -> None: - """Fake close.""" - pass - - # Command methods that get called but don't need to do anything in replay - def command_long_send(self, *args: Any, **kwargs: Any) -> None: - pass - - def set_position_target_local_ned_send(self, *args: Any, **kwargs: Any) -> None: - pass - - def set_position_target_global_int_send(self, *args: Any, **kwargs: Any) -> None: - pass - - # Set up fake mavlink - self.mavlink = FakeMavlink() - self.connected = True - - # Initialize position tracking (parent __init__ doesn't do this since connect wasn't called) - self._position = {"x": 0.0, "y": 0.0, "z": 0.0} - self._last_update = time.time() - - def takeoff(self, altitude: float = 3.0) -> bool: - """Fake takeoff - return immediately without blocking.""" - logger.info(f"[FAKE] Taking off to {altitude}m...") - return True - - def land(self) -> bool: - """Fake land - return immediately without blocking.""" - logger.info("[FAKE] Landing...") - return True diff --git a/dimos/robot/drone/test_drone.py b/dimos/robot/drone/test_drone.py deleted file mode 100644 index d9075beae3..0000000000 --- a/dimos/robot/drone/test_drone.py +++ /dev/null @@ -1,1035 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Core unit tests for drone module.""" - -import json -import os -import time -import unittest -from unittest.mock import MagicMock, patch - -import numpy as np - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.robot.drone.connection_module import DroneConnectionModule -from dimos.robot.drone.dji_video_stream import FakeDJIVideoStream -from dimos.robot.drone.drone import Drone -from dimos.robot.drone.mavlink_connection import FakeMavlinkConnection, MavlinkConnection - - -class TestMavlinkProcessing(unittest.TestCase): - """Test MAVLink message processing and coordinate conversions.""" - - def test_mavlink_message_processing(self) -> None: - """Test that MAVLink messages trigger correct odom/tf publishing.""" - conn = MavlinkConnection("udp:0.0.0.0:14550") - - # Mock the mavlink connection - conn.mavlink = MagicMock() - conn.connected = True - - # Track what gets published - published_odom = [] - conn._odom_subject.on_next = lambda x: published_odom.append(x) - - # Create ATTITUDE message and process it - attitude_msg = MagicMock() - attitude_msg.get_type.return_value = "ATTITUDE" - attitude_msg.to_dict.return_value = { - "mavpackettype": "ATTITUDE", - "roll": 0.1, - "pitch": 0.2, # Positive pitch = nose up in MAVLink - "yaw": 0.3, # Positive yaw = clockwise in MAVLink - } - - # Mock recv_match to return our message once then None - def recv_side_effect(*args, **kwargs): - if not hasattr(recv_side_effect, "called"): - recv_side_effect.called = True - return attitude_msg - return None - - conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) - - # Process the message - conn.update_telemetry(timeout=0.01) - - # Check telemetry was updated - self.assertEqual(conn.telemetry["ATTITUDE"]["roll"], 0.1) - self.assertEqual(conn.telemetry["ATTITUDE"]["pitch"], 0.2) - self.assertEqual(conn.telemetry["ATTITUDE"]["yaw"], 0.3) - - # Check odom was published with correct coordinate conversion - self.assertEqual(len(published_odom), 1) - pose = published_odom[0] - - # Verify NED to ROS conversion happened - # ROS uses different conventions: positive pitch = nose down, positive yaw = counter-clockwise - # So we expect sign flips in the quaternion conversion - self.assertIsNotNone(pose.orientation) - - def test_position_integration(self) -> None: - """Test velocity integration for indoor flight positioning.""" - conn = MavlinkConnection("udp:0.0.0.0:14550") - conn.mavlink = MagicMock() - conn.connected = True - - # Initialize position tracking - conn._position = {"x": 0.0, "y": 0.0, "z": 0.0} - conn._last_update = time.time() - - # Create GLOBAL_POSITION_INT with velocities - pos_msg = MagicMock() - pos_msg.get_type.return_value = "GLOBAL_POSITION_INT" - pos_msg.to_dict.return_value = { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 0, - "lon": 0, - "alt": 0, - "relative_alt": 1000, # 1m in mm - "vx": 100, # 1 m/s North in cm/s - "vy": 200, # 2 m/s East in cm/s - "vz": 0, - "hdg": 0, - } - - def recv_side_effect(*args, **kwargs): - if not hasattr(recv_side_effect, "called"): - recv_side_effect.called = True - return pos_msg - return None - - conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) - - # Process with known dt - old_time = conn._last_update - conn.update_telemetry(timeout=0.01) - dt = conn._last_update - old_time - - # Check position was integrated from velocities - # vx=1m/s North → +X in ROS - # vy=2m/s East → -Y in ROS (Y points West) - expected_x = 1.0 * dt # North velocity - expected_y = -2.0 * dt # East velocity (negated for ROS) - - self.assertAlmostEqual(conn._position["x"], expected_x, places=2) - self.assertAlmostEqual(conn._position["y"], expected_y, places=2) - - def test_ned_to_ros_coordinate_conversion(self) -> None: - """Test NED to ROS coordinate system conversion for all axes.""" - conn = MavlinkConnection("udp:0.0.0.0:14550") - conn.mavlink = MagicMock() - conn.connected = True - - # Initialize position - conn._position = {"x": 0.0, "y": 0.0, "z": 0.0} - conn._last_update = time.time() - - # Test with velocities in all directions - # NED: North-East-Down - # ROS: X(forward/North), Y(left/West), Z(up) - pos_msg = MagicMock() - pos_msg.get_type.return_value = "GLOBAL_POSITION_INT" - pos_msg.to_dict.return_value = { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 0, - "lon": 0, - "alt": 5000, # 5m altitude in mm - "relative_alt": 5000, - "vx": 300, # 3 m/s North (NED) - "vy": 400, # 4 m/s East (NED) - "vz": -100, # 1 m/s Up (negative in NED for up) - "hdg": 0, - } - - def recv_side_effect(*args, **kwargs): - if not hasattr(recv_side_effect, "called"): - recv_side_effect.called = True - return pos_msg - return None - - conn.mavlink.recv_match = MagicMock(side_effect=recv_side_effect) - - # Process message - old_time = conn._last_update - conn.update_telemetry(timeout=0.01) - dt = conn._last_update - old_time - - # Verify coordinate conversion: - # NED North (vx=3) → ROS +X - # NED East (vy=4) → ROS -Y (ROS Y points West/left) - # NED Down (vz=-1, up) → ROS +Z (ROS Z points up) - - # Position should integrate with converted velocities - self.assertGreater(conn._position["x"], 0) # North → positive X - self.assertLess(conn._position["y"], 0) # East → negative Y - self.assertEqual(conn._position["z"], 5.0) # Altitude from relative_alt (5000mm = 5m) - - # Check X,Y velocity integration (Z is set from altitude, not integrated) - self.assertAlmostEqual(conn._position["x"], 3.0 * dt, places=2) - self.assertAlmostEqual(conn._position["y"], -4.0 * dt, places=2) - - -class TestReplayMode(unittest.TestCase): - """Test replay mode functionality.""" - - def test_fake_mavlink_connection(self) -> None: - """Test FakeMavlinkConnection replays messages correctly.""" - with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: - # Mock the replay stream - MagicMock() - mock_messages = [ - {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, - {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, - ] - - # Make stream emit our messages - mock_replay.return_value.stream.return_value.subscribe = lambda callback: [ - callback(msg) for msg in mock_messages - ] - - conn = FakeMavlinkConnection("replay") - - # Check messages are available - msg1 = conn.mavlink.recv_match() - self.assertIsNotNone(msg1) - self.assertEqual(msg1.get_type(), "ATTITUDE") - - msg2 = conn.mavlink.recv_match() - self.assertIsNotNone(msg2) - self.assertEqual(msg2.get_type(), "HEARTBEAT") - - def test_fake_video_stream_no_throttling(self) -> None: - """Test FakeDJIVideoStream returns replay stream directly.""" - with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: - mock_stream = MagicMock() - mock_replay.return_value.stream.return_value = mock_stream - - stream = FakeDJIVideoStream(port=5600) - result_stream = stream.get_stream() - - # Verify stream is returned directly without throttling - self.assertEqual(result_stream, mock_stream) - - def test_connection_module_replay_mode(self) -> None: - """Test connection module uses Fake classes in replay mode.""" - with patch("dimos.robot.drone.mavlink_connection.FakeMavlinkConnection") as mock_fake_conn: - with patch("dimos.robot.drone.dji_video_stream.FakeDJIVideoStream") as mock_fake_video: - # Mock the fake connection - mock_conn_instance = MagicMock() - mock_conn_instance.connected = True - mock_conn_instance.odom_stream.return_value.subscribe = MagicMock( - return_value=lambda: None - ) - mock_conn_instance.status_stream.return_value.subscribe = MagicMock( - return_value=lambda: None - ) - mock_conn_instance.telemetry_stream.return_value.subscribe = MagicMock( - return_value=lambda: None - ) - mock_conn_instance.disconnect = MagicMock() - mock_fake_conn.return_value = mock_conn_instance - - # Mock the fake video - mock_video_instance = MagicMock() - mock_video_instance.start.return_value = True - mock_video_instance.get_stream.return_value.subscribe = MagicMock( - return_value=lambda: None - ) - mock_video_instance.stop = MagicMock() - mock_fake_video.return_value = mock_video_instance - - # Create module with replay connection string - module = DroneConnectionModule(connection_string="replay") - module.video = MagicMock() - module.movecmd = MagicMock() - module.movecmd.subscribe = MagicMock(return_value=lambda: None) - module.tf = MagicMock() - - try: - # Start should use Fake classes - module.start() - - mock_fake_conn.assert_called_once_with("replay") - mock_fake_video.assert_called_once() - finally: - # Always clean up - module.stop() - - def test_connection_module_replay_with_messages(self) -> None: - """Test connection module in replay mode receives and processes messages.""" - - os.environ["DRONE_CONNECTION"] = "replay" - - with patch("dimos.utils.testing.TimedSensorReplay") as mock_replay: - # Set up MAVLink replay stream - mavlink_messages = [ - {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, - {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, - { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 377810501, - "lon": -1224069671, - "alt": 0, - "relative_alt": 1000, - "vx": 100, - "vy": 0, - "vz": 0, - "hdg": 0, - }, - ] - - # Set up video replay stream - video_frames = [ - np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8), - np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8), - ] - - def create_mavlink_stream(): - stream = MagicMock() - - def subscribe(callback) -> None: - print("\n[TEST] MAVLink replay stream subscribed") - for msg in mavlink_messages: - print(f"[TEST] Replaying MAVLink: {msg['mavpackettype']}") - callback(msg) - - stream.subscribe = subscribe - return stream - - def create_video_stream(): - stream = MagicMock() - - def subscribe(callback) -> None: - print("[TEST] Video replay stream subscribed") - for i, frame in enumerate(video_frames): - print( - f"[TEST] Replaying video frame {i + 1}/{len(video_frames)}, shape: {frame.shape}" - ) - callback(frame) - - stream.subscribe = subscribe - return stream - - # Configure mock replay to return appropriate streams - def replay_side_effect(store_name: str): - print(f"[TEST] TimedSensorReplay created for: {store_name}") - mock = MagicMock() - if "mavlink" in store_name: - mock.stream.return_value = create_mavlink_stream() - elif "video" in store_name: - mock.stream.return_value = create_video_stream() - return mock - - mock_replay.side_effect = replay_side_effect - - # Create and start connection module - module = DroneConnectionModule(connection_string="replay") - - # Mock publishers to track what gets published - published_odom = [] - published_video = [] - published_status = [] - - module.odom = MagicMock( - publish=lambda x: ( - published_odom.append(x), - print( - f"[TEST] Published odom: position=({x.position.x:.2f}, {x.position.y:.2f}, {x.position.z:.2f})" - ), - ) - ) - module.video = MagicMock( - publish=lambda x: ( - published_video.append(x), - print( - f"[TEST] Published video frame with shape: {x.data.shape if hasattr(x, 'data') else 'unknown'}" - ), - ) - ) - module.status = MagicMock( - publish=lambda x: ( - published_status.append(x), - print( - f"[TEST] Published status: {x.data[:50]}..." - if hasattr(x, "data") - else "[TEST] Published status" - ), - ) - ) - module.telemetry = MagicMock() - module.tf = MagicMock() - module.movecmd = MagicMock() - - try: - print("\n[TEST] Starting connection module in replay mode...") - module.start() - - # Give time for messages to process - import time - - time.sleep(0.1) - - print("\n[TEST] Module started") - print(f"[TEST] Total odom messages published: {len(published_odom)}") - print(f"[TEST] Total video frames published: {len(published_video)}") - print(f"[TEST] Total status messages published: {len(published_status)}") - - # Verify module started and is processing messages - self.assertIsNotNone(module.connection) - self.assertIsNotNone(module.video_stream) - - # Should have published some messages - self.assertGreater( - len(published_odom) + len(published_video) + len(published_status), - 0, - "No messages were published in replay mode", - ) - finally: - # Clean up - module.stop() - - -class TestDroneFullIntegration(unittest.TestCase): - """Full integration test of Drone class with replay mode.""" - - def setUp(self) -> None: - """Set up test environment.""" - # Mock the DimOS core module - self.mock_dimos = MagicMock() - self.mock_dimos.deploy.return_value = MagicMock() - - # Mock pubsub.lcm.autoconf - self.pubsub_patch = patch("dimos.protocol.pubsub.lcm.autoconf") - self.pubsub_patch.start() - - # Mock FoxgloveBridge - self.foxglove_patch = patch("dimos.robot.drone.drone.FoxgloveBridge") - self.mock_foxglove = self.foxglove_patch.start() - - def tearDown(self) -> None: - """Clean up patches.""" - self.pubsub_patch.stop() - self.foxglove_patch.stop() - - @patch("dimos.robot.drone.drone.core.start") - @patch("dimos.utils.testing.TimedSensorReplay") - def test_full_system_with_replay(self, mock_replay, mock_core_start) -> None: - """Test full drone system initialization and operation with replay mode.""" - # Set up mock replay data - mavlink_messages = [ - {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193, "armed": True}, - {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, - { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 377810501, - "lon": -1224069671, - "alt": 5000, - "relative_alt": 5000, - "vx": 100, # 1 m/s North - "vy": 200, # 2 m/s East - "vz": -50, # 0.5 m/s Up - "hdg": 9000, # 90 degrees - }, - { - "mavpackettype": "BATTERY_STATUS", - "voltages": [3800, 3800, 3800, 3800], - "battery_remaining": 75, - }, - ] - - video_frames = [ - Image( - data=np.random.randint(0, 255, (360, 640, 3), dtype=np.uint8), - format=ImageFormat.BGR, - ) - ] - - def replay_side_effect(store_name: str): - mock = MagicMock() - if "mavlink" in store_name: - # Create stream that emits MAVLink messages - stream = MagicMock() - stream.subscribe = lambda callback: [callback(msg) for msg in mavlink_messages] - mock.stream.return_value = stream - elif "video" in store_name: - # Create stream that emits video frames - stream = MagicMock() - stream.subscribe = lambda callback: [callback(frame) for frame in video_frames] - mock.stream.return_value = stream - return mock - - mock_replay.side_effect = replay_side_effect - - # Mock DimOS core - mock_core_start.return_value = self.mock_dimos - - # Create drone in replay mode - drone = Drone(connection_string="replay", video_port=5600) - - # Mock the deployed modules - mock_connection = MagicMock() - mock_camera = MagicMock() - - # Set up return values for module methods - mock_connection.start.return_value = True - mock_connection.get_odom.return_value = PoseStamped( - position=Vector3(1.0, 2.0, 3.0), orientation=Quaternion(0, 0, 0, 1), frame_id="world" - ) - mock_connection.get_status.return_value = { - "armed": True, - "battery_voltage": 15.2, - "battery_remaining": 75, - "altitude": 5.0, - } - - mock_camera.start.return_value = True - - # Configure deploy to return our mocked modules - def deploy_side_effect(module_class, **kwargs): - if "DroneConnectionModule" in str(module_class): - return mock_connection - elif "DroneCameraModule" in str(module_class): - return mock_camera - return MagicMock() - - self.mock_dimos.deploy.side_effect = deploy_side_effect - - # Start the drone system - drone.start() - - # Verify modules were deployed - self.assertEqual(self.mock_dimos.deploy.call_count, 4) - - # Test get_odom - odom = drone.get_odom() - self.assertIsNotNone(odom) - self.assertEqual(odom.position.x, 1.0) - self.assertEqual(odom.position.y, 2.0) - self.assertEqual(odom.position.z, 3.0) - - # Test get_status - status = drone.get_status() - self.assertIsNotNone(status) - self.assertTrue(status["armed"]) - self.assertEqual(status["battery_remaining"], 75) - - # Test movement command - drone.move(Vector3(1.0, 0.0, 0.5), duration=2.0) - mock_connection.move.assert_called_once_with(Vector3(1.0, 0.0, 0.5), 2.0) - - # Test control commands - drone.arm() - mock_connection.arm.assert_called_once() - - drone.takeoff(altitude=10.0) - mock_connection.takeoff.assert_called_once_with(10.0) - - drone.land() - mock_connection.land.assert_called_once() - - drone.disarm() - mock_connection.disarm.assert_called_once() - - # Test mode setting - drone.set_mode("GUIDED") - mock_connection.set_mode.assert_called_once_with("GUIDED") - - # Clean up - drone.stop() - - # Verify cleanup was called - mock_connection.stop.assert_called_once() - mock_camera.stop.assert_called_once() - self.mock_dimos.close_all.assert_called_once() - - -class TestDroneControlCommands(unittest.TestCase): - """Test drone control commands with FakeMavlinkConnection.""" - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_arm_disarm_commands(self, mock_get_data, mock_replay) -> None: - """Test arm and disarm commands work with fake connection.""" - # Set up mock replay - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Test arm - result = conn.arm() - self.assertIsInstance(result, bool) # Should return bool without crashing - - # Test disarm - result = conn.disarm() - self.assertIsInstance(result, bool) # Should return bool without crashing - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_takeoff_land_commands(self, mock_get_data, mock_replay) -> None: - """Test takeoff and land commands with fake connection.""" - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Test takeoff - result = conn.takeoff(altitude=15.0) - # In fake mode, should accept but may return False if no ACK simulation - self.assertIsNotNone(result) - - # Test land - result = conn.land() - self.assertIsNotNone(result) - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_set_mode_command(self, mock_get_data, mock_replay) -> None: - """Test flight mode setting with fake connection.""" - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Test various flight modes - modes = ["STABILIZE", "GUIDED", "LAND", "RTL", "LOITER"] - for mode in modes: - result = conn.set_mode(mode) - # Should return True or False but not crash - self.assertIsInstance(result, bool) - - -class TestDronePerception(unittest.TestCase): - """Test drone perception capabilities.""" - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_video_stream_replay(self, mock_get_data, mock_replay) -> None: - """Test video stream works with replay data.""" - # Set up video frames - create a test pattern instead of random noise - import cv2 - - # Create a test pattern image with some structure - test_frame = np.zeros((360, 640, 3), dtype=np.uint8) - # Add some colored rectangles to make it visually obvious - cv2.rectangle(test_frame, (50, 50), (200, 150), (255, 0, 0), -1) # Blue - cv2.rectangle(test_frame, (250, 50), (400, 150), (0, 255, 0), -1) # Green - cv2.rectangle(test_frame, (450, 50), (600, 150), (0, 0, 255), -1) # Red - cv2.putText( - test_frame, - "DRONE TEST FRAME", - (150, 250), - cv2.FONT_HERSHEY_SIMPLEX, - 1.5, - (255, 255, 255), - 2, - ) - - video_frames = [test_frame, test_frame.copy()] - - # Mock replay stream - mock_stream = MagicMock() - received_frames = [] - - def subscribe_side_effect(callback) -> None: - for frame in video_frames: - img = Image(data=frame, format=ImageFormat.BGR) - callback(img) - received_frames.append(img) - - mock_stream.subscribe = subscribe_side_effect - mock_replay.return_value.stream.return_value = mock_stream - - # Create fake video stream - video_stream = FakeDJIVideoStream(port=5600) - stream = video_stream.get_stream() - - # Subscribe to stream - captured_frames = [] - stream.subscribe(captured_frames.append) - - # Verify frames were captured - self.assertEqual(len(received_frames), 2) - for i, frame in enumerate(received_frames): - self.assertIsInstance(frame, Image) - self.assertEqual(frame.data.shape, (360, 640, 3)) - - # Save first frame to file for visual inspection - if i == 0: - import os - - output_path = "/tmp/drone_test_frame.png" - cv2.imwrite(output_path, frame.data) - print(f"\n[TEST] Saved test frame to {output_path} for visual inspection") - if os.path.exists(output_path): - print(f"[TEST] File size: {os.path.getsize(output_path)} bytes") - - -class TestDroneMovementAndOdometry(unittest.TestCase): - """Test drone movement commands and odometry.""" - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_movement_command_conversion(self, mock_get_data, mock_replay) -> None: - """Test movement commands are properly converted from ROS to NED.""" - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Test movement in ROS frame - # ROS: X=forward, Y=left, Z=up - velocity_ros = Vector3(2.0, -1.0, 0.5) # Forward 2m/s, right 1m/s, up 0.5m/s - - result = conn.move(velocity_ros, duration=1.0) - self.assertTrue(result) - - # Movement should be converted to NED internally - # The fake connection doesn't actually send commands, but it should not crash - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_odometry_from_replay(self, mock_get_data, mock_replay) -> None: - """Test odometry is properly generated from replay messages.""" - # Set up replay messages - messages = [ - {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, - { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 377810501, - "lon": -1224069671, - "alt": 10000, - "relative_alt": 5000, - "vx": 200, # 2 m/s North - "vy": 100, # 1 m/s East - "vz": -50, # 0.5 m/s Up - "hdg": 18000, # 180 degrees - }, - ] - - def replay_stream_subscribe(callback) -> None: - for msg in messages: - callback(msg) - - mock_stream = MagicMock() - mock_stream.subscribe = replay_stream_subscribe - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Collect published odometry - published_odom = [] - conn._odom_subject.subscribe(published_odom.append) - - # Process messages - for _ in range(5): - conn.update_telemetry(timeout=0.01) - - # Should have published odometry - self.assertGreater(len(published_odom), 0) - - # Check odometry message - odom = published_odom[0] - self.assertIsInstance(odom, PoseStamped) - self.assertIsNotNone(odom.orientation) - self.assertEqual(odom.frame_id, "world") - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_position_integration_indoor(self, mock_get_data, mock_replay) -> None: - """Test position integration for indoor flight without GPS.""" - messages = [ - {"mavpackettype": "ATTITUDE", "roll": 0, "pitch": 0, "yaw": 0}, - { - "mavpackettype": "GLOBAL_POSITION_INT", - "lat": 0, # Invalid GPS - "lon": 0, - "alt": 0, - "relative_alt": 2000, # 2m altitude - "vx": 100, # 1 m/s North - "vy": 0, - "vz": 0, - "hdg": 0, - }, - ] - - def replay_stream_subscribe(callback) -> None: - for msg in messages: - callback(msg) - - mock_stream = MagicMock() - mock_stream.subscribe = replay_stream_subscribe - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Process messages multiple times to integrate position - initial_time = time.time() - conn._last_update = initial_time - - for _i in range(3): - conn.update_telemetry(timeout=0.01) - time.sleep(0.1) # Let some time pass for integration - - # Position should have been integrated - self.assertGreater(conn._position["x"], 0) # Moving North - self.assertEqual(conn._position["z"], 2.0) # Altitude from relative_alt - - -class TestDroneStatusAndTelemetry(unittest.TestCase): - """Test drone status and telemetry reporting.""" - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_status_extraction(self, mock_get_data, mock_replay) -> None: - """Test status is properly extracted from MAVLink messages.""" - messages = [ - {"mavpackettype": "HEARTBEAT", "type": 2, "base_mode": 193}, # Armed - { - "mavpackettype": "BATTERY_STATUS", - "voltages": [3700, 3700, 3700, 3700], - "current_battery": -1500, - "battery_remaining": 65, - }, - {"mavpackettype": "GPS_RAW_INT", "satellites_visible": 12, "fix_type": 3}, - {"mavpackettype": "GLOBAL_POSITION_INT", "relative_alt": 8000, "hdg": 27000}, - ] - - def replay_stream_subscribe(callback) -> None: - for msg in messages: - callback(msg) - - mock_stream = MagicMock() - mock_stream.subscribe = replay_stream_subscribe - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - - # Collect published status - published_status = [] - conn._status_subject.subscribe(published_status.append) - - # Process messages - for _ in range(5): - conn.update_telemetry(timeout=0.01) - - # Should have published status - self.assertGreater(len(published_status), 0) - - # Check status fields - status = published_status[-1] # Get latest - self.assertIn("armed", status) - self.assertIn("battery_remaining", status) - self.assertIn("satellites", status) - self.assertIn("altitude", status) - self.assertIn("heading", status) - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_telemetry_json_publishing(self, mock_get_data, mock_replay) -> None: - """Test full telemetry is published as JSON.""" - messages = [ - {"mavpackettype": "ATTITUDE", "roll": 0.1, "pitch": 0.2, "yaw": 0.3}, - {"mavpackettype": "GLOBAL_POSITION_INT", "lat": 377810501, "lon": -1224069671}, - ] - - def replay_stream_subscribe(callback) -> None: - for msg in messages: - callback(msg) - - mock_stream = MagicMock() - mock_stream.subscribe = replay_stream_subscribe - mock_replay.return_value.stream.return_value = mock_stream - - # Create connection module with replay - module = DroneConnectionModule(connection_string="replay") - - # Mock publishers - published_telemetry = [] - module.telemetry = MagicMock(publish=lambda x: published_telemetry.append(x)) - module.status = MagicMock() - module.odom = MagicMock() - module.tf = MagicMock() - module.video = MagicMock() - module.movecmd = MagicMock() - - # Start module - module.start() - - # Give time for processing - time.sleep(0.2) - - # Stop module - module.stop() - - # Check telemetry was published - self.assertGreater(len(published_telemetry), 0) - - # Telemetry should be JSON string - telem_msg = published_telemetry[0] - self.assertIsNotNone(telem_msg) - - # If it's a String message, check the data - if hasattr(telem_msg, "data"): - telem_dict = json.loads(telem_msg.data) - self.assertIn("timestamp", telem_dict) - - -class TestFlyToErrorHandling(unittest.TestCase): - """Test fly_to() error handling paths.""" - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_concurrency_lock(self, mock_get_data, mock_replay) -> None: - """flying_to_target=True rejects concurrent fly_to() calls.""" - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - conn.flying_to_target = True - - result = conn.fly_to(37.0, -122.0, 10.0) - self.assertIn("Already flying to target", result) - - @patch("dimos.utils.testing.TimedSensorReplay") - @patch("dimos.utils.data.get_data") - def test_error_when_not_connected(self, mock_get_data, mock_replay) -> None: - """connected=False returns error immediately.""" - mock_stream = MagicMock() - mock_stream.subscribe = lambda callback: None - mock_replay.return_value.stream.return_value = mock_stream - - conn = FakeMavlinkConnection("replay") - conn.connected = False - - result = conn.fly_to(37.0, -122.0, 10.0) - self.assertIn("Not connected", result) - - -class TestVisualServoingEdgeCases(unittest.TestCase): - """Test DroneVisualServoingController edge cases.""" - - def test_output_clamping(self) -> None: - """Large errors are clamped to max_velocity.""" - from dimos.robot.drone.drone_visual_servoing_controller import ( - DroneVisualServoingController, - ) - - # PID params: (kp, ki, kd, output_limits, integral_limit, deadband) - max_vel = 2.0 - controller = DroneVisualServoingController( - x_pid_params=(1.0, 0.0, 0.0, (-max_vel, max_vel), None, 0), - y_pid_params=(1.0, 0.0, 0.0, (-max_vel, max_vel), None, 0), - ) - - # Large error should be clamped - vx, vy, _vz = controller.compute_velocity_control( - target_x=1000, target_y=1000, center_x=0, center_y=0, dt=0.1 - ) - self.assertLessEqual(abs(vx), max_vel) - self.assertLessEqual(abs(vy), max_vel) - - def test_deadband_prevents_integral_windup(self) -> None: - """Deadband prevents integral accumulation for small errors.""" - from dimos.robot.drone.drone_visual_servoing_controller import ( - DroneVisualServoingController, - ) - - deadband = 10 # pixels - controller = DroneVisualServoingController( - x_pid_params=(0.0, 1.0, 0.0, (-2.0, 2.0), None, deadband), # integral only - y_pid_params=(0.0, 1.0, 0.0, (-2.0, 2.0), None, deadband), - ) - - # With error inside deadband, integral should stay at zero - for _ in range(10): - controller.compute_velocity_control( - target_x=5, target_y=5, center_x=0, center_y=0, dt=0.1 - ) - - # Integral should be zero since error < deadband - self.assertEqual(controller.x_pid.integral, 0.0) - self.assertEqual(controller.y_pid.integral, 0.0) - - def test_reset_clears_integral(self) -> None: - """reset() clears accumulated integral to prevent windup.""" - from dimos.robot.drone.drone_visual_servoing_controller import ( - DroneVisualServoingController, - ) - - controller = DroneVisualServoingController( - x_pid_params=(0.0, 1.0, 0.0, (-10.0, 10.0), None, 0), # Only integral - y_pid_params=(0.0, 1.0, 0.0, (-10.0, 10.0), None, 0), - ) - - # Accumulate integral by calling multiple times with error - for _ in range(10): - controller.compute_velocity_control( - target_x=100, target_y=100, center_x=0, center_y=0, dt=0.1 - ) - - # Integral should be non-zero - self.assertNotEqual(controller.x_pid.integral, 0.0) - - # Reset should clear it - controller.reset() - self.assertEqual(controller.x_pid.integral, 0.0) - self.assertEqual(controller.y_pid.integral, 0.0) - - -class TestVisualServoingVelocity(unittest.TestCase): - """Test visual servoing velocity calculations.""" - - def test_velocity_from_bbox_center_error(self) -> None: - """Bbox center offset produces proportional velocity command.""" - from dimos.robot.drone.drone_visual_servoing_controller import ( - DroneVisualServoingController, - ) - - controller = DroneVisualServoingController( - x_pid_params=(0.01, 0.0, 0.0, (-2.0, 2.0), None, 0), - y_pid_params=(0.01, 0.0, 0.0, (-2.0, 2.0), None, 0), - ) - - # Image center at (320, 180), bbox center at (400, 180) = 80px right - frame_center = (320, 180) - bbox_center = (400, 180) - - vx, vy, _vz = controller.compute_velocity_control( - target_x=bbox_center[0], - target_y=bbox_center[1], - center_x=frame_center[0], - center_y=frame_center[1], - dt=0.1, - ) - - # Object to the right -> drone should strafe right (positive vy) - self.assertGreater(vy, 0) - # No vertical offset -> vx should be ~0 - self.assertAlmostEqual(vx, 0, places=1) - - -if __name__ == "__main__": - unittest.main() diff --git a/dimos/robot/foxglove_bridge.py b/dimos/robot/foxglove_bridge.py deleted file mode 100644 index 529a14c838..0000000000 --- a/dimos/robot/foxglove_bridge.py +++ /dev/null @@ -1,120 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import asyncio -import logging -import threading -from typing import TYPE_CHECKING, Any - -from dimos_lcm.foxglove_bridge import ( - FoxgloveBridge as LCMFoxgloveBridge, -) - -from dimos.core import DimosCluster, Module, rpc -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.core.global_config import GlobalConfig - -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) - -logger = setup_logger() - - -class FoxgloveBridge(Module): - _thread: threading.Thread - _loop: asyncio.AbstractEventLoop - _global_config: "GlobalConfig | None" = None - - def __init__( - self, - *args: Any, - shm_channels: list[str] | None = None, - jpeg_shm_channels: list[str] | None = None, - global_config: "GlobalConfig | None" = None, - **kwargs: Any, - ) -> None: - super().__init__(*args, **kwargs) - self.shm_channels = shm_channels or [] - self.jpeg_shm_channels = jpeg_shm_channels or [] - self._global_config = global_config - - @rpc - def start(self) -> None: - super().start() - - # Skip if Rerun is the selected viewer backend - if self._global_config and self._global_config.viewer_backend.startswith("rerun"): - logger.info( - "Foxglove bridge skipped", viewer_backend=self._global_config.viewer_backend - ) - return - - def run_bridge() -> None: - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) - try: - for logger in ["lcm_foxglove_bridge", "FoxgloveServer"]: - logger = logging.getLogger(logger) # type: ignore[assignment] - logger.setLevel(logging.ERROR) # type: ignore[attr-defined] - for handler in logger.handlers: # type: ignore[attr-defined] - handler.setLevel(logging.ERROR) - - bridge = LCMFoxgloveBridge( - host="0.0.0.0", - port=8765, - debug=False, - num_threads=4, - shm_channels=self.shm_channels, - jpeg_shm_channels=self.jpeg_shm_channels, - ) - self._loop.run_until_complete(bridge.run()) - except Exception as e: - print(f"Foxglove bridge error: {e}") - - self._thread = threading.Thread(target=run_bridge, daemon=True) - self._thread.start() - - @rpc - def stop(self) -> None: - if self._loop and self._loop.is_running(): - self._loop.call_soon_threadsafe(self._loop.stop) - self._thread.join(timeout=2) - - super().stop() - - -def deploy( - dimos: DimosCluster, - shm_channels: list[str] | None = None, -) -> FoxgloveBridge: - if shm_channels is None: - shm_channels = [ - "/image#sensor_msgs.Image", - "/lidar#sensor_msgs.PointCloud2", - "/map#sensor_msgs.PointCloud2", - ] - foxglove_bridge = dimos.deploy( # type: ignore[attr-defined] - FoxgloveBridge, - shm_channels=shm_channels, - ) - foxglove_bridge.start() - return foxglove_bridge # type: ignore[no-any-return] - - -foxglove_bridge = FoxgloveBridge.blueprint - - -__all__ = ["FoxgloveBridge", "deploy", "foxglove_bridge"] diff --git a/dimos/robot/get_all_blueprints.py b/dimos/robot/get_all_blueprints.py deleted file mode 100644 index 8658e4f4ec..0000000000 --- a/dimos/robot/get_all_blueprints.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import Blueprint -from dimos.robot.all_blueprints import all_blueprints, all_modules - - -def get_blueprint_by_name(name: str) -> Blueprint: - if name not in all_blueprints: - raise ValueError(f"Unknown blueprint set name: {name}") - module_path, attr = all_blueprints[name].split(":") - module = __import__(module_path, fromlist=[attr]) - return getattr(module, attr) # type: ignore[no-any-return] - - -def get_module_by_name(name: str) -> Blueprint: - if name not in all_modules: - raise ValueError(f"Unknown module name: {name}") - python_module = __import__(all_modules[name], fromlist=[name]) - return getattr(python_module, name)() # type: ignore[no-any-return] diff --git a/dimos/robot/manipulators/__init__.py b/dimos/robot/manipulators/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/manipulators/piper/__init__.py b/dimos/robot/manipulators/piper/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/manipulators/piper/blueprints.py b/dimos/robot/manipulators/piper/blueprints.py deleted file mode 100644 index 68e02fc994..0000000000 --- a/dimos/robot/manipulators/piper/blueprints.py +++ /dev/null @@ -1,97 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Keyboard teleop blueprint for the Piper arm. - -Launches the ControlCoordinator (mock adapter + CartesianIK), the -ManipulationModule (Drake/Meshcat visualization), and a pygame keyboard -teleop UI — all wired together via autoconnect. - -Usage: - dimos run keyboard-teleop-piper -""" - -from dimos.control.components import HardwareComponent, HardwareType, make_joints -from dimos.control.coordinator import TaskConfig, control_coordinator -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.manipulation.manipulation_module import manipulation_module -from dimos.manipulation.planning.spec import RobotModelConfig -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.msgs.sensor_msgs import JointState -from dimos.teleop.keyboard.keyboard_teleop_module import keyboard_teleop_module -from dimos.utils.data import LfsPath, get_data - -_PIPER_MODEL_PATH = LfsPath("piper_description/mujoco_model/piper_no_gripper_description.xml") -_PIPER_DATA = get_data("piper_description") - -# Piper 6-DOF mock sim + keyboard teleop + Drake visualization -keyboard_teleop_piper = autoconnect( - keyboard_teleop_module(model_path=_PIPER_MODEL_PATH, ee_joint_id=6), - control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="cartesian_ik_arm", - type="cartesian_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_PIPER_MODEL_PATH, - ee_joint_id=6, - ), - ], - ), - manipulation_module( - robots=[ - RobotModelConfig( - name="arm", - urdf_path=_PIPER_DATA / "urdf" / "piper_description.xacro", - base_pose=PoseStamped( - position=Vector3(x=0.0, y=0.0, z=0.0), - orientation=Quaternion(0.0, 0.0, 0.0, 1.0), - ), - joint_names=["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"], - end_effector_link="gripper_base", - base_link="base_link", - package_paths={ - "piper_description": _PIPER_DATA, - "piper_gazebo": _PIPER_DATA, # xacro refs $(find piper_gazebo); unused by Drake - }, - joint_name_mapping={f"arm_joint{i}": f"joint{i}" for i in range(1, 7)}, - auto_convert_meshes=True, - home_joints=[0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - ), - ], - enable_viz=True, - ), -).transports( - { - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -__all__ = ["keyboard_teleop_piper"] diff --git a/dimos/robot/manipulators/xarm/__init__.py b/dimos/robot/manipulators/xarm/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/manipulators/xarm/blueprints.py b/dimos/robot/manipulators/xarm/blueprints.py deleted file mode 100644 index 9043e71e3b..0000000000 --- a/dimos/robot/manipulators/xarm/blueprints.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Keyboard teleop blueprints for XArm6 and XArm7. - -Launches the ControlCoordinator (mock adapter + CartesianIK), the -ManipulationModule (Drake/Meshcat visualization), and a pygame keyboard -teleop UI — all wired together via autoconnect. - -Usage: - dimos run keyboard-teleop-xarm6 - dimos run keyboard-teleop-xarm7 -""" - -from dimos.control.components import HardwareComponent, HardwareType, make_joints -from dimos.control.coordinator import TaskConfig, control_coordinator -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.manipulation.manipulation_blueprints import ( - _make_xarm6_config, - _make_xarm7_config, -) -from dimos.manipulation.manipulation_module import manipulation_module -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import JointState -from dimos.teleop.keyboard.keyboard_teleop_module import keyboard_teleop_module -from dimos.utils.data import LfsPath - -_XARM6_MODEL_PATH = LfsPath("xarm_description/urdf/xarm6/xarm6.urdf") -_XARM7_MODEL_PATH = LfsPath("xarm_description/urdf/xarm7/xarm7.urdf") - -# XArm6 mock sim + keyboard teleop + Drake visualization -keyboard_teleop_xarm6 = autoconnect( - keyboard_teleop_module(model_path=_XARM6_MODEL_PATH, ee_joint_id=6), - control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="cartesian_ik_arm", - type="cartesian_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(6)], - priority=10, - model_path=_XARM6_MODEL_PATH, - ee_joint_id=6, - ), - ], - ), - manipulation_module( - robots=[_make_xarm6_config(name="arm", joint_prefix="arm_", add_gripper=False)], - enable_viz=True, - ), -).transports( - { - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -# XArm7 mock sim + keyboard teleop + Drake visualization -keyboard_teleop_xarm7 = autoconnect( - keyboard_teleop_module(model_path=_XARM7_MODEL_PATH, ee_joint_id=7), - control_coordinator( - tick_rate=100.0, - publish_joint_state=True, - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 7), - adapter_type="mock", - ), - ], - tasks=[ - TaskConfig( - name="cartesian_ik_arm", - type="cartesian_ik", - joint_names=[f"arm_joint{i + 1}" for i in range(7)], - priority=10, - model_path=_XARM7_MODEL_PATH, - ee_joint_id=7, - ), - ], - ), - manipulation_module( - robots=[_make_xarm7_config(name="arm", joint_prefix="arm_", add_gripper=False)], - enable_viz=True, - ), -).transports( - { - ("cartesian_command", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - -__all__ = ["keyboard_teleop_xarm6", "keyboard_teleop_xarm7"] diff --git a/dimos/robot/position_stream.py b/dimos/robot/position_stream.py deleted file mode 100644 index 77a86bff4c..0000000000 --- a/dimos/robot/position_stream.py +++ /dev/null @@ -1,161 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Position stream provider for ROS-based robots. - -This module creates a reactive stream of position updates from ROS odometry or pose topics. -""" - -import logging -import time - -from geometry_msgs.msg import PoseStamped # type: ignore[attr-defined] -from nav_msgs.msg import Odometry # type: ignore[attr-defined] -from rclpy.node import Node -from reactivex import Observable, Subject, operators as ops - -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.INFO) - - -class PositionStreamProvider: - """ - A provider for streaming position updates from ROS. - - This class creates an Observable stream of position updates by subscribing - to ROS odometry or pose topics. - """ - - def __init__( - self, - ros_node: Node, - odometry_topic: str = "/odom", - pose_topic: str | None = None, - use_odometry: bool = True, - ) -> None: - """ - Initialize the position stream provider. - - Args: - ros_node: ROS node to use for subscriptions - odometry_topic: Name of the odometry topic (if use_odometry is True) - pose_topic: Name of the pose topic (if use_odometry is False) - use_odometry: Whether to use odometry (True) or pose (False) for position - """ - self.ros_node = ros_node - self.odometry_topic = odometry_topic - self.pose_topic = pose_topic - self.use_odometry = use_odometry - - self._subject = Subject() # type: ignore[var-annotated] - - self.last_position = None - self.last_update_time = None - - self._create_subscription() # type: ignore[no-untyped-call] - - logger.info( - f"PositionStreamProvider initialized with " - f"{'odometry topic' if use_odometry else 'pose topic'}: " - f"{odometry_topic if use_odometry else pose_topic}" - ) - - def _create_subscription(self): # type: ignore[no-untyped-def] - """Create the appropriate ROS subscription based on configuration.""" - if self.use_odometry: - self.subscription = self.ros_node.create_subscription( - Odometry, self.odometry_topic, self._odometry_callback, 10 - ) - logger.info(f"Subscribed to odometry topic: {self.odometry_topic}") - else: - if not self.pose_topic: - raise ValueError("Pose topic must be specified when use_odometry is False") - - self.subscription = self.ros_node.create_subscription( - PoseStamped, self.pose_topic, self._pose_callback, 10 - ) - logger.info(f"Subscribed to pose topic: {self.pose_topic}") - - def _odometry_callback(self, msg: Odometry) -> None: - """ - Process odometry messages and extract position. - - Args: - msg: Odometry message from ROS - """ - x = msg.pose.pose.position.x - y = msg.pose.pose.position.y - - self._update_position(x, y) - - def _pose_callback(self, msg: PoseStamped) -> None: - """ - Process pose messages and extract position. - - Args: - msg: PoseStamped message from ROS - """ - x = msg.pose.position.x - y = msg.pose.position.y - - self._update_position(x, y) - - def _update_position(self, x: float, y: float) -> None: - """ - Update the current position and emit to subscribers. - - Args: - x: X coordinate - y: Y coordinate - """ - current_time = time.time() - position = (x, y) - - if self.last_update_time: - update_rate = 1.0 / (current_time - self.last_update_time) - logger.debug(f"Position update rate: {update_rate:.1f} Hz") - - self.last_position = position # type: ignore[assignment] - self.last_update_time = current_time # type: ignore[assignment] - - self._subject.on_next(position) - logger.debug(f"Position updated: ({x:.2f}, {y:.2f})") - - def get_position_stream(self) -> Observable: # type: ignore[type-arg] - """ - Get an Observable stream of position updates. - - Returns: - Observable that emits (x, y) tuples - """ - return self._subject.pipe( - ops.share() # Share the stream among multiple subscribers - ) - - def get_current_position(self) -> tuple[float, float] | None: - """ - Get the most recent position. - - Returns: - Tuple of (x, y) coordinates, or None if no position has been received - """ - return self.last_position - - def cleanup(self) -> None: - """Clean up resources.""" - if hasattr(self, "subscription") and self.subscription: - self.ros_node.destroy_subscription(self.subscription) - logger.info("Position subscription destroyed") diff --git a/dimos/robot/robot.py b/dimos/robot/robot.py deleted file mode 100644 index b2b6feaf6d..0000000000 --- a/dimos/robot/robot.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Minimal robot interface for DIMOS robots.""" - -from abc import ABC, abstractmethod - -from dimos.types.robot_capabilities import RobotCapability - - -# TODO: Delete -class Robot(ABC): - """Minimal abstract base class for all DIMOS robots. - - This class provides the essential interface that all robot implementations - can share, with no required methods - just common properties and helpers. - """ - - def __init__(self) -> None: - """Initialize the robot with basic properties.""" - self.capabilities: list[RobotCapability] = [] - self.skill_library = None - - def has_capability(self, capability: RobotCapability) -> bool: - """Check if the robot has a specific capability. - - Args: - capability: The capability to check for - - Returns: - bool: True if the robot has the capability - """ - return capability in self.capabilities - - def get_skills(self): # type: ignore[no-untyped-def] - """Get the robot's skill library. - - Returns: - The robot's skill library for managing skills - """ - return self.skill_library - - @abstractmethod - def cleanup(self) -> None: - """Clean up robot resources. - - Override this method to provide cleanup logic. - """ - ... diff --git a/dimos/robot/ros_command_queue.py b/dimos/robot/ros_command_queue.py deleted file mode 100644 index 86115d7780..0000000000 --- a/dimos/robot/ros_command_queue.py +++ /dev/null @@ -1,473 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Queue-based command management system for robot commands. - -This module provides a unified approach to queueing and processing all robot commands, -including WebRTC requests and action client commands. -Commands are processed sequentially and only when the robot is in IDLE state. -""" - -from collections.abc import Callable -from enum import Enum, auto -from queue import Empty, PriorityQueue -import threading -import time -from typing import Any, NamedTuple -import uuid - -from dimos.utils.logging_config import setup_logger - -# Initialize logger for the ros command queue module -logger = setup_logger() - - -class CommandType(Enum): - """Types of commands that can be queued""" - - WEBRTC = auto() # WebRTC API requests - ACTION = auto() # Any action client or function call - - -class WebRTCRequest(NamedTuple): - """Class to represent a WebRTC request in the queue""" - - id: str # Unique ID for tracking - api_id: int # API ID for the command - topic: str # Topic to publish to - parameter: str # Optional parameter string - priority: int # Priority level - timeout: float # How long to wait for this request to complete - - -class ROSCommand(NamedTuple): - """Class to represent a command in the queue""" - - id: str # Unique ID for tracking - cmd_type: CommandType # Type of command - execute_func: Callable # type: ignore[type-arg] # Function to execute the command - params: dict[str, Any] # Parameters for the command (for debugging/logging) - priority: int # Priority level (lower is higher priority) - timeout: float # How long to wait for this command to complete - - -class ROSCommandQueue: - """ - Manages a queue of commands for the robot. - - Commands are executed sequentially, with only one command being processed at a time. - Commands are only executed when the robot is in the IDLE state. - """ - - def __init__( - self, - webrtc_func: Callable, # type: ignore[type-arg] - is_ready_func: Callable[[], bool] | None = None, - is_busy_func: Callable[[], bool] | None = None, - debug: bool = True, - ) -> None: - """ - Initialize the ROSCommandQueue. - - Args: - webrtc_func: Function to send WebRTC requests - is_ready_func: Function to check if the robot is ready for a command - is_busy_func: Function to check if the robot is busy - debug: Whether to enable debug logging - """ - self._webrtc_func = webrtc_func - self._is_ready_func = is_ready_func or (lambda: True) - self._is_busy_func = is_busy_func - self._debug = debug - - # Queue of commands to process - self._queue = PriorityQueue() # type: ignore[var-annotated] - self._current_command = None - self._last_command_time = 0 - - # Last known robot state - self._last_ready_state = None - self._last_busy_state = None - self._stuck_in_busy_since = None - - # Command execution status - self._should_stop = False - self._queue_thread = None - - # Stats - self._command_count = 0 - self._success_count = 0 - self._failure_count = 0 - self._command_history = [] # type: ignore[var-annotated] - - self._max_queue_wait_time = ( - 30.0 # Maximum time to wait for robot to be ready before forcing - ) - - logger.info("ROSCommandQueue initialized") - - def start(self) -> None: - """Start the queue processing thread""" - if self._queue_thread is not None and self._queue_thread.is_alive(): - logger.warning("Queue processing thread already running") - return - - self._should_stop = False - self._queue_thread = threading.Thread(target=self._process_queue, daemon=True) # type: ignore[assignment] - self._queue_thread.start() # type: ignore[attr-defined] - logger.info("Queue processing thread started") - - def stop(self, timeout: float = 2.0) -> None: - """ - Stop the queue processing thread - - Args: - timeout: Maximum time to wait for the thread to stop - """ - if self._queue_thread is None or not self._queue_thread.is_alive(): - logger.warning("Queue processing thread not running") - return - - self._should_stop = True - try: - self._queue_thread.join(timeout=timeout) - if self._queue_thread.is_alive(): - logger.warning(f"Queue processing thread did not stop within {timeout}s") - else: - logger.info("Queue processing thread stopped") - except Exception as e: - logger.error(f"Error stopping queue processing thread: {e}") - - def queue_webrtc_request( - self, - api_id: int, - topic: str | None = None, - parameter: str = "", - request_id: str | None = None, - data: dict[str, Any] | None = None, - priority: int = 0, - timeout: float = 30.0, - ) -> str: - """ - Queue a WebRTC request - - Args: - api_id: API ID for the command - topic: Topic to publish to - parameter: Optional parameter string - request_id: Unique ID for the request (will be generated if not provided) - data: Data to include in the request - priority: Priority level (lower is higher priority) - timeout: Maximum time to wait for the command to complete - - Returns: - str: Unique ID for the request - """ - request_id = request_id or str(uuid.uuid4()) - - # Create a function that will execute this WebRTC request - def execute_webrtc() -> bool: - try: - logger.info(f"Executing WebRTC request: {api_id} (ID: {request_id})") - if self._debug: - logger.debug(f"[WebRTC Queue] SENDING request: API ID {api_id}") - - result = self._webrtc_func( - api_id=api_id, - topic=topic, - parameter=parameter, - request_id=request_id, - data=data, - ) - if not result: - logger.warning(f"WebRTC request failed: {api_id} (ID: {request_id})") - if self._debug: - logger.debug(f"[WebRTC Queue] Request API ID {api_id} FAILED to send") - return False - - if self._debug: - logger.debug(f"[WebRTC Queue] Request API ID {api_id} sent SUCCESSFULLY") - - # Allow time for the robot to process the command - start_time = time.time() - stabilization_delay = 0.5 # Half-second delay for stabilization - time.sleep(stabilization_delay) - - # Wait for the robot to complete the command (timeout check) - while self._is_busy_func() and (time.time() - start_time) < timeout: # type: ignore[misc] - if ( - self._debug and (time.time() - start_time) % 5 < 0.1 - ): # Print every ~5 seconds - logger.debug( - f"[WebRTC Queue] Still waiting on API ID {api_id} - elapsed: {time.time() - start_time:.1f}s" - ) - time.sleep(0.1) - - # Check if we timed out - if self._is_busy_func() and (time.time() - start_time) >= timeout: # type: ignore[misc] - logger.warning(f"WebRTC request timed out: {api_id} (ID: {request_id})") - return False - - wait_time = time.time() - start_time - if self._debug: - logger.debug( - f"[WebRTC Queue] Request API ID {api_id} completed after {wait_time:.1f}s" - ) - - logger.info(f"WebRTC request completed: {api_id} (ID: {request_id})") - return True - except Exception as e: - logger.error(f"Error executing WebRTC request: {e}") - if self._debug: - logger.debug(f"[WebRTC Queue] ERROR processing request: {e}") - return False - - # Create the command and queue it - command = ROSCommand( - id=request_id, - cmd_type=CommandType.WEBRTC, - execute_func=execute_webrtc, - params={"api_id": api_id, "topic": topic, "request_id": request_id}, - priority=priority, - timeout=timeout, - ) - - # Queue the command - self._queue.put((priority, self._command_count, command)) - self._command_count += 1 - if self._debug: - logger.debug( - f"[WebRTC Queue] Added request ID {request_id} for API ID {api_id} - Queue size now: {self.queue_size}" - ) - logger.info(f"Queued WebRTC request: {api_id} (ID: {request_id}, Priority: {priority})") - - return request_id - - def queue_action_client_request( # type: ignore[no-untyped-def] - self, - action_name: str, - execute_func: Callable, # type: ignore[type-arg] - priority: int = 0, - timeout: float = 30.0, - **kwargs, - ) -> str: - """ - Queue any action client request or function - - Args: - action_name: Name of the action for logging/tracking - execute_func: Function to execute the command - priority: Priority level (lower is higher priority) - timeout: Maximum time to wait for the command to complete - **kwargs: Additional parameters to pass to the execute function - - Returns: - str: Unique ID for the request - """ - request_id = str(uuid.uuid4()) - - # Create the command - command = ROSCommand( - id=request_id, - cmd_type=CommandType.ACTION, - execute_func=execute_func, - params={"action_name": action_name, **kwargs}, - priority=priority, - timeout=timeout, - ) - - # Queue the command - self._queue.put((priority, self._command_count, command)) - self._command_count += 1 - - action_params = ", ".join([f"{k}={v}" for k, v in kwargs.items()]) - logger.info( - f"Queued action request: {action_name} (ID: {request_id}, Priority: {priority}, Params: {action_params})" - ) - - return request_id - - def _process_queue(self) -> None: - """Process commands in the queue""" - logger.info("Starting queue processing") - logger.info("[WebRTC Queue] Processing thread started") - - while not self._should_stop: - # Print queue status - self._print_queue_status() - - # Check if we're ready to process a command - if not self._queue.empty() and self._current_command is None: - current_time = time.time() - is_ready = self._is_ready_func() - is_busy = self._is_busy_func() if self._is_busy_func else False - - if self._debug: - logger.debug( - f"[WebRTC Queue] Status: {self.queue_size} requests waiting | Robot ready: {is_ready} | Robot busy: {is_busy}" - ) - - # Track robot state changes - if is_ready != self._last_ready_state: - logger.debug( - f"Robot ready state changed: {self._last_ready_state} -> {is_ready}" - ) - self._last_ready_state = is_ready # type: ignore[assignment] - - if is_busy != self._last_busy_state: - logger.debug(f"Robot busy state changed: {self._last_busy_state} -> {is_busy}") - self._last_busy_state = is_busy # type: ignore[assignment] - - # If the robot has transitioned to busy, record the time - if is_busy: - self._stuck_in_busy_since = current_time # type: ignore[assignment] - else: - self._stuck_in_busy_since = None - - # Check if we've been waiting too long for the robot to be ready - force_processing = False - if ( - not is_ready - and is_busy - and self._stuck_in_busy_since is not None - and current_time - self._stuck_in_busy_since > self._max_queue_wait_time - ): - logger.warning( - f"Robot has been busy for {current_time - self._stuck_in_busy_since:.1f}s, " - f"forcing queue to continue" - ) - force_processing = True - - # Process the next command if ready or forcing - if is_ready or force_processing: - if self._debug and is_ready: - logger.debug("[WebRTC Queue] Robot is READY for next command") - - try: - # Get the next command - _, _, command = self._queue.get(block=False) - self._current_command = command - self._last_command_time = current_time # type: ignore[assignment] - - # Log the command - cmd_info = f"ID: {command.id}, Type: {command.cmd_type.name}" - if command.cmd_type == CommandType.WEBRTC: - api_id = command.params.get("api_id") - cmd_info += f", API: {api_id}" - if self._debug: - logger.debug(f"[WebRTC Queue] DEQUEUED request: API ID {api_id}") - elif command.cmd_type == CommandType.ACTION: - action_name = command.params.get("action_name") - cmd_info += f", Action: {action_name}" - if self._debug: - logger.debug(f"[WebRTC Queue] DEQUEUED action: {action_name}") - - forcing_str = " (FORCED)" if force_processing else "" - logger.info(f"Processing command{forcing_str}: {cmd_info}") - - # Execute the command - try: - # Where command execution occurs - success = command.execute_func() - - if success: - self._success_count += 1 - logger.info(f"Command succeeded: {cmd_info}") - if self._debug: - logger.debug( - f"[WebRTC Queue] Command {command.id} marked as COMPLETED" - ) - else: - self._failure_count += 1 - logger.warning(f"Command failed: {cmd_info}") - if self._debug: - logger.debug(f"[WebRTC Queue] Command {command.id} FAILED") - - # Record command history - self._command_history.append( - { - "id": command.id, - "type": command.cmd_type.name, - "params": command.params, - "success": success, - "time": time.time() - self._last_command_time, - } - ) - - except Exception as e: - self._failure_count += 1 - logger.error(f"Error executing command: {e}") - if self._debug: - logger.debug(f"[WebRTC Queue] ERROR executing command: {e}") - - # Mark the command as complete - self._current_command = None - if self._debug: - logger.debug( - "[WebRTC Queue] Adding 0.5s stabilization delay before next command" - ) - time.sleep(0.5) - - except Empty: - pass - - # Sleep to avoid busy-waiting - time.sleep(0.1) - - logger.info("Queue processing stopped") - - def _print_queue_status(self) -> None: - """Print the current queue status""" - current_time = time.time() - - # Only print once per second to avoid spamming the log - if current_time - self._last_command_time < 1.0 and self._current_command is None: - return - - is_ready = self._is_ready_func() - self._is_busy_func() if self._is_busy_func else False - queue_size = self.queue_size - - # Get information about the current command - current_command_info = "None" - if self._current_command is not None: - current_command_info = f"{self._current_command.cmd_type.name}" - if self._current_command.cmd_type == CommandType.WEBRTC: - api_id = self._current_command.params.get("api_id") - current_command_info += f" (API: {api_id})" - elif self._current_command.cmd_type == CommandType.ACTION: - action_name = self._current_command.params.get("action_name") - current_command_info += f" (Action: {action_name})" - - # Print the status - status = ( - f"Queue: {queue_size} items | " - f"Robot: {'READY' if is_ready else 'BUSY'} | " - f"Current: {current_command_info} | " - f"Stats: {self._success_count} OK, {self._failure_count} FAIL" - ) - - logger.debug(status) - self._last_command_time = current_time # type: ignore[assignment] - - @property - def queue_size(self) -> int: - """Get the number of commands in the queue""" - return self._queue.qsize() - - @property - def current_command(self) -> ROSCommand | None: - """Get the current command being processed""" - return self._current_command diff --git a/dimos/robot/test_all_blueprints.py b/dimos/robot/test_all_blueprints.py deleted file mode 100644 index 16f657393b..0000000000 --- a/dimos/robot/test_all_blueprints.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pytest - -from dimos.core.blueprints import Blueprint -from dimos.robot.all_blueprints import all_blueprints -from dimos.robot.get_all_blueprints import get_blueprint_by_name - -# Optional dependencies that are allowed to be missing -OPTIONAL_DEPENDENCIES = {"pyrealsense2", "pyzed", "geometry_msgs", "turbojpeg"} -OPTIONAL_ERROR_SUBSTRINGS = { - "Unable to locate turbojpeg library automatically", -} - - -@pytest.mark.integration -@pytest.mark.parametrize("blueprint_name", all_blueprints.keys()) -def test_all_blueprints_are_valid(blueprint_name: str) -> None: - """Test that all blueprints in all_blueprints are valid Blueprint instances.""" - try: - blueprint = get_blueprint_by_name(blueprint_name) - except ModuleNotFoundError as e: - if e.name in OPTIONAL_DEPENDENCIES: - pytest.skip(f"Skipping due to missing optional dependency: {e.name}") - raise - except Exception as e: - message = str(e) - if any(substring in message for substring in OPTIONAL_ERROR_SUBSTRINGS): - pytest.skip(f"Skipping due to missing optional dependency: {message}") - raise - assert isinstance(blueprint, Blueprint), ( - f"Blueprint '{blueprint_name}' is not a Blueprint, got {type(blueprint)}" - ) diff --git a/dimos/robot/test_all_blueprints_generation.py b/dimos/robot/test_all_blueprints_generation.py deleted file mode 100644 index e7ba79a404..0000000000 --- a/dimos/robot/test_all_blueprints_generation.py +++ /dev/null @@ -1,214 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import ast -from collections.abc import Generator -import difflib -import os -from pathlib import Path -import subprocess - -import pytest - -from dimos.constants import DIMOS_PROJECT_ROOT - -IGNORED_FILES: set[str] = { - "dimos/robot/all_blueprints.py", - "dimos/robot/get_all_blueprints.py", - "dimos/robot/test_all_blueprints.py", - "dimos/robot/test_all_blueprints_generation.py", - "dimos/core/blueprints.py", - "dimos/core/test_blueprints.py", -} -BLUEPRINT_METHODS = {"transports", "global_config", "remappings", "requirements"} - - -def test_all_blueprints_is_current() -> None: - root = DIMOS_PROJECT_ROOT / "dimos" - all_blueprints, all_modules = _scan_for_blueprints(root) - generated_content = _generate_all_blueprints_content(all_blueprints, all_modules) - - file_path = root / "robot" / "all_blueprints.py" - - if "CI" in os.environ: - if not file_path.exists(): - pytest.fail(f"all_blueprints.py does not exist at {file_path}") - - current_content = file_path.read_text() - if current_content != generated_content: - diff = difflib.unified_diff( - current_content.splitlines(keepends=True), - generated_content.splitlines(keepends=True), - fromfile="all_blueprints.py (current)", - tofile="all_blueprints.py (generated)", - ) - diff_str = "".join(diff) - pytest.fail( - f"all_blueprints.py is out of date. Run " - f"`pytest dimos/robot/test_all_blueprints_generation.py` locally to update.\n\n" - f"Diff:\n{diff_str}" - ) - else: - file_path.write_text(generated_content) - - if _check_for_uncommitted_changes(file_path): - pytest.fail( - "all_blueprints.py was updated and has uncommitted changes. " - "Please commit the changes." - ) - - -def _scan_for_blueprints(root: Path) -> tuple[dict[str, str], dict[str, str]]: - all_blueprints: dict[str, str] = {} - all_modules: dict[str, str] = {} - - for file_path in sorted(_get_all_python_files(root)): - module_name = _path_to_module_name(file_path, root) - blueprint_vars, module_vars = _find_blueprints_in_file(file_path) - - for var_name in blueprint_vars: - full_path = f"{module_name}:{var_name}" - cli_name = var_name.replace("_", "-") - all_blueprints[cli_name] = full_path - - for var_name in module_vars: - full_path = f"{module_name}:{var_name}" - all_modules[var_name] = module_name - - return all_blueprints, all_modules - - -def _generate_all_blueprints_content( - all_blueprints: dict[str, str], - all_modules: dict[str, str], -) -> str: - lines = [ - "# Copyright 2025-2026 Dimensional Inc.", - "#", - '# Licensed under the Apache License, Version 2.0 (the "License");', - "# you may not use this file except in compliance with the License.", - "# You may obtain a copy of the License at", - "#", - "# http://www.apache.org/licenses/LICENSE-2.0", - "#", - "# Unless required by applicable law or agreed to in writing, software", - '# distributed under the License is distributed on an "AS IS" BASIS,', - "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", - "# See the License for the specific language governing permissions and", - "# limitations under the License.", - "", - "# This file is auto-generated. Do not edit manually.", - "# Run `pytest dimos/robot/test_all_blueprints_generation.py` to regenerate.", - "", - "all_blueprints = {", - ] - - for name in sorted(all_blueprints.keys()): - lines.append(f' "{name}": "{all_blueprints[name]}",') - - lines.append("}\n\n") - lines.append("all_modules = {") - - for name in sorted(all_modules.keys()): - lines.append(f' "{name}": "{all_modules[name]}",') - - lines.append("}\n") - - return "\n".join(lines) - - -def _check_for_uncommitted_changes(file_path: Path) -> bool: - try: - result = subprocess.run( - ["git", "diff", "--quiet", str(file_path)], - capture_output=True, - cwd=file_path.parent, - ) - return result.returncode != 0 - except Exception: - return False - - -def _get_all_python_files(root: Path) -> Generator[Path, None, None]: - for path in root.rglob("*.py"): - rel_path = str(path.relative_to(root.parent)) - if "__pycache__" in str(path) or rel_path in IGNORED_FILES: - continue - yield path - - -def _path_to_module_name(path: Path, root: Path) -> str: - parts = list(path.relative_to(root.parent).parts) - parts[-1] = parts[-1].removesuffix(".py") - return ".".join(parts) - - -def _find_blueprints_in_file(file_path: Path) -> tuple[list[str], list[str]]: - blueprint_vars: list[str] = [] - module_vars: list[str] = [] - - try: - source = file_path.read_text(encoding="utf-8") - tree = ast.parse(source, filename=str(file_path)) - except Exception: - return [], [] - - # Only look at top-level statements (direct children of the Module node) - for node in tree.body: - if not isinstance(node, ast.Assign): - continue - - # Get the variable name(s) - for target in node.targets: - if not isinstance(target, ast.Name): - continue - var_name = target.id - - if var_name.startswith("_"): - continue - - # Check if it's a blueprint (ModuleBlueprintSet instance) - if _is_autoconnect_call(node.value) or _ends_with_blueprint_method(node.value): - blueprint_vars.append(var_name) - # Check if it's a module factory (SomeModule.blueprint) - elif _is_blueprint_factory(node.value): - module_vars.append(var_name) - - return blueprint_vars, module_vars - - -def _is_autoconnect_call(node: ast.expr) -> bool: - if isinstance(node, ast.Call): - func = node.func - # Direct call: autoconnect(...) - if isinstance(func, ast.Name) and func.id == "autoconnect": - return True - # Attribute call: module.autoconnect(...) - if isinstance(func, ast.Attribute) and func.attr == "autoconnect": - return True - return False - - -def _ends_with_blueprint_method(node: ast.expr) -> bool: - if isinstance(node, ast.Call): - func = node.func - if isinstance(func, ast.Attribute) and func.attr in BLUEPRINT_METHODS: - return True - return False - - -def _is_blueprint_factory(node: ast.expr) -> bool: - if isinstance(node, ast.Attribute): - return node.attr == "blueprint" - return False diff --git a/dimos/robot/unitree/__init__.py b/dimos/robot/unitree/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/unitree/b1/README.md b/dimos/robot/unitree/b1/README.md deleted file mode 100644 index 1443067a2a..0000000000 --- a/dimos/robot/unitree/b1/README.md +++ /dev/null @@ -1,219 +0,0 @@ -# Unitree B1 Dimensional Integration - -This module provides UDP-based control for the Unitree B1 quadruped robot with DimOS integration with ROS Twist cmd_vel interface. - -## Overview - -The system consists of two components: -1. **Server Side**: C++ UDP server running on the B1's internal computer -2. **Client Side**: Python control module running on external machine - -Key features: -- 50Hz continuous UDP streaming -- 100ms command timeout for automatic stop -- Standard Twist velocity interface -- Emergency stop (Space/Q keys) -- IDLE/STAND/WALK mode control -- Optional pygame joystick interface - -## Server Side Setup (B1 Internal Computer) - -### Prerequisites - -The B1 robot runs Ubuntu with the following requirements: -- Unitree Legged SDK v3.8.3 for B1 -- Boost (>= 1.71.0) -- CMake (>= 3.16.3) -- g++ (>= 9.4.0) - -### Step 1: Connect to B1 Robot - -1. **Connect to B1's WiFi Access Point**: - - SSID: `Unitree_B1_XXXXX` (where XXXXX is your robot's ID) - - Password: `00000000` (8 zeros) - -2. **SSH into the B1**: - ```bash - ssh unitree@192.168.12.1 - # Default password: 123 - ``` - -### Step 2: Build the UDP Server - -1. **Add joystick_server_udp.cpp to CMakeLists.txt**: - ```bash - # Edit the CMakeLists.txt in the unitree_legged_sdk_B1 directory - vim CMakeLists.txt - - # Add this line with the other add_executable statements: - add_executable(joystick_server example/joystick_server_udp.cpp) - target_link_libraries(joystick_server ${EXTRA_LIBS})``` - -2. **Build the server**: - ```bash - mkdir build - cd build - cmake ../ - make - ``` - -### Step 3: Run the UDP Server - -```bash -# Navigate to build directory -cd Unitree/sdk/unitree_legged_sdk_B1/build/ -./joystick_server - -# You should see: -# UDP Unitree B1 Joystick Control Server -# Communication level: HIGH-level -# Server port: 9090 -# WARNING: Make sure the robot is standing on the ground. -# Press Enter to continue... -``` - -The server will now listen for UDP packets on port 9090 and control the B1 robot. - -### Server Safety Features - -- **100ms timeout**: Robot stops if no packets received for 100ms -- **Packet validation**: Only accepts correctly formatted 19-byte packets -- **Mode restrictions**: Velocities only applied in WALK mode -- **Emergency stop**: Mode 0 (IDLE) stops all movement - -## Client Side Setup (External Machine) - -### Prerequisites - -- Python 3.10+ -- DimOS framework installed -- pygame (optional, for joystick control) - -### Step 1: Install Dependencies - -```bash -# Install Dimensional -pip install -e .[cpu,sim] -``` - -### Step 2: Connect to B1 Network - -1. **Connect your machine to B1's WiFi**: - - SSID: `Unitree_B1_XXXXX` - - Password: `00000000` - -2. **Verify connection**: - ```bash - ping 192.168.12.1 # Should get responses - ``` - -### Step 3: Run the Client - -#### With Joystick Control (Recommended for Testing) - -```bash -python -m dimos.robot.unitree.b1.unitree_b1 \ - --ip 192.168.12.1 \ - --port 9090 \ - --joystick -``` - -**Joystick Controls**: -- `0/1/2` - Switch between IDLE/STAND/WALK modes -- `WASD` - Move forward/backward, turn left/right (only in WALK mode) -- `JL` - Strafe left/right (only in WALK mode) -- `Space/Q` - Emergency stop (switches to IDLE) -- `ESC` - Quit pygame window -- `Ctrl+C` - Exit program - -#### Test Mode (No Robot Required) - -```bash -python -m dimos.robot.unitree.b1.unitree_b1 \ - --test \ - --joystick -``` - -This prints commands instead of sending UDP packets - useful for development. - -## Safety Features - -### Client Side -- **Command freshness tracking**: Stops sending if no new commands for 100ms -- **Emergency stop**: Q or Space immediately sets IDLE mode -- **Mode safety**: Movement only allowed in WALK mode -- **Graceful shutdown**: Sends stop commands on exit - -### Server Side -- **Packet timeout**: Robot stops if no packets for 100ms -- **Continuous monitoring**: Checks timeout before every control update -- **Safe defaults**: Starts in IDLE mode -- **Packet validation**: Rejects malformed packets - -## Architecture - -``` -External Machine (Client) B1 Robot (Server) -┌─────────────────────┐ ┌──────────────────┐ -│ Joystick Module │ │ │ -│ (pygame input) │ │ joystick_server │ -│ ↓ │ │ _udp.cpp │ -│ Twist msg │ │ │ -│ ↓ │ WiFi AP │ │ -│ B1ConnectionModule │◄─────────►│ UDP Port 9090 │ -│ (Twist → B1Command) │ 192.168. │ │ -│ ↓ │ 12.1 │ │ -│ UDP packets 50Hz │ │ Unitree SDK │ -└─────────────────────┘ └──────────────────┘ -``` - -## Setting up ROS Navigation stack with Unitree B1 - -### Setup external Wireless USB Adapter on onboard hardware -This is because the onboard hardware (mini PC, jetson, etc.) needs to connect to both the B1 wifi AP network to send cmd_vel messages over UDP, as well as the network running dimensional - - -Plug in wireless adapter -```bash -nmcli device status -nmcli device wifi list ifname *DEVICE_NAME* -# Connect to b1 network -nmcli device wifi connect "Unitree_B1-251" password "00000000" ifname *DEVICE_NAME* -# Verify connection -nmcli connection show --active -``` - -### *TODO: add more docs* - - -## Troubleshooting - -### Cannot connect to B1 -- Ensure WiFi connection to B1's AP -- Check IP: should be `192.168.12.1` -- Verify server is running: `ssh unitree@192.168.12.1` - -### Robot not responding -- Verify server shows "Client connected" message -- Check robot is in WALK mode (press '2') -- Ensure no timeout messages in server output - -### Timeout issues -- Check network latency: `ping 192.168.12.1` -- Ensure 50Hz sending rate is maintained -- Look for "Command timeout" messages - -### Emergency situations -- Press Space or Q for immediate stop -- Use Ctrl+C to exit cleanly -- Robot auto-stops after 100ms without commands - -## Development Notes - -- Packets are 19 bytes: 4 floats + uint16 + uint8 -- Coordinate system: B1 uses different conventions, hence negations in `b1_command.py` -- LCM topics: `/cmd_vel` for Twist, `/b1/mode` for Int32 mode changes - -## License - -Copyright 2025 Dimensional Inc. Licensed under Apache License 2.0. diff --git a/dimos/robot/unitree/b1/__init__.py b/dimos/robot/unitree/b1/__init__.py deleted file mode 100644 index db85984070..0000000000 --- a/dimos/robot/unitree/b1/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. - -"""Unitree B1 robot module.""" - -from .unitree_b1 import UnitreeB1 - -__all__ = ["UnitreeB1"] diff --git a/dimos/robot/unitree/b1/b1_command.py b/dimos/robot/unitree/b1/b1_command.py deleted file mode 100644 index 3fa57043d1..0000000000 --- a/dimos/robot/unitree/b1/b1_command.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Internal B1 command structure for UDP communication.""" - -import struct - -from pydantic import BaseModel, Field - - -class B1Command(BaseModel): - """Internal B1 robot command matching UDP packet structure. - - This is an internal type - external interfaces use standard Twist messages. - """ - - # Direct joystick values matching C++ NetworkJoystickCmd struct - lx: float = Field(default=0.0, ge=-1.0, le=1.0) # Turn velocity (left stick X) - ly: float = Field(default=0.0, ge=-1.0, le=1.0) # Forward/back velocity (left stick Y) - rx: float = Field(default=0.0, ge=-1.0, le=1.0) # Strafe velocity (right stick X) - ry: float = Field(default=0.0, ge=-1.0, le=1.0) # Pitch/height adjustment (right stick Y) - buttons: int = Field(default=0, ge=0, le=65535) # Button states (uint16) - mode: int = Field( - default=0, ge=0, le=255 - ) # Control mode (uint8): 0=idle, 1=stand, 2=walk, 6=recovery - - @classmethod - def from_twist(cls, twist, mode: int = 2): # type: ignore[no-untyped-def] - """Create B1Command from standard ROS Twist message. - - This is the key integration point for navigation and planning. - - Args: - twist: ROS Twist message with linear and angular velocities - mode: Robot mode (default is walk mode for navigation) - - Returns: - B1Command configured for the given Twist - """ - # Max velocities from ROS needed to clamp to joystick ranges properly - MAX_LINEAR_VEL = 1.0 # m/s - MAX_ANGULAR_VEL = 2.0 # rad/s - - if mode == 2: # WALK mode - velocity control - return cls( - # Scale and clamp to joystick range [-1, 1] - lx=max(-1.0, min(1.0, -twist.angular.z / MAX_ANGULAR_VEL)), - ly=max(-1.0, min(1.0, twist.linear.x / MAX_LINEAR_VEL)), - rx=max(-1.0, min(1.0, -twist.linear.y / MAX_LINEAR_VEL)), - ry=0.0, # No pitch control in walk mode - mode=mode, - ) - elif mode == 1: # STAND mode - body pose control - # Map Twist pose controls to B1 joystick axes - # Already in normalized units, just clamp to [-1, 1] - return cls( - lx=max(-1.0, min(1.0, -twist.angular.z)), # ROS yaw → B1 yaw - ly=max(-1.0, min(1.0, twist.linear.z)), # ROS height → B1 bodyHeight - rx=max(-1.0, min(1.0, -twist.angular.x)), # ROS roll → B1 roll - ry=max(-1.0, min(1.0, twist.angular.y)), # ROS pitch → B1 pitch - mode=mode, - ) - else: - # IDLE mode - no controls - return cls(mode=mode) - - def to_bytes(self) -> bytes: - """Pack to 19-byte UDP packet matching C++ struct. - - Format: 4 floats + uint16 + uint8 = 19 bytes (little-endian) - """ - return struct.pack(" str: - """Human-readable representation.""" - mode_names = {0: "IDLE", 1: "STAND", 2: "WALK", 6: "RECOVERY"} - mode_str = mode_names.get(self.mode, f"MODE_{self.mode}") - - if self.lx != 0 or self.ly != 0 or self.rx != 0 or self.ry != 0: - return f"B1Cmd[{mode_str}] LX:{self.lx:+.2f} LY:{self.ly:+.2f} RX:{self.rx:+.2f} RY:{self.ry:+.2f}" - else: - return f"B1Cmd[{mode_str}] (idle)" diff --git a/dimos/robot/unitree/b1/connection.py b/dimos/robot/unitree/b1/connection.py deleted file mode 100644 index bae4bc0844..0000000000 --- a/dimos/robot/unitree/b1/connection.py +++ /dev/null @@ -1,422 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""B1 Connection Module that accepts standard Twist commands and converts to UDP packets.""" - -import logging -import socket -import threading -import time - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped, Twist, TwistStamped -from dimos.msgs.nav_msgs.Odometry import Odometry -from dimos.msgs.std_msgs import Int32 -from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.utils.logging_config import setup_logger - -from .b1_command import B1Command - -# Setup logger with DEBUG level for troubleshooting -logger = setup_logger(level=logging.DEBUG) - - -class RobotMode: - """Constants for B1 robot modes.""" - - IDLE = 0 - STAND = 1 - WALK = 2 - RECOVERY = 6 - - -class B1ConnectionModule(Module): - """UDP connection module for B1 robot with standard Twist interface. - - Accepts standard ROS Twist messages on /cmd_vel and mode changes on /b1/mode, - internally converts to B1Command format, and sends UDP packets at 50Hz. - """ - - # LCM ports (inter-module communication) - cmd_vel: In[TwistStamped] - mode_cmd: In[Int32] - odom_in: In[Odometry] - - odom_pose: Out[PoseStamped] - - # ROS In ports (receiving from ROS via ROSTransport) - ros_cmd_vel: In[TwistStamped] - ros_odom_in: In[Odometry] - ros_tf: In[TFMessage] - - def __init__( # type: ignore[no-untyped-def] - self, ip: str = "192.168.12.1", port: int = 9090, test_mode: bool = False, *args, **kwargs - ) -> None: - """Initialize B1 connection module. - - Args: - ip: Robot IP address - port: UDP port for joystick server - test_mode: If True, print commands instead of sending UDP - """ - Module.__init__(self, *args, **kwargs) - - self.ip = ip - self.port = port - self.test_mode = test_mode - self.current_mode = RobotMode.IDLE # Start in IDLE mode - self._current_cmd = B1Command(mode=RobotMode.IDLE) - self.cmd_lock = threading.Lock() # Thread lock for _current_cmd access - # Thread control - self.running = False - self.send_thread = None - self.socket = None - self.packet_count = 0 - self.last_command_time = time.time() - self.command_timeout = 0.2 # 200ms safety timeout - self.watchdog_thread = None - self.watchdog_running = False - self.timeout_active = False - - @rpc - def start(self) -> None: - """Start the connection and subscribe to command streams.""" - - super().start() - - # Setup UDP socket (unless in test mode) - if not self.test_mode: - self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # type: ignore[assignment] - logger.info(f"B1 Connection started - UDP to {self.ip}:{self.port} at 50Hz") - else: - logger.info(f"[TEST MODE] B1 Connection started - would send to {self.ip}:{self.port}") - - # Subscribe to input streams - if self.cmd_vel: - unsub = self.cmd_vel.subscribe(self.handle_twist_stamped) - self._disposables.add(Disposable(unsub)) - if self.mode_cmd: - unsub = self.mode_cmd.subscribe(self.handle_mode) - self._disposables.add(Disposable(unsub)) - if self.odom_in: - unsub = self.odom_in.subscribe(self._publish_odom_pose) - self._disposables.add(Disposable(unsub)) - - # Subscribe to ROS In ports - if self.ros_cmd_vel: - unsub = self.ros_cmd_vel.subscribe(self.handle_twist_stamped) - self._disposables.add(Disposable(unsub)) - if self.ros_odom_in: - unsub = self.ros_odom_in.subscribe(self._publish_odom_pose) - self._disposables.add(Disposable(unsub)) - if self.ros_tf: - unsub = self.ros_tf.subscribe(self._on_ros_tf) - self._disposables.add(Disposable(unsub)) - - # Start threads - self.running = True - self.watchdog_running = True - - # Start 50Hz sending thread - self.send_thread = threading.Thread(target=self._send_loop, daemon=True) # type: ignore[assignment] - self.send_thread.start() # type: ignore[attr-defined] - - # Start watchdog thread - self.watchdog_thread = threading.Thread(target=self._watchdog_loop, daemon=True) # type: ignore[assignment] - self.watchdog_thread.start() # type: ignore[attr-defined] - - @rpc - def stop(self) -> None: - """Stop the connection and send stop commands.""" - - self.set_mode(RobotMode.IDLE) # IDLE - with self.cmd_lock: - self._current_cmd = B1Command(mode=RobotMode.IDLE) # Zero all velocities - - # Send multiple stop packets - if not self.test_mode and self.socket: - stop_cmd = B1Command(mode=RobotMode.IDLE) - for _ in range(5): - data = stop_cmd.to_bytes() - self.socket.sendto(data, (self.ip, self.port)) - time.sleep(0.02) - - self.running = False - self.watchdog_running = False - - if self.send_thread: - self.send_thread.join(timeout=0.5) - if self.watchdog_thread: - self.watchdog_thread.join(timeout=0.5) - - if self.socket: - self.socket.close() - self.socket = None - - super().stop() - - def handle_twist_stamped(self, twist_stamped: TwistStamped) -> None: - """Handle timestamped Twist message and convert to B1Command. - - This is called automatically when messages arrive on cmd_vel input. - """ - # Extract Twist from TwistStamped - twist = Twist(linear=twist_stamped.linear, angular=twist_stamped.angular) - - logger.debug( - f"Received cmd_vel: linear=({twist.linear.x:.3f}, {twist.linear.y:.3f}, {twist.linear.z:.3f}), angular=({twist.angular.x:.3f}, {twist.angular.y:.3f}, {twist.angular.z:.3f})" - ) - - # In STAND mode, all twist values control body pose, not movement - # W/S: height (linear.z), A/D: yaw (angular.z), J/L: roll (angular.x), I/K: pitch (angular.y) - if self.current_mode == RobotMode.STAND: - # In STAND mode, don't auto-switch since all inputs are valid body pose controls - has_movement = False - else: - # In other modes, consider linear x/y and angular.z as movement - has_movement = ( - abs(twist.linear.x) > 0.01 - or abs(twist.linear.y) > 0.01 - or abs(twist.angular.z) > 0.01 - ) - - if has_movement and self.current_mode not in (RobotMode.STAND, RobotMode.WALK): - logger.info("Auto-switching to WALK mode for ROS control") - self.set_mode(RobotMode.WALK) - elif not has_movement and self.current_mode == RobotMode.WALK: - logger.info("Auto-switching to IDLE mode (zero velocities)") - self.set_mode(RobotMode.IDLE) - - if self.test_mode: - logger.info( - f"[TEST] Received TwistStamped: linear=({twist.linear.x:.2f}, {twist.linear.y:.2f}), angular.z={twist.angular.z:.2f}" - ) - - with self.cmd_lock: - self._current_cmd = B1Command.from_twist(twist, self.current_mode) - - logger.debug(f"Converted to B1Command: {self._current_cmd}") - - self.last_command_time = time.time() - self.timeout_active = False # Reset timeout state since we got a new command - - def handle_mode(self, mode_msg: Int32) -> None: - """Handle mode change message. - - This is called automatically when messages arrive on mode_cmd input. - """ - logger.debug(f"Received mode change: {mode_msg.data}") - if self.test_mode: - logger.info(f"[TEST] Received mode change: {mode_msg.data}") - self.set_mode(mode_msg.data) - - @rpc - def set_mode(self, mode: int) -> bool: - """Set robot mode (0=idle, 1=stand, 2=walk, 6=recovery).""" - self.current_mode = mode - with self.cmd_lock: - self._current_cmd.mode = mode - - # Clear velocities when not in walk mode - if mode != RobotMode.WALK: - self._current_cmd.lx = 0.0 - self._current_cmd.ly = 0.0 - self._current_cmd.rx = 0.0 - self._current_cmd.ry = 0.0 - - mode_names = { - RobotMode.IDLE: "IDLE", - RobotMode.STAND: "STAND", - RobotMode.WALK: "WALK", - RobotMode.RECOVERY: "RECOVERY", - } - logger.info(f"Mode changed to: {mode_names.get(mode, mode)}") - if self.test_mode: - logger.info(f"[TEST] Mode changed to: {mode_names.get(mode, mode)}") - - return True - - def _send_loop(self) -> None: - """Continuously send current command at 50Hz. - - The watchdog thread handles timeout and zeroing commands, so this loop - just sends whatever is in self._current_cmd at 50Hz. - """ - while self.running: - try: - # Watchdog handles timeout, we just send current command - with self.cmd_lock: - cmd_to_send = self._current_cmd - - # Log status every second (50 packets) - if self.packet_count % 50 == 0: - logger.info( - f"Sending B1 commands at 50Hz | Mode: {self.current_mode} | Count: {self.packet_count}" - ) - if not self.test_mode: - logger.debug(f"Current B1Command: {self._current_cmd}") - data = cmd_to_send.to_bytes() - hex_str = " ".join(f"{b:02x}" for b in data) - logger.debug(f"UDP packet ({len(data)} bytes): {hex_str}") - - if self.socket: - data = cmd_to_send.to_bytes() - self.socket.sendto(data, (self.ip, self.port)) - - self.packet_count += 1 - - # 50Hz rate (20ms between packets) - time.sleep(0.020) - - except Exception as e: - if self.running: - logger.error(f"Send error: {e}") - - def _publish_odom_pose(self, msg: Odometry) -> None: - """Convert and publish odometry as PoseStamped. - - This matches G1's approach of receiving external odometry. - """ - if self.odom_pose: - pose_stamped = PoseStamped( - ts=msg.ts, - frame_id=msg.frame_id, - position=msg.pose.pose.position, - orientation=msg.pose.pose.orientation, - ) - self.odom_pose.publish(pose_stamped) - - def _on_ros_tf(self, msg: TFMessage) -> None: - """Forward ROS TF messages to the module's TF tree.""" - self.tf.publish(*msg.transforms) - - def _watchdog_loop(self) -> None: - """Single watchdog thread that monitors command freshness.""" - while self.watchdog_running: - try: - time_since_last_cmd = time.time() - self.last_command_time - - if time_since_last_cmd > self.command_timeout: - if not self.timeout_active: - # First time detecting timeout - logger.warning( - f"Watchdog timeout ({time_since_last_cmd:.1f}s) - zeroing commands" - ) - if self.test_mode: - logger.info("[TEST] Watchdog timeout - zeroing commands") - - with self.cmd_lock: - self._current_cmd.lx = 0.0 - self._current_cmd.ly = 0.0 - self._current_cmd.rx = 0.0 - self._current_cmd.ry = 0.0 - - self.timeout_active = True - else: - if self.timeout_active: - logger.info("Watchdog: Commands resumed - control restored") - if self.test_mode: - logger.info("[TEST] Watchdog: Commands resumed") - self.timeout_active = False - - # Check every 50ms - time.sleep(0.05) - - except Exception as e: - if self.watchdog_running: - logger.error(f"Watchdog error: {e}") - - @rpc - def idle(self) -> bool: - """Set robot to idle mode.""" - self.set_mode(RobotMode.IDLE) - return True - - @rpc - def pose(self) -> bool: - """Set robot to stand/pose mode for reaching ground objects with manipulator.""" - self.set_mode(RobotMode.STAND) - return True - - @rpc - def walk(self) -> bool: - """Set robot to walk mode.""" - self.set_mode(RobotMode.WALK) - return True - - @rpc - def recovery(self) -> bool: - """Set robot to recovery mode.""" - self.set_mode(RobotMode.RECOVERY) - return True - - @rpc - def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> bool: - """Direct RPC method for sending TwistStamped commands. - - Args: - twist_stamped: Timestamped velocity command - duration: Not used, kept for compatibility - """ - self.handle_twist_stamped(twist_stamped) - return True - - -class MockB1ConnectionModule(B1ConnectionModule): - """Test connection module that prints commands instead of sending UDP.""" - - def __init__(self, ip: str = "127.0.0.1", port: int = 9090, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - """Initialize test connection without creating socket.""" - super().__init__(ip, port, test_mode=True, *args, **kwargs) # type: ignore[misc] - - def _send_loop(self) -> None: - """Override to provide better test output with timeout detection.""" - timeout_warned = False - - while self.running: - time_since_last_cmd = time.time() - self.last_command_time - is_timeout = time_since_last_cmd > self.command_timeout - - # Show timeout transitions - if is_timeout and not timeout_warned: - logger.info( - f"[TEST] Command timeout! Sending zeros after {time_since_last_cmd:.1f}s" - ) - timeout_warned = True - elif not is_timeout and timeout_warned: - logger.info("[TEST] Commands resumed - control restored") - timeout_warned = False - - # Print current state every 0.5 seconds - if self.packet_count % 25 == 0: - if is_timeout: - logger.info(f"[TEST] B1Cmd[ZEROS] (timeout) | Count: {self.packet_count}") - else: - logger.info(f"[TEST] {self._current_cmd} | Count: {self.packet_count}") - - self.packet_count += 1 - time.sleep(0.020) - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() diff --git a/dimos/robot/unitree/b1/joystick_module.py b/dimos/robot/unitree/b1/joystick_module.py deleted file mode 100644 index bb07094973..0000000000 --- a/dimos/robot/unitree/b1/joystick_module.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Pygame Joystick Module for testing B1 control via LCM.""" - -import os -import threading - -# Force X11 driver to avoid OpenGL threading issues -os.environ["SDL_VIDEODRIVER"] = "x11" - -import time - -from dimos.core import Module, Out, rpc -from dimos.msgs.geometry_msgs import Twist, TwistStamped, Vector3 -from dimos.msgs.std_msgs import Int32 - - -class JoystickModule(Module): - """Pygame-based joystick control module for B1 testing. - - Outputs timestamped Twist messages on /cmd_vel and mode changes on /b1/mode. - This allows testing the same interface that navigation will use. - """ - - twist_out: Out[TwistStamped] # Timestamped velocity commands - mode_out: Out[Int32] # Mode changes - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - Module.__init__(self, *args, **kwargs) - self.pygame_ready = False - self.running = False - self.current_mode = 0 # Start in IDLE mode for safety - - @rpc - def start(self) -> None: - """Initialize pygame and start control loop.""" - - super().start() - - try: - import pygame # noqa: F401 - except ImportError: - print("ERROR: pygame not installed. Install with: pip install pygame") - return - - self.keys_held = set() # type: ignore[var-annotated] - self.pygame_ready = True - self.running = True - - # Start pygame loop in background thread - ALL pygame ops will happen there - self._thread = threading.Thread(target=self._pygame_loop, daemon=True) - self._thread.start() - - return - - @rpc - def stop(self) -> None: - """Stop the joystick module.""" - - self.running = False - self.pygame_ready = False - - # Send stop command - stop_twist = Twist() - stop_twist_stamped = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=stop_twist.linear, - angular=stop_twist.angular, - ) - self.twist_out.publish(stop_twist_stamped) - - self._thread.join(2) - - super().stop() - - def _pygame_loop(self) -> None: - """Main pygame event loop - ALL pygame operations happen here.""" - import pygame - - # Initialize pygame and create display IN THIS THREAD - pygame.init() - self.screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) - pygame.display.set_caption("B1 Joystick Control (LCM)") - self.clock = pygame.time.Clock() - self.font = pygame.font.Font(None, 24) - - print("JoystickModule started - Focus pygame window to control") - print("Controls:") - print(" Walk Mode: WASD = Move/Turn, JL = Strafe") - print(" Stand Mode: WASD = Height/Yaw, JL = Roll, IK = Pitch") - print(" 1/2/0 = Stand/Walk/Idle modes") - print(" Space/Q = Emergency Stop") - print(" ESC = Quit (or use Ctrl+C)") - - while self.running and self.pygame_ready: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self.running = False - elif event.type == pygame.KEYDOWN: - self.keys_held.add(event.key) - - # Mode changes - publish to mode_out for connection module - if event.key == pygame.K_0: - self.current_mode = 0 - mode_msg = Int32() - mode_msg.data = 0 - self.mode_out.publish(mode_msg) - print("Mode: IDLE") - elif event.key == pygame.K_1: - self.current_mode = 1 - mode_msg = Int32() - mode_msg.data = 1 - self.mode_out.publish(mode_msg) - print("Mode: STAND") - elif event.key == pygame.K_2: - self.current_mode = 2 - mode_msg = Int32() - mode_msg.data = 2 - self.mode_out.publish(mode_msg) - print("Mode: WALK") - elif event.key == pygame.K_SPACE or event.key == pygame.K_q: - self.keys_held.clear() - # Send IDLE mode for emergency stop - self.current_mode = 0 - mode_msg = Int32() - mode_msg.data = 0 - self.mode_out.publish(mode_msg) - # Also send zero twist - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - stop_twist_stamped = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=stop_twist.linear, - angular=stop_twist.angular, - ) - self.twist_out.publish(stop_twist_stamped) - print("EMERGENCY STOP!") - elif event.key == pygame.K_ESCAPE: - # ESC still quits for development convenience - self.running = False - - elif event.type == pygame.KEYUP: - self.keys_held.discard(event.key) - - # Generate Twist message from held keys - twist = Twist() - twist.linear = Vector3(0, 0, 0) - twist.angular = Vector3(0, 0, 0) - - # Apply controls based on mode - if self.current_mode == 2: # WALK mode - movement control - # Forward/backward (W/S) - if pygame.K_w in self.keys_held: - twist.linear.x = 1.0 # Forward - if pygame.K_s in self.keys_held: - twist.linear.x = -1.0 # Backward - - # Turning (A/D) - if pygame.K_a in self.keys_held: - twist.angular.z = 1.0 # Turn left - if pygame.K_d in self.keys_held: - twist.angular.z = -1.0 # Turn right - - # Strafing (J/L) - if pygame.K_j in self.keys_held: - twist.linear.y = 1.0 # Strafe left - if pygame.K_l in self.keys_held: - twist.linear.y = -1.0 # Strafe right - - elif self.current_mode == 1: # STAND mode - body pose control - # Height control (W/S) - use linear.z for body height - if pygame.K_w in self.keys_held: - twist.linear.z = 1.0 # Raise body - if pygame.K_s in self.keys_held: - twist.linear.z = -1.0 # Lower body - - # Yaw control (A/D) - use angular.z for body yaw - if pygame.K_a in self.keys_held: - twist.angular.z = 1.0 # Rotate body left - if pygame.K_d in self.keys_held: - twist.angular.z = -1.0 # Rotate body right - - # Roll control (J/L) - use angular.x for body roll - if pygame.K_j in self.keys_held: - twist.angular.x = 1.0 # Roll left - if pygame.K_l in self.keys_held: - twist.angular.x = -1.0 # Roll right - - # Pitch control (I/K) - use angular.y for body pitch - if pygame.K_i in self.keys_held: - twist.angular.y = 1.0 # Pitch forward - if pygame.K_k in self.keys_held: - twist.angular.y = -1.0 # Pitch backward - - twist_stamped = TwistStamped( - ts=time.time(), frame_id="base_link", linear=twist.linear, angular=twist.angular - ) - self.twist_out.publish(twist_stamped) - - # Update pygame display - self._update_display(twist) - - # Maintain 50Hz rate - self.clock.tick(50) - - pygame.quit() - print("JoystickModule stopped") - - def _update_display(self, twist) -> None: # type: ignore[no-untyped-def] - """Update pygame window with current status.""" - import pygame - - self.screen.fill((30, 30, 30)) - - # Mode display - y_pos = 20 - mode_text = ["IDLE", "STAND", "WALK"][self.current_mode if self.current_mode < 3 else 0] - mode_color = ( - (0, 255, 0) - if self.current_mode == 2 - else (255, 255, 0) - if self.current_mode == 1 - else (100, 100, 100) - ) - - texts = [ - f"Mode: {mode_text}", - "", - f"Linear X: {twist.linear.x:+.2f}", - f"Linear Y: {twist.linear.y:+.2f}", - f"Linear Z: {twist.linear.z:+.2f}", - f"Angular X: {twist.angular.x:+.2f}", - f"Angular Y: {twist.angular.y:+.2f}", - f"Angular Z: {twist.angular.z:+.2f}", - "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self.keys_held if k < 256]), - ] - - for i, text in enumerate(texts): - if text: - color = mode_color if i == 0 else (255, 255, 255) - surf = self.font.render(text, True, color) - self.screen.blit(surf, (20, y_pos)) - y_pos += 30 - - if ( - twist.linear.x != 0 - or twist.linear.y != 0 - or twist.linear.z != 0 - or twist.angular.x != 0 - or twist.angular.y != 0 - or twist.angular.z != 0 - ): - pygame.draw.circle(self.screen, (255, 0, 0), (450, 30), 15) # Red = moving - else: - pygame.draw.circle(self.screen, (0, 255, 0), (450, 30), 15) # Green = stopped - - y_pos = 300 - help_texts = ["WASD: Move | JL: Strafe | 1/2/0: Modes", "Space/Q: E-Stop | ESC: Quit"] - for text in help_texts: - surf = self.font.render(text, True, (150, 150, 150)) - self.screen.blit(surf, (20, y_pos)) - y_pos += 25 - - pygame.display.flip() diff --git a/dimos/robot/unitree/b1/joystick_server_udp.cpp b/dimos/robot/unitree/b1/joystick_server_udp.cpp deleted file mode 100644 index e86e999b8d..0000000000 --- a/dimos/robot/unitree/b1/joystick_server_udp.cpp +++ /dev/null @@ -1,366 +0,0 @@ -/***************************************************************** - UDP Joystick Control Server for Unitree B1 Robot - With timeout protection and guaranteed packet boundaries -******************************************************************/ - -#include "unitree_legged_sdk/unitree_legged_sdk.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace UNITREE_LEGGED_SDK; - -// Joystick command structure received over network -struct NetworkJoystickCmd { - float lx; // left stick x (-1 to 1) - float ly; // left stick y (-1 to 1) - float rx; // right stick x (-1 to 1) - float ry; // right stick y (-1 to 1) - uint16_t buttons; // button states - uint8_t mode; // control mode -}; - -class JoystickServer { -public: - JoystickServer(uint8_t level, int server_port) : - safe(LeggedType::B1), - udp(level, 8090, "192.168.123.220", 8082), - server_port_(server_port), - running_(false) { - udp.InitCmdData(cmd); - memset(&joystick_cmd_, 0, sizeof(joystick_cmd_)); - joystick_cmd_.mode = 0; // Start in idle mode - last_packet_time_ = std::chrono::steady_clock::now(); - } - - void Start(); - void Stop(); - -private: - void UDPRecv(); - void UDPSend(); - void RobotControl(); - void NetworkServerThread(); - void ParseJoystickCommand(const NetworkJoystickCmd& net_cmd); - void CheckTimeout(); - - Safety safe; - UDP udp; - HighCmd cmd = {0}; - HighState state = {0}; - - NetworkJoystickCmd joystick_cmd_; - std::mutex cmd_mutex_; - - int server_port_; - int server_socket_; - bool running_; - std::thread server_thread_; - - // Client tracking for debug - struct sockaddr_in last_client_addr_; - bool has_client_ = false; - - // SAFETY: Timeout tracking - std::chrono::steady_clock::time_point last_packet_time_; - const int PACKET_TIMEOUT_MS = 100; // Stop if no packet for 100ms - - float dt = 0.002; - - // Control parameters - const float MAX_FORWARD_SPEED = 0.2f; // m/s - const float MAX_SIDE_SPEED = 0.2f; // m/s - const float MAX_YAW_SPEED = 0.2f; // rad/s - const float MAX_BODY_HEIGHT = 0.1f; // m - const float MAX_EULER_ANGLE = 0.3f; // rad - const float DEADZONE = 0.0f; // joystick deadzone -}; - -void JoystickServer::Start() { - running_ = true; - - // Start network server thread - server_thread_ = std::thread(&JoystickServer::NetworkServerThread, this); - - // Initialize environment - InitEnvironment(); - - // Start control loops - LoopFunc loop_control("control_loop", dt, boost::bind(&JoystickServer::RobotControl, this)); - LoopFunc loop_udpSend("udp_send", dt, 3, boost::bind(&JoystickServer::UDPSend, this)); - LoopFunc loop_udpRecv("udp_recv", dt, 3, boost::bind(&JoystickServer::UDPRecv, this)); - - loop_udpSend.start(); - loop_udpRecv.start(); - loop_control.start(); - - std::cout << "UDP Joystick server started on port " << server_port_ << std::endl; - std::cout << "Timeout protection: " << PACKET_TIMEOUT_MS << "ms" << std::endl; - std::cout << "Expected packet size: 19 bytes" << std::endl; - std::cout << "Robot control loops started" << std::endl; - - // Keep running - while (running_) { - sleep(1); - } -} - -void JoystickServer::Stop() { - running_ = false; - close(server_socket_); - if (server_thread_.joinable()) { - server_thread_.join(); - } -} - -void JoystickServer::NetworkServerThread() { - // Create UDP socket - server_socket_ = socket(AF_INET, SOCK_DGRAM, 0); - if (server_socket_ < 0) { - std::cerr << "Failed to create UDP socket" << std::endl; - return; - } - - // Allow socket reuse - int opt = 1; - setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); - - // Bind socket - struct sockaddr_in server_addr; - server_addr.sin_family = AF_INET; - server_addr.sin_addr.s_addr = INADDR_ANY; - server_addr.sin_port = htons(server_port_); - - if (bind(server_socket_, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { - std::cerr << "Failed to bind UDP socket to port " << server_port_ << std::endl; - close(server_socket_); - return; - } - - std::cout << "UDP server listening on port " << server_port_ << std::endl; - std::cout << "Waiting for joystick packets..." << std::endl; - - NetworkJoystickCmd net_cmd; - struct sockaddr_in client_addr; - socklen_t client_len; - - while (running_) { - client_len = sizeof(client_addr); - - // Receive UDP datagram (blocks until packet arrives) - ssize_t bytes = recvfrom(server_socket_, &net_cmd, sizeof(net_cmd), - 0, (struct sockaddr*)&client_addr, &client_len); - - if (bytes == 19) { - // Perfect packet size from Python client - if (!has_client_) { - std::cout << "Client connected from " << inet_ntoa(client_addr.sin_addr) - << ":" << ntohs(client_addr.sin_port) << std::endl; - has_client_ = true; - last_client_addr_ = client_addr; - } - ParseJoystickCommand(net_cmd); - } else if (bytes == sizeof(NetworkJoystickCmd)) { - // C++ client with padding (20 bytes) - if (!has_client_) { - std::cout << "C++ Client connected from " << inet_ntoa(client_addr.sin_addr) - << ":" << ntohs(client_addr.sin_port) << std::endl; - has_client_ = true; - last_client_addr_ = client_addr; - } - ParseJoystickCommand(net_cmd); - } else if (bytes > 0) { - // Wrong packet size - ignore but log - static int error_count = 0; - if (error_count++ < 5) { // Only log first 5 errors - std::cerr << "Ignored packet with wrong size: " << bytes - << " bytes (expected 19)" << std::endl; - } - } - // Note: recvfrom returns -1 on error, which we ignore - } -} - -void JoystickServer::ParseJoystickCommand(const NetworkJoystickCmd& net_cmd) { - std::lock_guard lock(cmd_mutex_); - joystick_cmd_ = net_cmd; - - // SAFETY: Update timestamp for timeout tracking - last_packet_time_ = std::chrono::steady_clock::now(); - - // Apply deadzone to analog sticks - if (fabs(joystick_cmd_.lx) < DEADZONE) joystick_cmd_.lx = 0; - if (fabs(joystick_cmd_.ly) < DEADZONE) joystick_cmd_.ly = 0; - if (fabs(joystick_cmd_.rx) < DEADZONE) joystick_cmd_.rx = 0; - if (fabs(joystick_cmd_.ry) < DEADZONE) joystick_cmd_.ry = 0; -} - -void JoystickServer::CheckTimeout() { - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - now - last_packet_time_).count(); - - static bool timeout_printed = false; - - if (elapsed > PACKET_TIMEOUT_MS) { - joystick_cmd_.lx = 0; - joystick_cmd_.ly = 0; - joystick_cmd_.rx = 0; - joystick_cmd_.ry = 0; - joystick_cmd_.buttons = 0; - - if (!timeout_printed) { - std::cout << "SAFETY: Packet timeout - stopping movement!" << std::endl; - timeout_printed = true; - } - } else { - // Reset flag when packets resume - if (timeout_printed) { - std::cout << "Packets resumed - control restored" << std::endl; - timeout_printed = false; - } - } -} - -void JoystickServer::UDPRecv() { - udp.Recv(); -} - -void JoystickServer::UDPSend() { - udp.Send(); -} - -void JoystickServer::RobotControl() { - udp.GetRecv(state); - - // SAFETY: Check for packet timeout - NetworkJoystickCmd current_cmd; - { - std::lock_guard lock(cmd_mutex_); - CheckTimeout(); // This may zero movement if timeout - current_cmd = joystick_cmd_; - } - - cmd.mode = 0; - cmd.gaitType = 0; - cmd.speedLevel = 0; - cmd.footRaiseHeight = 0; - cmd.bodyHeight = 0; - cmd.euler[0] = 0; - cmd.euler[1] = 0; - cmd.euler[2] = 0; - cmd.velocity[0] = 0.0f; - cmd.velocity[1] = 0.0f; - cmd.yawSpeed = 0.0f; - cmd.reserve = 0; - - // Set mode from joystick - cmd.mode = current_cmd.mode; - - // Map joystick to robot control based on mode - switch (current_cmd.mode) { - case 0: // Idle - // Robot stops - break; - - case 1: // Force stand with body control - // Left stick controls body height and yaw - cmd.bodyHeight = current_cmd.ly * MAX_BODY_HEIGHT; - cmd.euler[2] = current_cmd.lx * MAX_EULER_ANGLE; - - // Right stick controls pitch and roll - cmd.euler[1] = current_cmd.ry * MAX_EULER_ANGLE; - cmd.euler[0] = current_cmd.rx * MAX_EULER_ANGLE; - break; - - case 2: // Walk mode - cmd.velocity[0] = std::clamp(current_cmd.ly * MAX_FORWARD_SPEED, -MAX_FORWARD_SPEED, MAX_FORWARD_SPEED); - cmd.yawSpeed = std::clamp(-current_cmd.lx * MAX_YAW_SPEED, -MAX_YAW_SPEED, MAX_YAW_SPEED); - cmd.velocity[1] = std::clamp(-current_cmd.rx * MAX_SIDE_SPEED, -MAX_SIDE_SPEED, MAX_SIDE_SPEED); - - // Check button states for gait type - if (current_cmd.buttons & 0x0001) { // Button A - cmd.gaitType = 0; // Trot - } else if (current_cmd.buttons & 0x0002) { // Button B - cmd.gaitType = 1; // Trot running - } else if (current_cmd.buttons & 0x0004) { // Button X - cmd.gaitType = 2; // Climb mode - } else if (current_cmd.buttons & 0x0008) { // Button Y - cmd.gaitType = 3; // Trot obstacle - } - break; - - case 5: // Damping mode - case 6: // Recovery stand up - break; - - default: - cmd.mode = 0; // Default to idle for safety - break; - } - - // Debug output - static int counter = 0; - if (counter++ % 500 == 0) { // Print every second - auto now = std::chrono::steady_clock::now(); - auto elapsed = std::chrono::duration_cast( - now - last_packet_time_).count(); - - std::cout << "Mode: " << (int)cmd.mode - << " Vel: [" << cmd.velocity[0] << ", " << cmd.velocity[1] << "]" - << " Yaw: " << cmd.yawSpeed - << " Last packet: " << elapsed << "ms ago" - << " IMU: " << state.imu.rpy[2] << std::endl; - } - - udp.SetSend(cmd); -} - -// Signal handler for clean shutdown -JoystickServer* g_server = nullptr; - -void signal_handler(int sig) { - if (g_server) { - std::cout << "\nShutting down server..." << std::endl; - g_server->Stop(); - } - exit(0); -} - -int main(int argc, char* argv[]) { - int port = 9090; // Default port - - if (argc > 1) { - port = atoi(argv[1]); - } - - std::cout << "UDP Unitree B1 Joystick Control Server" << std::endl; - std::cout << "Communication level: HIGH-level" << std::endl; - std::cout << "Protocol: UDP (datagram)" << std::endl; - std::cout << "Server port: " << port << std::endl; - std::cout << "Packet size: 19 bytes (Python) or 20 bytes (C++)" << std::endl; - std::cout << "Update rate: 50Hz expected" << std::endl; - std::cout << "WARNING: Make sure the robot is standing on the ground." << std::endl; - std::cout << "Press Enter to continue..." << std::endl; - std::cin.ignore(); - - JoystickServer server(HIGHLEVEL, port); - g_server = &server; - - // Set up signal handler - signal(SIGINT, signal_handler); - signal(SIGTERM, signal_handler); - - server.Start(); - - return 0; -} diff --git a/dimos/robot/unitree/b1/test_connection.py b/dimos/robot/unitree/b1/test_connection.py deleted file mode 100644 index e43a3124dc..0000000000 --- a/dimos/robot/unitree/b1/test_connection.py +++ /dev/null @@ -1,431 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -"""Comprehensive tests for Unitree B1 connection module Timer implementation.""" - -# TODO: These tests are reaching too much into `conn` by setting and shutting -# down threads manually. That code is already in the connection module, and -# should be used and tested. Additionally, tests should always use `try-finally` -# to clean up even if the test fails. - -import threading -import time - -from dimos.msgs.geometry_msgs import TwistStamped, Vector3 -from dimos.msgs.std_msgs.Int32 import Int32 - -from .connection import MockB1ConnectionModule - - -class TestB1Connection: - """Test suite for B1 connection module with Timer implementation.""" - - def test_watchdog_actually_zeros_commands(self) -> None: - """Test that watchdog thread zeros commands after timeout.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Send a forward command - twist_stamped = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist_stamped) - - # Verify command is set - assert conn._current_cmd.ly == 1.0 - assert conn._current_cmd.mode == 2 - assert not conn.timeout_active - - # Wait for watchdog timeout (200ms + buffer) - time.sleep(0.3) - - # Verify commands were zeroed by watchdog - assert conn._current_cmd.ly == 0.0 - assert conn._current_cmd.lx == 0.0 - assert conn._current_cmd.rx == 0.0 - assert conn._current_cmd.ry == 0.0 - assert conn._current_cmd.mode == 2 # Mode maintained - assert conn.timeout_active - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_watchdog_resets_on_new_command(self) -> None: - """Test that watchdog timeout resets when new command arrives.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Send first command - twist1 = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist1) - assert conn._current_cmd.ly == 1.0 - - # Wait 150ms (not enough to trigger timeout) - time.sleep(0.15) - - # Send second command before timeout - twist2 = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(0.5, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist2) - - # Command should be updated and no timeout - assert conn._current_cmd.ly == 0.5 - assert not conn.timeout_active - - # Wait another 150ms (total 300ms from second command) - time.sleep(0.15) - # Should still not timeout since we reset the timer - assert not conn.timeout_active - assert conn._current_cmd.ly == 0.5 - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_watchdog_thread_efficiency(self) -> None: - """Test that watchdog uses only one thread regardless of command rate.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Count threads before sending commands - initial_thread_count = threading.active_count() - - # Send many commands rapidly (would create many Timer threads in old implementation) - for i in range(50): - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(i * 0.01, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - time.sleep(0.01) # 100Hz command rate - - # Thread count should be same (no new threads created) - final_thread_count = threading.active_count() - assert final_thread_count == initial_thread_count, "No new threads should be created" - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_watchdog_with_send_loop_blocking(self) -> None: - """Test that watchdog still works if send loop blocks.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - - # Mock the send loop to simulate blocking - original_send_loop = conn._send_loop - block_event = threading.Event() - - def blocking_send_loop() -> None: - # Block immediately - block_event.wait() - # Then run normally - original_send_loop() - - conn._send_loop = blocking_send_loop - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Send command - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - assert conn._current_cmd.ly == 1.0 - - # Wait for watchdog timeout - time.sleep(0.3) - - # Watchdog should have zeroed commands despite blocked send loop - assert conn._current_cmd.ly == 0.0 - assert conn.timeout_active - - # Unblock send loop - block_event.set() - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_continuous_commands_prevent_timeout(self) -> None: - """Test that continuous commands prevent watchdog timeout.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Send commands continuously for 500ms (should prevent timeout) - start = time.time() - commands_sent = 0 - while time.time() - start < 0.5: - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(0.5, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - commands_sent += 1 - time.sleep(0.05) # 50ms between commands (well under 200ms timeout) - - # Should never timeout - assert not conn.timeout_active, "Should not timeout with continuous commands" - assert conn._current_cmd.ly == 0.5, "Commands should still be active" - assert commands_sent >= 9, f"Should send at least 9 commands in 500ms, sent {commands_sent}" - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_watchdog_timing_accuracy(self) -> None: - """Test that watchdog zeros commands at approximately 200ms.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Send command and record time - start_time = time.time() - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - - # Wait for timeout checking periodically - timeout_time = None - while time.time() - start_time < 0.5: - if conn.timeout_active: - timeout_time = time.time() - break - time.sleep(0.01) - - assert timeout_time is not None, "Watchdog should timeout within 500ms" - - # Check timing (should be close to 200ms + up to 50ms watchdog interval) - elapsed = timeout_time - start_time - print(f"\nWatchdog timeout occurred at exactly {elapsed:.3f} seconds") - assert 0.19 <= elapsed <= 0.3, f"Watchdog timed out at {elapsed:.3f}s, expected ~0.2-0.25s" - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_mode_changes_with_watchdog(self) -> None: - """Test that mode changes work correctly with watchdog.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Give threads time to initialize - time.sleep(0.05) - - # Send walk command - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - assert conn.current_mode == 2 - assert conn._current_cmd.ly == 1.0 - - # Wait for timeout first (0.2s timeout + 0.15s margin for reliability) - time.sleep(0.35) - assert conn.timeout_active - assert conn._current_cmd.ly == 0.0 # Watchdog zeroed it - - # Now change mode to STAND - mode_msg = Int32() - mode_msg.data = 1 # STAND - conn.handle_mode(mode_msg) - assert conn.current_mode == 1 - assert conn._current_cmd.mode == 1 - # timeout_active stays true since we didn't send new movement commands - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_watchdog_stops_movement_when_commands_stop(self) -> None: - """Verify watchdog zeros commands when packets stop being sent.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Simulate sending movement commands for a while - for _i in range(5): - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(1.0, 0, 0), - angular=Vector3(0, 0, 0.5), # Forward and turning - ) - conn.handle_twist_stamped(twist) - time.sleep(0.05) # Send at 20Hz - - # Verify robot is moving - assert conn._current_cmd.ly == 1.0 - assert conn._current_cmd.lx == -0.25 # angular.z * 0.5 -> lx (for turning) - assert conn.current_mode == 2 # WALK mode - assert not conn.timeout_active - - # Wait for watchdog to detect timeout (200ms + buffer) - time.sleep(0.3) - - assert conn.timeout_active, "Watchdog should have detected timeout" - assert conn._current_cmd.ly == 0.0, "Forward velocity should be zeroed" - assert conn._current_cmd.lx == 0.0, "Lateral velocity should be zeroed" - assert conn._current_cmd.rx == 0.0, "Rotation X should be zeroed" - assert conn._current_cmd.ry == 0.0, "Rotation Y should be zeroed" - assert conn.current_mode == 2, "Mode should stay as WALK" - - # Verify recovery works - send new command - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(0.5, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - - # Give watchdog time to detect recovery - time.sleep(0.1) - - assert not conn.timeout_active, "Should recover from timeout" - assert conn._current_cmd.ly == 0.5, "Should accept new commands" - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() - - def test_rapid_command_thread_safety(self) -> None: - """Test thread safety with rapid commands from multiple threads.""" - conn = MockB1ConnectionModule(ip="127.0.0.1", port=9090) - conn.running = True - conn.watchdog_running = True - conn.send_thread = threading.Thread(target=conn._send_loop, daemon=True) - conn.send_thread.start() - conn.watchdog_thread = threading.Thread(target=conn._watchdog_loop, daemon=True) - conn.watchdog_thread.start() - - # Count initial threads - initial_threads = threading.active_count() - - # Send commands from multiple threads rapidly - def send_commands(thread_id) -> None: - for _i in range(10): - twist = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(thread_id * 0.1, 0, 0), - angular=Vector3(0, 0, 0), - ) - conn.handle_twist_stamped(twist) - time.sleep(0.01) - - threads = [] - for i in range(3): - t = threading.Thread(target=send_commands, args=(i,)) - threads.append(t) - t.start() - - for t in threads: - t.join() - - # Thread count should only increase by the 3 sender threads we created - # No additional Timer threads should be created - final_threads = threading.active_count() - assert final_threads <= initial_threads, "No extra threads should be created by watchdog" - - # Commands should still work correctly - assert conn._current_cmd.ly >= 0, "Last command should be set" - assert not conn.timeout_active, "Should not be in timeout with recent commands" - - conn.running = False - conn.watchdog_running = False - conn.send_thread.join(timeout=0.5) - conn.watchdog_thread.join(timeout=0.5) - conn._close_module() diff --git a/dimos/robot/unitree/b1/unitree_b1.py b/dimos/robot/unitree/b1/unitree_b1.py deleted file mode 100644 index a2dd6c718d..0000000000 --- a/dimos/robot/unitree/b1/unitree_b1.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -""" -Unitree B1 quadruped robot with simplified UDP control. -Uses standard Twist interface for velocity commands. -""" - -import logging -import os - -from dimos import core -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.resource import Resource -from dimos.core.transport import ROSTransport -from dimos.msgs.geometry_msgs import PoseStamped, TwistStamped -from dimos.msgs.nav_msgs.Odometry import Odometry -from dimos.msgs.std_msgs import Int32 -from dimos.msgs.tf2_msgs.TFMessage import TFMessage -from dimos.robot.robot import Robot -from dimos.robot.unitree.b1.connection import ( - B1ConnectionModule, - MockB1ConnectionModule, -) -from dimos.skills.skills import SkillLibrary -from dimos.types.robot_capabilities import RobotCapability -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.INFO) - - -class UnitreeB1(Robot, Resource): - """Unitree B1 quadruped robot with UDP control. - - Simplified architecture: - - Connection module handles Twist → B1Command conversion - - Standard /cmd_vel interface for navigation compatibility - - Optional joystick module for testing - """ - - def __init__( - self, - ip: str = "192.168.123.14", - port: int = 9090, - output_dir: str | None = None, - skill_library: SkillLibrary | None = None, - enable_joystick: bool = False, - test_mode: bool = False, - ) -> None: - """Initialize the B1 robot. - - Args: - ip: Robot IP address (or server running joystick_server_udp) - port: UDP port for joystick server (default 9090) - output_dir: Directory for saving outputs - skill_library: Skill library instance (optional) - enable_joystick: Enable pygame joystick control module - test_mode: Test mode - print commands instead of sending UDP - """ - super().__init__() - self.ip = ip - self.port = port - self.output_dir = output_dir or os.path.join(os.getcwd(), "assets", "output") - self.enable_joystick = enable_joystick - self.test_mode = test_mode - self.capabilities = [RobotCapability.LOCOMOTION] - self.connection = None - self.joystick = None - self._dimos = ModuleCoordinator(n=2) - - os.makedirs(self.output_dir, exist_ok=True) - logger.info(f"Robot outputs will be saved to: {self.output_dir}") - - def start(self) -> None: - """Start the B1 robot - initialize DimOS, deploy modules, and start them.""" - - logger.info("Initializing DimOS...") - self._dimos.start() - - logger.info("Deploying connection module...") - if self.test_mode: - self.connection = self._dimos.deploy(MockB1ConnectionModule, self.ip, self.port) # type: ignore[assignment] - else: - self.connection = self._dimos.deploy(B1ConnectionModule, self.ip, self.port) # type: ignore[assignment] - - # Configure LCM transports for connection (matching G1 pattern) - self.connection.cmd_vel.transport = core.LCMTransport("/cmd_vel", TwistStamped) # type: ignore[attr-defined] - self.connection.mode_cmd.transport = core.LCMTransport("/b1/mode", Int32) # type: ignore[attr-defined] - self.connection.odom_in.transport = core.LCMTransport("/state_estimation", Odometry) # type: ignore[attr-defined] - self.connection.odom_pose.transport = core.LCMTransport("/odom", PoseStamped) # type: ignore[attr-defined] - - # Configure ROS transports for connection - self.connection.ros_cmd_vel.transport = ROSTransport("/cmd_vel", TwistStamped) # type: ignore[attr-defined] - self.connection.ros_odom_in.transport = ROSTransport("/state_estimation", Odometry) # type: ignore[attr-defined] - self.connection.ros_tf.transport = ROSTransport("/tf", TFMessage) # type: ignore[attr-defined] - - # Deploy joystick move_vel control - if self.enable_joystick: - from dimos.robot.unitree.b1.joystick_module import JoystickModule - - self.joystick = self._dimos.deploy(JoystickModule) # type: ignore[assignment] - self.joystick.twist_out.transport = core.LCMTransport("/cmd_vel", TwistStamped) # type: ignore[attr-defined] - self.joystick.mode_out.transport = core.LCMTransport("/b1/mode", Int32) # type: ignore[attr-defined] - logger.info("Joystick module deployed - pygame window will open") - - self._dimos.start_all_modules() - - self.connection.idle() # type: ignore[attr-defined] # Start in IDLE mode for safety - logger.info("B1 started in IDLE mode (safety)") - - logger.info(f"UnitreeB1 initialized - UDP control to {self.ip}:{self.port}") - if self.enable_joystick: - logger.info("Pygame joystick module enabled for testing") - - def stop(self) -> None: - self._dimos.stop() - - # Robot control methods (standard interface) - def move(self, twist_stamped: TwistStamped, duration: float = 0.0) -> None: - """Send movement command to robot using timestamped Twist. - - Args: - twist_stamped: TwistStamped message with linear and angular velocities - duration: How long to move (not used for B1) - """ - if self.connection: - self.connection.move(twist_stamped, duration) - - def stand(self) -> None: - """Put robot in stand mode.""" - if self.connection: - self.connection.stand() - logger.info("B1 switched to STAND mode") - - def walk(self) -> None: - """Put robot in walk mode.""" - if self.connection: - self.connection.walk() - logger.info("B1 switched to WALK mode") - - def idle(self) -> None: - """Put robot in idle mode.""" - if self.connection: - self.connection.idle() - logger.info("B1 switched to IDLE mode") - - -def main() -> None: - """Main entry point for testing B1 robot.""" - import argparse - - parser = argparse.ArgumentParser(description="Unitree B1 Robot Control") - parser.add_argument("--ip", default="192.168.12.1", help="Robot IP address") - parser.add_argument("--port", type=int, default=9090, help="UDP port") - parser.add_argument("--joystick", action="store_true", help="Enable pygame joystick control") - parser.add_argument("--output-dir", help="Output directory for logs/data") - parser.add_argument( - "--test", action="store_true", help="Test mode - print commands instead of UDP" - ) - - args = parser.parse_args() - - robot = UnitreeB1( # type: ignore[abstract] - ip=args.ip, - port=args.port, - output_dir=args.output_dir, - enable_joystick=args.joystick, - test_mode=args.test, - ) - - robot.start() - - try: - if args.joystick: - print("\n" + "=" * 50) - print("B1 JOYSTICK CONTROL") - print("=" * 50) - print("Focus the pygame window to control") - print("Press keys in pygame window:") - print(" 0/1/2 = Idle/Stand/Walk modes") - print(" WASD = Move/Turn") - print(" JL = Strafe") - print(" Space/Q = Emergency Stop") - print(" ESC = Quit pygame (then Ctrl+C to exit)") - print("=" * 50 + "\n") - - import time - - while True: - time.sleep(1) - else: - # Manual control example - print("\nB1 Robot ready for commands") - print("Use robot.idle(), robot.stand(), robot.walk() to change modes") - print("ROS topics active via ROSTransport: /cmd_vel, /state_estimation, /tf") - print("Press Ctrl+C to exit\n") - - import time - - while True: - time.sleep(1) - - except KeyboardInterrupt: - print("\nShutting down...") - finally: - robot.stop() - - -if __name__ == "__main__": - main() diff --git a/dimos/robot/unitree/connection.py b/dimos/robot/unitree/connection.py index f3f8ffaafb..29999941ba 100644 --- a/dimos/robot/unitree/connection.py +++ b/dimos/robot/unitree/connection.py @@ -111,10 +111,25 @@ def start_background_loop() -> None: self.task = self.loop.create_task(async_connect()) self.loop.run_forever() + CONNECT_TIMEOUT = 30 # seconds + self.loop = asyncio.new_event_loop() self.thread = threading.Thread(target=start_background_loop, daemon=True) self.thread.start() - self.connection_ready.wait() + + if not self.connection_ready.wait(timeout=CONNECT_TIMEOUT): + if self.loop.is_running(): + self.loop.call_soon_threadsafe(self.loop.stop) + if self.thread.is_alive(): + self.thread.join(timeout=2.0) + raise TimeoutError( + f"WebRTC connection to {self.ip} timed out after {CONNECT_TIMEOUT}s. " + "Common causes:\n" + " - Another WebRTC client is connected (close the Unitree mobile app)\n" + " - Robot is unreachable on the network\n" + " - Port 9991 (encrypted SDP) is not responding\n" + "Tip: only one WebRTC client can connect to the Go2 at a time." + ) def start(self) -> None: pass diff --git a/dimos/robot/unitree/demo_error_on_name_conflicts.py b/dimos/robot/unitree/demo_error_on_name_conflicts.py deleted file mode 100644 index 63f37ad723..0000000000 --- a/dimos/robot/unitree/demo_error_on_name_conflicts.py +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.core.stream import In, Out - - -class Data1: - pass - - -class Data2: - pass - - -class ModuleA(Module): - shared_data: Out[Data1] - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() - - -class ModuleB(Module): - shared_data: In[Data2] - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() - - -demo_error_on_name_conflicts = autoconnect(ModuleA.blueprint(), ModuleB.blueprint()) diff --git a/dimos/robot/unitree/depth_module.py b/dimos/robot/unitree/depth_module.py deleted file mode 100644 index 07f065caea..0000000000 --- a/dimos/robot/unitree/depth_module.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -from dimos_lcm.sensor_msgs import CameraInfo -import numpy as np - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig -from dimos.msgs.sensor_msgs import Image, ImageFormat -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class DepthModule(Module): - """ - Depth module for Unitree Go2 that processes RGB images to generate depth using Metric3D. - - Subscribes to: - - /go2/color_image: RGB camera images from Unitree - - /go2/camera_info: Camera calibration information - - Publishes: - - /go2/depth_image: Depth images generated by Metric3D - """ - - # LCM inputs - color_image: In[Image] - camera_info: In[CameraInfo] - - # LCM outputs - depth_image: Out[Image] - - def __init__( # type: ignore[no-untyped-def] - self, - gt_depth_scale: float = 0.5, - cfg: GlobalConfig | None = None, - **kwargs, - ) -> None: - """ - Initialize Depth Module. - - Args: - gt_depth_scale: Ground truth depth scaling factor - """ - super().__init__(**kwargs) - - self.camera_intrinsics = None - self.gt_depth_scale = gt_depth_scale - self.metric3d = None - self._camera_info_received = False - - # Processing state - self._running = False - self._latest_frame = None - self._last_image = None - self._last_timestamp = None - self._last_depth = None - self._cannot_process_depth = False - - # Threading - self._processing_thread: threading.Thread | None = None - self._stop_processing = threading.Event() - - if cfg: - if cfg.simulation: - self.gt_depth_scale = 1.0 - - @rpc - def start(self) -> None: - super().start() - - if self._running: - logger.warning("Camera module already running") - return - - # Set running flag before starting - self._running = True - - # Subscribe to video and camera info inputs - self.color_image.subscribe(self._on_video) - self.camera_info.subscribe(self._on_camera_info) - - # Start processing thread - self._start_processing_thread() - - logger.info("Depth module started") - - @rpc - def stop(self) -> None: - if not self._running: - return - - self._running = False - self._stop_processing.set() - - # Wait for thread to finish - if self._processing_thread and self._processing_thread.is_alive(): - self._processing_thread.join(timeout=2.0) - - super().stop() - - def _on_camera_info(self, msg: CameraInfo) -> None: - """Process camera info to extract intrinsics.""" - if self.metric3d is not None: - return # Already initialized - - try: - # Extract intrinsics from camera matrix K - K = msg.K - fx = K[0] - fy = K[4] - cx = K[2] - cy = K[5] - - self.camera_intrinsics = [fx, fy, cx, cy] # type: ignore[assignment] - - # Initialize Metric3D with camera intrinsics - from dimos.models.depth.metric3d import Metric3D - - self.metric3d = Metric3D(camera_intrinsics=self.camera_intrinsics) # type: ignore[assignment] - self._camera_info_received = True - - logger.info( - f"Initialized Metric3D with intrinsics from camera_info: {self.camera_intrinsics}" - ) - - except Exception as e: - logger.error(f"Error processing camera info: {e}") - - def _on_video(self, msg: Image) -> None: - """Store latest video frame for processing.""" - if not self._running: - return - - # Simply store the latest frame - processing happens in main loop - self._latest_frame = msg # type: ignore[assignment] - logger.debug( - f"Received video frame: format={msg.format}, shape={msg.data.shape if hasattr(msg.data, 'shape') else 'unknown'}" - ) - - def _start_processing_thread(self) -> None: - """Start the processing thread.""" - self._stop_processing.clear() - self._processing_thread = threading.Thread(target=self._main_processing_loop, daemon=True) - self._processing_thread.start() - logger.info("Started depth processing thread") - - def _main_processing_loop(self) -> None: - """Main processing loop that continuously processes latest frames.""" - logger.info("Starting main processing loop") - - while not self._stop_processing.is_set(): - # Process latest frame if available - if self._latest_frame is not None: - try: - msg = self._latest_frame - self._latest_frame = None # Clear to avoid reprocessing - # Store for publishing - self._last_image = msg.data - self._last_timestamp = msg.ts if msg.ts else time.time() - # Process depth - self._process_depth(self._last_image) - - except Exception as e: - logger.error(f"Error in main processing loop: {e}", exc_info=True) - else: - # Small sleep to avoid busy waiting - time.sleep(0.001) - - logger.info("Main processing loop stopped") - - def _process_depth(self, img_array: np.ndarray) -> None: # type: ignore[type-arg] - """Process depth estimation using Metric3D.""" - if self._cannot_process_depth: - self._last_depth = None - return - - # Wait for camera info to initialize Metric3D - if self.metric3d is None: - logger.debug("Waiting for camera_info to initialize Metric3D") - return - - try: - logger.debug(f"Processing depth for image shape: {img_array.shape}") - - # Generate depth map - depth_array = self.metric3d.infer_depth(img_array) * self.gt_depth_scale - - self._last_depth = depth_array - logger.debug(f"Generated depth map shape: {depth_array.shape}") - - self._publish_depth() - - except Exception as e: - logger.error(f"Error processing depth: {e}") - self._cannot_process_depth = True - - def _publish_depth(self) -> None: - """Publish depth image.""" - if not self._running: - return - - try: - # Publish depth image - if self._last_depth is not None: - # Convert depth to uint16 (millimeters) for more efficient storage - # Clamp to valid range [0, 65.535] meters before converting - depth_clamped = np.clip(self._last_depth, 0, 65.535) - depth_uint16 = (depth_clamped * 1000).astype(np.uint16) - depth_msg = Image( - data=depth_uint16, - format=ImageFormat.DEPTH16, # Use DEPTH16 format for uint16 depth - frame_id="camera_link", - ts=self._last_timestamp, - ) - self.depth_image.publish(depth_msg) - logger.debug(f"Published depth image (uint16): shape={depth_uint16.shape}") - - except Exception as e: - logger.error(f"Error publishing depth data: {e}", exc_info=True) - - -depth_module = DepthModule.blueprint - - -__all__ = ["DepthModule", "depth_module"] diff --git a/dimos/robot/unitree/g1/blueprints/__init__.py b/dimos/robot/unitree/g1/blueprints/__init__.py deleted file mode 100644 index ebc18da8d3..0000000000 --- a/dimos/robot/unitree/g1/blueprints/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Cascaded G1 blueprints split into focused modules.""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "agentic._agentic_skills": ["_agentic_skills"], - "agentic.unitree_g1_agentic": ["unitree_g1_agentic"], - "agentic.unitree_g1_agentic_sim": ["unitree_g1_agentic_sim"], - "agentic.unitree_g1_full": ["unitree_g1_full"], - "basic.unitree_g1_basic": ["unitree_g1_basic"], - "basic.unitree_g1_basic_sim": ["unitree_g1_basic_sim"], - "basic.unitree_g1_joystick": ["unitree_g1_joystick"], - "perceptive._perception_and_memory": ["_perception_and_memory"], - "perceptive.unitree_g1": ["unitree_g1"], - "perceptive.unitree_g1_detection": ["unitree_g1_detection"], - "perceptive.unitree_g1_shm": ["unitree_g1_shm"], - "perceptive.unitree_g1_sim": ["unitree_g1_sim"], - "primitive.uintree_g1_primitive_no_nav": ["uintree_g1_primitive_no_nav", "basic_no_nav"], - }, -) diff --git a/dimos/robot/unitree/g1/blueprints/agentic/__init__.py b/dimos/robot/unitree/g1/blueprints/agentic/__init__.py deleted file mode 100644 index 5e6db90d91..0000000000 --- a/dimos/robot/unitree/g1/blueprints/agentic/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agentic blueprints for Unitree G1.""" diff --git a/dimos/robot/unitree/g1/blueprints/agentic/_agentic_skills.py b/dimos/robot/unitree/g1/blueprints/agentic/_agentic_skills.py deleted file mode 100644 index 74ce41f7f1..0000000000 --- a/dimos/robot/unitree/g1/blueprints/agentic/_agentic_skills.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agentic skills used by higher-level G1 blueprints.""" - -from dimos.agents.agent import agent -from dimos.agents.skills.navigation import navigation_skill -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.skill_container import g1_skills - -_agentic_skills = autoconnect( - agent(), - navigation_skill(), - g1_skills(), -) - -__all__ = ["_agentic_skills"] diff --git a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic.py b/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic.py deleted file mode 100644 index a90c2bfe2c..0000000000 --- a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Full G1 stack with agentic skills.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.agentic._agentic_skills import _agentic_skills -from dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1 import unitree_g1 - -unitree_g1_agentic = autoconnect( - unitree_g1, - _agentic_skills, -) - -__all__ = ["unitree_g1_agentic"] diff --git a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic_sim.py b/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic_sim.py deleted file mode 100644 index b7371b96b5..0000000000 --- a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_agentic_sim.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agentic G1 sim stack.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.agentic._agentic_skills import _agentic_skills -from dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1_sim import unitree_g1_sim - -unitree_g1_agentic_sim = autoconnect( - unitree_g1_sim, - _agentic_skills, -) - -__all__ = ["unitree_g1_agentic_sim"] diff --git a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_full.py b/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_full.py deleted file mode 100644 index 7f826f2eec..0000000000 --- a/dimos/robot/unitree/g1/blueprints/agentic/unitree_g1_full.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Full featured G1 stack with agentic skills and teleop.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.agentic._agentic_skills import _agentic_skills -from dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1_shm import unitree_g1_shm -from dimos.robot.unitree.keyboard_teleop import keyboard_teleop - -unitree_g1_full = autoconnect( - unitree_g1_shm, - _agentic_skills, - keyboard_teleop(), -) - -__all__ = ["unitree_g1_full"] diff --git a/dimos/robot/unitree/g1/blueprints/basic/__init__.py b/dimos/robot/unitree/g1/blueprints/basic/__init__.py deleted file mode 100644 index 87e6586f56..0000000000 --- a/dimos/robot/unitree/g1/blueprints/basic/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Basic blueprints for Unitree G1.""" diff --git a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic.py b/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic.py deleted file mode 100644 index 1fb591e895..0000000000 --- a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Basic G1 stack: base sensors plus real robot connection and ROS nav.""" - -from dimos.core.blueprints import autoconnect -from dimos.navigation.rosnav import ros_nav -from dimos.robot.unitree.g1.blueprints.primitive.uintree_g1_primitive_no_nav import ( - uintree_g1_primitive_no_nav, -) -from dimos.robot.unitree.g1.connection import g1_connection - -unitree_g1_basic = autoconnect( - uintree_g1_primitive_no_nav, - g1_connection(), - ros_nav(), -) - -__all__ = ["unitree_g1_basic"] diff --git a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic_sim.py b/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic_sim.py deleted file mode 100644 index 603a9535ee..0000000000 --- a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_basic_sim.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Basic G1 sim stack: base sensors plus sim connection and planner.""" - -from dimos.core.blueprints import autoconnect -from dimos.navigation.replanning_a_star.module import replanning_a_star_planner -from dimos.robot.unitree.g1.blueprints.primitive.uintree_g1_primitive_no_nav import ( - uintree_g1_primitive_no_nav, -) -from dimos.robot.unitree.g1.sim import g1_sim_connection - -unitree_g1_basic_sim = autoconnect( - uintree_g1_primitive_no_nav, - g1_sim_connection(), - replanning_a_star_planner(), -) - -__all__ = ["unitree_g1_basic_sim"] diff --git a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_joystick.py b/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_joystick.py deleted file mode 100644 index 0242556189..0000000000 --- a/dimos/robot/unitree/g1/blueprints/basic/unitree_g1_joystick.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""G1 stack with keyboard teleop.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic import unitree_g1_basic -from dimos.robot.unitree.keyboard_teleop import keyboard_teleop - -unitree_g1_joystick = autoconnect( - unitree_g1_basic, - keyboard_teleop(), # Pygame-based joystick control -) - -__all__ = ["unitree_g1_joystick"] diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/__init__.py b/dimos/robot/unitree/g1/blueprints/perceptive/__init__.py deleted file mode 100644 index 9bd838e8b8..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Perceptive blueprints for Unitree G1.""" diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/_perception_and_memory.py b/dimos/robot/unitree/g1/blueprints/perceptive/_perception_and_memory.py deleted file mode 100644 index 47dc2588b9..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/_perception_and_memory.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Perception and memory modules used by higher-level G1 blueprints.""" - -from dimos.core.blueprints import autoconnect -from dimos.perception.object_tracker import object_tracking -from dimos.perception.spatial_perception import spatial_memory -from dimos.utils.monitoring import utilization - -_perception_and_memory = autoconnect( - spatial_memory(), - object_tracking(frame_id="camera_link"), - utilization(), -) - -__all__ = ["_perception_and_memory"] diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1.py b/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1.py deleted file mode 100644 index 483928ec54..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""G1 stack with perception and memory.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic import unitree_g1_basic -from dimos.robot.unitree.g1.blueprints.perceptive._perception_and_memory import ( - _perception_and_memory, -) - -unitree_g1 = autoconnect( - unitree_g1_basic, - _perception_and_memory, -).global_config(n_dask_workers=8) - -__all__ = ["unitree_g1"] diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_detection.py b/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_detection.py deleted file mode 100644 index 6e2da40a2c..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_detection.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""G1 stack with person tracking and 3D detection.""" - -from dimos_lcm.foxglove_msgs import SceneUpdate -from dimos_lcm.foxglove_msgs.ImageAnnotations import ImageAnnotations - -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera import zed -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.detectors.person.yolo import YoloPersonDetector -from dimos.perception.detection.module3D import Detection3DModule, detection3d_module -from dimos.perception.detection.moduleDB import ObjectDBModule, detection_db_module -from dimos.perception.detection.person_tracker import PersonTracker, person_tracker_module -from dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic import unitree_g1_basic - -unitree_g1_detection = ( - autoconnect( - unitree_g1_basic, - # Person detection modules with YOLO - detection3d_module( - camera_info=zed.CameraInfo.SingleWebcam, - detector=YoloPersonDetector, - ), - detection_db_module( - camera_info=zed.CameraInfo.SingleWebcam, - filter=lambda det: det.class_id == 0, # Filter for person class only - ), - person_tracker_module( - cameraInfo=zed.CameraInfo.SingleWebcam, - ), - ) - .global_config(n_dask_workers=8) - .remappings( - [ - # Connect detection modules to camera and lidar - (Detection3DModule, "image", "color_image"), - (Detection3DModule, "pointcloud", "pointcloud"), - (ObjectDBModule, "image", "color_image"), - (ObjectDBModule, "pointcloud", "pointcloud"), - (PersonTracker, "image", "color_image"), - (PersonTracker, "detections", "detections_2d"), - ] - ) - .transports( - { - # Detection 3D module outputs - ("detections", Detection3DModule): LCMTransport( - "/detector3d/detections", Detection2DArray - ), - ("annotations", Detection3DModule): LCMTransport( - "/detector3d/annotations", ImageAnnotations - ), - ("scene_update", Detection3DModule): LCMTransport( - "/detector3d/scene_update", SceneUpdate - ), - ("detected_pointcloud_0", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/0", PointCloud2 - ), - ("detected_pointcloud_1", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/1", PointCloud2 - ), - ("detected_pointcloud_2", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/2", PointCloud2 - ), - ("detected_image_0", Detection3DModule): LCMTransport("/detector3d/image/0", Image), - ("detected_image_1", Detection3DModule): LCMTransport("/detector3d/image/1", Image), - ("detected_image_2", Detection3DModule): LCMTransport("/detector3d/image/2", Image), - # Detection DB module outputs - ("detections", ObjectDBModule): LCMTransport( - "/detectorDB/detections", Detection2DArray - ), - ("annotations", ObjectDBModule): LCMTransport( - "/detectorDB/annotations", ImageAnnotations - ), - ("scene_update", ObjectDBModule): LCMTransport("/detectorDB/scene_update", SceneUpdate), - ("detected_pointcloud_0", ObjectDBModule): LCMTransport( - "/detectorDB/pointcloud/0", PointCloud2 - ), - ("detected_pointcloud_1", ObjectDBModule): LCMTransport( - "/detectorDB/pointcloud/1", PointCloud2 - ), - ("detected_pointcloud_2", ObjectDBModule): LCMTransport( - "/detectorDB/pointcloud/2", PointCloud2 - ), - ("detected_image_0", ObjectDBModule): LCMTransport("/detectorDB/image/0", Image), - ("detected_image_1", ObjectDBModule): LCMTransport("/detectorDB/image/1", Image), - ("detected_image_2", ObjectDBModule): LCMTransport("/detectorDB/image/2", Image), - # Person tracker outputs - ("target", PersonTracker): LCMTransport("/person_tracker/target", PoseStamped), - } - ) -) - -__all__ = ["unitree_g1_detection"] diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_shm.py b/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_shm.py deleted file mode 100644 index 5ee4d4c9d1..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_shm.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""G1 stack with shared memory image transport.""" - -from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE -from dimos.core.blueprints import autoconnect -from dimos.core.transport import pSHMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.robot.foxglove_bridge import foxglove_bridge -from dimos.robot.unitree.g1.blueprints.perceptive.unitree_g1 import unitree_g1 - -unitree_g1_shm = autoconnect( - unitree_g1.transports( - { - ("color_image", Image): pSHMTransport( - "/color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ), - } - ), - foxglove_bridge( - shm_channels=[ - "/color_image#sensor_msgs.Image", - ] - ), -) - -__all__ = ["unitree_g1_shm"] diff --git a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_sim.py b/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_sim.py deleted file mode 100644 index 059102c7a5..0000000000 --- a/dimos/robot/unitree/g1/blueprints/perceptive/unitree_g1_sim.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""G1 sim stack with perception and memory.""" - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.g1.blueprints.basic.unitree_g1_basic_sim import unitree_g1_basic_sim -from dimos.robot.unitree.g1.blueprints.perceptive._perception_and_memory import ( - _perception_and_memory, -) - -unitree_g1_sim = autoconnect( - unitree_g1_basic_sim, - _perception_and_memory, -).global_config(n_dask_workers=8) - -__all__ = ["unitree_g1_sim"] diff --git a/dimos/robot/unitree/g1/blueprints/primitive/__init__.py b/dimos/robot/unitree/g1/blueprints/primitive/__init__.py deleted file mode 100644 index 833f767728..0000000000 --- a/dimos/robot/unitree/g1/blueprints/primitive/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Primitive blueprints for Unitree G1.""" diff --git a/dimos/robot/unitree/g1/blueprints/primitive/uintree_g1_primitive_no_nav.py b/dimos/robot/unitree/g1/blueprints/primitive/uintree_g1_primitive_no_nav.py deleted file mode 100644 index 36bf569f72..0000000000 --- a/dimos/robot/unitree/g1/blueprints/primitive/uintree_g1_primitive_no_nav.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Minimal G1 stack without navigation, used as a base for larger blueprints.""" - -from dimos_lcm.sensor_msgs import CameraInfo - -from dimos.core.blueprints import autoconnect -from dimos.core.global_config import global_config -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera import zed -from dimos.hardware.sensors.camera.module import camera_module # type: ignore[attr-defined] -from dimos.hardware.sensors.camera.webcam import Webcam -from dimos.mapping.costmapper import cost_mapper -from dimos.mapping.voxels import voxel_mapper -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Transform, Twist, Vector3 -from dimos.msgs.nav_msgs import Odometry, Path -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.std_msgs import Bool -from dimos.navigation.frontier_exploration import wavefront_frontier_explorer -from dimos.protocol.pubsub.impl.lcmpubsub import LCM -from dimos.web.websocket_vis.websocket_vis_module import websocket_vis - -rerun_config = { - "pubsubs": [LCM(autoconf=True)], - "visual_override": { - "world/camera_info": lambda camera_info: camera_info.to_rerun( - image_topic="/world/color_image", - optical_frame="camera_optical", - ), - "world/global_map": lambda grid: grid.to_rerun(voxel_size=0.1, mode="boxes"), - "world/navigation_costmap": lambda grid: grid.to_rerun( - colormap="Accent", - z_offset=0.015, - opacity=0.2, - background="#484981", - ), - }, - "static": { - "world/tf/base_link": lambda rr: [ - rr.Boxes3D( - half_sizes=[0.2, 0.15, 0.75], - colors=[(0, 255, 127)], - fill_mode="MajorWireframe", - ), - rr.Transform3D(parent_frame="tf#/base_link"), - ] - }, -} - -match global_config.viewer_backend: - case "foxglove": - from dimos.robot.foxglove_bridge import foxglove_bridge - - _with_vis = autoconnect(foxglove_bridge()) - case "rerun": - from dimos.visualization.rerun.bridge import rerun_bridge - - _with_vis = autoconnect(rerun_bridge(**rerun_config)) - case "rerun-web": - from dimos.visualization.rerun.bridge import rerun_bridge - - _with_vis = autoconnect(rerun_bridge(viewer_mode="web", **rerun_config)) - case _: - _with_vis = autoconnect() - -_camera = ( - autoconnect( - camera_module( - transform=Transform( - translation=Vector3(0.05, 0.0, 0.6), # height of camera on G1 robot - rotation=Quaternion.from_euler(Vector3(0.0, 0.2, 0.0)), - frame_id="sensor", - child_frame_id="camera_link", - ), - hardware=lambda: Webcam( - camera_index=0, - fps=15, - stereo_slice="left", - camera_info=zed.CameraInfo.SingleWebcam, - ), - ), - ) - if not global_config.simulation - else autoconnect() -) - -uintree_g1_primitive_no_nav = ( - autoconnect( - _with_vis, - _camera, - voxel_mapper(voxel_size=0.1), - cost_mapper(), - wavefront_frontier_explorer(), - # Visualization - websocket_vis(), - ) - .global_config(n_dask_workers=4, robot_model="unitree_g1") - .transports( - { - # G1 uses Twist for movement commands - ("cmd_vel", Twist): LCMTransport("/cmd_vel", Twist), - # State estimation from ROS - ("state_estimation", Odometry): LCMTransport("/state_estimation", Odometry), - # Odometry output from ROSNavigationModule - ("odom", PoseStamped): LCMTransport("/odom", PoseStamped), - # Navigation module topics from nav_bot - ("goal_req", PoseStamped): LCMTransport("/goal_req", PoseStamped), - ("goal_active", PoseStamped): LCMTransport("/goal_active", PoseStamped), - ("path_active", Path): LCMTransport("/path_active", Path), - ("pointcloud", PointCloud2): LCMTransport("/lidar", PointCloud2), - ("global_pointcloud", PointCloud2): LCMTransport("/map", PointCloud2), - # Original navigation topics for backwards compatibility - ("goal_pose", PoseStamped): LCMTransport("/goal_pose", PoseStamped), - ("goal_reached", Bool): LCMTransport("/goal_reached", Bool), - ("cancel_goal", Bool): LCMTransport("/cancel_goal", Bool), - # Camera topics - ("color_image", Image): LCMTransport("/color_image", Image), - ("camera_info", CameraInfo): LCMTransport("/camera_info", CameraInfo), - } - ) -) - -__all__ = ["uintree_g1_primitive_no_nav"] diff --git a/dimos/robot/unitree/g1/connection.py b/dimos/robot/unitree/g1/connection.py deleted file mode 100644 index f12d0ee0e6..0000000000 --- a/dimos/robot/unitree/g1/connection.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from typing import Any - -from reactivex.disposable import Disposable - -from dimos import spec -from dimos.core import DimosCluster, In, Module, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.msgs.geometry_msgs import Twist -from dimos.robot.unitree.connection import UnitreeWebRTCConnection -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class G1Connection(Module): - cmd_vel: In[Twist] - ip: str | None - connection_type: str | None = None - _global_config: GlobalConfig - - connection: UnitreeWebRTCConnection | None - - def __init__( - self, - ip: str | None = None, - connection_type: str | None = None, - cfg: GlobalConfig = global_config, - *args: Any, - **kwargs: Any, - ) -> None: - self._global_config = cfg - self.ip = ip if ip is not None else self._global_config.robot_ip - self.connection_type = connection_type or self._global_config.unitree_connection_type - self.connection = None - super().__init__(*args, **kwargs) - - @rpc - def start(self) -> None: - super().start() - - match self.connection_type: - case "webrtc": - assert self.ip is not None, "IP address must be provided" - self.connection = UnitreeWebRTCConnection(self.ip) - case "replay": - raise ValueError("Replay connection not implemented for G1 robot") - case "mujoco": - raise ValueError( - "This module does not support simulation, use G1SimConnection instead" - ) - case _: - raise ValueError(f"Unknown connection type: {self.connection_type}") - - assert self.connection is not None - self.connection.start() - - self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) - - @rpc - def stop(self) -> None: - assert self.connection is not None - self.connection.stop() - super().stop() - - @rpc - def move(self, twist: Twist, duration: float = 0.0) -> None: - assert self.connection is not None - self.connection.move(twist, duration) - - @rpc - def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: - logger.info(f"Publishing request to topic: {topic} with data: {data}") - assert self.connection is not None - return self.connection.publish_request(topic, data) # type: ignore[no-any-return] - - -g1_connection = G1Connection.blueprint - - -def deploy(dimos: DimosCluster, ip: str, local_planner: spec.LocalPlanner) -> G1Connection: - connection = dimos.deploy(G1Connection, ip) # type: ignore[attr-defined] - connection.cmd_vel.connect(local_planner.cmd_vel) - connection.start() - return connection # type: ignore[no-any-return] - - -__all__ = ["G1Connection", "deploy", "g1_connection"] diff --git a/dimos/robot/unitree/g1/sim.py b/dimos/robot/unitree/g1/sim.py deleted file mode 100644 index 6888ae74aa..0000000000 --- a/dimos/robot/unitree/g1/sim.py +++ /dev/null @@ -1,177 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import threading -from threading import Thread -import time -from typing import TYPE_CHECKING, Any - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.msgs.geometry_msgs import ( - PoseStamped, - Quaternion, - Transform, - Twist, - Vector3, -) -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.robot.unitree.type.odometry import Odometry as SimOdometry -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.robot.unitree.mujoco_connection import MujocoConnection - -logger = setup_logger() - - -def _camera_info_static() -> CameraInfo: - """Camera intrinsics for rerun visualization (matches Go2 convention).""" - fx, fy, cx, cy = (819.553492, 820.646595, 625.284099, 336.808987) - width, height = (1280, 720) - - return CameraInfo( - frame_id="camera_optical", - height=height, - width=width, - distortion_model="plumb_bob", - D=[0.0, 0.0, 0.0, 0.0, 0.0], - K=[fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0], - R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], - P=[fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0], - binning_x=0, - binning_y=0, - ) - - -class G1SimConnection(Module): - cmd_vel: In[Twist] - lidar: Out[PointCloud2] - odom: Out[PoseStamped] - color_image: Out[Image] - camera_info: Out[CameraInfo] - ip: str | None - _global_config: GlobalConfig - _camera_info_thread: Thread | None = None - - def __init__( - self, - ip: str | None = None, - cfg: GlobalConfig = global_config, - *args: Any, - **kwargs: Any, - ) -> None: - self._global_config = cfg - self.ip = ip if ip is not None else self._global_config.robot_ip - self.connection: MujocoConnection | None = None - self._stop_event = threading.Event() - super().__init__(*args, **kwargs) - - @rpc - def start(self) -> None: - super().start() - - from dimos.robot.unitree.mujoco_connection import MujocoConnection - - self.connection = MujocoConnection(self._global_config) - assert self.connection is not None - self.connection.start() - - self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) - self._disposables.add(self.connection.odom_stream().subscribe(self._publish_sim_odom)) - self._disposables.add(self.connection.lidar_stream().subscribe(self.lidar.publish)) - self._disposables.add(self.connection.video_stream().subscribe(self.color_image.publish)) - - self._camera_info_thread = Thread( - target=self._publish_camera_info_loop, - daemon=True, - ) - self._camera_info_thread.start() - - @rpc - def stop(self) -> None: - self._stop_event.set() - assert self.connection is not None - self.connection.stop() - if self._camera_info_thread and self._camera_info_thread.is_alive(): - self._camera_info_thread.join(timeout=1.0) - super().stop() - - def _publish_camera_info_loop(self) -> None: - info = _camera_info_static() - while not self._stop_event.is_set(): - self.camera_info.publish(info) - self._stop_event.wait(1.0) - - def _publish_tf(self, msg: PoseStamped) -> None: - self.odom.publish(msg) - - self.tf.publish(Transform.from_pose("base_link", msg)) - - # Publish camera_link and camera_optical transforms - camera_link = Transform( - translation=Vector3(0.05, 0.0, 0.6), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=time.time(), - ) - - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=time.time(), - ) - - map_to_world = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="map", - child_frame_id="world", - ts=time.time(), - ) - - self.tf.publish(camera_link, camera_optical, map_to_world) - - def _publish_sim_odom(self, msg: SimOdometry) -> None: - self._publish_tf( - PoseStamped( - ts=msg.ts, - frame_id=msg.frame_id, - position=msg.position, - orientation=msg.orientation, - ) - ) - - @rpc - def move(self, twist: Twist, duration: float = 0.0) -> None: - assert self.connection is not None - self.connection.move(twist, duration) - - @rpc - def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: - logger.info(f"Publishing request to topic: {topic} with data: {data}") - assert self.connection is not None - return self.connection.publish_request(topic, data) - - -g1_sim_connection = G1SimConnection.blueprint - - -__all__ = ["G1SimConnection", "g1_sim_connection"] diff --git a/dimos/robot/unitree/g1/skill_container.py b/dimos/robot/unitree/g1/skill_container.py deleted file mode 100644 index 7ce9730686..0000000000 --- a/dimos/robot/unitree/g1/skill_container.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Unitree G1 skill container for the new agents framework. -Dynamically generates skills for G1 humanoid robot including arm controls and movement modes. -""" - -import difflib - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.msgs.geometry_msgs import Twist, Vector3 -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -# G1 Arm Actions - all use api_id 7106 on topic "rt/api/arm/request" -G1_ARM_CONTROLS = [ - ("Handshake", 27, "Perform a handshake gesture with the right hand."), - ("HighFive", 18, "Give a high five with the right hand."), - ("Hug", 19, "Perform a hugging gesture with both arms."), - ("HighWave", 26, "Wave with the hand raised high."), - ("Clap", 17, "Clap hands together."), - ("FaceWave", 25, "Wave near the face level."), - ("LeftKiss", 12, "Blow a kiss with the left hand."), - ("ArmHeart", 20, "Make a heart shape with both arms overhead."), - ("RightHeart", 21, "Make a heart gesture with the right hand."), - ("HandsUp", 15, "Raise both hands up in the air."), - ("XRay", 24, "Hold arms in an X-ray pose position."), - ("RightHandUp", 23, "Raise only the right hand up."), - ("Reject", 22, "Make a rejection or 'no' gesture."), - ("CancelAction", 99, "Cancel any current arm action and return hands to neutral position."), -] - -# G1 Movement Modes - all use api_id 7101 on topic "rt/api/sport/request" -G1_MODE_CONTROLS = [ - ("WalkMode", 500, "Switch to normal walking mode."), - ("WalkControlWaist", 501, "Switch to walking mode with waist control."), - ("RunMode", 801, "Switch to running mode."), -] - -_ARM_COMMANDS: dict[str, tuple[int, str]] = { - name: (id_, description) for name, id_, description in G1_ARM_CONTROLS -} - -_MODE_COMMANDS: dict[str, tuple[int, str]] = { - name: (id_, description) for name, id_, description in G1_MODE_CONTROLS -} - - -class UnitreeG1SkillContainer(Module): - rpc_calls: list[str] = [ - "G1Connection.move", - "G1Connection.publish_request", - ] - - @rpc - def start(self) -> None: - super().start() - - @rpc - def stop(self) -> None: - super().stop() - - @skill - def move(self, x: float, y: float = 0.0, yaw: float = 0.0, duration: float = 0.0) -> str: - """Move the robot using direct velocity commands. Determine duration required based on user distance instructions. - - Example call: - args = { "x": 0.5, "y": 0.0, "yaw": 0.0, "duration": 2.0 } - move(**args) - - Args: - x: Forward velocity (m/s) - y: Left/right velocity (m/s) - yaw: Rotational velocity (rad/s) - duration: How long to move (seconds) - """ - - move_rpc = self.get_rpc_calls("G1Connection.move") - twist = Twist(linear=Vector3(x, y, 0), angular=Vector3(0, 0, yaw)) - move_rpc(twist, duration=duration) - return f"Started moving with velocity=({x}, {y}, {yaw}) for {duration} seconds" - - @skill - def execute_arm_command(self, command_name: str) -> str: - return self._execute_g1_command(_ARM_COMMANDS, 7106, "rt/api/arm/request", command_name) - - @skill - def execute_mode_command(self, command_name: str) -> str: - return self._execute_g1_command(_MODE_COMMANDS, 7101, "rt/api/sport/request", command_name) - - def _execute_g1_command( - self, - command_dict: dict[str, tuple[int, str]], - api_id: int, - topic: str, - command_name: str, - ) -> str: - publish_request_rpc = self.get_rpc_calls("G1Connection.publish_request") - - if command_name not in command_dict: - suggestions = difflib.get_close_matches( - command_name, command_dict.keys(), n=3, cutoff=0.6 - ) - return f"There's no '{command_name}' command. Did you mean: {suggestions}" - - id_, _ = command_dict[command_name] - - try: - publish_request_rpc(topic, {"api_id": api_id, "parameter": {"data": id_}}) - return f"'{command_name}' command executed successfully." - except Exception as e: - logger.error(f"Failed to execute {command_name}: {e}") - return "Failed to execute the command." - - -_arm_commands = "\n".join( - [f'- "{name}": {description}' for name, (_, description) in _ARM_COMMANDS.items()] -) - -UnitreeG1SkillContainer.execute_arm_command.__doc__ = f"""Execute a Unitree G1 arm command. - -Example usage: - - execute_arm_command("ArmHeart") - -Here are all the command names and what they do. - -{_arm_commands} -""" - -_mode_commands = "\n".join( - [f'- "{name}": {description}' for name, (_, description) in _MODE_COMMANDS.items()] -) - -UnitreeG1SkillContainer.execute_mode_command.__doc__ = f"""Execute a Unitree G1 mode command. - -Example usage: - - execute_mode_command("RunMode") - -Here are all the command names and what they do. - -{_mode_commands} -""" - -g1_skills = UnitreeG1SkillContainer.blueprint - -__all__ = ["UnitreeG1SkillContainer", "g1_skills"] diff --git a/dimos/robot/unitree/go2/blueprints/__init__.py b/dimos/robot/unitree/go2/blueprints/__init__.py deleted file mode 100644 index cbc49694f3..0000000000 --- a/dimos/robot/unitree/go2/blueprints/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Cascaded GO2 blueprints split into focused modules.""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "agentic._common_agentic": ["_common_agentic"], - "agentic.unitree_go2_agentic": ["unitree_go2_agentic"], - "agentic.unitree_go2_agentic_huggingface": ["unitree_go2_agentic_huggingface"], - "agentic.unitree_go2_agentic_mcp": ["unitree_go2_agentic_mcp"], - "agentic.unitree_go2_agentic_ollama": ["unitree_go2_agentic_ollama"], - "agentic.unitree_go2_temporal_memory": ["unitree_go2_temporal_memory"], - "basic.unitree_go2_basic": ["_linux", "_mac", "unitree_go2_basic"], - "smart._with_jpeg": ["_with_jpeglcm"], - "smart.unitree_go2": ["unitree_go2"], - "smart.unitree_go2_detection": ["unitree_go2_detection"], - "smart.unitree_go2_ros": ["unitree_go2_ros"], - "smart.unitree_go2_spatial": ["unitree_go2_spatial"], - "smart.unitree_go2_vlm_stream_test": ["unitree_go2_vlm_stream_test"], - }, -) diff --git a/dimos/robot/unitree/go2/blueprints/agentic/__init__.py b/dimos/robot/unitree/go2/blueprints/agentic/__init__.py deleted file mode 100644 index 84d1b41b23..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Agentic blueprints for Unitree GO2.""" diff --git a/dimos/robot/unitree/go2/blueprints/agentic/_common_agentic.py b/dimos/robot/unitree/go2/blueprints/agentic/_common_agentic.py deleted file mode 100644 index 817d5e3a7d..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/_common_agentic.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.skills.navigation import navigation_skill -from dimos.agents.skills.person_follow import person_follow_skill -from dimos.agents.skills.speak_skill import speak_skill -from dimos.agents.web_human_input import web_input -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.connection import GO2Connection -from dimos.robot.unitree.unitree_skill_container import unitree_skills - -_common_agentic = autoconnect( - navigation_skill(), - person_follow_skill(camera_info=GO2Connection.camera_info_static), - unitree_skills(), - web_input(), - speak_skill(), -) - -__all__ = ["_common_agentic"] diff --git a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic.py b/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic.py deleted file mode 100644 index 2fb1a4cb74..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.blueprints.agentic._common_agentic import _common_agentic -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial import unitree_go2_spatial - -unitree_go2_agentic = autoconnect( - unitree_go2_spatial, - agent(), - _common_agentic, -) - -__all__ = ["unitree_go2_agentic"] diff --git a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_huggingface.py b/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_huggingface.py deleted file mode 100644 index 1c998b7495..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_huggingface.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.blueprints.agentic._common_agentic import _common_agentic -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial import unitree_go2_spatial - -unitree_go2_agentic_huggingface = autoconnect( - unitree_go2_spatial, - agent(model="huggingface:Qwen/Qwen2.5-1.5B-Instruct"), - _common_agentic, -) - -__all__ = ["unitree_go2_agentic_huggingface"] diff --git a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_mcp.py b/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_mcp.py deleted file mode 100644 index bbc3e4c216..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_mcp.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.protocol.mcp.mcp import MCPModule -from dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic import unitree_go2_agentic - -unitree_go2_agentic_mcp = autoconnect( - unitree_go2_agentic, - MCPModule.blueprint(), -) - -__all__ = ["unitree_go2_agentic_mcp"] diff --git a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_ollama.py b/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_ollama.py deleted file mode 100644 index 6a518ad831..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_agentic_ollama.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.agent import agent -from dimos.agents.ollama_agent import ollama_installed -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.blueprints.agentic._common_agentic import _common_agentic -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2_spatial import unitree_go2_spatial - -unitree_go2_agentic_ollama = autoconnect( - unitree_go2_spatial, - agent(model="ollama:qwen3:8b"), - _common_agentic, -).requirements( - ollama_installed, -) - -__all__ = ["unitree_go2_agentic_ollama"] diff --git a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_temporal_memory.py b/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_temporal_memory.py deleted file mode 100644 index 017ccaba2b..0000000000 --- a/dimos/robot/unitree/go2/blueprints/agentic/unitree_go2_temporal_memory.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.perception.experimental.temporal_memory import temporal_memory -from dimos.robot.unitree.go2.blueprints.agentic.unitree_go2_agentic import unitree_go2_agentic - -unitree_go2_temporal_memory = autoconnect( - unitree_go2_agentic, - temporal_memory(), -) - -__all__ = ["unitree_go2_temporal_memory"] diff --git a/dimos/robot/unitree/go2/blueprints/basic/__init__.py b/dimos/robot/unitree/go2/blueprints/basic/__init__.py deleted file mode 100644 index 79964b0297..0000000000 --- a/dimos/robot/unitree/go2/blueprints/basic/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Basic blueprints for Unitree GO2.""" diff --git a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_basic.py b/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_basic.py deleted file mode 100644 index cfd53abe51..0000000000 --- a/dimos/robot/unitree/go2/blueprints/basic/unitree_go2_basic.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import platform - -from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE -from dimos.core.blueprints import autoconnect -from dimos.core.global_config import global_config -from dimos.core.transport import pSHMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.protocol.pubsub.impl.lcmpubsub import LCM -from dimos.robot.unitree.go2.connection import go2_connection -from dimos.web.websocket_vis.websocket_vis_module import websocket_vis - -# Mac has some issue with high bandwidth UDP, so we use pSHMTransport for color_image -# actually we can use pSHMTransport for all platforms, and for all streams -# TODO need a global transport toggle on blueprints/global config -_mac_transports: dict[tuple[str, type], pSHMTransport[Image]] = { - ("color_image", Image): pSHMTransport( - "color_image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ), -} - -_transports_base = ( - autoconnect() if platform.system() == "Linux" else autoconnect().transports(_mac_transports) -) - -rerun_config = { - # any pubsub that supports subscribe_all and topic that supports str(topic) - # is acceptable here - "pubsubs": [LCM(autoconf=True)], - # Custom converters for specific rerun entity paths - # Normally all these would be specified in their respectative modules - # Until this is implemented we have central overrides here - # - # This is unsustainable once we move to multi robot etc - "visual_override": { - "world/camera_info": lambda camera_info: camera_info.to_rerun( - image_topic="/world/color_image", - optical_frame="camera_optical", - ), - "world/global_map": lambda grid: grid.to_rerun(voxel_size=0.1, mode="boxes"), - "world/navigation_costmap": lambda grid: grid.to_rerun( - colormap="Accent", - z_offset=0.015, - opacity=0.2, - background="#484981", - ), - }, - # slapping a go2 shaped box on top of tf/base_link - "static": { - "world/tf/base_link": lambda rr: [ - rr.Boxes3D( - half_sizes=[0.35, 0.155, 0.2], - colors=[(0, 255, 127)], - fill_mode="wireframe", - ), - rr.Transform3D(parent_frame="tf#/base_link"), - ] - }, -} - - -match global_config.viewer_backend: - case "foxglove": - from dimos.robot.foxglove_bridge import foxglove_bridge - - with_vis = autoconnect( - _transports_base, - foxglove_bridge(shm_channels=["/color_image#sensor_msgs.Image"]), - ) - case "rerun": - from dimos.visualization.rerun.bridge import rerun_bridge - - with_vis = autoconnect(_transports_base, rerun_bridge(**rerun_config)) - case "rerun-web": - from dimos.visualization.rerun.bridge import rerun_bridge - - with_vis = autoconnect(_transports_base, rerun_bridge(viewer_mode="web", **rerun_config)) - case _: - with_vis = _transports_base - -unitree_go2_basic = autoconnect( - with_vis, - go2_connection(), - websocket_vis(), -).global_config(n_dask_workers=4, robot_model="unitree_go2") - -__all__ = [ - "unitree_go2_basic", -] diff --git a/dimos/robot/unitree/go2/blueprints/smart/__init__.py b/dimos/robot/unitree/go2/blueprints/smart/__init__.py deleted file mode 100644 index 7d5bdbc3ab..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Smart blueprints for Unitree GO2.""" diff --git a/dimos/robot/unitree/go2/blueprints/smart/_with_jpeg.py b/dimos/robot/unitree/go2/blueprints/smart/_with_jpeg.py deleted file mode 100644 index 9c77d599cf..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/_with_jpeg.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.transport import JpegLcmTransport -from dimos.msgs.sensor_msgs import Image -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2 - -_with_jpeglcm = unitree_go2.transports( - { - ("color_image", Image): JpegLcmTransport("/color_image", Image), - } -) - -__all__ = ["_with_jpeglcm"] diff --git a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2.py b/dimos/robot/unitree/go2/blueprints/smart/unitree_go2.py deleted file mode 100644 index 5d096444d5..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.mapping.costmapper import cost_mapper -from dimos.mapping.voxels import voxel_mapper -from dimos.navigation.frontier_exploration import wavefront_frontier_explorer -from dimos.navigation.replanning_a_star.module import replanning_a_star_planner -from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import unitree_go2_basic - -unitree_go2 = autoconnect( - unitree_go2_basic, - voxel_mapper(voxel_size=0.1), - cost_mapper(), - replanning_a_star_planner(), - wavefront_frontier_explorer(), -).global_config(n_dask_workers=6, robot_model="unitree_go2") - -__all__ = ["unitree_go2"] diff --git a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py b/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py deleted file mode 100644 index f2edf2cb3b..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, # type: ignore[import-untyped] -) -from dimos_lcm.foxglove_msgs.SceneUpdate import SceneUpdate # type: ignore[import-untyped] - -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.vision_msgs import Detection2DArray -from dimos.perception.detection.module3D import Detection3DModule, detection3d_module -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2 -from dimos.robot.unitree.go2.connection import GO2Connection - -unitree_go2_detection = ( - autoconnect( - unitree_go2, - detection3d_module( - camera_info=GO2Connection.camera_info_static, - ), - ) - .remappings( - [ - (Detection3DModule, "pointcloud", "global_map"), - ] - ) - .transports( - { - # Detection 3D module outputs - ("detections", Detection3DModule): LCMTransport( - "/detector3d/detections", Detection2DArray - ), - ("annotations", Detection3DModule): LCMTransport( - "/detector3d/annotations", ImageAnnotations - ), - ("scene_update", Detection3DModule): LCMTransport( - "/detector3d/scene_update", SceneUpdate - ), - ("detected_pointcloud_0", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/0", PointCloud2 - ), - ("detected_pointcloud_1", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/1", PointCloud2 - ), - ("detected_pointcloud_2", Detection3DModule): LCMTransport( - "/detector3d/pointcloud/2", PointCloud2 - ), - ("detected_image_0", Detection3DModule): LCMTransport("/detector3d/image/0", Image), - ("detected_image_1", Detection3DModule): LCMTransport("/detector3d/image/1", Image), - ("detected_image_2", Detection3DModule): LCMTransport("/detector3d/image/2", Image), - } - ) -) - -__all__ = ["unitree_go2_detection"] diff --git a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_ros.py b/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_ros.py deleted file mode 100644 index a335b1e9af..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_ros.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.transport import ROSTransport -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2 - -unitree_go2_ros = unitree_go2.transports( - { - ("lidar", PointCloud2): ROSTransport("lidar", PointCloud2), - ("global_map", PointCloud2): ROSTransport("global_map", PointCloud2), - ("odom", PoseStamped): ROSTransport("odom", PoseStamped), - ("color_image", Image): ROSTransport("color_image", Image), - } -) - -__all__ = ["unitree_go2_ros"] diff --git a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_spatial.py b/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_spatial.py deleted file mode 100644 index e2695f9bfb..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_spatial.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.perception.spatial_perception import spatial_memory -from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2 -from dimos.utils.monitoring import utilization - -unitree_go2_spatial = autoconnect( - unitree_go2, - spatial_memory(), - utilization(), -).global_config(n_dask_workers=8) - -__all__ = ["unitree_go2_spatial"] diff --git a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_vlm_stream_test.py b/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_vlm_stream_test.py deleted file mode 100644 index 194d3973c6..0000000000 --- a/dimos/robot/unitree/go2/blueprints/smart/unitree_go2_vlm_stream_test.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.agents.vlm_agent import vlm_agent -from dimos.agents.vlm_stream_tester import vlm_stream_tester -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import unitree_go2_basic - -unitree_go2_vlm_stream_test = autoconnect( - unitree_go2_basic, - vlm_agent(), - vlm_stream_tester(), -) - -__all__ = ["unitree_go2_vlm_stream_test"] diff --git a/dimos/robot/unitree/go2/connection.py b/dimos/robot/unitree/go2/connection.py deleted file mode 100644 index 82aa34c97a..0000000000 --- a/dimos/robot/unitree/go2/connection.py +++ /dev/null @@ -1,333 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -from threading import Thread -import time -from typing import Any, Protocol - -from reactivex.disposable import Disposable -from reactivex.observable import Observable -import rerun.blueprint as rrb - -from dimos import spec -from dimos.agents.annotation import skill -from dimos.core import DimosCluster, In, LCMTransport, Module, Out, pSHMTransport, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.msgs.geometry_msgs import ( - PoseStamped, - Quaternion, - Transform, - Twist, - Vector3, -) -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.msgs.sensor_msgs.Image import ImageFormat -from dimos.robot.unitree.connection import UnitreeWebRTCConnection -from dimos.utils.data import get_data -from dimos.utils.decorators.decorators import simple_mcache -from dimos.utils.testing.replay import TimedSensorReplay, TimedSensorStorage - -logger = logging.getLogger(__name__) - - -class Go2ConnectionProtocol(Protocol): - """Protocol defining the interface for Go2 robot connections.""" - - def start(self) -> None: ... - def stop(self) -> None: ... - def lidar_stream(self) -> Observable: ... # type: ignore[type-arg] - def odom_stream(self) -> Observable: ... # type: ignore[type-arg] - def video_stream(self) -> Observable: ... # type: ignore[type-arg] - def move(self, twist: Twist, duration: float = 0.0) -> bool: ... - def standup(self) -> bool: ... - def liedown(self) -> bool: ... - def publish_request(self, topic: str, data: dict) -> dict: ... # type: ignore[type-arg] - - -def _camera_info_static() -> CameraInfo: - fx, fy, cx, cy = (819.553492, 820.646595, 625.284099, 336.808987) - width, height = (1280, 720) - - return CameraInfo( - frame_id="camera_optical", - height=height, - width=width, - distortion_model="plumb_bob", - D=[0.0, 0.0, 0.0, 0.0, 0.0], - K=[fx, 0.0, cx, 0.0, fy, cy, 0.0, 0.0, 1.0], - R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], - P=[fx, 0.0, cx, 0.0, 0.0, fy, cy, 0.0, 0.0, 0.0, 1.0, 0.0], - binning_x=0, - binning_y=0, - ) - - -class ReplayConnection(UnitreeWebRTCConnection): - dir_name = "unitree_go2_bigoffice" - - # we don't want UnitreeWebRTCConnection to init - def __init__( # type: ignore[no-untyped-def] - self, - **kwargs, - ) -> None: - get_data(self.dir_name) - self.replay_config = { - "loop": kwargs.get("loop"), - "seek": kwargs.get("seek"), - "duration": kwargs.get("duration"), - } - - def connect(self) -> None: - pass - - def start(self) -> None: - pass - - def standup(self) -> bool: - return True - - def liedown(self) -> bool: - return True - - @simple_mcache - def lidar_stream(self): # type: ignore[no-untyped-def] - lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") # type: ignore[var-annotated] - return lidar_store.stream(**self.replay_config) # type: ignore[arg-type] - - @simple_mcache - def odom_stream(self): # type: ignore[no-untyped-def] - odom_store = TimedSensorReplay(f"{self.dir_name}/odom") # type: ignore[var-annotated] - return odom_store.stream(**self.replay_config) # type: ignore[arg-type] - - # we don't have raw video stream in the data set - @simple_mcache - def video_stream(self): # type: ignore[no-untyped-def] - # Legacy Unitree recordings can have RGB bytes that were tagged/assumed as BGR. - # Fix at replay-time by coercing everything to RGB before publishing/logging. - def _autocast_video(x): # type: ignore[no-untyped-def] - # If the old recording tagged it as BGR, relabel to RGB (do NOT channel-swap again). - if isinstance(x, Image): - if x.format == ImageFormat.BGR: - x.format = ImageFormat.RGB - if not x.frame_id: - x.frame_id = "camera_optical" - return x - - # Some recordings may store raw arrays or frame wrappers. - arr = x.to_ndarray(format="rgb24") if hasattr(x, "to_ndarray") else x - return Image.from_numpy(arr, format=ImageFormat.RGB, frame_id="camera_optical") - - video_store = TimedSensorReplay(f"{self.dir_name}/video", autocast=_autocast_video) # type: ignore[var-annotated] - return video_store.stream(**self.replay_config) # type: ignore[arg-type] - - def move(self, twist: Twist, duration: float = 0.0) -> bool: - return True - - def publish_request(self, topic: str, data: dict): # type: ignore[no-untyped-def, type-arg] - """Fake publish request for testing.""" - return {"status": "ok", "message": "Fake publish"} - - -class GO2Connection(Module, spec.Camera, spec.Pointcloud): - cmd_vel: In[Twist] - pointcloud: Out[PointCloud2] - odom: Out[PoseStamped] - lidar: Out[PointCloud2] - color_image: Out[Image] - camera_info: Out[CameraInfo] - - connection: Go2ConnectionProtocol - camera_info_static: CameraInfo = _camera_info_static() - _global_config: GlobalConfig - _camera_info_thread: Thread | None = None - _latest_video_frame: Image | None = None - - @classmethod - def rerun_views(cls): # type: ignore[no-untyped-def] - """Return Rerun view blueprints for GO2 camera visualization.""" - return [ - rrb.Spatial2DView( - name="Camera", - origin="world/robot/camera/rgb", - ), - ] - - def __init__( # type: ignore[no-untyped-def] - self, - ip: str | None = None, - cfg: GlobalConfig = global_config, - *args, - **kwargs, - ) -> None: - self._global_config = cfg - - ip = ip if ip is not None else self._global_config.robot_ip - - connection_type = self._global_config.unitree_connection_type - - if ip in ["fake", "mock", "replay"] or connection_type == "replay": - self.connection = ReplayConnection() - elif ip == "mujoco" or connection_type == "mujoco": - from dimos.robot.unitree.mujoco_connection import MujocoConnection - - self.connection = MujocoConnection(self._global_config) - else: - assert ip is not None, "IP address must be provided" - self.connection = UnitreeWebRTCConnection(ip) - - Module.__init__(self, *args, **kwargs) - - @rpc - def record(self, recording_name: str) -> None: - lidar_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/lidar") # type: ignore[type-arg] - lidar_store.consume_stream(self.connection.lidar_stream()) - - odom_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/odom") # type: ignore[type-arg] - odom_store.consume_stream(self.connection.odom_stream()) - - video_store: TimedSensorStorage = TimedSensorStorage(f"{recording_name}/video") # type: ignore[type-arg] - video_store.consume_stream(self.connection.video_stream()) - - @rpc - def start(self) -> None: - super().start() - - self.connection.start() - - def onimage(image: Image) -> None: - self.color_image.publish(image) - self._latest_video_frame = image - - self._disposables.add(self.connection.lidar_stream().subscribe(self.lidar.publish)) - self._disposables.add(self.connection.odom_stream().subscribe(self._publish_tf)) - self._disposables.add(self.connection.video_stream().subscribe(onimage)) - self._disposables.add(Disposable(self.cmd_vel.subscribe(self.move))) - - self._camera_info_thread = Thread( - target=self.publish_camera_info, - daemon=True, - ) - self._camera_info_thread.start() - - self.standup() - # self.record("go2_bigoffice") - - @rpc - def stop(self) -> None: - self.liedown() - - if self.connection: - self.connection.stop() - - if self._camera_info_thread and self._camera_info_thread.is_alive(): - self._camera_info_thread.join(timeout=1.0) - - super().stop() - - @classmethod - def _odom_to_tf(cls, odom: PoseStamped) -> list[Transform]: - camera_link = Transform( - translation=Vector3(0.3, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=odom.ts, - ) - - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=odom.ts, - ) - - return [ - Transform.from_pose("base_link", odom), - camera_link, - camera_optical, - ] - - def _publish_tf(self, msg: PoseStamped) -> None: - transforms = self._odom_to_tf(msg) - self.tf.publish(*transforms) - if self.odom.transport: - self.odom.publish(msg) - - def publish_camera_info(self) -> None: - while True: - self.camera_info.publish(_camera_info_static()) - time.sleep(1.0) - - @rpc - def move(self, twist: Twist, duration: float = 0.0) -> bool: - """Send movement command to robot.""" - return self.connection.move(twist, duration) - - @rpc - def standup(self) -> bool: - """Make the robot stand up.""" - return self.connection.standup() - - @rpc - def liedown(self) -> bool: - """Make the robot lie down.""" - return self.connection.liedown() - - @rpc - def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: - """Publish a request to the WebRTC connection. - Args: - topic: The RTC topic to publish to - data: The data dictionary to publish - Returns: - The result of the publish request - """ - return self.connection.publish_request(topic, data) - - @skill - def observe(self) -> Image | None: - """Returns the latest video frame from the robot camera. Use this skill for any visual world queries. - - This skill provides the current camera view for perception tasks. - Returns None if no frame has been captured yet. - """ - return self._latest_video_frame - - -go2_connection = GO2Connection.blueprint - - -def deploy(dimos: DimosCluster, ip: str, prefix: str = "") -> GO2Connection: - from dimos.constants import DEFAULT_CAPACITY_COLOR_IMAGE - - connection = dimos.deploy(GO2Connection, ip) # type: ignore[attr-defined] - - connection.pointcloud.transport = pSHMTransport( - f"{prefix}/lidar", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - connection.color_image.transport = pSHMTransport( - f"{prefix}/image", default_capacity=DEFAULT_CAPACITY_COLOR_IMAGE - ) - - connection.cmd_vel.transport = LCMTransport(f"{prefix}/cmd_vel", Twist) - - connection.camera_info.transport = LCMTransport(f"{prefix}/camera_info", CameraInfo) - connection.start() - - return connection # type: ignore[no-any-return] - - -__all__ = ["GO2Connection", "deploy", "go2_connection"] diff --git a/dimos/robot/unitree/go2/go2.urdf b/dimos/robot/unitree/go2/go2.urdf deleted file mode 100644 index 4e67e9ca8e..0000000000 --- a/dimos/robot/unitree/go2/go2.urdf +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/dimos/robot/unitree/keyboard_teleop.py b/dimos/robot/unitree/keyboard_teleop.py deleted file mode 100644 index 3d7d4c263e..0000000000 --- a/dimos/robot/unitree/keyboard_teleop.py +++ /dev/null @@ -1,205 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import threading - -import pygame - -from dimos.core import Module, Out, rpc -from dimos.msgs.geometry_msgs import Twist, Vector3 - -# Force X11 driver to avoid OpenGL threading issues -os.environ["SDL_VIDEODRIVER"] = "x11" - - -class KeyboardTeleop(Module): - """Pygame-based keyboard control module. - - Outputs standard Twist messages on /cmd_vel for velocity control. - """ - - cmd_vel: Out[Twist] # Standard velocity commands - - _stop_event: threading.Event - _keys_held: set[int] | None = None - _thread: threading.Thread | None = None - _screen: pygame.Surface | None = None - _clock: pygame.time.Clock | None = None - _font: pygame.font.Font | None = None - - def __init__(self) -> None: - super().__init__() - self._stop_event = threading.Event() - - @rpc - def start(self) -> None: - super().start() - - self._keys_held = set() - self._stop_event.clear() - - self._thread = threading.Thread(target=self._pygame_loop, daemon=True) - self._thread.start() - - return - - @rpc - def stop(self) -> None: - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - self.cmd_vel.publish(stop_twist) - - self._stop_event.set() - - if self._thread is None: - raise RuntimeError("Cannot stop: thread was never started") - self._thread.join(2) - - super().stop() - - def _pygame_loop(self) -> None: - if self._keys_held is None: - raise RuntimeError("_keys_held not initialized") - - pygame.init() - self._screen = pygame.display.set_mode((500, 400), pygame.SWSURFACE) - pygame.display.set_caption("Keyboard Teleop") - self._clock = pygame.time.Clock() - self._font = pygame.font.Font(None, 24) - - while not self._stop_event.is_set(): - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self._stop_event.set() - elif event.type == pygame.KEYDOWN: - self._keys_held.add(event.key) - - if event.key == pygame.K_SPACE: - # Emergency stop - clear all keys and send zero twist - self._keys_held.clear() - stop_twist = Twist() - stop_twist.linear = Vector3(0, 0, 0) - stop_twist.angular = Vector3(0, 0, 0) - self.cmd_vel.publish(stop_twist) - print("EMERGENCY STOP!") - elif event.key == pygame.K_ESCAPE: - # ESC quits - self._stop_event.set() - - elif event.type == pygame.KEYUP: - self._keys_held.discard(event.key) - - # Generate Twist message from held keys - twist = Twist() - twist.linear = Vector3(0, 0, 0) - twist.angular = Vector3(0, 0, 0) - - # Forward/backward (W/S) - if pygame.K_w in self._keys_held: - twist.linear.x = 0.5 - if pygame.K_s in self._keys_held: - twist.linear.x = -0.5 - - # Strafe left/right (Q/E) - if pygame.K_q in self._keys_held: - twist.linear.y = 0.5 - if pygame.K_e in self._keys_held: - twist.linear.y = -0.5 - - # Turning (A/D) - if pygame.K_a in self._keys_held: - twist.angular.z = 0.8 - if pygame.K_d in self._keys_held: - twist.angular.z = -0.8 - - # Apply speed modifiers (Shift = 2x, Ctrl = 0.5x) - speed_multiplier = 1.0 - if pygame.K_LSHIFT in self._keys_held or pygame.K_RSHIFT in self._keys_held: - speed_multiplier = 2.0 - elif pygame.K_LCTRL in self._keys_held or pygame.K_RCTRL in self._keys_held: - speed_multiplier = 0.5 - - twist.linear.x *= speed_multiplier - twist.linear.y *= speed_multiplier - twist.angular.z *= speed_multiplier - - # Always publish twist at 50Hz - self.cmd_vel.publish(twist) - - self._update_display(twist) - - # Maintain 50Hz rate - if self._clock is None: - raise RuntimeError("_clock not initialized") - self._clock.tick(50) - - pygame.quit() - - def _update_display(self, twist: Twist) -> None: - if self._screen is None or self._font is None or self._keys_held is None: - raise RuntimeError("Not initialized correctly") - - self._screen.fill((30, 30, 30)) - - y_pos = 20 - - # Determine active speed multiplier - speed_mult_text = "" - if pygame.K_LSHIFT in self._keys_held or pygame.K_RSHIFT in self._keys_held: - speed_mult_text = " [BOOST 2x]" - elif pygame.K_LCTRL in self._keys_held or pygame.K_RCTRL in self._keys_held: - speed_mult_text = " [SLOW 0.5x]" - - texts = [ - "Keyboard Teleop" + speed_mult_text, - "", - f"Linear X (Forward/Back): {twist.linear.x:+.2f} m/s", - f"Linear Y (Strafe L/R): {twist.linear.y:+.2f} m/s", - f"Angular Z (Turn L/R): {twist.angular.z:+.2f} rad/s", - "", - "Keys: " + ", ".join([pygame.key.name(k).upper() for k in self._keys_held if k < 256]), - ] - - for text in texts: - if text: - color = (0, 255, 255) if text.startswith("Keyboard Teleop") else (255, 255, 255) - surf = self._font.render(text, True, color) - self._screen.blit(surf, (20, y_pos)) - y_pos += 30 - - if twist.linear.x != 0 or twist.linear.y != 0 or twist.angular.z != 0: - pygame.draw.circle(self._screen, (255, 0, 0), (450, 30), 15) # Red = moving - else: - pygame.draw.circle(self._screen, (0, 255, 0), (450, 30), 15) # Green = stopped - - y_pos = 280 - help_texts = [ - "WS: Move | AD: Turn | QE: Strafe", - "Shift: Boost | Ctrl: Slow", - "Space: E-Stop | ESC: Quit", - ] - for text in help_texts: - surf = self._font.render(text, True, (150, 150, 150)) - self._screen.blit(surf, (20, y_pos)) - y_pos += 25 - - pygame.display.flip() - - -keyboard_teleop = KeyboardTeleop.blueprint - -__all__ = ["KeyboardTeleop", "keyboard_teleop"] diff --git a/dimos/robot/unitree/modular/detect.py b/dimos/robot/unitree/modular/detect.py deleted file mode 100644 index e5999e9fd8..0000000000 --- a/dimos/robot/unitree/modular/detect.py +++ /dev/null @@ -1,186 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pickle - -from dimos_lcm.sensor_msgs import CameraInfo - -from dimos.msgs.sensor_msgs import Image, PointCloud2 -from dimos.msgs.std_msgs import Header -from dimos.robot.unitree.type.lidar import pointcloud2_from_webrtc_lidar -from dimos.robot.unitree.type.odometry import Odometry - -image_resize_factor = 1 -originalwidth, originalheight = (1280, 720) - - -def camera_info() -> CameraInfo: - fx, fy, cx, cy = list( - map( - lambda x: int(x / image_resize_factor), - [819.553492, 820.646595, 625.284099, 336.808987], - ) - ) - width, height = tuple( - map( - lambda x: int(x / image_resize_factor), - [originalwidth, originalheight], - ) - ) - - # Camera matrix K (3x3) - K = [fx, 0, cx, 0, fy, cy, 0, 0, 1] - - # No distortion coefficients for now - D = [0.0, 0.0, 0.0, 0.0, 0.0] - - # Identity rotation matrix - R = [1, 0, 0, 0, 1, 0, 0, 0, 1] - - # Projection matrix P (3x4) - P = [fx, 0, cx, 0, 0, fy, cy, 0, 0, 0, 1, 0] - - base_msg = { - "D_length": len(D), - "height": height, - "width": width, - "distortion_model": "plumb_bob", - "D": D, - "K": K, - "R": R, - "P": P, - "binning_x": 0, - "binning_y": 0, - } - - return CameraInfo( - **base_msg, - header=Header("camera_optical"), - ) - - -def transform_chain(odom_frame: Odometry) -> list: # type: ignore[type-arg] - from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 - from dimos.protocol.tf import TF - - camera_link = Transform( - translation=Vector3(0.3, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=odom_frame.ts, - ) - - camera_optical = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=camera_link.ts, - ) - - tf = TF() - tf.publish( - Transform.from_pose("base_link", odom_frame), - camera_link, - camera_optical, - ) - - return tf # type: ignore[return-value] - - -def broadcast( # type: ignore[no-untyped-def] - timestamp: float, - lidar_frame: PointCloud2, - video_frame: Image, - odom_frame: Odometry, - detections, - annotations, -) -> None: - from dimos_lcm.foxglove_msgs.ImageAnnotations import ( - ImageAnnotations, - ) - - from dimos.core import LCMTransport - from dimos.msgs.geometry_msgs import PoseStamped - - lidar_transport = LCMTransport("/lidar", PointCloud2) # type: ignore[var-annotated] - odom_transport = LCMTransport("/odom", PoseStamped) # type: ignore[var-annotated] - video_transport = LCMTransport("/image", Image) # type: ignore[var-annotated] - camera_info_transport = LCMTransport("/camera_info", CameraInfo) # type: ignore[var-annotated] - - lidar_transport.broadcast(None, lidar_frame) - video_transport.broadcast(None, video_frame) - odom_transport.broadcast(None, odom_frame) - camera_info_transport.broadcast(None, camera_info()) - - transform_chain(odom_frame) - - print(lidar_frame) - print(video_frame) - print(odom_frame) - video_transport = LCMTransport("/image", Image) - annotations_transport = LCMTransport("/annotations", ImageAnnotations) # type: ignore[var-annotated] - annotations_transport.broadcast(None, annotations) - - -def process_data(): # type: ignore[no-untyped-def] - from dimos.msgs.sensor_msgs import Image - from dimos.perception.detection.module2D import ( # type: ignore[attr-defined] - Detection2DModule, - build_imageannotations, - ) - from dimos.robot.unitree.type.odometry import Odometry - from dimos.utils.data import get_data - from dimos.utils.testing import TimedSensorReplay - - get_data("unitree_office_walk") - target = 1751591272.9654856 - lidar_store = TimedSensorReplay( - "unitree_office_walk/lidar", autocast=pointcloud2_from_webrtc_lidar - ) - video_store = TimedSensorReplay("unitree_office_walk/video", autocast=Image.from_numpy) - odom_store = TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - def attach_frame_id(image: Image) -> Image: - image.frame_id = "camera_optical" - return image - - lidar_frame = lidar_store.find_closest(target, tolerance=1) - video_frame = attach_frame_id(video_store.find_closest(target, tolerance=1)) # type: ignore[arg-type] - odom_frame = odom_store.find_closest(target, tolerance=1) - - detector = Detection2DModule() - detections = detector.detect(video_frame) # type: ignore[attr-defined] - annotations = build_imageannotations(detections) - - data = (target, lidar_frame, video_frame, odom_frame, detections, annotations) - - with open("filename.pkl", "wb") as file: - pickle.dump(data, file) - - return data - - -def main() -> None: - try: - with open("filename.pkl", "rb") as file: - data = pickle.load(file) - except FileNotFoundError: - print("Processing data and creating pickle file...") - data = process_data() # type: ignore[no-untyped-call] - broadcast(*data) - - -main() diff --git a/dimos/robot/unitree/mujoco_connection.py b/dimos/robot/unitree/mujoco_connection.py deleted file mode 100644 index f998ae1dd9..0000000000 --- a/dimos/robot/unitree/mujoco_connection.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import atexit -import base64 -from collections.abc import Callable -import functools -import json -import pickle -import subprocess -import sys -import threading -import time -from typing import Any, TypeVar -import weakref - -import numpy as np -from numpy.typing import NDArray -from reactivex import Observable -from reactivex.abc import ObserverBase, SchedulerBase -from reactivex.disposable import Disposable - -from dimos.core.global_config import GlobalConfig -from dimos.msgs.geometry_msgs import Quaternion, Twist, Vector3 -from dimos.msgs.sensor_msgs import CameraInfo, Image, ImageFormat, PointCloud2 -from dimos.robot.unitree.type.odometry import Odometry -from dimos.simulation.mujoco.constants import ( - LAUNCHER_PATH, - LIDAR_FPS, - VIDEO_CAMERA_FOV, - VIDEO_FPS, - VIDEO_HEIGHT, - VIDEO_WIDTH, -) -from dimos.simulation.mujoco.shared_memory import ShmWriter -from dimos.utils.data import get_data -from dimos.utils.logging_config import setup_logger - -ODOM_FREQUENCY = 50 - -logger = setup_logger() - -T = TypeVar("T") - - -class MujocoConnection: - """MuJoCo simulator connection that runs in a separate subprocess.""" - - def __init__(self, global_config: GlobalConfig) -> None: - try: - import mujoco # noqa: F401 - except ImportError: - raise ImportError("'mujoco' is not installed. Use `pip install -e .[sim]`") - - # Pre-download the mujoco_sim data. - get_data("mujoco_sim") - - # Trigger the download of the mujoco_menagerie package. This is so it - # doesn't trigger in the mujoco process where it can time out. - from mujoco_playground._src import mjx_env - - mjx_env.ensure_menagerie_exists() - - self.global_config = global_config - self.process: subprocess.Popen[bytes] | None = None - self.shm_data: ShmWriter | None = None - self._last_video_seq = 0 - self._last_odom_seq = 0 - self._last_lidar_seq = 0 - self._stop_timer: threading.Timer | None = None - - self._stream_threads: list[threading.Thread] = [] - self._stop_events: list[threading.Event] = [] - self._is_cleaned_up = False - - @staticmethod - def _compute_camera_info() -> CameraInfo: - """Compute camera intrinsics from MuJoCo camera parameters. - - Uses pinhole camera model: f = height / (2 * tan(fovy / 2)) - """ - import math - - fovy = math.radians(VIDEO_CAMERA_FOV) - f = VIDEO_HEIGHT / (2 * math.tan(fovy / 2)) - cx = VIDEO_WIDTH / 2.0 - cy = VIDEO_HEIGHT / 2.0 - - return CameraInfo( - frame_id="camera_optical", - height=VIDEO_HEIGHT, - width=VIDEO_WIDTH, - distortion_model="plumb_bob", - D=[0.0, 0.0, 0.0, 0.0, 0.0], - K=[f, 0.0, cx, 0.0, f, cy, 0.0, 0.0, 1.0], - R=[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], - P=[f, 0.0, cx, 0.0, 0.0, f, cy, 0.0, 0.0, 0.0, 1.0, 0.0], - ) - - camera_info_static: CameraInfo = _compute_camera_info() - - def start(self) -> None: - self.shm_data = ShmWriter() - - config_pickle = base64.b64encode(pickle.dumps(self.global_config)).decode("ascii") - shm_names_json = json.dumps(self.shm_data.shm.to_names()) - - # Launch the subprocess - try: - # mjpython must be used macOS (because of launch_passive inside mujoco_process.py) - executable = sys.executable if sys.platform != "darwin" else "mjpython" - - self.process = subprocess.Popen( - [executable, str(LAUNCHER_PATH), config_pickle, shm_names_json], - ) - - except Exception as e: - self.shm_data.cleanup() - raise RuntimeError(f"Failed to start MuJoCo subprocess: {e}") from e - - # Wait for process to be ready - ready_timeout = 300.0 - start_time = time.time() - assert self.process is not None - while time.time() - start_time < ready_timeout: - if self.process.poll() is not None: - exit_code = self.process.returncode - self.stop() - raise RuntimeError(f"MuJoCo process failed to start (exit code {exit_code})") - if self.shm_data.is_ready(): - logger.info("MuJoCo process started successfully") - # Register atexit handler to ensure subprocess is cleaned up - # Use weakref to avoid preventing garbage collection - weak_self = weakref.ref(self) - - def cleanup_on_exit( - weak_self: "weakref.ReferenceType[MujocoConnection]" = weak_self, - ) -> None: - instance = weak_self() - if instance is not None: - instance.stop() - - atexit.register(cleanup_on_exit) - return - time.sleep(0.1) - - # Timeout - self.stop() - raise RuntimeError("MuJoCo process failed to start (timeout)") - - def stop(self) -> None: - if self._is_cleaned_up: - return - - self._is_cleaned_up = True - - # clean up open file descriptors - if self.process: - if self.process.stderr: - self.process.stderr.close() - if self.process.stdout: - self.process.stdout.close() - - # Cancel any pending timers - if self._stop_timer: - self._stop_timer.cancel() - self._stop_timer = None - - # Stop all stream threads - for stop_event in self._stop_events: - stop_event.set() - - # Wait for threads to finish - for thread in self._stream_threads: - if thread.is_alive(): - thread.join(timeout=2.0) - if thread.is_alive(): - logger.warning(f"Stream thread {thread.name} did not stop gracefully") - - # Signal subprocess to stop - if self.shm_data: - self.shm_data.signal_stop() - - # Wait for process to finish - if self.process: - try: - self.process.terminate() - try: - self.process.wait(timeout=5) - except subprocess.TimeoutExpired: - logger.warning("MuJoCo process did not stop gracefully, killing") - self.process.kill() - self.process.wait(timeout=2) - except Exception as e: - logger.error(f"Error stopping MuJoCo process: {e}") - - self.process = None - - # Clean up shared memory - if self.shm_data: - self.shm_data.cleanup() - self.shm_data = None - - # Clear references - self._stream_threads.clear() - self._stop_events.clear() - - self.lidar_stream.cache_clear() - self.odom_stream.cache_clear() - self.video_stream.cache_clear() - - def standup(self) -> bool: - return True - - def liedown(self) -> bool: - return True - - def get_video_frame(self) -> NDArray[Any] | None: - if self.shm_data is None: - return None - - frame, seq = self.shm_data.read_video() - if seq > self._last_video_seq: - self._last_video_seq = seq - return frame - - return None - - def get_odom_message(self) -> Odometry | None: - if self.shm_data is None: - return None - - odom_data, seq = self.shm_data.read_odom() - if seq > self._last_odom_seq and odom_data is not None: - self._last_odom_seq = seq - pos, quat_wxyz, timestamp = odom_data - - # Convert quaternion from (w,x,y,z) to (x,y,z,w) for ROS/Dimos - orientation = Quaternion(quat_wxyz[1], quat_wxyz[2], quat_wxyz[3], quat_wxyz[0]) - - return Odometry( - position=Vector3(pos[0], pos[1], pos[2]), - orientation=orientation, - ts=timestamp, - frame_id="world", - ) - - return None - - def get_lidar_message(self) -> PointCloud2 | None: - if self.shm_data is None: - return None - - lidar_msg, seq = self.shm_data.read_lidar() - if seq > self._last_lidar_seq and lidar_msg is not None: - self._last_lidar_seq = seq - return lidar_msg - - return None - - def _create_stream( - self, - getter: Callable[[], T | None], - frequency: float, - stream_name: str, - ) -> Observable[T]: - def on_subscribe(observer: ObserverBase[T], _scheduler: SchedulerBase | None) -> Disposable: - if self._is_cleaned_up: - observer.on_completed() - return Disposable(lambda: None) - - stop_event = threading.Event() - self._stop_events.append(stop_event) - - def run() -> None: - try: - while not stop_event.is_set() and not self._is_cleaned_up: - data = getter() - if data is not None: - observer.on_next(data) - time.sleep(1 / frequency) - except Exception as e: - logger.error(f"{stream_name} stream error: {e}") - finally: - observer.on_completed() - - thread = threading.Thread(target=run, daemon=True) - self._stream_threads.append(thread) - thread.start() - - def dispose() -> None: - stop_event.set() - - return Disposable(dispose) - - return Observable(on_subscribe) - - @functools.cache - def lidar_stream(self) -> Observable[PointCloud2]: - return self._create_stream(self.get_lidar_message, LIDAR_FPS, "Lidar") - - @functools.cache - def odom_stream(self) -> Observable[Odometry]: - return self._create_stream(self.get_odom_message, ODOM_FREQUENCY, "Odom") - - @functools.cache - def video_stream(self) -> Observable[Image]: - def get_video_as_image() -> Image | None: - frame = self.get_video_frame() - # MuJoCo renderer returns RGB uint8 frames; Image.from_numpy defaults to BGR. - return Image.from_numpy(frame, format=ImageFormat.RGB) if frame is not None else None - - return self._create_stream(get_video_as_image, VIDEO_FPS, "Video") - - def move(self, twist: Twist, duration: float = 0.0) -> bool: - if self._is_cleaned_up or self.shm_data is None: - return True - - linear = np.array([twist.linear.x, twist.linear.y, twist.linear.z], dtype=np.float32) - angular = np.array([twist.angular.x, twist.angular.y, twist.angular.z], dtype=np.float32) - self.shm_data.write_command(linear, angular) - - if duration > 0: - if self._stop_timer: - self._stop_timer.cancel() - - def stop_movement() -> None: - if self.shm_data: - self.shm_data.write_command( - np.zeros(3, dtype=np.float32), np.zeros(3, dtype=np.float32) - ) - self._stop_timer = None - - self._stop_timer = threading.Timer(duration, stop_movement) - self._stop_timer.daemon = True - self._stop_timer.start() - return True - - def publish_request(self, topic: str, data: dict[str, Any]) -> dict[Any, Any]: - print(f"publishing request, topic={topic}, data={data}") - return {} diff --git a/dimos/robot/unitree/params/front_camera_720.yaml b/dimos/robot/unitree/params/front_camera_720.yaml deleted file mode 100644 index 0030d5fc6c..0000000000 --- a/dimos/robot/unitree/params/front_camera_720.yaml +++ /dev/null @@ -1,26 +0,0 @@ -image_width: 1280 -image_height: 720 -camera_name: narrow_stereo -camera_matrix: - rows: 3 - cols: 3 - data: [864.39938, 0. , 639.19798, - 0. , 863.73849, 373.28118, - 0. , 0. , 1. ] -distortion_model: plumb_bob -distortion_coefficients: - rows: 1 - cols: 5 - data: [-0.354630, 0.102054, -0.001614, -0.001249, 0.000000] -rectification_matrix: - rows: 3 - cols: 3 - data: [1., 0., 0., - 0., 1., 0., - 0., 0., 1.] -projection_matrix: - rows: 3 - cols: 4 - data: [651.42609, 0. , 633.16224, 0. , - 0. , 804.93951, 373.8537 , 0. , - 0. , 0. , 1. , 0. ] diff --git a/dimos/robot/unitree/params/sim_camera.yaml b/dimos/robot/unitree/params/sim_camera.yaml deleted file mode 100644 index 6a5ac3e6d8..0000000000 --- a/dimos/robot/unitree/params/sim_camera.yaml +++ /dev/null @@ -1,26 +0,0 @@ -image_width: 320 -image_height: 240 -camera_name: sim_camera -camera_matrix: - rows: 3 - cols: 3 - data: [277., 0. , 160. , - 0. , 277., 120. , - 0. , 0. , 1. ] -distortion_model: plumb_bob -distortion_coefficients: - rows: 1 - cols: 5 - data: [0.0, 0.0, 0.0, 0.0, 0.0] -rectification_matrix: - rows: 3 - cols: 3 - data: [1., 0., 0., - 0., 1., 0., - 0., 0., 1.] -projection_matrix: - rows: 3 - cols: 4 - data: [277., 0. , 160. , 0. , - 0. , 277., 120. , 0. , - 0. , 0. , 1. , 0. ] diff --git a/dimos/robot/unitree/rosnav.py b/dimos/robot/unitree/rosnav.py deleted file mode 100644 index 7a9b98b678..0000000000 --- a/dimos/robot/unitree/rosnav.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import time - -from dimos.core import In, Module, Out, rpc -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Joy -from dimos.msgs.std_msgs.Bool import Bool -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.INFO) - - -# TODO: Remove, deprecated -class NavigationModule(Module): - goal_pose: Out[PoseStamped] - goal_reached: In[Bool] - cancel_goal: Out[Bool] - joy: Out[Joy] - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - """Initialize NavigationModule.""" - Module.__init__(self, *args, **kwargs) - self.goal_reach = None - - @rpc - def start(self) -> None: - """Start the navigation module.""" - if self.goal_reached: - self.goal_reached.subscribe(self._on_goal_reached) - logger.info("NavigationModule started") - - def _on_goal_reached(self, msg: Bool) -> None: - """Handle goal reached status messages.""" - self.goal_reach = msg.data # type: ignore[assignment] - - def _set_autonomy_mode(self) -> None: - """ - Set autonomy mode by publishing Joy message. - """ - - joy_msg = Joy( - frame_id="dimos", - axes=[ - 0.0, # axis 0 - 0.0, # axis 1 - -1.0, # axis 2 - 0.0, # axis 3 - 1.0, # axis 4 - 1.0, # axis 5 - 0.0, # axis 6 - 0.0, # axis 7 - ], - buttons=[ - 0, # button 0 - 0, # button 1 - 0, # button 2 - 0, # button 3 - 0, # button 4 - 0, # button 5 - 0, # button 6 - 1, # button 7 - controls autonomy mode - 0, # button 8 - 0, # button 9 - 0, # button 10 - ], - ) - - if self.joy: - self.joy.publish(joy_msg) - logger.info("Setting autonomy mode via Joy message") - - @rpc - def go_to(self, pose: PoseStamped, timeout: float = 60.0) -> bool: - """ - Navigate to a target pose by publishing to LCM topics. - - Args: - pose: Target pose to navigate to - blocking: If True, block until goal is reached - timeout: Maximum time to wait for goal (seconds) - - Returns: - True if navigation was successful (or started if non-blocking) - """ - logger.info( - f"Navigating to goal: ({pose.position.x:.2f}, {pose.position.y:.2f}, {pose.position.z:.2f})" - ) - - self.goal_reach = None - self._set_autonomy_mode() - self.goal_pose.publish(pose) - time.sleep(0.2) - self.goal_pose.publish(pose) - - start_time = time.time() - while time.time() - start_time < timeout: - if self.goal_reach is not None: - return self.goal_reach - time.sleep(0.1) - - self.stop() - - logger.warning(f"Navigation timed out after {timeout} seconds") - return False - - @rpc - def stop(self) -> None: - """ - Cancel current navigation by publishing to cancel_goal. - - Returns: - True if cancel command was sent successfully - """ - logger.info("Cancelling navigation") - - if self.cancel_goal: - cancel_msg = Bool(data=True) - self.cancel_goal.publish(cancel_msg) - return - - return diff --git a/dimos/robot/unitree/testing/__init__.py b/dimos/robot/unitree/testing/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/unitree/testing/helpers.py b/dimos/robot/unitree/testing/helpers.py deleted file mode 100644 index aaf188dbc3..0000000000 --- a/dimos/robot/unitree/testing/helpers.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Iterable -import time -from typing import Any, Protocol - -import open3d as o3d # type: ignore[import-untyped] -from reactivex.observable import Observable - -color1 = [1, 0.706, 0] -color2 = [0, 0.651, 0.929] -color3 = [0.8, 0.196, 0.6] -color4 = [0.235, 0.702, 0.443] -color = [color1, color2, color3, color4] - - -# benchmarking function can return int, which will be applied to the time. -# -# (in case there is some preparation within the fuction and this time needs to be subtracted -# from the benchmark target) -def benchmark(calls: int, targetf: Callable[[], int | None]) -> float: - start = time.time() - timemod = 0 - for _ in range(calls): - res = targetf() - if res is not None: - timemod += res - end = time.time() - return (end - start + timemod) * 1000 / calls - - -O3dDrawable = ( - o3d.geometry.Geometry - | o3d.geometry.LineSet - | o3d.geometry.TriangleMesh - | o3d.geometry.PointCloud -) - - -class ReturnsDrawable(Protocol): - def o3d_geometry(self) -> O3dDrawable: ... # type: ignore[valid-type] - - -Drawable = O3dDrawable | ReturnsDrawable - - -def show3d(*components: Iterable[Drawable], title: str = "open3d") -> o3d.visualization.Visualizer: # type: ignore[valid-type] - vis = o3d.visualization.Visualizer() - vis.create_window(window_name=title) - for component in components: - # our custom drawable components should return an open3d geometry - if hasattr(component, "o3d_geometry"): - vis.add_geometry(component.o3d_geometry) - else: - vis.add_geometry(component) - - opt = vis.get_render_option() - opt.background_color = [0, 0, 0] - opt.point_size = 10 - vis.poll_events() - vis.update_renderer() - return vis - - -def multivis(*vis: o3d.visualization.Visualizer) -> None: - while True: - for v in vis: - v.poll_events() - v.update_renderer() - - -def show3d_stream( - geometry_observable: Observable[Any], - clearframe: bool = False, - title: str = "open3d", -) -> o3d.visualization.Visualizer: - """ - Visualize a stream of geometries using Open3D. The first geometry initializes the visualizer. - Subsequent geometries update the visualizer. If no new geometry, just poll events. - geometry_observable: Observable of objects with .o3d_geometry or Open3D geometry - """ - import queue - import threading - import time - from typing import Any - - q: queue.Queue[Any] = queue.Queue() - stop_flag = threading.Event() - - def on_next(geometry: O3dDrawable) -> None: # type: ignore[valid-type] - q.put(geometry) - - def on_error(e: Exception) -> None: - print(f"Visualization error: {e}") - stop_flag.set() - - def on_completed() -> None: - print("Geometry stream completed") - stop_flag.set() - - subscription = geometry_observable.subscribe( - on_next=on_next, - on_error=on_error, - on_completed=on_completed, - ) - - def geom(geometry: Drawable) -> O3dDrawable: # type: ignore[valid-type] - """Extracts the Open3D geometry from the given object.""" - return geometry.o3d_geometry if hasattr(geometry, "o3d_geometry") else geometry # type: ignore[attr-defined, no-any-return] - - # Wait for the first geometry - first_geometry = None - while first_geometry is None and not stop_flag.is_set(): - try: - first_geometry = q.get(timeout=100) - except queue.Empty: - print("No geometry received to visualize.") - return - - scene_geometries = [] - first_geom_obj = geom(first_geometry) - - scene_geometries.append(first_geom_obj) - - vis = show3d(first_geom_obj, title=title) - - try: - while not stop_flag.is_set(): - try: - geometry = q.get_nowait() - geom_obj = geom(geometry) - if clearframe: - scene_geometries = [] - vis.clear_geometries() - - vis.add_geometry(geom_obj) - scene_geometries.append(geom_obj) - else: - if geom_obj in scene_geometries: - print("updating existing geometry") - vis.update_geometry(geom_obj) - else: - print("new geometry") - vis.add_geometry(geom_obj) - scene_geometries.append(geom_obj) - except queue.Empty: - pass - vis.poll_events() - vis.update_renderer() - time.sleep(0.1) - - except KeyboardInterrupt: - print("closing visualizer...") - stop_flag.set() - vis.destroy_window() - subscription.dispose() - - return vis diff --git a/dimos/robot/unitree/testing/mock.py b/dimos/robot/unitree/testing/mock.py deleted file mode 100644 index 26e6a90018..0000000000 --- a/dimos/robot/unitree/testing/mock.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Iterator -import glob -import os -import pickle -from typing import cast, overload - -from reactivex import from_iterable, interval, operators as ops -from reactivex.observable import Observable - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.lidar import RawLidarMsg, pointcloud2_from_webrtc_lidar - - -class Mock: - def __init__(self, root: str = "office", autocast: bool = True) -> None: - current_dir = os.path.dirname(os.path.abspath(__file__)) - self.root = os.path.join(current_dir, f"mockdata/{root}") - self.autocast = autocast - self.cnt = 0 - - @overload - def load(self, name: int | str, /) -> PointCloud2: ... - @overload - def load(self, *names: int | str) -> list[PointCloud2]: ... - - def load(self, *names: int | str) -> PointCloud2 | list[PointCloud2]: - if len(names) == 1: - return self.load_one(names[0]) - return list(map(lambda name: self.load_one(name), names)) - - def load_one(self, name: int | str) -> PointCloud2: - if isinstance(name, int): - file_name = f"/lidar_data_{name:03d}.pickle" - else: - file_name = f"/{name}.pickle" - - full_path = self.root + file_name - with open(full_path, "rb") as f: - return pointcloud2_from_webrtc_lidar(cast("RawLidarMsg", pickle.load(f))) - - def iterate(self) -> Iterator[PointCloud2]: - pattern = os.path.join(self.root, "lidar_data_*.pickle") - print("loading data", pattern) - for file_path in sorted(glob.glob(pattern)): - basename = os.path.basename(file_path) - filename = os.path.splitext(basename)[0] - yield self.load_one(filename) - - def stream(self, rate_hz: float = 10.0): # type: ignore[no-untyped-def] - sleep_time = 1.0 / rate_hz - - return from_iterable(self.iterate()).pipe( - ops.zip(interval(sleep_time)), - ops.map(lambda x: x[0] if isinstance(x, tuple) else x), - ) - - def save_stream(self, observable: Observable[PointCloud2]): # type: ignore[no-untyped-def] - return observable.pipe(ops.map(lambda frame: self.save_one(frame))) # type: ignore[no-untyped-call] - - def save(self, *frames): # type: ignore[no-untyped-def] - [self.save_one(frame) for frame in frames] # type: ignore[no-untyped-call] - return self.cnt - - def save_one(self, frame): # type: ignore[no-untyped-def] - file_name = f"/lidar_data_{self.cnt:03d}.pickle" - full_path = self.root + file_name - - self.cnt += 1 - - if os.path.isfile(full_path): - raise Exception(f"file {full_path} exists") - - # Note: This saves the PointCloud2 directly. For raw message saving, - # use the raw message before conversion. - with open(full_path, "wb") as f: - pickle.dump(frame, f) - - return self.cnt diff --git a/dimos/robot/unitree/testing/test_actors.py b/dimos/robot/unitree/testing/test_actors.py deleted file mode 100644 index 9366092eb6..0000000000 --- a/dimos/robot/unitree/testing/test_actors.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import asyncio -from collections.abc import Callable -import time - -import pytest - -from dimos import core -from dimos.core import Module, rpc -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.map import Map as Mapper - - -@pytest.fixture -def dimos(): - return core.start(2) - - -@pytest.fixture -def client(): - return core.start(2) - - -class Consumer: - testf: Callable[[int], int] - - def __init__(self, counter=None) -> None: - self.testf = counter - self._tasks: set[asyncio.Task[None]] = set() - print("consumer init with", counter) - - async def waitcall(self, n: int): - async def task() -> None: - await asyncio.sleep(n) - - print("sleep finished, calling") - res = await self.testf(n) - print("res is", res) - - background_task = asyncio.create_task(task()) - self._tasks.add(background_task) - background_task.add_done_callback(self._tasks.discard) - return n - - -class Counter(Module): - @rpc - def addten(self, x: int): - print(f"counter adding to {x}") - return x + 10 - - -@pytest.mark.tool -def test_wait(client) -> None: - counter = client.submit(Counter, actor=True).result() - - async def addten(n): - return await counter.addten(n) - - consumer = client.submit(Consumer, counter=addten, actor=True).result() - - print("waitcall1", consumer.waitcall(2).result()) - print("waitcall2", consumer.waitcall(2).result()) - time.sleep(1) - - -@pytest.mark.tool -def test_basic(dimos) -> None: - counter = dimos.deploy(Counter) - consumer = dimos.deploy( - Consumer, - counter=lambda x: counter.addten(x).result(), - ) - - print(consumer) - print(counter) - print("starting consumer") - consumer.start().result() - - res = consumer.inc(10).result() - - print("result is", res) - assert res == 20 - - -@pytest.mark.tool -def test_mapper_start(dimos) -> None: - mapper = dimos.deploy(Mapper) - mapper.lidar.transport = core.LCMTransport("/lidar", PointCloud2) - print("start res", mapper.start().result()) - - -if __name__ == "__main__": - dimos = core.start(2) - test_basic(dimos) - test_mapper_start(dimos) - - -@pytest.mark.tool -def test_counter(dimos) -> None: - counter = dimos.deploy(Counter) - assert counter.addten(10) == 20 diff --git a/dimos/robot/unitree/testing/test_tooling.py b/dimos/robot/unitree/testing/test_tooling.py deleted file mode 100644 index d1f2eeb169..0000000000 --- a/dimos/robot/unitree/testing/test_tooling.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos.robot.unitree.type.lidar import pointcloud2_from_webrtc_lidar -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.reactive import backpressure -from dimos.utils.testing import TimedSensorReplay - - -@pytest.mark.tool -def test_replay_all() -> None: - lidar_store = TimedSensorReplay("unitree/lidar", autocast=pointcloud2_from_webrtc_lidar) - odom_store = TimedSensorReplay("unitree/odom", autocast=Odometry.from_msg) - video_store = TimedSensorReplay("unitree/video") - - backpressure(odom_store.stream()).subscribe(print) - backpressure(lidar_store.stream()).subscribe(print) - backpressure(video_store.stream()).subscribe(print) - - print("Replaying for 3 seconds...") - time.sleep(3) - print("Stopping replay after 3 seconds") diff --git a/dimos/robot/unitree/type/__init__.py b/dimos/robot/unitree/type/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/robot/unitree/type/lidar.py b/dimos/robot/unitree/type/lidar.py deleted file mode 100644 index df2909dc38..0000000000 --- a/dimos/robot/unitree/type/lidar.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unitree WebRTC lidar message parsing utilities.""" - -import time -from typing import TypedDict - -import numpy as np -import open3d as o3d # type: ignore[import-untyped] - -from dimos.msgs.sensor_msgs import PointCloud2 - -# Backwards compatibility alias for pickled data -LidarMessage = PointCloud2 - - -class RawLidarPoints(TypedDict): - points: np.ndarray # type: ignore[type-arg] # Shape (N, 3) array of 3D points [x, y, z] - - -class RawLidarData(TypedDict): - """Data portion of the LIDAR message""" - - frame_id: str - origin: list[float] - resolution: float - src_size: int - stamp: float - width: list[int] - data: RawLidarPoints - - -class RawLidarMsg(TypedDict): - """Static type definition for raw LIDAR message from Unitree WebRTC.""" - - type: str - topic: str - data: RawLidarData - - -def pointcloud2_from_webrtc_lidar(raw_message: RawLidarMsg, ts: float | None = None) -> PointCloud2: - """Convert a raw Unitree WebRTC lidar message to PointCloud2. - - Args: - raw_message: Raw lidar message from Unitree WebRTC API - ts: Optional timestamp override. If None, uses current time. - - Returns: - PointCloud2 message with the lidar points - """ - data = raw_message["data"] - points = data["data"]["points"] - - pointcloud = o3d.geometry.PointCloud() - pointcloud.points = o3d.utility.Vector3dVector(points) - - return PointCloud2( - pointcloud=pointcloud, - # webrtc stamp is broken (e.g., "stamp": 1.758148e+09), use current time - ts=ts if ts is not None else time.time(), - frame_id="world", - ) diff --git a/dimos/robot/unitree/type/lowstate.py b/dimos/robot/unitree/type/lowstate.py deleted file mode 100644 index 3e7926424a..0000000000 --- a/dimos/robot/unitree/type/lowstate.py +++ /dev/null @@ -1,93 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Literal, TypedDict - -raw_odom_msg_sample = { - "type": "msg", - "topic": "rt/lf/lowstate", - "data": { - "imu_state": {"rpy": [0.008086, -0.007515, 2.981771]}, - "motor_state": [ - {"q": 0.098092, "temperature": 40, "lost": 0, "reserve": [0, 674]}, - {"q": 0.757921, "temperature": 32, "lost": 0, "reserve": [0, 674]}, - {"q": -1.490911, "temperature": 38, "lost": 6, "reserve": [0, 674]}, - {"q": -0.072477, "temperature": 42, "lost": 0, "reserve": [0, 674]}, - {"q": 1.020276, "temperature": 32, "lost": 5, "reserve": [0, 674]}, - {"q": -2.007172, "temperature": 38, "lost": 5, "reserve": [0, 674]}, - {"q": 0.071382, "temperature": 50, "lost": 5, "reserve": [0, 674]}, - {"q": 0.963379, "temperature": 36, "lost": 6, "reserve": [0, 674]}, - {"q": -1.978311, "temperature": 40, "lost": 5, "reserve": [0, 674]}, - {"q": -0.051066, "temperature": 48, "lost": 0, "reserve": [0, 674]}, - {"q": 0.73103, "temperature": 34, "lost": 10, "reserve": [0, 674]}, - {"q": -1.466473, "temperature": 38, "lost": 6, "reserve": [0, 674]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - {"q": 0, "temperature": 0, "lost": 0, "reserve": [0, 0]}, - ], - "bms_state": { - "version_high": 1, - "version_low": 18, - "soc": 55, - "current": -2481, - "cycle": 56, - "bq_ntc": [30, 29], - "mcu_ntc": [33, 32], - }, - "foot_force": [97, 84, 81, 81], - "temperature_ntc1": 48, - "power_v": 28.331045, - }, -} - - -class MotorState(TypedDict): - q: float - temperature: int - lost: int - reserve: list[int] - - -class ImuState(TypedDict): - rpy: list[float] - - -class BmsState(TypedDict): - version_high: int - version_low: int - soc: int - current: int - cycle: int - bq_ntc: list[int] - mcu_ntc: list[int] - - -class LowStateData(TypedDict): - imu_state: ImuState - motor_state: list[MotorState] - bms_state: BmsState - foot_force: list[int] - temperature_ntc1: int - power_v: float - - -class LowStateMsg(TypedDict): - type: Literal["msg"] - topic: str - data: LowStateData diff --git a/dimos/robot/unitree/type/map.py b/dimos/robot/unitree/type/map.py deleted file mode 100644 index a771467246..0000000000 --- a/dimos/robot/unitree/type/map.py +++ /dev/null @@ -1,128 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path -import time -from typing import Any - -import open3d as o3d # type: ignore[import-untyped] -from reactivex import interval -from reactivex.disposable import Disposable - -from dimos.core import DimosCluster, In, LCMTransport, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.mapping.pointclouds.accumulators.general import GeneralPointCloudAccumulator -from dimos.mapping.pointclouds.accumulators.protocol import PointCloudAccumulator -from dimos.mapping.pointclouds.occupancy import general_occupancy -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.go2.connection import Go2ConnectionProtocol - - -class Map(Module): - lidar: In[PointCloud2] - global_map: Out[PointCloud2] - global_costmap: Out[OccupancyGrid] - - _point_cloud_accumulator: PointCloudAccumulator - _global_config: GlobalConfig - _preloaded_occupancy: OccupancyGrid | None = None - - def __init__( # type: ignore[no-untyped-def] - self, - voxel_size: float = 0.05, - cost_resolution: float = 0.05, - global_publish_interval: float | None = None, - min_height: float = 0.10, - max_height: float = 0.5, - cfg: GlobalConfig = global_config, - **kwargs, - ) -> None: - self.voxel_size = voxel_size - self.cost_resolution = cost_resolution - self.global_publish_interval = global_publish_interval - self.min_height = min_height - self.max_height = max_height - self._global_config = cfg - self._point_cloud_accumulator = GeneralPointCloudAccumulator( - self.voxel_size, self._global_config - ) - - if self._global_config.simulation: - self.min_height = 0.3 - - super().__init__(**kwargs) - - @rpc - def start(self) -> None: - super().start() - - self._disposables.add(Disposable(self.lidar.subscribe(self.add_frame))) - - if self.global_publish_interval is not None: - unsub = interval(self.global_publish_interval).subscribe(self._publish) - self._disposables.add(unsub) - - @rpc - def stop(self) -> None: - super().stop() - - def to_PointCloud2(self) -> PointCloud2: - return PointCloud2( - pointcloud=self._point_cloud_accumulator.get_point_cloud(), - ts=time.time(), - ) - - # TODO: Why is this RPC? - @rpc - def add_frame(self, frame: PointCloud2) -> None: - self._point_cloud_accumulator.add(frame.pointcloud) - - @property - def o3d_geometry(self) -> o3d.geometry.PointCloud: - return self._point_cloud_accumulator.get_point_cloud() - - def _publish(self, _: Any) -> None: - self.global_map.publish(self.to_PointCloud2()) - - occupancygrid = general_occupancy( - self.to_PointCloud2(), - resolution=self.cost_resolution, - min_height=self.min_height, - max_height=self.max_height, - ) - - # When debugging occupancy navigation, load a predefined occupancy grid. - if self._global_config.mujoco_global_costmap_from_occupancy: - if self._preloaded_occupancy is None: - path = Path(self._global_config.mujoco_global_costmap_from_occupancy) - self._preloaded_occupancy = OccupancyGrid.from_path(path) - occupancygrid = self._preloaded_occupancy - - self.global_costmap.publish(occupancygrid) - - -mapper = Map.blueprint - - -def deploy(dimos: DimosCluster, connection: Go2ConnectionProtocol): # type: ignore[no-untyped-def] - mapper = dimos.deploy(Map, global_publish_interval=1.0) # type: ignore[attr-defined] - mapper.global_map.transport = LCMTransport("/global_map", PointCloud2) - mapper.global_costmap.transport = LCMTransport("/global_costmap", OccupancyGrid) - mapper.lidar.connect(connection.pointcloud) # type: ignore[attr-defined] - mapper.start() - return mapper - - -__all__ = ["Map", "mapper"] diff --git a/dimos/robot/unitree/type/odometry.py b/dimos/robot/unitree/type/odometry.py deleted file mode 100644 index aa664b32ef..0000000000 --- a/dimos/robot/unitree/type/odometry.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from typing import Literal, TypedDict - -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.robot.unitree.type.timeseries import ( - Timestamped, -) -from dimos.types.timestamped import to_timestamp - -raw_odometry_msg_sample = { - "type": "msg", - "topic": "rt/utlidar/robot_pose", - "data": { - "header": {"stamp": {"sec": 1746565669, "nanosec": 448350564}, "frame_id": "odom"}, - "pose": { - "position": {"x": 5.961965, "y": -2.916958, "z": 0.319509}, - "orientation": {"x": 0.002787, "y": -0.000902, "z": -0.970244, "w": -0.242112}, - }, - }, -} - - -class TimeStamp(TypedDict): - sec: int - nanosec: int - - -class Header(TypedDict): - stamp: TimeStamp - frame_id: str - - -class RawPosition(TypedDict): - x: float - y: float - z: float - - -class Orientation(TypedDict): - x: float - y: float - z: float - w: float - - -class PoseData(TypedDict): - position: RawPosition - orientation: Orientation - - -class OdometryData(TypedDict): - header: Header - pose: PoseData - - -class RawOdometryMessage(TypedDict): - type: Literal["msg"] - topic: str - data: OdometryData - - -class Odometry(PoseStamped, Timestamped): # type: ignore[misc] - name = "geometry_msgs.PoseStamped" - - def __init__(self, frame_id: str = "base_link", *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(frame_id=frame_id, *args, **kwargs) # type: ignore[misc] - - @classmethod - def from_msg(cls, msg: RawOdometryMessage) -> "Odometry": - pose = msg["data"]["pose"] - - # Extract position - pos = Vector3( - pose["position"].get("x"), - pose["position"].get("y"), - pose["position"].get("z"), - ) - - rot = Quaternion( - pose["orientation"].get("x"), - pose["orientation"].get("y"), - pose["orientation"].get("z"), - pose["orientation"].get("w"), - ) - - ts = to_timestamp(msg["data"]["header"]["stamp"]) - return Odometry(position=pos, orientation=rot, ts=ts, frame_id="world") - - def __repr__(self) -> str: - return f"Odom pos({self.position}), rot({self.orientation})" diff --git a/dimos/robot/unitree/type/test_lidar.py b/dimos/robot/unitree/type/test_lidar.py deleted file mode 100644 index 719088d77a..0000000000 --- a/dimos/robot/unitree/type/test_lidar.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import itertools -from typing import cast - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.lidar import RawLidarMsg, pointcloud2_from_webrtc_lidar -from dimos.utils.testing import SensorReplay - - -def test_init() -> None: - lidar = SensorReplay("office_lidar") - - for raw_frame in itertools.islice(lidar.iterate(), 5): - assert isinstance(raw_frame, dict) - frame = pointcloud2_from_webrtc_lidar(cast("RawLidarMsg", raw_frame)) - assert isinstance(frame, PointCloud2) diff --git a/dimos/robot/unitree/type/test_odometry.py b/dimos/robot/unitree/type/test_odometry.py deleted file mode 100644 index d0fe2b290e..0000000000 --- a/dimos/robot/unitree/type/test_odometry.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import pytest - -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.testing import SensorReplay - -_EXPECTED_TOTAL_RAD = -4.05212 - - -def test_dataset_size() -> None: - """Ensure the replay contains the expected number of messages.""" - assert sum(1 for _ in SensorReplay(name="raw_odometry_rotate_walk").iterate()) == 179 - - -def test_odometry_conversion_and_count() -> None: - """Each replay entry converts to :class:`Odometry` and count is correct.""" - for raw in SensorReplay(name="raw_odometry_rotate_walk").iterate(): - odom = Odometry.from_msg(raw) - assert isinstance(raw, dict) - assert isinstance(odom, Odometry) - - -def test_total_rotation_travel_iterate() -> None: - total_rad = 0.0 - prev_yaw: float | None = None - - for odom in SensorReplay(name="raw_odometry_rotate_walk", autocast=Odometry.from_msg).iterate(): - yaw = odom.orientation.radians.z - if prev_yaw is not None: - diff = yaw - prev_yaw - total_rad += diff - prev_yaw = yaw - - assert total_rad == pytest.approx(_EXPECTED_TOTAL_RAD, abs=0.001) diff --git a/dimos/robot/unitree/type/test_timeseries.py b/dimos/robot/unitree/type/test_timeseries.py deleted file mode 100644 index 5164d91a94..0000000000 --- a/dimos/robot/unitree/type/test_timeseries.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime, timedelta - -from dimos.robot.unitree.type.timeseries import TEvent, TList - -fixed_date = datetime(2025, 5, 13, 15, 2, 5).astimezone() -start_event = TEvent(fixed_date, 1) -end_event = TEvent(fixed_date + timedelta(seconds=10), 9) - -sample_list = TList([start_event, TEvent(fixed_date + timedelta(seconds=2), 5), end_event]) - - -def test_repr() -> None: - assert ( - str(sample_list) - == "Timeseries(date=2025-05-13, start=15:02:05, end=15:02:15, duration=0:00:10, events=3, freq=0.30Hz)" - ) - - -def test_equals() -> None: - assert start_event == TEvent(start_event.ts, 1) - assert start_event != TEvent(start_event.ts, 2) - assert start_event != TEvent(start_event.ts + timedelta(seconds=1), 1) - - -def test_range() -> None: - assert sample_list.time_range() == (start_event.ts, end_event.ts) - - -def test_duration() -> None: - assert sample_list.duration() == timedelta(seconds=10) diff --git a/dimos/robot/unitree/type/timeseries.py b/dimos/robot/unitree/type/timeseries.py deleted file mode 100644 index b75a41b932..0000000000 --- a/dimos/robot/unitree/type/timeseries.py +++ /dev/null @@ -1,149 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from abc import ABC, abstractmethod -from datetime import datetime, timedelta, timezone -from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar, Union - -if TYPE_CHECKING: - from collections.abc import Iterable - -PAYLOAD = TypeVar("PAYLOAD") - - -class RosStamp(TypedDict): - sec: int - nanosec: int - - -EpochLike = Union[int, float, datetime, RosStamp] - - -def from_ros_stamp(stamp: dict[str, int], tz: timezone | None = None) -> datetime: - """Convert ROS-style timestamp {'sec': int, 'nanosec': int} to datetime.""" - return datetime.fromtimestamp(stamp["sec"] + stamp["nanosec"] / 1e9, tz=tz) - - -def to_human_readable(ts: EpochLike) -> str: - dt = to_datetime(ts) - return dt.strftime("%Y-%m-%d %H:%M:%S") - - -def to_datetime(ts: EpochLike, tz: timezone | None = None) -> datetime: - if isinstance(ts, datetime): - # if ts.tzinfo is None: - # ts = ts.astimezone(tz) - return ts - if isinstance(ts, int | float): - return datetime.fromtimestamp(ts, tz=tz) - if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: - return datetime.fromtimestamp(ts["sec"] + ts["nanosec"] / 1e9, tz=tz) - raise TypeError("unsupported timestamp type") - - -class Timestamped(ABC): - """Abstract class for an event with a timestamp.""" - - ts: datetime - - def __init__(self, ts: EpochLike) -> None: - self.ts = to_datetime(ts) - - -class TEvent(Timestamped, Generic[PAYLOAD]): - """Concrete class for an event with a timestamp and data.""" - - def __init__(self, timestamp: EpochLike, data: PAYLOAD) -> None: - super().__init__(timestamp) - self.data = data - - def __eq__(self, other: object) -> bool: - if not isinstance(other, TEvent): - return NotImplemented - return self.ts == other.ts and self.data == other.data - - def __repr__(self) -> str: - return f"TEvent(ts={self.ts}, data={self.data})" - - -EVENT = TypeVar("EVENT", bound=Timestamped) # any object that is a subclass of Timestamped - - -class Timeseries(ABC, Generic[EVENT]): - """Abstract class for an iterable of events with timestamps.""" - - @abstractmethod - def __iter__(self) -> Iterable[EVENT]: ... - - @property - def start_time(self) -> datetime: - """Return the timestamp of the earliest event, assuming the data is sorted.""" - return next(iter(self)).ts # type: ignore[call-overload, no-any-return, type-var] - - @property - def end_time(self) -> datetime: - """Return the timestamp of the latest event, assuming the data is sorted.""" - return next(reversed(list(self))).ts # type: ignore[call-overload, no-any-return] - - @property - def frequency(self) -> float: - """Calculate the frequency of events in Hz.""" - return len(list(self)) / (self.duration().total_seconds() or 1) # type: ignore[call-overload] - - def time_range(self) -> tuple[datetime, datetime]: - """Return (earliest_ts, latest_ts). Empty input ⇒ ValueError.""" - return self.start_time, self.end_time - - def duration(self) -> timedelta: - """Total time spanned by the iterable (Δ = last - first).""" - return self.end_time - self.start_time - - def closest_to(self, timestamp: EpochLike) -> EVENT: - """Return the event closest to the given timestamp. Assumes timeseries is sorted.""" - print("closest to", timestamp) - target = to_datetime(timestamp) - print("converted to", target) - target_ts = target.timestamp() - - closest = None - min_dist = float("inf") - - for event in self: # type: ignore[attr-defined] - dist = abs(event.ts - target_ts) - if dist > min_dist: - break - - min_dist = dist - closest = event - - print(f"closest: {closest}") - return closest # type: ignore[return-value] - - def __repr__(self) -> str: - """Return a string representation of the Timeseries.""" - return f"Timeseries(date={self.start_time.strftime('%Y-%m-%d')}, start={self.start_time.strftime('%H:%M:%S')}, end={self.end_time.strftime('%H:%M:%S')}, duration={self.duration()}, events={len(list(self))}, freq={self.frequency:.2f}Hz)" # type: ignore[call-overload] - - def __str__(self) -> str: - """Return a string representation of the Timeseries.""" - return self.__repr__() - - -class TList(list[EVENT], Timeseries[EVENT]): - """A test class that inherits from both list and Timeseries.""" - - def __repr__(self) -> str: - """Return a string representation of the TList using Timeseries repr method.""" - return Timeseries.__repr__(self) diff --git a/dimos/robot/unitree/type/vector.py b/dimos/robot/unitree/type/vector.py deleted file mode 100644 index 58438c0a98..0000000000 --- a/dimos/robot/unitree/type/vector.py +++ /dev/null @@ -1,442 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import builtins -from collections.abc import Iterable -from typing import ( - Any, - Protocol, - TypeVar, - Union, - runtime_checkable, -) - -import numpy as np -from numpy.typing import NDArray - -T = TypeVar("T", bound="Vector") - - -class Vector: - """A wrapper around numpy arrays for vector operations with intuitive syntax.""" - - def __init__(self, *args: Any) -> None: - """Initialize a vector from components or another iterable. - - Examples: - Vector(1, 2) # 2D vector - Vector(1, 2, 3) # 3D vector - Vector([1, 2, 3]) # From list - Vector(np.array([1, 2, 3])) # From numpy array - """ - if len(args) == 1 and hasattr(args[0], "__iter__"): - self._data = np.array(args[0], dtype=float) - elif len(args) == 1: - self._data = np.array([args[0].x, args[0].y, args[0].z], dtype=float) - - else: - self._data = np.array(args, dtype=float) - - @property - def yaw(self) -> float: - return self.x - - @property - def tuple(self) -> tuple[float, ...]: - """Tuple representation of the vector.""" - return tuple(self._data) - - @property - def x(self) -> float: - """X component of the vector.""" - return self._data[0] if len(self._data) > 0 else 0.0 - - @property - def y(self) -> float: - """Y component of the vector.""" - return self._data[1] if len(self._data) > 1 else 0.0 - - @property - def z(self) -> float: - """Z component of the vector.""" - return self._data[2] if len(self._data) > 2 else 0.0 - - @property - def dim(self) -> int: - """Dimensionality of the vector.""" - return len(self._data) - - @property - def data(self) -> NDArray[np.float64]: - """Get the underlying numpy array.""" - return self._data - - def __len__(self) -> int: - return len(self._data) - - def __getitem__(self, idx: int) -> float: - return float(self._data[idx]) - - def __iter__(self) -> Iterable[float]: - return iter(self._data) # type: ignore[no-any-return] - - def __repr__(self) -> str: - components = ",".join(f"{x:.6g}" for x in self._data) - return f"({components})" - - def __str__(self) -> str: - if self.dim < 2: - return self.__repr__() - - def getArrow() -> str: - repr = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"] - - if self.y == 0 and self.x == 0: - return "·" - - # Calculate angle in radians and convert to directional index - angle = np.arctan2(self.y, self.x) - # Map angle to 0-7 index (8 directions) with proper orientation - dir_index = int(((angle + np.pi) * 4 / np.pi) % 8) - # Get directional arrow symbol - return repr[dir_index] - - return f"{getArrow()} Vector {self.__repr__()}" - - def serialize(self) -> dict: # type: ignore[type-arg] - """Serialize the vector to a dictionary.""" - return {"type": "vector", "c": self._data.tolist()} - - def __eq__(self, other: Any) -> bool: - if isinstance(other, Vector): - return np.array_equal(self._data, other._data) - return np.array_equal(self._data, np.array(other, dtype=float)) - - def __add__(self: T, other: Union["Vector", Iterable[float]]) -> T: - if isinstance(other, Vector): - return self.__class__(self._data + other._data) - return self.__class__(self._data + np.array(other, dtype=float)) - - def __sub__(self: T, other: Union["Vector", Iterable[float]]) -> T: - if isinstance(other, Vector): - return self.__class__(self._data - other._data) - return self.__class__(self._data - np.array(other, dtype=float)) - - def __mul__(self: T, scalar: float) -> T: - return self.__class__(self._data * scalar) - - def __rmul__(self: T, scalar: float) -> T: - return self.__mul__(scalar) - - def __truediv__(self: T, scalar: float) -> T: - return self.__class__(self._data / scalar) - - def __neg__(self: T) -> T: - return self.__class__(-self._data) - - def dot(self, other: Union["Vector", Iterable[float]]) -> float: - """Compute dot product.""" - if isinstance(other, Vector): - return float(np.dot(self._data, other._data)) - return float(np.dot(self._data, np.array(other, dtype=float))) - - def cross(self: T, other: Union["Vector", Iterable[float]]) -> T: - """Compute cross product (3D vectors only).""" - if self.dim != 3: - raise ValueError("Cross product is only defined for 3D vectors") - - if isinstance(other, Vector): - other_data = other._data - else: - other_data = np.array(other, dtype=float) - - if len(other_data) != 3: - raise ValueError("Cross product requires two 3D vectors") - - return self.__class__(np.cross(self._data, other_data)) - - def length(self) -> float: - """Compute the Euclidean length (magnitude) of the vector.""" - return float(np.linalg.norm(self._data)) - - def length_squared(self) -> float: - """Compute the squared length of the vector (faster than length()).""" - return float(np.sum(self._data * self._data)) - - def normalize(self: T) -> T: - """Return a normalized unit vector in the same direction.""" - length = self.length() - if length < 1e-10: # Avoid division by near-zero - return self.__class__(np.zeros_like(self._data)) - return self.__class__(self._data / length) - - def to_2d(self: T) -> T: - """Convert a vector to a 2D vector by taking only the x and y components.""" - return self.__class__(self._data[:2]) - - def distance(self, other: Union["Vector", Iterable[float]]) -> float: - """Compute Euclidean distance to another vector.""" - if isinstance(other, Vector): - return float(np.linalg.norm(self._data - other._data)) - return float(np.linalg.norm(self._data - np.array(other, dtype=float))) - - def distance_squared(self, other: Union["Vector", Iterable[float]]) -> float: - """Compute squared Euclidean distance to another vector (faster than distance()).""" - if isinstance(other, Vector): - diff = self._data - other._data - else: - diff = self._data - np.array(other, dtype=float) - return float(np.sum(diff * diff)) - - def angle(self, other: Union["Vector", Iterable[float]]) -> float: - """Compute the angle (in radians) between this vector and another.""" - if self.length() < 1e-10 or (isinstance(other, Vector) and other.length() < 1e-10): - return 0.0 - - if isinstance(other, Vector): - other_data = other._data - else: - other_data = np.array(other, dtype=float) - - cos_angle = np.clip( - np.dot(self._data, other_data) - / (np.linalg.norm(self._data) * np.linalg.norm(other_data)), - -1.0, - 1.0, - ) - return float(np.arccos(cos_angle)) - - def project(self: T, onto: Union["Vector", Iterable[float]]) -> T: - """Project this vector onto another vector.""" - if isinstance(onto, Vector): - onto_data = onto._data - else: - onto_data = np.array(onto, dtype=float) - - onto_length_sq = np.sum(onto_data * onto_data) - if onto_length_sq < 1e-10: - return self.__class__(np.zeros_like(self._data)) - - scalar_projection = np.dot(self._data, onto_data) / onto_length_sq - return self.__class__(scalar_projection * onto_data) - - @classmethod - def zeros(cls: type[T], dim: int) -> T: - """Create a zero vector of given dimension.""" - return cls(np.zeros(dim)) - - @classmethod - def ones(cls: type[T], dim: int) -> T: - """Create a vector of ones with given dimension.""" - return cls(np.ones(dim)) - - @classmethod - def unit_x(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the x direction.""" - v = np.zeros(dim) - v[0] = 1.0 - return cls(v) - - @classmethod - def unit_y(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the y direction.""" - v = np.zeros(dim) - v[1] = 1.0 - return cls(v) - - @classmethod - def unit_z(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the z direction.""" - v = np.zeros(dim) - if dim > 2: - v[2] = 1.0 - return cls(v) - - def to_list(self) -> list[float]: - """Convert the vector to a list.""" - return [float(x) for x in self._data] - - def to_tuple(self) -> builtins.tuple[float, ...]: - """Convert the vector to a tuple.""" - return tuple(self._data) - - def to_numpy(self) -> NDArray[np.float64]: - """Convert the vector to a numpy array.""" - return self._data - - -# Protocol approach for static type checking -@runtime_checkable -class VectorLike(Protocol): - """Protocol for types that can be treated as vectors.""" - - def __getitem__(self, key: int) -> float: ... - def __len__(self) -> int: ... - def __iter__(self) -> Iterable[float]: ... - - -def to_numpy(value: VectorLike) -> NDArray[np.float64]: - """Convert a vector-compatible value to a numpy array. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Numpy array representation - """ - if isinstance(value, Vector): - return value.data - elif isinstance(value, np.ndarray): - return value - else: - return np.array(value, dtype=float) - - -def to_vector(value: VectorLike) -> Vector: - """Convert a vector-compatible value to a Vector object. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Vector object - """ - if isinstance(value, Vector): - return value - else: - return Vector(value) - - -def to_tuple(value: VectorLike) -> tuple[float, ...]: - """Convert a vector-compatible value to a tuple. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Tuple of floats - """ - if isinstance(value, Vector): - return tuple(float(x) for x in value.data) - elif isinstance(value, np.ndarray): - return tuple(float(x) for x in value) - elif isinstance(value, tuple): - return tuple(float(x) for x in value) - else: - # Convert to list first to ensure we have an indexable sequence - data = [value[i] for i in range(len(value))] - return tuple(float(x) for x in data) - - -def to_list(value: VectorLike) -> list[float]: - """Convert a vector-compatible value to a list. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - List of floats - """ - if isinstance(value, Vector): - return [float(x) for x in value.data] - elif isinstance(value, np.ndarray): - return [float(x) for x in value] - elif isinstance(value, list): - return [float(x) for x in value] - else: - # Convert to list using indexing - return [float(value[i]) for i in range(len(value))] - - -# Helper functions to check dimensionality -def is_2d(value: VectorLike) -> bool: - """Check if a vector-compatible value is 2D. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - True if the value is 2D - """ - if isinstance(value, Vector): - return len(value) == 2 - elif isinstance(value, np.ndarray): - return value.shape[-1] == 2 or value.size == 2 - else: - return len(value) == 2 - - -def is_3d(value: VectorLike) -> bool: - """Check if a vector-compatible value is 3D. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - True if the value is 3D - """ - if isinstance(value, Vector): - return len(value) == 3 - elif isinstance(value, np.ndarray): - return value.shape[-1] == 3 or value.size == 3 - else: - return len(value) == 3 - - -# Extraction functions for XYZ components -def x(value: VectorLike) -> float: - """Get the X component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - X component as a float - """ - if isinstance(value, Vector): - return value.x - else: - return float(to_numpy(value)[0]) - - -def y(value: VectorLike) -> float: - """Get the Y component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Y component as a float - """ - if isinstance(value, Vector): - return value.y - else: - arr = to_numpy(value) - return float(arr[1]) if len(arr) > 1 else 0.0 - - -def z(value: VectorLike) -> float: - """Get the Z component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Z component as a float - """ - if isinstance(value, Vector): - return value.z - else: - arr = to_numpy(value) - return float(arr[2]) if len(arr) > 2 else 0.0 diff --git a/dimos/robot/unitree/unitree_skill_container.py b/dimos/robot/unitree/unitree_skill_container.py deleted file mode 100644 index d2f15b9efe..0000000000 --- a/dimos/robot/unitree/unitree_skill_container.py +++ /dev/null @@ -1,339 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import datetime -import difflib -import math -import time - -from unitree_webrtc_connect.constants import RTC_TOPIC - -from dimos.agents.annotation import skill -from dimos.core.core import rpc -from dimos.core.module import Module -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 -from dimos.navigation.base import NavigationState -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -UNITREE_WEBRTC_CONTROLS: list[tuple[str, int, str]] = [ - # ("Damp", 1001, "Lowers the robot to the ground fully."), - ( - "BalanceStand", - 1002, - "Activates a mode that maintains the robot in a balanced standing position.", - ), - ( - "StandUp", - 1004, - "Commands the robot to transition from a sitting or prone position to a standing posture.", - ), - ( - "StandDown", - 1005, - "Instructs the robot to move from a standing position to a sitting or prone posture.", - ), - ( - "RecoveryStand", - 1006, - "Recovers the robot to a state from which it can take more commands. Useful to run after multiple dynamic commands like front flips, Must run after skills like sit and jump and standup.", - ), - ("Sit", 1009, "Commands the robot to sit down from a standing or moving stance."), - ( - "RiseSit", - 1010, - "Commands the robot to rise back to a standing position from a sitting posture.", - ), - ( - "SwitchGait", - 1011, - "Switches the robot's walking pattern or style dynamically, suitable for different terrains or speeds.", - ), - ("Trigger", 1012, "Triggers a specific action or custom routine programmed into the robot."), - ( - "BodyHeight", - 1013, - "Adjusts the height of the robot's body from the ground, useful for navigating various obstacles.", - ), - ( - "FootRaiseHeight", - 1014, - "Controls how high the robot lifts its feet during movement, which can be adjusted for different surfaces.", - ), - ( - "SpeedLevel", - 1015, - "Sets or adjusts the speed at which the robot moves, with various levels available for different operational needs.", - ), - ( - "Hello", - 1016, - "Performs a greeting action, which could involve a wave or other friendly gesture.", - ), - ("Stretch", 1017, "Engages the robot in a stretching routine."), - ( - "TrajectoryFollow", - 1018, - "Directs the robot to follow a predefined trajectory, which could involve complex paths or maneuvers.", - ), - ( - "ContinuousGait", - 1019, - "Enables a mode for continuous walking or running, ideal for long-distance travel.", - ), - ("Content", 1020, "To display or trigger when the robot is happy."), - ("Wallow", 1021, "The robot falls onto its back and rolls around."), - ( - "Dance1", - 1022, - "Performs a predefined dance routine 1, programmed for entertainment or demonstration.", - ), - ("Dance2", 1023, "Performs another variant of a predefined dance routine 2."), - ("GetBodyHeight", 1024, "Retrieves the current height of the robot's body from the ground."), - ( - "GetFootRaiseHeight", - 1025, - "Retrieves the current height at which the robot's feet are being raised during movement.", - ), - ( - "GetSpeedLevel", - 1026, - "Retrieves the current speed level setting of the robot.", - ), - ( - "SwitchJoystick", - 1027, - "Switches the robot's control mode to respond to joystick input for manual operation.", - ), - ( - "Pose", - 1028, - "Commands the robot to assume a specific pose or posture as predefined in its programming.", - ), - ("Scrape", 1029, "The robot performs a scraping motion."), - ( - "FrontFlip", - 1030, - "Commands the robot to perform a front flip, showcasing its agility and dynamic movement capabilities.", - ), - ( - "FrontJump", - 1031, - "Instructs the robot to jump forward, demonstrating its explosive movement capabilities.", - ), - ( - "FrontPounce", - 1032, - "Commands the robot to perform a pouncing motion forward.", - ), - ( - "WiggleHips", - 1033, - "The robot performs a hip wiggling motion, often used for entertainment or demonstration purposes.", - ), - ( - "GetState", - 1034, - "Retrieves the current operational state of the robot, including its mode, position, and status.", - ), - ( - "EconomicGait", - 1035, - "Engages a more energy-efficient walking or running mode to conserve battery life.", - ), - ("FingerHeart", 1036, "Performs a finger heart gesture while on its hind legs."), - ( - "Handstand", - 1301, - "Commands the robot to perform a handstand, demonstrating balance and control.", - ), - ( - "CrossStep", - 1302, - "Commands the robot to perform cross-step movements.", - ), - ( - "OnesidedStep", - 1303, - "Commands the robot to perform one-sided step movements.", - ), - ("Bound", 1304, "Commands the robot to perform bounding movements."), - ("MoonWalk", 1305, "Commands the robot to perform a moonwalk motion."), - ("LeftFlip", 1042, "Executes a flip towards the left side."), - ("RightFlip", 1043, "Performs a flip towards the right side."), - ("Backflip", 1044, "Executes a backflip, a complex and dynamic maneuver."), -] - - -_UNITREE_COMMANDS = { - name: (id_, description) - for name, id_, description in UNITREE_WEBRTC_CONTROLS - if name not in ["Reverse", "Spin"] -} - - -class UnitreeSkillContainer(Module): - """Container for Unitree Go2 robot skills using the new framework.""" - - rpc_calls: list[str] = [ - "NavigationInterface.set_goal", - "NavigationInterface.get_state", - "NavigationInterface.is_goal_reached", - "NavigationInterface.cancel_goal", - "GO2Connection.publish_request", - ] - - @rpc - def start(self) -> None: - super().start() - # Initialize TF early so it can start receiving transforms. - _ = self.tf - - @rpc - def stop(self) -> None: - super().stop() - - @skill - def relative_move(self, forward: float = 0.0, left: float = 0.0, degrees: float = 0.0) -> str: - """Move the robot relative to its current position. - - The `degrees` arguments refers to the rotation the robot should be at the end, relative to its current rotation. - - Example calls: - - # Move to a point that's 2 meters forward and 1 to the right. - relative_move(forward=2, left=-1, degrees=0) - - # Move back 1 meter, while still facing the same direction. - relative_move(forward=-1, left=0, degrees=0) - - # Rotate 90 degrees to the right (in place) - relative_move(forward=0, left=0, degrees=-90) - - # Move 3 meters left, and face that direction - relative_move(forward=0, left=3, degrees=90) - """ - forward, left, degrees = float(forward), float(left), float(degrees) - - tf = self.tf.get("world", "base_link") - if tf is None: - return "Failed to get the position of the robot." - - try: - set_goal_rpc, get_state_rpc, is_goal_reached_rpc = self.get_rpc_calls( - "NavigationInterface.set_goal", - "NavigationInterface.get_state", - "NavigationInterface.is_goal_reached", - ) - except Exception: - logger.error("Navigation module not connected properly") - return "Failed to connect to navigation module." - - # TODO: Improve this. This is not a nice way to do it. I should - # subscribe to arrival/cancellation events instead. - - set_goal_rpc(self._generate_new_goal(tf.to_pose(), forward, left, degrees)) - - time.sleep(1.0) - - start_time = time.monotonic() - timeout = 100.0 - while get_state_rpc() == NavigationState.FOLLOWING_PATH: - if time.monotonic() - start_time > timeout: - return "Navigation timed out" - time.sleep(0.1) - - time.sleep(1.0) - - if not is_goal_reached_rpc(): - return "Navigation was cancelled or failed" - else: - return "Navigation goal reached" - - def _generate_new_goal( - self, current_pose: PoseStamped, forward: float, left: float, degrees: float - ) -> PoseStamped: - local_offset = Vector3(forward, left, 0) - global_offset = current_pose.orientation.rotate_vector(local_offset) - goal_position = current_pose.position + global_offset - - current_euler = current_pose.orientation.to_euler() - goal_yaw = current_euler.yaw + math.radians(degrees) - goal_euler = Vector3(current_euler.roll, current_euler.pitch, goal_yaw) - goal_orientation = Quaternion.from_euler(goal_euler) - - return PoseStamped(position=goal_position, orientation=goal_orientation) - - @skill - def wait(self, seconds: float) -> str: - """Wait for a specified amount of time. - - Args: - seconds: Seconds to wait - """ - time.sleep(seconds) - return f"Wait completed with length={seconds}s" - - @skill - def current_time(self) -> str: - """Provides current time.""" - return str(datetime.datetime.now()) - - @skill - def execute_sport_command(self, command_name: str) -> str: - try: - publish_request = self.get_rpc_calls("GO2Connection.publish_request") - except Exception: - logger.error("GO2Connection not connected properly") - return "Failed to connect to GO2Connection." - - if command_name not in _UNITREE_COMMANDS: - suggestions = difflib.get_close_matches( - command_name, _UNITREE_COMMANDS.keys(), n=3, cutoff=0.6 - ) - return f"There's no '{command_name}' command. Did you mean: {suggestions}" - - id_, _ = _UNITREE_COMMANDS[command_name] - - try: - publish_request(RTC_TOPIC["SPORT_MOD"], {"api_id": id_}) - return f"'{command_name}' command executed successfully." - except Exception as e: - logger.error(f"Failed to execute {command_name}: {e}") - return "Failed to execute the command." - - -_commands = "\n".join( - [f'- "{name}": {description}' for name, (_, description) in _UNITREE_COMMANDS.items()] -) - -UnitreeSkillContainer.execute_sport_command.__doc__ = f"""Execute a Unitree sport command. - -Example usage: - - execute_sport_command("FrontPounce") - -Here are all the command names and what they do. - -{_commands} -""" - - -unitree_skills = UnitreeSkillContainer.blueprint - -__all__ = ["UnitreeSkillContainer", "unitree_skills"] diff --git a/dimos/robot/unitree_webrtc/README.md b/dimos/robot/unitree_webrtc/README.md deleted file mode 100644 index ce39201c8b..0000000000 --- a/dimos/robot/unitree_webrtc/README.md +++ /dev/null @@ -1 +0,0 @@ -This directory only exists because some of the --replay tests depend on its existence (python pickle uses module names/paths so we would need to redo the pickle files). diff --git a/dimos/robot/unitree_webrtc/__init__.py b/dimos/robot/unitree_webrtc/__init__.py deleted file mode 100644 index 4524bba226..0000000000 --- a/dimos/robot/unitree_webrtc/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility package for legacy dimos.robot.unitree_webrtc imports.""" - -from importlib import import_module -import sys - -_ALIAS_MODULES = { - "demo_error_on_name_conflicts": "dimos.robot.unitree.demo_error_on_name_conflicts", - "depth_module": "dimos.robot.unitree.depth_module", - "keyboard_teleop": "dimos.robot.unitree.keyboard_teleop", - "mujoco_connection": "dimos.robot.unitree.mujoco_connection", - "type": "dimos.robot.unitree.type", - "unitree_g1_skill_container": "dimos.robot.unitree.g1.skill_container", - "unitree_skill_container": "dimos.robot.unitree.unitree_skill_container", -} - -for alias, target in _ALIAS_MODULES.items(): - sys.modules[f"{__name__}.{alias}"] = import_module(target) diff --git a/dimos/robot/unitree_webrtc/type/__init__.py b/dimos/robot/unitree_webrtc/type/__init__.py deleted file mode 100644 index 03ff4f4563..0000000000 --- a/dimos/robot/unitree_webrtc/type/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-exports for legacy dimos.robot.unitree_webrtc.type.* imports.""" - -import importlib - -__all__ = [] - - -def __getattr__(name: str): # type: ignore[no-untyped-def] - module = importlib.import_module("dimos.robot.unitree.type") - try: - return getattr(module, name) - except AttributeError as exc: - raise AttributeError(f"No {__name__} attribute {name}") from exc - - -def __dir__() -> list[str]: - module = importlib.import_module("dimos.robot.unitree.type") - return [name for name in dir(module) if not name.startswith("_")] diff --git a/dimos/robot/unitree_webrtc/type/lidar.py b/dimos/robot/unitree_webrtc/type/lidar.py deleted file mode 100644 index d8dbe98fd2..0000000000 --- a/dimos/robot/unitree_webrtc/type/lidar.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.lidar import * # noqa: F403 diff --git a/dimos/robot/unitree_webrtc/type/lowstate.py b/dimos/robot/unitree_webrtc/type/lowstate.py deleted file mode 100644 index d92ee4d5b1..0000000000 --- a/dimos/robot/unitree_webrtc/type/lowstate.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.lowstate import * # noqa: F403 diff --git a/dimos/robot/unitree_webrtc/type/map.py b/dimos/robot/unitree_webrtc/type/map.py deleted file mode 100644 index 69bbb409c7..0000000000 --- a/dimos/robot/unitree_webrtc/type/map.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.map import * # noqa: F403 diff --git a/dimos/robot/unitree_webrtc/type/odometry.py b/dimos/robot/unitree_webrtc/type/odometry.py deleted file mode 100644 index 111ba0b945..0000000000 --- a/dimos/robot/unitree_webrtc/type/odometry.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.odometry import * # noqa: F403 diff --git a/dimos/robot/unitree_webrtc/type/timeseries.py b/dimos/robot/unitree_webrtc/type/timeseries.py deleted file mode 100644 index 34f9587ade..0000000000 --- a/dimos/robot/unitree_webrtc/type/timeseries.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.timeseries import * # noqa: F403 diff --git a/dimos/robot/unitree_webrtc/type/vector.py b/dimos/robot/unitree_webrtc/type/vector.py deleted file mode 100644 index 20d07c76e8..0000000000 --- a/dimos/robot/unitree_webrtc/type/vector.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Compatibility re-export for dimos.robot.unitree_webrtc.type.${name}.""" - -from dimos.robot.unitree.type.vector import * # noqa: F403 diff --git a/dimos/robot/utils/README.md b/dimos/robot/utils/README.md deleted file mode 100644 index 5a84b20c4a..0000000000 --- a/dimos/robot/utils/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Robot Utils - -## RobotDebugger - -The `RobotDebugger` provides a way to debug a running robot through the python shell. - -Requirements: - -```bash -pip install rpyc -``` - -### Usage - -1. **Add to your robot application:** - ```python - from dimos.robot.utils.robot_debugger import RobotDebugger - - # In your robot application's context manager or main loop: - with RobotDebugger(robot): - # Your robot code here - pass - - # Or better, with an exit stack. - exit_stack.enter_context(RobotDebugger(robot)) - ``` - -2. **Start your robot with debugging enabled:** - ```bash - ROBOT_DEBUGGER=true python your_robot_script.py - ``` - -3. **Open the python shell:** - ```bash - ./bin/robot-debugger - >>> robot.explore() - True - ``` diff --git a/dimos/robot/utils/robot_debugger.py b/dimos/robot/utils/robot_debugger.py deleted file mode 100644 index c7f3cd7291..0000000000 --- a/dimos/robot/utils/robot_debugger.py +++ /dev/null @@ -1,59 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -from dimos.core.resource import Resource -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class RobotDebugger(Resource): - def __init__(self, robot) -> None: # type: ignore[no-untyped-def] - self._robot = robot - self._threaded_server = None - - def start(self) -> None: - if not os.getenv("ROBOT_DEBUGGER"): - return - - try: - import rpyc # type: ignore[import-not-found] - from rpyc.utils.server import ThreadedServer # type: ignore[import-not-found] - except ImportError: - return - - logger.info( - "Starting the robot debugger. You can open a python shell with `./bin/robot-debugger`" - ) - - robot = self._robot - - class RobotService(rpyc.Service): # type: ignore[misc] - def exposed_robot(self): # type: ignore[no-untyped-def] - return robot - - self._threaded_server = ThreadedServer( - RobotService, - port=18861, - protocol_config={ - "allow_all_attrs": True, - }, - ) - self._threaded_server.start() # type: ignore[attr-defined] - - def stop(self) -> None: - if self._threaded_server: - self._threaded_server.close() diff --git a/dimos/rxpy_backpressure/LICENSE.txt b/dimos/rxpy_backpressure/LICENSE.txt deleted file mode 100644 index 8e1d704dc7..0000000000 --- a/dimos/rxpy_backpressure/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Mark Haynes - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/dimos/rxpy_backpressure/__init__.py b/dimos/rxpy_backpressure/__init__.py deleted file mode 100644 index ff3b1f37c0..0000000000 --- a/dimos/rxpy_backpressure/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from dimos.rxpy_backpressure.backpressure import BackPressure - -__all__ = [BackPressure] diff --git a/dimos/rxpy_backpressure/backpressure.py b/dimos/rxpy_backpressure/backpressure.py deleted file mode 100644 index bf84fa95bd..0000000000 --- a/dimos/rxpy_backpressure/backpressure.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) rxpy_backpressure -from dimos.rxpy_backpressure.drop import ( - wrap_observer_with_buffer_strategy, - wrap_observer_with_drop_strategy, -) -from dimos.rxpy_backpressure.latest import wrap_observer_with_latest_strategy - - -class BackPressure: - """ - Latest strategy will remember the next most recent message to process and will call the observer with it when - the observer has finished processing its current message. - """ - - LATEST = wrap_observer_with_latest_strategy - - """ - Drop strategy accepts a cache size, the strategy will remember the most recent messages and remove older - messages from the cache. The strategy guarantees that the oldest messages in the cache are passed to the - observer first. - :param cache_size: int = 10 is default - """ - DROP = wrap_observer_with_drop_strategy - - """ - Buffer strategy has a unbounded cache and will pass all messages to its consumer in the order it received them - beware of Memory leaks due to a build up of messages. - """ - BUFFER = wrap_observer_with_buffer_strategy diff --git a/dimos/rxpy_backpressure/drop.py b/dimos/rxpy_backpressure/drop.py deleted file mode 100644 index 6273042f42..0000000000 --- a/dimos/rxpy_backpressure/drop.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) rxpy_backpressure -from typing import Any - -from dimos.rxpy_backpressure.function_runner import thread_function_runner -from dimos.rxpy_backpressure.locks import BooleanLock, Lock -from dimos.rxpy_backpressure.observer import Observer - - -class DropBackPressureStrategy(Observer): - def __init__(self, wrapped_observer: Observer, cache_size: int): - self.wrapped_observer: Observer = wrapped_observer - self.__function_runner = thread_function_runner - self.__lock: Lock = BooleanLock() - self.__cache_size: int | None = cache_size - self.__message_cache: list = [] - self.__error_cache: list = [] - - def on_next(self, message): - if self.__lock.is_locked(): - self.__update_cache(self.__message_cache, message) - else: - self.__lock.lock() - self.__function_runner(self, self.__on_next, message) - - @staticmethod - def __on_next(self, message: any): - self.wrapped_observer.on_next(message) - if len(self.__message_cache) > 0: - self.__function_runner(self, self.__on_next, self.__message_cache.pop(0)) - else: - self.__lock.unlock() - - def on_error(self, error: any): - if self.__lock.is_locked(): - self.__update_cache(self.__error_cache, error) - else: - self.__lock.lock() - self.__function_runner(self, self.__on_error, error) - - @staticmethod - def __on_error(self, error: any): - self.wrapped_observer.on_error(error) - if len(self.__error_cache) > 0: - self.__function_runner(self, self.__on_error, self.__error_cache.pop(0)) - else: - self.__lock.unlock() - - def __update_cache(self, cache: list, item: Any): - if self.__cache_size is None or len(cache) < self.__cache_size: - cache.append(item) - else: - cache.pop(0) - cache.append(item) - - def on_completed(self): - self.wrapped_observer.on_completed() - - def is_locked(self): - return self.__lock.is_locked() - - -def wrap_observer_with_drop_strategy(observer: Observer, cache_size: int = 10) -> Observer: - return DropBackPressureStrategy(observer, cache_size=cache_size) - - -def wrap_observer_with_buffer_strategy(observer: Observer) -> Observer: - return DropBackPressureStrategy(observer, cache_size=None) diff --git a/dimos/rxpy_backpressure/function_runner.py b/dimos/rxpy_backpressure/function_runner.py deleted file mode 100644 index 7779016d41..0000000000 --- a/dimos/rxpy_backpressure/function_runner.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) rxpy_backpressure -from threading import Thread - - -def thread_function_runner(self, func, message): - Thread(target=func, args=(self, message)).start() diff --git a/dimos/rxpy_backpressure/latest.py b/dimos/rxpy_backpressure/latest.py deleted file mode 100644 index 73a4ebc8d9..0000000000 --- a/dimos/rxpy_backpressure/latest.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) rxpy_backpressure -from typing import Optional - -from dimos.rxpy_backpressure.function_runner import thread_function_runner -from dimos.rxpy_backpressure.locks import BooleanLock, Lock -from dimos.rxpy_backpressure.observer import Observer - - -class LatestBackPressureStrategy(Observer): - def __init__(self, wrapped_observer: Observer): - self.wrapped_observer: Observer = wrapped_observer - self.__function_runner = thread_function_runner - self.__lock: Lock = BooleanLock() - self.__message_cache: Optional = None - self.__error_cache: Optional = None - - def on_next(self, message): - if self.__lock.is_locked(): - self.__message_cache = message - else: - self.__lock.lock() - self.__function_runner(self, self.__on_next, message) - - @staticmethod - def __on_next(self, message: any): - self.wrapped_observer.on_next(message) - if self.__message_cache is not None: - self.__function_runner(self, self.__on_next, self.__message_cache) - self.__message_cache = None - else: - self.__lock.unlock() - - def on_error(self, error: any): - if self.__lock.is_locked(): - self.__error_cache = error - else: - self.__lock.lock() - self.__function_runner(self, self.__on_error, error) - - @staticmethod - def __on_error(self, error: any): - self.wrapped_observer.on_error(error) - if self.__error_cache: - self.__function_runner(self, self.__on_error, self.__error_cache) - self.__error_cache = None - else: - self.__lock.unlock() - - def on_completed(self): - self.wrapped_observer.on_completed() - - def is_locked(self): - return self.__lock.is_locked() - - -def wrap_observer_with_latest_strategy(observer: Observer) -> Observer: - return LatestBackPressureStrategy(observer) diff --git a/dimos/rxpy_backpressure/locks.py b/dimos/rxpy_backpressure/locks.py deleted file mode 100644 index 62c58c25b2..0000000000 --- a/dimos/rxpy_backpressure/locks.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) rxpy_backpressure -from abc import abstractmethod - - -class Lock: - @abstractmethod - def is_locked(self) -> bool: - return NotImplemented - - @abstractmethod - def unlock(self): - return NotImplemented - - @abstractmethod - def lock(self): - return NotImplemented - - -class BooleanLock(Lock): - def __init__(self): - self.locked: bool = False - - def is_locked(self) -> bool: - return self.locked - - def unlock(self): - self.locked = False - - def lock(self): - self.locked = True diff --git a/dimos/rxpy_backpressure/observer.py b/dimos/rxpy_backpressure/observer.py deleted file mode 100644 index 7cf023c04f..0000000000 --- a/dimos/rxpy_backpressure/observer.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright (c) rxpy_backpressure -from abc import ABCMeta, abstractmethod - - -class Observer: - __metaclass__ = ABCMeta - - @abstractmethod - def on_next(self, value): - return NotImplemented - - @abstractmethod - def on_error(self, error): - return NotImplemented - - @abstractmethod - def on_completed(self): - return NotImplemented diff --git a/dimos/simulation/__init__.py b/dimos/simulation/__init__.py deleted file mode 100644 index 1a68191a36..0000000000 --- a/dimos/simulation/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Try to import Isaac Sim components -try: - from .isaac import IsaacSimulator, IsaacStream -except ImportError: - IsaacSimulator = None # type: ignore[assignment, misc] - IsaacStream = None # type: ignore[assignment, misc] - -# Try to import Genesis components -try: - from .genesis import GenesisSimulator, GenesisStream -except ImportError: - GenesisSimulator = None # type: ignore[assignment, misc] - GenesisStream = None # type: ignore[assignment, misc] - -__all__ = ["GenesisSimulator", "GenesisStream", "IsaacSimulator", "IsaacStream"] diff --git a/dimos/simulation/base/__init__.py b/dimos/simulation/base/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/simulation/base/simulator_base.py b/dimos/simulation/base/simulator_base.py deleted file mode 100644 index 59e366a1d3..0000000000 --- a/dimos/simulation/base/simulator_base.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - - -class SimulatorBase(ABC): - """Base class for simulators.""" - - @abstractmethod - def __init__( - self, - headless: bool = True, - open_usd: str | None = None, # Keep for Isaac compatibility - entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] # Add for Genesis - ) -> None: - """Initialize the simulator. - - Args: - headless: Whether to run without visualization - open_usd: Path to USD file (for Isaac) - entities: List of entity configurations (for Genesis) - """ - self.headless = headless - self.open_usd = open_usd - self.stage = None - - @abstractmethod - def get_stage(self): # type: ignore[no-untyped-def] - """Get the current stage/scene.""" - pass - - @abstractmethod - def close(self): # type: ignore[no-untyped-def] - """Close the simulation.""" - pass diff --git a/dimos/simulation/base/stream_base.py b/dimos/simulation/base/stream_base.py deleted file mode 100644 index 9f8898439e..0000000000 --- a/dimos/simulation/base/stream_base.py +++ /dev/null @@ -1,116 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -from pathlib import Path -import subprocess -from typing import Literal - -AnnotatorType = Literal["rgb", "normals", "bounding_box_3d", "motion_vectors"] -TransportType = Literal["tcp", "udp"] - - -class StreamBase(ABC): - """Base class for simulation streaming.""" - - @abstractmethod - def __init__( # type: ignore[no-untyped-def] - self, - simulator, - width: int = 1920, - height: int = 1080, - fps: int = 60, - camera_path: str = "/World/camera", - annotator_type: AnnotatorType = "rgb", - transport: TransportType = "tcp", - rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: str | Path | None = None, - ) -> None: - """Initialize the stream. - - Args: - simulator: Simulator instance - width: Stream width in pixels - height: Stream height in pixels - fps: Frames per second - camera_path: Camera path in scene - annotator: Type of annotator to use - transport: Transport protocol - rtsp_url: RTSP stream URL - usd_path: Optional USD file path to load - """ - self.simulator = simulator - self.width = width - self.height = height - self.fps = fps - self.camera_path = camera_path - self.annotator_type = annotator_type - self.transport = transport - self.rtsp_url = rtsp_url - self.proc = None - - @abstractmethod - def _load_stage(self, usd_path: str | Path): # type: ignore[no-untyped-def] - """Load stage from file.""" - pass - - @abstractmethod - def _setup_camera(self): # type: ignore[no-untyped-def] - """Setup and validate camera.""" - pass - - def _setup_ffmpeg(self) -> None: - """Setup FFmpeg process for streaming.""" - command = [ - "ffmpeg", - "-y", - "-f", - "rawvideo", - "-vcodec", - "rawvideo", - "-pix_fmt", - "bgr24", - "-s", - f"{self.width}x{self.height}", - "-r", - str(self.fps), - "-i", - "-", - "-an", - "-c:v", - "h264_nvenc", - "-preset", - "fast", - "-f", - "rtsp", - "-rtsp_transport", - self.transport, - self.rtsp_url, - ] - self.proc = subprocess.Popen(command, stdin=subprocess.PIPE) # type: ignore[assignment] - - @abstractmethod - def _setup_annotator(self): # type: ignore[no-untyped-def] - """Setup annotator.""" - pass - - @abstractmethod - def stream(self): # type: ignore[no-untyped-def] - """Start streaming.""" - pass - - @abstractmethod - def cleanup(self): # type: ignore[no-untyped-def] - """Cleanup resources.""" - pass diff --git a/dimos/simulation/engines/__init__.py b/dimos/simulation/engines/__init__.py deleted file mode 100644 index d437f9a7cd..0000000000 --- a/dimos/simulation/engines/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Simulation engines for manipulator backends.""" - -from __future__ import annotations - -from typing import Literal - -from dimos.simulation.engines.base import SimulationEngine -from dimos.simulation.engines.mujoco_engine import MujocoEngine - -EngineType = Literal["mujoco"] - -_ENGINES: dict[EngineType, type[SimulationEngine]] = { - "mujoco": MujocoEngine, -} - - -def get_engine(engine_name: EngineType) -> type[SimulationEngine]: - return _ENGINES[engine_name] - - -__all__ = [ - "EngineType", - "SimulationEngine", - "get_engine", -] diff --git a/dimos/simulation/engines/base.py b/dimos/simulation/engines/base.py deleted file mode 100644 index d450614c62..0000000000 --- a/dimos/simulation/engines/base.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Base interfaces for simulator engines.""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from pathlib import Path - - from dimos.msgs.sensor_msgs import JointState - - -class SimulationEngine(ABC): - """Abstract base class for a simulator engine instance.""" - - def __init__(self, config_path: Path, headless: bool) -> None: - self._config_path = config_path - self._headless = headless - - @property - def config_path(self) -> Path: - return self._config_path - - @property - def headless(self) -> bool: - return self._headless - - @abstractmethod - def connect(self) -> bool: - """Connect to simulation and start the engine.""" - - @abstractmethod - def disconnect(self) -> bool: - """Disconnect from simulation and stop the engine.""" - - @property - @abstractmethod - def connected(self) -> bool: - """Whether the engine is connected.""" - - @property - @abstractmethod - def num_joints(self) -> int: - """Number of joints for the loaded robot.""" - - @property - @abstractmethod - def joint_names(self) -> list[str]: - """Joint names for the loaded robot.""" - - @abstractmethod - def read_joint_positions(self) -> list[float]: - """Read joint positions in radians.""" - - @abstractmethod - def read_joint_velocities(self) -> list[float]: - """Read joint velocities in rad/s.""" - - @abstractmethod - def read_joint_efforts(self) -> list[float]: - """Read joint efforts in Nm.""" - - @abstractmethod - def write_joint_command(self, command: JointState) -> None: - """Command joints using a JointState message.""" - - @abstractmethod - def hold_current_position(self) -> None: - """Hold current joint positions.""" diff --git a/dimos/simulation/engines/mujoco_engine.py b/dimos/simulation/engines/mujoco_engine.py deleted file mode 100644 index ddaaa25ad3..0000000000 --- a/dimos/simulation/engines/mujoco_engine.py +++ /dev/null @@ -1,300 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""MuJoCo simulation engine implementation.""" - -from __future__ import annotations - -import threading -import time -from typing import TYPE_CHECKING - -import mujoco -import mujoco.viewer as viewer # type: ignore[import-untyped,import-not-found] - -from dimos.simulation.engines.base import SimulationEngine -from dimos.simulation.utils.xml_parser import JointMapping, build_joint_mappings -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from pathlib import Path - - from dimos.msgs.sensor_msgs import JointState - -logger = setup_logger() - - -class MujocoEngine(SimulationEngine): - """ - MuJoCo simulation engine. - - - starts MuJoCo simulation engine - - loads robot/environment into simulation - - applies control commands - """ - - def __init__(self, config_path: Path, headless: bool) -> None: - super().__init__(config_path=config_path, headless=headless) - - xml_path = self._resolve_xml_path(config_path) - self._model = mujoco.MjModel.from_xml_path(str(xml_path)) - self._xml_path = xml_path - - self._data = mujoco.MjData(self._model) - self._joint_mappings = build_joint_mappings(self._xml_path, self._model) - self._joint_names = [mapping.name for mapping in self._joint_mappings] - self._num_joints = len(self._joint_names) - timestep = float(self._model.opt.timestep) - self._control_frequency = 1.0 / timestep if timestep > 0.0 else 100.0 - - self._connected = False - self._lock = threading.Lock() - self._stop_event = threading.Event() - self._sim_thread: threading.Thread | None = None - - self._joint_positions = [0.0] * self._num_joints - self._joint_velocities = [0.0] * self._num_joints - self._joint_efforts = [0.0] * self._num_joints - - self._joint_position_targets = [0.0] * self._num_joints - self._joint_velocity_targets = [0.0] * self._num_joints - self._joint_effort_targets = [0.0] * self._num_joints - self._command_mode = "position" - for i, mapping in enumerate(self._joint_mappings): - current_pos = self._current_position(mapping) - self._joint_position_targets[i] = current_pos - self._joint_positions[i] = current_pos - - def _resolve_xml_path(self, config_path: Path) -> Path: - if config_path is None: - raise ValueError("config_path is required for MuJoCo simulation loading") - resolved = config_path.expanduser() - xml_path = resolved / "scene.xml" if resolved.is_dir() else resolved - if not xml_path.exists(): - raise FileNotFoundError(f"MuJoCo XML not found: {xml_path}") - return xml_path - - def _current_position(self, mapping: JointMapping) -> float: - if mapping.joint_id is not None and mapping.qpos_adr is not None: - return float(self._data.qpos[mapping.qpos_adr]) - if mapping.tendon_qpos_adrs: - return float( - sum(self._data.qpos[adr] for adr in mapping.tendon_qpos_adrs) - / len(mapping.tendon_qpos_adrs) - ) - if mapping.actuator_id is not None: - return float(self._data.actuator_length[mapping.actuator_id]) - return 0.0 - - def _apply_control(self) -> None: - with self._lock: - if self._command_mode == "effort": - targets = list(self._joint_effort_targets) - elif self._command_mode == "velocity": - targets = list(self._joint_velocity_targets) - elif self._command_mode == "position": - targets = list(self._joint_position_targets) - for i, mapping in enumerate(self._joint_mappings): - if mapping.actuator_id is None: - continue - if i < len(targets): - self._data.ctrl[mapping.actuator_id] = targets[i] - - def _update_joint_state(self) -> None: - with self._lock: - for i, mapping in enumerate(self._joint_mappings): - if mapping.joint_id is not None: - if mapping.qpos_adr is not None: - self._joint_positions[i] = float(self._data.qpos[mapping.qpos_adr]) - if mapping.dof_adr is not None: - self._joint_velocities[i] = float(self._data.qvel[mapping.dof_adr]) - self._joint_efforts[i] = float(self._data.qfrc_actuator[mapping.dof_adr]) - continue - - if mapping.tendon_qpos_adrs: - pos_sum = sum(self._data.qpos[adr] for adr in mapping.tendon_qpos_adrs) - count = len(mapping.tendon_qpos_adrs) - self._joint_positions[i] = float(pos_sum / count) - if mapping.tendon_dof_adrs: - vel_sum = sum(self._data.qvel[adr] for adr in mapping.tendon_dof_adrs) - self._joint_velocities[i] = float(vel_sum / len(mapping.tendon_dof_adrs)) - else: - self._joint_velocities[i] = 0.0 - elif mapping.actuator_id is not None: - self._joint_positions[i] = float( - self._data.actuator_length[mapping.actuator_id] - ) - self._joint_velocities[i] = 0.0 - - if mapping.actuator_id is not None: - self._joint_efforts[i] = float(self._data.actuator_force[mapping.actuator_id]) - - def connect(self) -> bool: - try: - logger.info(f"{self.__class__.__name__}: connect()") - with self._lock: - self._connected = True - self._stop_event.clear() - - if self._sim_thread is None or not self._sim_thread.is_alive(): - self._sim_thread = threading.Thread( - target=self._sim_loop, - name=f"{self.__class__.__name__}Sim", - daemon=True, - ) - self._sim_thread.start() - return True - except Exception as e: - logger.error(f"{self.__class__.__name__}: connect() failed: {e}") - return False - - def disconnect(self) -> bool: - try: - logger.info(f"{self.__class__.__name__}: disconnect()") - with self._lock: - self._connected = False - self._stop_event.set() - if self._sim_thread and self._sim_thread.is_alive(): - self._sim_thread.join(timeout=2.0) - self._sim_thread = None - return True - except Exception as e: - logger.error(f"{self.__class__.__name__}: disconnect() failed: {e}") - return False - - def _sim_loop(self) -> None: - logger.info(f"{self.__class__.__name__}: sim loop started") - dt = 1.0 / self._control_frequency - - def _step_once(sync_viewer: bool) -> None: - loop_start = time.time() - self._apply_control() - mujoco.mj_step(self._model, self._data) - if sync_viewer: - m_viewer.sync() - self._update_joint_state() - - elapsed = time.time() - loop_start - sleep_time = dt - elapsed - if sleep_time > 0: - time.sleep(sleep_time) - - if self._headless: - while not self._stop_event.is_set(): - _step_once(sync_viewer=False) - else: - with viewer.launch_passive( - self._model, self._data, show_left_ui=False, show_right_ui=False - ) as m_viewer: - while m_viewer.is_running() and not self._stop_event.is_set(): - _step_once(sync_viewer=True) - - logger.info(f"{self.__class__.__name__}: sim loop stopped") - - @property - def connected(self) -> bool: - with self._lock: - return self._connected - - @property - def num_joints(self) -> int: - return self._num_joints - - @property - def joint_names(self) -> list[str]: - return list(self._joint_names) - - @property - def model(self) -> mujoco.MjModel: - return self._model - - @property - def joint_positions(self) -> list[float]: - with self._lock: - return list(self._joint_positions) - - @property - def joint_velocities(self) -> list[float]: - with self._lock: - return list(self._joint_velocities) - - @property - def joint_efforts(self) -> list[float]: - with self._lock: - return list(self._joint_efforts) - - @property - def control_frequency(self) -> float: - return self._control_frequency - - def read_joint_positions(self) -> list[float]: - return self.joint_positions - - def read_joint_velocities(self) -> list[float]: - return self.joint_velocities - - def read_joint_efforts(self) -> list[float]: - return self.joint_efforts - - def write_joint_command(self, command: JointState) -> None: - if command.position: - self._command_mode = "position" - self._set_position_targets(command.position) - return - if command.velocity: - self._command_mode = "velocity" - self._set_velocity_targets(command.velocity) - return - if command.effort: - self._command_mode = "effort" - self._set_effort_targets(command.effort) - return - - def _set_position_targets(self, positions: list[float]) -> None: - if len(positions) > self._num_joints: - raise ValueError( - f"Position command has {len(positions)} joints, expected at most {self._num_joints}" - ) - with self._lock: - for i in range(len(positions)): - self._joint_position_targets[i] = float(positions[i]) - - def _set_velocity_targets(self, velocities: list[float]) -> None: - if len(velocities) > self._num_joints: - raise ValueError( - f"Velocity command has {len(velocities)} joints, expected at most {self._num_joints}" - ) - with self._lock: - for i in range(len(velocities)): - self._joint_velocity_targets[i] = float(velocities[i]) - - def _set_effort_targets(self, efforts: list[float]) -> None: - if len(efforts) > self._num_joints: - raise ValueError( - f"Effort command has {len(efforts)} joints, expected at most {self._num_joints}" - ) - with self._lock: - for i in range(len(efforts)): - self._joint_effort_targets[i] = float(efforts[i]) - - def hold_current_position(self) -> None: - with self._lock: - self._command_mode = "position" - for i, mapping in enumerate(self._joint_mappings): - self._joint_position_targets[i] = self._current_position(mapping) - - -__all__ = [ - "MujocoEngine", -] diff --git a/dimos/simulation/genesis/__init__.py b/dimos/simulation/genesis/__init__.py deleted file mode 100644 index 5657d9167b..0000000000 --- a/dimos/simulation/genesis/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .simulator import GenesisSimulator -from .stream import GenesisStream - -__all__ = ["GenesisSimulator", "GenesisStream"] diff --git a/dimos/simulation/genesis/simulator.py b/dimos/simulation/genesis/simulator.py deleted file mode 100644 index 4e679dcfa3..0000000000 --- a/dimos/simulation/genesis/simulator.py +++ /dev/null @@ -1,159 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import genesis as gs # type: ignore[import-not-found] - -from ..base.simulator_base import SimulatorBase - - -class GenesisSimulator(SimulatorBase): - """Genesis simulator implementation.""" - - def __init__( - self, - headless: bool = True, - open_usd: str | None = None, # Keep for compatibility - entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] - ) -> None: - """Initialize the Genesis simulation. - - Args: - headless: Whether to run without visualization - open_usd: Path to USD file (for Isaac) - entities: List of entity configurations to load. Each entity is a dict with: - - type: str ('mesh', 'urdf', 'mjcf', 'primitive') - - path: str (file path for mesh/urdf/mjcf) - - params: dict (parameters for primitives or loading options) - """ - super().__init__(headless, open_usd, entities) - - # Initialize Genesis - gs.init() - - # Create scene with viewer options - self.scene = gs.Scene( - show_viewer=not headless, - viewer_options=gs.options.ViewerOptions( - res=(1280, 960), - camera_pos=(3.5, 0.0, 2.5), - camera_lookat=(0.0, 0.0, 0.5), - camera_fov=40, - max_FPS=60, - ), - vis_options=gs.options.VisOptions( - show_world_frame=True, - world_frame_size=1.0, - show_link_frame=False, - show_cameras=False, - plane_reflection=True, - ambient_light=(0.1, 0.1, 0.1), - ), - renderer=gs.renderers.Rasterizer(), - ) - - # Handle USD parameter for compatibility - if open_usd: - print(f"[Warning] USD files not supported in Genesis. Ignoring: {open_usd}") - - # Load entities if provided - if entities: - self._load_entities(entities) - - # Don't build scene yet - let stream add camera first - self.is_built = False - - def _load_entities(self, entities: list[dict[str, str | dict]]): # type: ignore[no-untyped-def, type-arg] - """Load multiple entities into the scene.""" - for entity in entities: - entity_type = entity.get("type", "").lower() # type: ignore[union-attr] - path = entity.get("path", "") - params = entity.get("params", {}) - - try: - if entity_type == "mesh": - mesh = gs.morphs.Mesh( - file=path, # Explicit file argument - **params, - ) - self.scene.add_entity(mesh) - print(f"[Genesis] Added mesh from {path}") - - elif entity_type == "urdf": - robot = gs.morphs.URDF( - file=path, # Explicit file argument - **params, - ) - self.scene.add_entity(robot) - print(f"[Genesis] Added URDF robot from {path}") - - elif entity_type == "mjcf": - mujoco = gs.morphs.MJCF( - file=path, # Explicit file argument - **params, - ) - self.scene.add_entity(mujoco) - print(f"[Genesis] Added MJCF model from {path}") - - elif entity_type == "primitive": - shape_type = params.pop("shape", "plane") # type: ignore[union-attr] - if shape_type == "plane": - morph = gs.morphs.Plane(**params) - elif shape_type == "box": - morph = gs.morphs.Box(**params) - elif shape_type == "sphere": - morph = gs.morphs.Sphere(**params) - else: - raise ValueError(f"Unsupported primitive shape: {shape_type}") - - # Add position if not specified - if "pos" not in params: - if shape_type == "plane": - morph.pos = [0, 0, 0] - else: - morph.pos = [0, 0, 1] # Lift objects above ground - - self.scene.add_entity(morph) - print(f"[Genesis] Added {shape_type} at position {morph.pos}") - - else: - raise ValueError(f"Unsupported entity type: {entity_type}") - - except Exception as e: - print(f"[Warning] Failed to load entity {entity}: {e!s}") - - def add_entity(self, entity_type: str, path: str = "", **params) -> None: # type: ignore[no-untyped-def] - """Add a single entity to the scene. - - Args: - entity_type: Type of entity ('mesh', 'urdf', 'mjcf', 'primitive') - path: File path for mesh/urdf/mjcf entities - **params: Additional parameters for entity creation - """ - self._load_entities([{"type": entity_type, "path": path, "params": params}]) - - def get_stage(self): # type: ignore[no-untyped-def] - """Get the current stage/scene.""" - return self.scene - - def build(self) -> None: - """Build the scene if not already built.""" - if not self.is_built: - self.scene.build() - self.is_built = True - - def close(self) -> None: - """Close the simulation.""" - # Genesis handles cleanup automatically - pass diff --git a/dimos/simulation/genesis/stream.py b/dimos/simulation/genesis/stream.py deleted file mode 100644 index 0d3bcc6832..0000000000 --- a/dimos/simulation/genesis/stream.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path -import time - -import cv2 -import numpy as np - -from ..base.stream_base import AnnotatorType, StreamBase, TransportType - - -class GenesisStream(StreamBase): - """Genesis stream implementation.""" - - def __init__( # type: ignore[no-untyped-def] - self, - simulator, - width: int = 1920, - height: int = 1080, - fps: int = 60, - camera_path: str = "/camera", - annotator_type: AnnotatorType = "rgb", - transport: TransportType = "tcp", - rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: str | Path | None = None, - ) -> None: - """Initialize the Genesis stream.""" - super().__init__( - simulator=simulator, - width=width, - height=height, - fps=fps, - camera_path=camera_path, - annotator_type=annotator_type, - transport=transport, - rtsp_url=rtsp_url, - usd_path=usd_path, - ) - - self.scene = simulator.get_stage() - - # Initialize components - if usd_path: - self._load_stage(usd_path) - self._setup_camera() - self._setup_ffmpeg() - self._setup_annotator() - - # Build scene after camera is set up - simulator.build() - - def _load_stage(self, usd_path: str | Path) -> None: - """Load stage from file.""" - # Genesis handles stage loading through simulator - pass - - def _setup_camera(self) -> None: - """Setup and validate camera.""" - self.camera = self.scene.add_camera( - res=(self.width, self.height), - pos=(3.5, 0.0, 2.5), - lookat=(0, 0, 0.5), - fov=30, - GUI=False, - ) - - def _setup_annotator(self) -> None: - """Setup the specified annotator.""" - # Genesis handles different render types through camera.render() - pass - - def stream(self) -> None: - """Start the streaming loop.""" - try: - print("[Stream] Starting Genesis camera stream...") - frame_count = 0 - start_time = time.time() - - while True: - frame_start = time.time() - - # Step simulation and get frame - step_start = time.time() - self.scene.step() - step_time = time.time() - step_start - print(f"[Stream] Simulation step took {step_time * 1000:.2f}ms") - - # Get frame based on annotator type - if self.annotator_type == "rgb": - frame, _, _, _ = self.camera.render(rgb=True) - elif self.annotator_type == "normals": - _, _, _, frame = self.camera.render(normal=True) - else: - frame, _, _, _ = self.camera.render(rgb=True) # Default to RGB - - # Convert frame format if needed - if isinstance(frame, np.ndarray): - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - - # Write to FFmpeg - self.proc.stdin.write(frame.tobytes()) # type: ignore[attr-defined] - self.proc.stdin.flush() # type: ignore[attr-defined] - - # Log metrics - frame_time = time.time() - frame_start - print(f"[Stream] Total frame processing took {frame_time * 1000:.2f}ms") - frame_count += 1 - - if frame_count % 100 == 0: - elapsed_time = time.time() - start_time - current_fps = frame_count / elapsed_time - print( - f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}" - ) - - except KeyboardInterrupt: - print("\n[Stream] Received keyboard interrupt, stopping stream...") - finally: - self.cleanup() - - def cleanup(self) -> None: - """Cleanup resources.""" - print("[Cleanup] Stopping FFmpeg process...") - if hasattr(self, "proc"): - self.proc.stdin.close() # type: ignore[attr-defined] - self.proc.wait() # type: ignore[attr-defined] - print("[Cleanup] Closing simulation...") - try: - self.simulator.close() - except AttributeError: - print("[Cleanup] Warning: Could not close simulator properly") - print("[Cleanup] Successfully cleaned up resources") diff --git a/dimos/simulation/isaac/__init__.py b/dimos/simulation/isaac/__init__.py deleted file mode 100644 index 2b9bdc082d..0000000000 --- a/dimos/simulation/isaac/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .simulator import IsaacSimulator -from .stream import IsaacStream - -__all__ = ["IsaacSimulator", "IsaacStream"] diff --git a/dimos/simulation/isaac/simulator.py b/dimos/simulation/isaac/simulator.py deleted file mode 100644 index 1b524e1cb5..0000000000 --- a/dimos/simulation/isaac/simulator.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from isaacsim import SimulationApp # type: ignore[import-not-found] - -from ..base.simulator_base import SimulatorBase - - -class IsaacSimulator(SimulatorBase): - """Isaac Sim simulator implementation.""" - - def __init__( - self, - headless: bool = True, - open_usd: str | None = None, - entities: list[dict[str, str | dict]] | None = None, # type: ignore[type-arg] # Add but ignore - ) -> None: - """Initialize the Isaac Sim simulation.""" - super().__init__(headless, open_usd) - self.app = SimulationApp({"headless": headless, "open_usd": open_usd}) - - def get_stage(self): # type: ignore[no-untyped-def] - """Get the current USD stage.""" - import omni.usd # type: ignore[import-not-found] - - self.stage = omni.usd.get_context().get_stage() - return self.stage - - def close(self) -> None: - """Close the simulation.""" - if hasattr(self, "app"): - self.app.close() diff --git a/dimos/simulation/isaac/stream.py b/dimos/simulation/isaac/stream.py deleted file mode 100644 index e927c4bad4..0000000000 --- a/dimos/simulation/isaac/stream.py +++ /dev/null @@ -1,137 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path -import time - -import cv2 - -from ..base.stream_base import AnnotatorType, StreamBase, TransportType - - -class IsaacStream(StreamBase): - """Isaac Sim stream implementation.""" - - def __init__( # type: ignore[no-untyped-def] - self, - simulator, - width: int = 1920, - height: int = 1080, - fps: int = 60, - camera_path: str = "/World/alfred_parent_prim/alfred_base_descr/chest_cam_rgb_camera_frame/chest_cam", - annotator_type: AnnotatorType = "rgb", - transport: TransportType = "tcp", - rtsp_url: str = "rtsp://mediamtx:8554/stream", - usd_path: str | Path | None = None, - ) -> None: - """Initialize the Isaac Sim stream.""" - super().__init__( - simulator=simulator, - width=width, - height=height, - fps=fps, - camera_path=camera_path, - annotator_type=annotator_type, - transport=transport, - rtsp_url=rtsp_url, - usd_path=usd_path, - ) - - # Import omni.replicator after SimulationApp initialization - import omni.replicator.core as rep # type: ignore[import-not-found] - - self.rep = rep - - # Initialize components - if usd_path: - self._load_stage(usd_path) - self._setup_camera() # type: ignore[no-untyped-call] - self._setup_ffmpeg() - self._setup_annotator() - - def _load_stage(self, usd_path: str | Path): # type: ignore[no-untyped-def] - """Load USD stage from file.""" - import omni.usd # type: ignore[import-not-found] - - abs_path = str(Path(usd_path).resolve()) - omni.usd.get_context().open_stage(abs_path) - self.stage = self.simulator.get_stage() - if not self.stage: - raise RuntimeError(f"Failed to load stage: {abs_path}") - - def _setup_camera(self): # type: ignore[no-untyped-def] - """Setup and validate camera.""" - self.stage = self.simulator.get_stage() - camera_prim = self.stage.GetPrimAtPath(self.camera_path) - if not camera_prim: - raise RuntimeError(f"Failed to find camera at path: {self.camera_path}") - - self.render_product = self.rep.create.render_product( - self.camera_path, resolution=(self.width, self.height) - ) - - def _setup_annotator(self) -> None: - """Setup the specified annotator.""" - self.annotator = self.rep.AnnotatorRegistry.get_annotator(self.annotator_type) - self.annotator.attach(self.render_product) - - def stream(self) -> None: - """Start the streaming loop.""" - try: - print("[Stream] Starting camera stream loop...") - frame_count = 0 - start_time = time.time() - - while True: - frame_start = time.time() - - # Step simulation and get frame - step_start = time.time() - self.rep.orchestrator.step() - step_time = time.time() - step_start - print(f"[Stream] Simulation step took {step_time * 1000:.2f}ms") - - frame = self.annotator.get_data() - frame = cv2.cvtColor(frame, cv2.COLOR_RGBA2BGR) - - # Write to FFmpeg - self.proc.stdin.write(frame.tobytes()) # type: ignore[attr-defined] - self.proc.stdin.flush() # type: ignore[attr-defined] - - # Log metrics - frame_time = time.time() - frame_start - print(f"[Stream] Total frame processing took {frame_time * 1000:.2f}ms") - frame_count += 1 - - if frame_count % 100 == 0: - elapsed_time = time.time() - start_time - current_fps = frame_count / elapsed_time - print( - f"[Stream] Processed {frame_count} frames | Current FPS: {current_fps:.2f}" - ) - - except KeyboardInterrupt: - print("\n[Stream] Received keyboard interrupt, stopping stream...") - finally: - self.cleanup() - - def cleanup(self) -> None: - """Cleanup resources.""" - print("[Cleanup] Stopping FFmpeg process...") - if hasattr(self, "proc"): - self.proc.stdin.close() # type: ignore[attr-defined] - self.proc.wait() # type: ignore[attr-defined] - print("[Cleanup] Closing simulation...") - self.simulator.close() - print("[Cleanup] Successfully cleaned up resources") diff --git a/dimos/simulation/manipulators/__init__.py b/dimos/simulation/manipulators/__init__.py deleted file mode 100644 index 816de0a18d..0000000000 --- a/dimos/simulation/manipulators/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Simulation manipulator utilities.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from dimos.simulation.manipulators.sim_manip_interface import SimManipInterface - from dimos.simulation.manipulators.sim_module import ( - SimulationModule, - SimulationModuleConfig, - simulation, - ) - -__all__ = [ - "SimManipInterface", - "SimulationModule", - "SimulationModuleConfig", - "simulation", -] - - -def __getattr__(name: str): # type: ignore[no-untyped-def] - if name == "SimManipInterface": - from dimos.simulation.manipulators.sim_manip_interface import SimManipInterface - - return SimManipInterface - if name in {"SimulationModule", "SimulationModuleConfig", "simulation"}: - from dimos.simulation.manipulators.sim_module import ( - SimulationModule, - SimulationModuleConfig, - simulation, - ) - - return { - "SimulationModule": SimulationModule, - "SimulationModuleConfig": SimulationModuleConfig, - "simulation": simulation, - }[name] - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/dimos/simulation/manipulators/sim_manip_interface.py b/dimos/simulation/manipulators/sim_manip_interface.py deleted file mode 100644 index c829f0c864..0000000000 --- a/dimos/simulation/manipulators/sim_manip_interface.py +++ /dev/null @@ -1,200 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Simulation-agnostic manipulator interface.""" - -from __future__ import annotations - -import logging -import math -from typing import TYPE_CHECKING - -from dimos.hardware.manipulators.spec import ControlMode, JointLimits, ManipulatorInfo -from dimos.msgs.sensor_msgs import JointState - -if TYPE_CHECKING: - from dimos.simulation.engines.base import SimulationEngine - - -class SimManipInterface: - """Adapter wrapper around a simulation engine to provide a uniform manipulator API.""" - - def __init__(self, engine: SimulationEngine) -> None: - self.logger = logging.getLogger(self.__class__.__name__) - self._engine = engine - self._joint_names = list(engine.joint_names) - self._dof = len(self._joint_names) - self._connected = False - self._servos_enabled = False - self._control_mode = ControlMode.POSITION - self._error_code = 0 - self._error_message = "" - - def connect(self) -> bool: - """Connect to the simulation engine.""" - try: - self.logger.info("Connecting to simulation engine...") - if not self._engine.connect(): - self.logger.error("Failed to connect to simulation engine") - return False - if self._engine.connected: - self._connected = True - self._servos_enabled = True - self._joint_names = list(self._engine.joint_names) - self._dof = len(self._joint_names) - self.logger.info( - "Successfully connected to simulation", - extra={"dof": self._dof}, - ) - return True - self.logger.error("Failed to connect to simulation engine") - return False - except Exception as exc: - self.logger.error(f"Sim connection failed: {exc}") - return False - - def disconnect(self) -> bool: - """Disconnect from simulation.""" - try: - return self._engine.disconnect() - except Exception as exc: - self._connected = False - self.logger.error(f"Sim disconnection failed: {exc}") - return False - - def is_connected(self) -> bool: - return bool(self._connected and self._engine.connected) - - def get_info(self) -> ManipulatorInfo: - vendor = "Simulation" - model = "Simulation" - dof = self._dof - return ManipulatorInfo( - vendor=vendor, - model=model, - dof=dof, - firmware_version=None, - serial_number=None, - ) - - def get_dof(self) -> int: - return self._dof - - def get_joint_names(self) -> list[str]: - return list(self._joint_names) - - def get_limits(self) -> JointLimits: - lower = [-math.pi] * self._dof - upper = [math.pi] * self._dof - max_vel_rad = math.radians(180.0) - return JointLimits( - position_lower=lower, - position_upper=upper, - velocity_max=[max_vel_rad] * self._dof, - ) - - def set_control_mode(self, mode: ControlMode) -> bool: - self._control_mode = mode - return True - - def get_control_mode(self) -> ControlMode: - return self._control_mode - - def read_joint_positions(self) -> list[float]: - positions = self._engine.read_joint_positions() - return positions[: self._dof] - - def read_joint_velocities(self) -> list[float]: - velocities = self._engine.read_joint_velocities() - return velocities[: self._dof] - - def read_joint_efforts(self) -> list[float]: - efforts = self._engine.read_joint_efforts() - return efforts[: self._dof] - - def read_state(self) -> dict[str, int]: - velocities = self.read_joint_velocities() - is_moving = any(abs(v) > 1e-4 for v in velocities) - mode_int = list(ControlMode).index(self._control_mode) - return { - "state": 1 if is_moving else 0, - "mode": mode_int, - } - - def read_error(self) -> tuple[int, str]: - return self._error_code, self._error_message - - def write_joint_positions(self, positions: list[float]) -> bool: - if not self._servos_enabled: - return False - self._control_mode = ControlMode.POSITION - self._engine.write_joint_command(JointState(position=positions[: self._dof])) - return True - - def write_joint_velocities(self, velocities: list[float]) -> bool: - if not self._servos_enabled: - return False - self._control_mode = ControlMode.VELOCITY - self._engine.write_joint_command(JointState(velocity=velocities[: self._dof])) - return True - - def write_joint_efforts(self, efforts: list[float]) -> bool: - if not self._servos_enabled: - return False - self._control_mode = ControlMode.TORQUE - self._engine.write_joint_command(JointState(effort=efforts[: self._dof])) - return True - - def write_stop(self) -> bool: - self._engine.hold_current_position() - return True - - def write_enable(self, enable: bool) -> bool: - self._servos_enabled = enable - return True - - def read_enabled(self) -> bool: - return self._servos_enabled - - def write_clear_errors(self) -> bool: - self._error_code = 0 - self._error_message = "" - return True - - def read_cartesian_position(self) -> dict[str, float] | None: - return None - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - _pose = pose - _velocity = velocity - return False - - def read_gripper_position(self) -> float | None: - return None - - def write_gripper_position(self, position: float) -> bool: - _ = position - return False - - def read_force_torque(self) -> list[float] | None: - return None - - -__all__ = [ - "SimManipInterface", -] diff --git a/dimos/simulation/manipulators/sim_module.py b/dimos/simulation/manipulators/sim_module.py deleted file mode 100644 index 4f1bb986d3..0000000000 --- a/dimos/simulation/manipulators/sim_module.py +++ /dev/null @@ -1,247 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Simulator-agnostic manipulator simulation module.""" - -from __future__ import annotations - -from dataclasses import dataclass -import threading -import time -from typing import TYPE_CHECKING, Any - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.sensor_msgs import JointCommand, JointState, RobotState -from dimos.simulation.engines import EngineType, get_engine -from dimos.simulation.manipulators.sim_manip_interface import SimManipInterface - -if TYPE_CHECKING: - from collections.abc import Callable - from pathlib import Path - - -@dataclass(kw_only=True) -class SimulationModuleConfig(ModuleConfig): - engine: EngineType - config_path: Path | Callable[[], Path] - headless: bool = False - - -class SimulationModule(Module[SimulationModuleConfig]): - """Module wrapper for manipulator simulation across engines.""" - - default_config = SimulationModuleConfig - config: SimulationModuleConfig - - joint_state: Out[JointState] - robot_state: Out[RobotState] - joint_position_command: In[JointCommand] - joint_velocity_command: In[JointCommand] - - MIN_CONTROL_RATE = 1.0 - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._backend: SimManipInterface | None = None - self._control_rate = 100.0 - self._monitor_rate = 100.0 - self._joint_prefix = "joint" - self._stop_event = threading.Event() - self._control_thread: threading.Thread | None = None - self._monitor_thread: threading.Thread | None = None - self._command_lock = threading.Lock() - self._pending_positions: list[float] | None = None - self._pending_velocities: list[float] | None = None - - def _create_backend(self) -> SimManipInterface: - engine_cls = get_engine(self.config.engine) - config_path = ( - self.config.config_path() - if callable(self.config.config_path) - else self.config.config_path - ) - engine = engine_cls( - config_path=config_path, - headless=self.config.headless, - ) - return SimManipInterface(engine=engine) - - @rpc - def start(self) -> None: - super().start() - if self._backend is None: - self._backend = self._create_backend() - if not self._backend.connect(): - raise RuntimeError("Failed to connect to simulation backend") - self._backend.write_enable(True) - - self._disposables.add( - Disposable(self.joint_position_command.subscribe(self._on_joint_position_command)) - ) - self._disposables.add( - Disposable(self.joint_velocity_command.subscribe(self._on_joint_velocity_command)) - ) - - self._stop_event.clear() - self._control_thread = threading.Thread( - target=self._control_loop, - daemon=True, - name=f"{self.__class__.__name__}-control", - ) - self._monitor_thread = threading.Thread( - target=self._monitor_loop, - daemon=True, - name=f"{self.__class__.__name__}-monitor", - ) - self._control_thread.start() - self._monitor_thread.start() - - @rpc - def stop(self) -> None: - self._stop_event.set() - if self._control_thread and self._control_thread.is_alive(): - self._control_thread.join(timeout=2.0) - if self._monitor_thread and self._monitor_thread.is_alive(): - self._monitor_thread.join(timeout=2.0) - if self._backend: - self._backend.disconnect() - super().stop() - - @rpc - def enable_servos(self) -> bool: - if not self._backend: - return False - return self._backend.write_enable(True) - - @rpc - def disable_servos(self) -> bool: - if not self._backend: - return False - return self._backend.write_enable(False) - - @rpc - def clear_errors(self) -> bool: - if not self._backend: - return False - return self._backend.write_clear_errors() - - @rpc - def emergency_stop(self) -> bool: - if not self._backend: - return False - return self._backend.write_stop() - - def _on_joint_position_command(self, msg: JointCommand) -> None: - with self._command_lock: - self._pending_positions = list(msg.positions) - self._pending_velocities = None - - def _on_joint_velocity_command(self, msg: JointCommand) -> None: - with self._command_lock: - self._pending_velocities = list(msg.positions) - self._pending_positions = None - - def _control_loop(self) -> None: - period = 1.0 / max(self._control_rate, self.MIN_CONTROL_RATE) - next_tick = time.monotonic() # monotonic time used to avoid time drift - while not self._stop_event.is_set(): - with self._command_lock: - positions = ( - None if self._pending_positions is None else list(self._pending_positions) - ) - velocities = ( - None if self._pending_velocities is None else list(self._pending_velocities) - ) - - if self._backend: - if positions is not None: - self._backend.write_joint_positions(positions) - elif velocities is not None: - self._backend.write_joint_velocities(velocities) - dof = self._backend.get_dof() - names = self._resolve_joint_names(dof) - positions = self._backend.read_joint_positions() - velocities = self._backend.read_joint_velocities() - efforts = self._backend.read_joint_efforts() - self.joint_state.publish( - JointState( - frame_id=self.frame_id, - name=names, - position=positions, - velocity=velocities, - effort=efforts, - ) - ) - next_tick += period - sleep_for = next_tick - time.monotonic() - if sleep_for > 0: - if self._stop_event.wait(sleep_for): - break - else: - next_tick = time.monotonic() - - def _monitor_loop(self) -> None: - period = 1.0 / max(self._monitor_rate, self.MIN_CONTROL_RATE) - next_tick = time.monotonic() # monotonic time used to avoid time drift - while not self._stop_event.is_set(): - if not self._backend: - pass - else: - dof = self._backend.get_dof() - self._resolve_joint_names(dof) - positions = self._backend.read_joint_positions() - self._backend.read_joint_velocities() - self._backend.read_joint_efforts() - state = self._backend.read_state() - error_code, _ = self._backend.read_error() - self.robot_state.publish( - RobotState( - state=state.get("state", 0), - mode=state.get("mode", 0), - error_code=error_code, - warn_code=0, - cmdnum=0, - mt_brake=0, - mt_able=1 if self._backend.read_enabled() else 0, - tcp_pose=[], - tcp_offset=[], - joints=[float(p) for p in positions], - ) - ) - next_tick += period - sleep_for = next_tick - time.monotonic() - if sleep_for > 0: - if self._stop_event.wait(sleep_for): - break - else: - next_tick = time.monotonic() - - def _resolve_joint_names(self, dof: int) -> list[str]: - if self._backend: - names = self._backend.get_joint_names() - if len(names) >= dof: - return list(names[:dof]) - return [f"{self._joint_prefix}{i + 1}" for i in range(dof)] - - -simulation = SimulationModule.blueprint - -__all__ = [ - "SimulationModule", - "SimulationModuleConfig", - "simulation", -] diff --git a/dimos/simulation/manipulators/test_sim_module.py b/dimos/simulation/manipulators/test_sim_module.py deleted file mode 100644 index 334e2ce85f..0000000000 --- a/dimos/simulation/manipulators/test_sim_module.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path -import threading - -import pytest - -from dimos.simulation.manipulators.sim_module import SimulationModule - - -class _DummyRPC: - def serve_module_rpc(self, _module) -> None: # type: ignore[no-untyped-def] - return None - - def start(self) -> None: - return None - - def stop(self) -> None: - return None - - -class _FakeBackend: - def __init__(self) -> None: - self._names = ["joint1", "joint2", "joint3"] - - def get_dof(self) -> int: - return len(self._names) - - def get_joint_names(self) -> list[str]: - return list(self._names) - - def read_joint_positions(self) -> list[float]: - return [0.1, 0.2, 0.3] - - def read_joint_velocities(self) -> list[float]: - return [0.0, 0.0, 0.0] - - def read_joint_efforts(self) -> list[float]: - return [0.0, 0.0, 0.0] - - def read_state(self) -> dict[str, int]: - return {"state": 1, "mode": 2} - - def read_error(self) -> tuple[int, str]: - return 0, "" - - def read_enabled(self) -> bool: - return True - - def disconnect(self) -> None: - return None - - -def _run_single_monitor_iteration(module: SimulationModule, monkeypatch) -> None: # type: ignore[no-untyped-def] - def _wait_once(_: float) -> bool: - module._stop_event.set() - raise StopIteration - - monkeypatch.setattr(module._stop_event, "wait", _wait_once) - with pytest.raises(StopIteration): - module._monitor_loop() - - -def _run_single_control_iteration(module: SimulationModule, monkeypatch) -> None: # type: ignore[no-untyped-def] - def _wait_once(_: float) -> bool: - module._stop_event.set() - raise StopIteration - - monkeypatch.setattr(module._stop_event, "wait", _wait_once) - with pytest.raises(StopIteration): - module._control_loop() - - -def test_simulation_module_publishes_joint_state(monkeypatch) -> None: - module = SimulationModule( - engine="mujoco", - config_path=Path("."), - rpc_transport=_DummyRPC, - ) - module._backend = _FakeBackend() # type: ignore[assignment] - module._stop_event = threading.Event() - - joint_states: list[object] = [] - module.joint_state.subscribe(joint_states.append) - try: - _run_single_control_iteration(module, monkeypatch) - finally: - module.stop() - - assert len(joint_states) == 1 - assert joint_states[0].name == ["joint1", "joint2", "joint3"] - - -def test_simulation_module_publishes_robot_state(monkeypatch) -> None: - module = SimulationModule( - engine="mujoco", - config_path=Path("."), - rpc_transport=_DummyRPC, - ) - module._backend = _FakeBackend() # type: ignore[assignment] - module._stop_event = threading.Event() - - robot_states: list[object] = [] - module.robot_state.subscribe(robot_states.append) - try: - _run_single_monitor_iteration(module, monkeypatch) - finally: - module.stop() - - assert len(robot_states) == 1 - assert robot_states[0].state == 1 diff --git a/dimos/simulation/mujoco/constants.py b/dimos/simulation/mujoco/constants.py deleted file mode 100644 index 4e35011530..0000000000 --- a/dimos/simulation/mujoco/constants.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - -# Video/Camera constants -VIDEO_WIDTH = 320 -VIDEO_HEIGHT = 240 -VIDEO_CAMERA_FOV = 45 # MuJoCo default FOV for head_camera (degrees) -DEPTH_CAMERA_FOV = 160 - -# Depth camera range/filtering constants -MAX_RANGE = 3 -MIN_RANGE = 0.2 -MAX_HEIGHT = 1.2 - -# Lidar constants -LIDAR_RESOLUTION = 0.05 - -# Simulation timing constants -VIDEO_FPS = 20 -LIDAR_FPS = 2 - -LAUNCHER_PATH = Path(__file__).parent / "mujoco_process.py" diff --git a/dimos/simulation/mujoco/depth_camera.py b/dimos/simulation/mujoco/depth_camera.py deleted file mode 100644 index 486b740ffd..0000000000 --- a/dimos/simulation/mujoco/depth_camera.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math -from typing import Any - -import numpy as np -from numpy.typing import NDArray -import open3d as o3d # type: ignore[import-untyped] - -from dimos.simulation.mujoco.constants import MAX_HEIGHT, MAX_RANGE, MIN_RANGE - - -def depth_image_to_point_cloud( - depth_image: NDArray[Any], - camera_pos: NDArray[Any], - camera_mat: NDArray[Any], - fov_degrees: float = 120, -) -> NDArray[Any]: - """ - Convert a depth image from a camera to a 3D point cloud using perspective projection. - - Args: - depth_image: 2D numpy array of depth values in meters - camera_pos: 3D position of camera in world coordinates - camera_mat: 3x3 camera rotation matrix in world coordinates - fov_degrees: Vertical field of view of the camera in degrees - min_range: Minimum distance from camera to include points (meters) - - Returns: - numpy array of 3D points in world coordinates, shape (N, 3) - """ - height, width = depth_image.shape - - # Calculate camera intrinsics similar to StackOverflow approach - fovy = math.radians(fov_degrees) - f = height / (2 * math.tan(fovy / 2)) # focal length in pixels - cx = width / 2 # principal point x - cy = height / 2 # principal point y - - # Create Open3D camera intrinsics - cam_intrinsics = o3d.camera.PinholeCameraIntrinsic(width, height, f, f, cx, cy) - - # Convert numpy depth array to Open3D Image - o3d_depth = o3d.geometry.Image(depth_image.astype(np.float32)) - - # Create point cloud from depth image using Open3D - o3d_cloud = o3d.geometry.PointCloud.create_from_depth_image(o3d_depth, cam_intrinsics) - - # Convert Open3D point cloud to numpy array - camera_points: NDArray[Any] = np.asarray(o3d_cloud.points) - - if camera_points.size == 0: - return np.array([]).reshape(0, 3) - - # Flip y and z axes - camera_points[:, 1] = -camera_points[:, 1] - camera_points[:, 2] = -camera_points[:, 2] - - # y (index 1) is up here - valid_mask = ( - (np.abs(camera_points[:, 0]) <= MAX_RANGE) - & (np.abs(camera_points[:, 1]) <= MAX_HEIGHT) - & (np.abs(camera_points[:, 2]) >= MIN_RANGE) - & (np.abs(camera_points[:, 2]) <= MAX_RANGE) - ) - camera_points = camera_points[valid_mask] - - if camera_points.size == 0: - return np.array([]).reshape(0, 3) - - # Transform to world coordinates - world_points: NDArray[Any] = (camera_mat @ camera_points.T).T + camera_pos - - return world_points diff --git a/dimos/simulation/mujoco/input_controller.py b/dimos/simulation/mujoco/input_controller.py deleted file mode 100644 index 9ebe7ed98a..0000000000 --- a/dimos/simulation/mujoco/input_controller.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any, Protocol - -from numpy.typing import NDArray - - -class InputController(Protocol): - """A protocol for input devices to control the robot.""" - - def get_command(self) -> NDArray[Any]: ... - def stop(self) -> None: ... diff --git a/dimos/simulation/mujoco/model.py b/dimos/simulation/mujoco/model.py deleted file mode 100644 index bc309b7307..0000000000 --- a/dimos/simulation/mujoco/model.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from pathlib import Path -import xml.etree.ElementTree as ET - -from etils import epath -import mujoco -from mujoco_playground._src import mjx_env -import numpy as np - -from dimos.core.global_config import GlobalConfig -from dimos.mapping.occupancy.extrude_occupancy import generate_mujoco_scene -from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid -from dimos.simulation.mujoco.input_controller import InputController -from dimos.simulation.mujoco.policy import G1OnnxController, Go1OnnxController, OnnxController -from dimos.utils.data import get_data - - -def _get_data_dir() -> epath.Path: - return epath.Path(str(get_data("mujoco_sim"))) - - -def get_assets() -> dict[str, bytes]: - data_dir = _get_data_dir() - assets: dict[str, bytes] = {} - - # Assets used from https://sketchfab.com/3d-models/mersus-office-8714be387bcd406898b2615f7dae3a47 - # Created by Ryan Cassidy and Coleman Costello - mjx_env.update_assets(assets, data_dir, "*.xml") - mjx_env.update_assets(assets, data_dir / "scene_office1/textures", "*.png") - mjx_env.update_assets(assets, data_dir / "scene_office1/office_split", "*.obj") - mjx_env.update_assets(assets, mjx_env.MENAGERIE_PATH / "unitree_go1" / "assets") - mjx_env.update_assets(assets, mjx_env.MENAGERIE_PATH / "unitree_g1" / "assets") - - # From: https://sketchfab.com/3d-models/jeong-seun-34-42956ca979404a038b8e0d3e496160fd - person_dir = epath.Path(str(get_data("person"))) - mjx_env.update_assets(assets, person_dir, "*.obj") - mjx_env.update_assets(assets, person_dir, "*.png") - - return assets - - -def load_model( - input_device: InputController, robot: str, scene_xml: str -) -> tuple[mujoco.MjModel, mujoco.MjData]: - mujoco.set_mjcb_control(None) - - xml_string = get_model_xml(robot, scene_xml) - model = mujoco.MjModel.from_xml_string(xml_string, assets=get_assets()) - data = mujoco.MjData(model) - - mujoco.mj_resetDataKeyframe(model, data, 0) - - match robot: - case "unitree_g1": - sim_dt = 0.002 - case _: - sim_dt = 0.005 - - ctrl_dt = 0.02 - n_substeps = round(ctrl_dt / sim_dt) - model.opt.timestep = sim_dt - - params = { - "policy_path": (_get_data_dir() / f"{robot}_policy.onnx").as_posix(), - "default_angles": np.array(model.keyframe("home").qpos[7:]), - "n_substeps": n_substeps, - "action_scale": 0.5, - "input_controller": input_device, - "ctrl_dt": ctrl_dt, - } - - match robot: - case "unitree_go1": - policy: OnnxController = Go1OnnxController(**params) - case "unitree_g1": - policy = G1OnnxController(**params, drift_compensation=[-0.18, 0.0, -0.09]) - case _: - raise ValueError(f"Unknown robot policy: {robot}") - - mujoco.set_mjcb_control(policy.get_control) - - return model, data - - -def get_model_xml(robot: str, scene_xml: str) -> str: - root = ET.fromstring(scene_xml) - root.set("model", f"{robot}_scene") - root.insert(0, ET.Element("include", file=f"{robot}.xml")) - - # Ensure visual/map element exists with znear and zfar - visual = root.find("visual") - if visual is None: - visual = ET.SubElement(root, "visual") - map_elem = visual.find("map") - if map_elem is None: - map_elem = ET.SubElement(visual, "map") - map_elem.set("znear", "0.01") - map_elem.set("zfar", "10000") - - _add_person_object(root) - - return ET.tostring(root, encoding="unicode") - - -def _add_person_object(root: ET.Element) -> None: - asset = root.find("asset") - - if asset is None: - asset = ET.SubElement(root, "asset") - - ET.SubElement(asset, "mesh", name="person_mesh", file="jeong_seun_34.obj") - ET.SubElement(asset, "texture", name="person_texture", file="material_0.png", type="2d") - ET.SubElement(asset, "material", name="person_material", texture="person_texture") - - worldbody = root.find("worldbody") - - if worldbody is None: - worldbody = ET.SubElement(root, "worldbody") - - person_body = ET.SubElement(worldbody, "body", name="person", pos="0 0 0", mocap="true") - - ET.SubElement( - person_body, - "geom", - type="mesh", - mesh="person_mesh", - material="person_material", - euler="1.5708 0 0", - ) - - -def load_scene_xml(config: GlobalConfig) -> str: - if config.mujoco_room_from_occupancy: - path = Path(config.mujoco_room_from_occupancy) - return generate_mujoco_scene(OccupancyGrid.from_path(path)) - - mujoco_room = config.mujoco_room or "office1" - xml_file = (_get_data_dir() / f"scene_{mujoco_room}.xml").as_posix() - with open(xml_file) as f: - return f.read() diff --git a/dimos/simulation/mujoco/mujoco_process.py b/dimos/simulation/mujoco/mujoco_process.py deleted file mode 100755 index 8529de976b..0000000000 --- a/dimos/simulation/mujoco/mujoco_process.py +++ /dev/null @@ -1,246 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import json -import pickle -import signal -import sys -import time -from typing import Any - -import mujoco -from mujoco import viewer -import numpy as np -from numpy.typing import NDArray -import open3d as o3d # type: ignore[import-untyped] - -from dimos.core.global_config import GlobalConfig -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.simulation.mujoco.constants import ( - DEPTH_CAMERA_FOV, - LIDAR_FPS, - LIDAR_RESOLUTION, - VIDEO_FPS, - VIDEO_HEIGHT, - VIDEO_WIDTH, -) -from dimos.simulation.mujoco.depth_camera import depth_image_to_point_cloud -from dimos.simulation.mujoco.model import load_model, load_scene_xml -from dimos.simulation.mujoco.person_on_track import PersonPositionController -from dimos.simulation.mujoco.shared_memory import ShmReader -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class MockController: - """Controller that reads commands from shared memory.""" - - def __init__(self, shm_interface: ShmReader) -> None: - self.shm = shm_interface - self._command = np.zeros(3, dtype=np.float32) - - def get_command(self) -> NDArray[Any]: - """Get the current movement command.""" - cmd_data = self.shm.read_command() - if cmd_data is not None: - linear, angular = cmd_data - # MuJoCo expects [forward, lateral, rotational] - self._command[0] = linear[0] # forward/backward - self._command[1] = linear[1] # left/right - self._command[2] = angular[2] # rotation - result: NDArray[Any] = self._command.copy() - return result - - def stop(self) -> None: - """Stop method to satisfy InputController protocol.""" - pass - - -def _run_simulation(config: GlobalConfig, shm: ShmReader) -> None: - robot_name = config.robot_model or "unitree_go1" - if robot_name == "unitree_go2": - robot_name = "unitree_go1" - - controller = MockController(shm) - model, data = load_model(controller, robot=robot_name, scene_xml=load_scene_xml(config)) - - if model is None or data is None: - raise ValueError("Failed to load MuJoCo model: model or data is None") - - match robot_name: - case "unitree_go1": - z = 0.3 - case "unitree_g1": - z = 0.8 - case _: - z = 0 - - pos = config.mujoco_start_pos_float - - data.qpos[0:3] = [pos[0], pos[1], z] - - mujoco.mj_forward(model, data) - - camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "head_camera") - lidar_camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_front_camera") - - person_position_controller = PersonPositionController(model) - - lidar_left_camera_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_left_camera") - lidar_right_camera_id = mujoco.mj_name2id( - model, mujoco.mjtObj.mjOBJ_CAMERA, "lidar_right_camera" - ) - - shm.signal_ready() - - with viewer.launch_passive(model, data, show_left_ui=False, show_right_ui=False) as m_viewer: - camera_size = (VIDEO_WIDTH, VIDEO_HEIGHT) - - # Create renderers - rgb_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) - depth_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) - depth_renderer.enable_depth_rendering() - - depth_left_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) - depth_left_renderer.enable_depth_rendering() - - depth_right_renderer = mujoco.Renderer(model, height=camera_size[1], width=camera_size[0]) - depth_right_renderer.enable_depth_rendering() - - scene_option = mujoco.MjvOption() - - # Timing control - last_video_time = 0.0 - last_lidar_time = 0.0 - video_interval = 1.0 / VIDEO_FPS - lidar_interval = 1.0 / LIDAR_FPS - - m_viewer.cam.lookat = config.mujoco_camera_position_float[0:3] - m_viewer.cam.distance = config.mujoco_camera_position_float[3] - m_viewer.cam.azimuth = config.mujoco_camera_position_float[4] - m_viewer.cam.elevation = config.mujoco_camera_position_float[5] - - while m_viewer.is_running() and not shm.should_stop(): - step_start = time.time() - - # Step simulation - for _ in range(config.mujoco_steps_per_frame): - mujoco.mj_step(model, data) - - person_position_controller.tick(data) - - m_viewer.sync() - - # Always update odometry - pos = data.qpos[0:3].copy() - quat = data.qpos[3:7].copy() # (w, x, y, z) - shm.write_odom(pos, quat, time.time()) - - current_time = time.time() - - # Video rendering - if current_time - last_video_time >= video_interval: - rgb_renderer.update_scene(data, camera=camera_id, scene_option=scene_option) - pixels = rgb_renderer.render() - shm.write_video(pixels) - last_video_time = current_time - - # Lidar/depth rendering - if current_time - last_lidar_time >= lidar_interval: - # Render all depth cameras - depth_renderer.update_scene(data, camera=lidar_camera_id, scene_option=scene_option) - depth_front = depth_renderer.render() - - depth_left_renderer.update_scene( - data, camera=lidar_left_camera_id, scene_option=scene_option - ) - depth_left = depth_left_renderer.render() - - depth_right_renderer.update_scene( - data, camera=lidar_right_camera_id, scene_option=scene_option - ) - depth_right = depth_right_renderer.render() - - shm.write_depth(depth_front, depth_left, depth_right) - - # Process depth images into lidar message - all_points = [] - cameras_data = [ - ( - depth_front, - data.cam_xpos[lidar_camera_id], - data.cam_xmat[lidar_camera_id].reshape(3, 3), - ), - ( - depth_left, - data.cam_xpos[lidar_left_camera_id], - data.cam_xmat[lidar_left_camera_id].reshape(3, 3), - ), - ( - depth_right, - data.cam_xpos[lidar_right_camera_id], - data.cam_xmat[lidar_right_camera_id].reshape(3, 3), - ), - ] - - for depth_image, camera_pos, camera_mat in cameras_data: - points = depth_image_to_point_cloud( - depth_image, camera_pos, camera_mat, fov_degrees=DEPTH_CAMERA_FOV - ) - if points.size > 0: - all_points.append(points) - - if all_points: - combined_points = np.vstack(all_points) - pcd = o3d.geometry.PointCloud() - pcd.points = o3d.utility.Vector3dVector(combined_points) - pcd = pcd.voxel_down_sample(voxel_size=LIDAR_RESOLUTION) - - lidar_msg = PointCloud2( - pointcloud=pcd, - ts=time.time(), - frame_id="world", - ) - shm.write_lidar(lidar_msg) - - last_lidar_time = current_time - - # Control simulation speed - time_until_next_step = model.opt.timestep - (time.time() - step_start) - if time_until_next_step > 0: - time.sleep(time_until_next_step) - - person_position_controller.stop() - - -if __name__ == "__main__": - - def signal_handler(_signum: int, _frame: Any) -> None: - sys.exit(0) - - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - global_config = pickle.loads(base64.b64decode(sys.argv[1])) - shm_names = json.loads(sys.argv[2]) - - shm = ShmReader(shm_names) - try: - _run_simulation(global_config, shm) - finally: - shm.cleanup() diff --git a/dimos/simulation/mujoco/person_on_track.py b/dimos/simulation/mujoco/person_on_track.py deleted file mode 100644 index a816b5f3ee..0000000000 --- a/dimos/simulation/mujoco/person_on_track.py +++ /dev/null @@ -1,160 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -import mujoco -import numpy as np -from numpy.typing import NDArray - -from dimos.core.transport import LCMTransport -from dimos.msgs.geometry_msgs import Pose - - -class PersonPositionController: - """Controls the person position in MuJoCo by subscribing to LCM pose updates.""" - - def __init__(self, model: mujoco.MjModel) -> None: - person_body_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, "person") - self._person_mocap_id = model.body_mocapid[person_body_id] - self._latest_pose: Pose | None = None - self._transport: LCMTransport[Pose] = LCMTransport("/person_pose", Pose) - self._transport.subscribe(self._on_pose) - - def _on_pose(self, pose: Pose) -> None: - self._latest_pose = pose - - def tick(self, data: mujoco.MjData) -> None: - if self._latest_pose is None: - return - - pose = self._latest_pose - data.mocap_pos[self._person_mocap_id][0] = pose.position.x - data.mocap_pos[self._person_mocap_id][1] = pose.position.y - data.mocap_pos[self._person_mocap_id][2] = pose.position.z - data.mocap_quat[self._person_mocap_id] = [ - pose.orientation.w, - pose.orientation.x, - pose.orientation.y, - pose.orientation.z, - ] - - def stop(self) -> None: - self._transport.stop() - - -class PersonTrackPublisher: - """Publishes person poses along a track via LCM.""" - - def __init__(self, track: list[tuple[float, float]]) -> None: - self._speed = 0.004 - self._waypoint_threshold = 0.1 - self._rotation_radius = 1.0 - self._track = track - self._current_waypoint_idx = 0 - self._initialized = False - self._current_pos = np.array([0.0, 0.0]) - self._transport: LCMTransport[Pose] = LCMTransport("/person_pose", Pose) - - def _get_segment_heading(self, from_idx: int, to_idx: int) -> float: - """Get heading angle for traveling from one waypoint to another.""" - from_wp = np.array(self._track[from_idx]) - to_wp = np.array(self._track[to_idx]) - direction = to_wp - from_wp - return float(np.arctan2(direction[1], direction[0])) - - def _lerp_angle(self, a1: float, a2: float, t: float) -> float: - """Interpolate between two angles, handling wrapping.""" - diff = a2 - a1 - while diff > np.pi: - diff -= 2 * np.pi - while diff < -np.pi: - diff += 2 * np.pi - return a1 + diff * t - - def tick(self) -> None: - if not self._initialized: - first_point = self._track[0] - self._current_pos = np.array([first_point[0], first_point[1]]) - self._current_waypoint_idx = 1 - heading = self._get_segment_heading(0, 1) - self._publish_pose(self._current_pos, heading) - self._initialized = True - return - - n = len(self._track) - - prev_idx = (self._current_waypoint_idx - 1) % n - curr_idx = self._current_waypoint_idx - next_idx = (self._current_waypoint_idx + 1) % n - prev_prev_idx = (prev_idx - 1) % n - - prev_wp = np.array(self._track[prev_idx]) - curr_wp = np.array(self._track[curr_idx]) - - to_target = curr_wp - self._current_pos - distance_to_curr = float(np.linalg.norm(to_target)) - distance_from_prev = float(np.linalg.norm(self._current_pos - prev_wp)) - - # Headings for current turn (at curr_wp) - incoming_heading = self._get_segment_heading(prev_idx, curr_idx) - outgoing_heading = self._get_segment_heading(curr_idx, next_idx) - - # Headings for previous turn (at prev_wp) - prev_incoming_heading = self._get_segment_heading(prev_prev_idx, prev_idx) - prev_outgoing_heading = incoming_heading - - # Determine heading based on position in rotation zones - in_leaving_zone = distance_from_prev < self._rotation_radius - in_approaching_zone = distance_to_curr < self._rotation_radius - - if in_leaving_zone and in_approaching_zone: - # Overlap - prioritize approaching zone - t = 0.5 * (1.0 - distance_to_curr / self._rotation_radius) - heading = self._lerp_angle(incoming_heading, outgoing_heading, t) - elif in_leaving_zone: - # Finishing turn after passing prev_wp (t goes from 0.5 to 1.0) - t = 0.5 + 0.5 * (distance_from_prev / self._rotation_radius) - heading = self._lerp_angle(prev_incoming_heading, prev_outgoing_heading, t) - elif in_approaching_zone: - # Starting turn before reaching curr_wp (t goes from 0.0 to 0.5) - t = 0.5 * (1.0 - distance_to_curr / self._rotation_radius) - heading = self._lerp_angle(incoming_heading, outgoing_heading, t) - else: - # Between zones, use segment heading - heading = incoming_heading - - # Move toward target - if distance_to_curr > 0: - dir_norm = to_target / distance_to_curr - self._current_pos[0] += dir_norm[0] * self._speed - self._current_pos[1] += dir_norm[1] * self._speed - - # Check if reached waypoint - if distance_to_curr < self._waypoint_threshold: - self._current_waypoint_idx = next_idx - - # Publish pose - self._publish_pose(self._current_pos, heading + np.pi) - - def _publish_pose(self, pos: NDArray[np.floating[Any]], heading: float) -> None: - c, s = np.cos(heading / 2), np.sin(heading / 2) - pose = Pose( - position=[pos[0], pos[1], 0.0], - orientation=[0.0, 0.0, s, c], # x, y, z, w - ) - self._transport.broadcast(None, pose) - - def stop(self) -> None: - self._transport.stop() diff --git a/dimos/simulation/mujoco/policy.py b/dimos/simulation/mujoco/policy.py deleted file mode 100644 index 212c7ac60a..0000000000 --- a/dimos/simulation/mujoco/policy.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from abc import ABC, abstractmethod -from typing import Any - -import mujoco -import numpy as np -import onnxruntime as ort # type: ignore[import-untyped] - -from dimos.simulation.mujoco.input_controller import InputController -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class OnnxController(ABC): - def __init__( - self, - policy_path: str, - default_angles: np.ndarray[Any, Any], - n_substeps: int, - action_scale: float, - input_controller: InputController, - ctrl_dt: float | None = None, - drift_compensation: list[float] | None = None, - ) -> None: - self._output_names = ["continuous_actions"] - self._policy = ort.InferenceSession(policy_path, providers=ort.get_available_providers()) - logger.info(f"Loaded policy: {policy_path} with providers: {self._policy.get_providers()}") - - self._action_scale = action_scale - self._default_angles = default_angles - self._last_action = np.zeros_like(default_angles, dtype=np.float32) - - self._counter = 0 - self._n_substeps = n_substeps - self._input_controller = input_controller - - self._drift_compensation = np.array(drift_compensation or [0.0, 0.0, 0.0], dtype=np.float32) - - @abstractmethod - def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: - pass - - def get_control(self, model: mujoco.MjModel, data: mujoco.MjData) -> None: - self._counter += 1 - if self._counter % self._n_substeps == 0: - obs = self.get_obs(model, data) - onnx_input = {"obs": obs.reshape(1, -1)} - onnx_pred = self._policy.run(self._output_names, onnx_input)[0][0] - self._last_action = onnx_pred.copy() - data.ctrl[:] = onnx_pred * self._action_scale + self._default_angles - self._post_control_update() - - def _post_control_update(self) -> None: # noqa: B027 - pass - - -class Go1OnnxController(OnnxController): - def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: - linvel = data.sensor("local_linvel").data - gyro = data.sensor("gyro").data - imu_xmat = data.site_xmat[model.site("imu").id].reshape(3, 3) - gravity = imu_xmat.T @ np.array([0, 0, -1]) - joint_angles = data.qpos[7:] - self._default_angles - joint_velocities = data.qvel[6:] - obs = np.hstack( - [ - linvel, - gyro, - gravity, - joint_angles, - joint_velocities, - self._last_action, - self._input_controller.get_command(), - ] - ) - return obs.astype(np.float32) - - -class G1OnnxController(OnnxController): - def __init__( - self, - policy_path: str, - default_angles: np.ndarray[Any, Any], - ctrl_dt: float, - n_substeps: int, - action_scale: float, - input_controller: InputController, - drift_compensation: list[float] | None = None, - ) -> None: - super().__init__( - policy_path, - default_angles, - n_substeps, - action_scale, - input_controller, - ctrl_dt, - drift_compensation, - ) - - self._phase = np.array([0.0, np.pi]) - self._gait_freq = 1.5 - self._phase_dt = 2 * np.pi * self._gait_freq * ctrl_dt - - def get_obs(self, model: mujoco.MjModel, data: mujoco.MjData) -> np.ndarray[Any, Any]: - linvel = data.sensor("local_linvel_pelvis").data - gyro = data.sensor("gyro_pelvis").data - imu_xmat = data.site_xmat[model.site("imu_in_pelvis").id].reshape(3, 3) - gravity = imu_xmat.T @ np.array([0, 0, -1]) - joint_angles = data.qpos[7:] - self._default_angles - joint_velocities = data.qvel[6:] - phase = np.concatenate([np.cos(self._phase), np.sin(self._phase)]) - command = self._input_controller.get_command() - command[0] = command[0] * 2 - command[1] = command[1] * 2 - command[0] += self._drift_compensation[0] - command[1] += self._drift_compensation[1] - command[2] += self._drift_compensation[2] - obs = np.hstack( - [ - linvel, - gyro, - gravity, - command, - joint_angles, - joint_velocities, - self._last_action, - phase, - ] - ) - return obs.astype(np.float32) - - def _post_control_update(self) -> None: - phase_tp1 = self._phase + self._phase_dt - self._phase = np.fmod(phase_tp1 + np.pi, 2 * np.pi) - np.pi diff --git a/dimos/simulation/mujoco/shared_memory.py b/dimos/simulation/mujoco/shared_memory.py deleted file mode 100644 index 70ba50af2b..0000000000 --- a/dimos/simulation/mujoco/shared_memory.py +++ /dev/null @@ -1,286 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass -from multiprocessing import resource_tracker -from multiprocessing.shared_memory import SharedMemory -import pickle -from typing import Any - -import numpy as np -from numpy.typing import NDArray - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.simulation.mujoco.constants import VIDEO_HEIGHT, VIDEO_WIDTH -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -# Video buffer: VIDEO_WIDTH x VIDEO_HEIGHT x 3 RGB -_video_size = VIDEO_WIDTH * VIDEO_HEIGHT * 3 -# Depth buffers: 3 cameras x VIDEO_WIDTH x VIDEO_HEIGHT float32 -_depth_size = VIDEO_WIDTH * VIDEO_HEIGHT * 4 # float32 = 4 bytes -# Odometry buffer: position(3) + quaternion(4) + timestamp(1) = 8 floats -_odom_size = 8 * 8 # 8 float64 values -# Command buffer: linear(3) + angular(3) = 6 floats -_cmd_size = 6 * 4 # 6 float32 values -# Lidar message buffer: for serialized lidar data -_lidar_size = 1024 * 1024 * 4 # 4MB should be enough for point cloud -# Sequence/version numbers for detecting updates -_seq_size = 8 * 8 # 8 int64 values for different data types -# Control buffer: ready flag + stop flag -_control_size = 2 * 4 # 2 int32 values - -_shm_sizes = { - "video": _video_size, - "depth_front": _depth_size, - "depth_left": _depth_size, - "depth_right": _depth_size, - "odom": _odom_size, - "cmd": _cmd_size, - "lidar": _lidar_size, - "lidar_len": 4, - "seq": _seq_size, - "control": _control_size, -} - - -def _unregister(shm: SharedMemory) -> SharedMemory: - try: - resource_tracker.unregister(shm._name, "shared_memory") # type: ignore[attr-defined] - except Exception: - pass - return shm - - -@dataclass(frozen=True) -class ShmSet: - video: SharedMemory - depth_front: SharedMemory - depth_left: SharedMemory - depth_right: SharedMemory - odom: SharedMemory - cmd: SharedMemory - lidar: SharedMemory - lidar_len: SharedMemory - seq: SharedMemory - control: SharedMemory - - @classmethod - def from_names(cls, shm_names: dict[str, str]) -> "ShmSet": - return cls(**{k: _unregister(SharedMemory(name=shm_names[k])) for k in _shm_sizes.keys()}) - - @classmethod - def from_sizes(cls) -> "ShmSet": - return cls( - **{ - k: _unregister(SharedMemory(create=True, size=_shm_sizes[k])) - for k in _shm_sizes.keys() - } - ) - - def to_names(self) -> dict[str, str]: - return {k: getattr(self, k).name for k in _shm_sizes.keys()} - - def as_list(self) -> list[SharedMemory]: - return [getattr(self, k) for k in _shm_sizes.keys()] - - -class ShmReader: - shm: ShmSet - _last_cmd_seq: int - - def __init__(self, shm_names: dict[str, str]) -> None: - self.shm = ShmSet.from_names(shm_names) - self._last_cmd_seq = 0 - - def signal_ready(self) -> None: - control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) - control_array[0] = 1 # ready flag - - def should_stop(self) -> bool: - control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) - return bool(control_array[1] == 1) # stop flag - - def write_video(self, pixels: NDArray[Any]) -> None: - video_array: NDArray[Any] = np.ndarray( - (VIDEO_HEIGHT, VIDEO_WIDTH, 3), dtype=np.uint8, buffer=self.shm.video.buf - ) - video_array[:] = pixels - self._increment_seq(0) - - def write_depth(self, front: NDArray[Any], left: NDArray[Any], right: NDArray[Any]) -> None: - # Front camera - depth_array: NDArray[Any] = np.ndarray( - (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_front.buf - ) - depth_array[:] = front - - # Left camera - depth_array = np.ndarray( - (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_left.buf - ) - depth_array[:] = left - - # Right camera - depth_array = np.ndarray( - (VIDEO_HEIGHT, VIDEO_WIDTH), dtype=np.float32, buffer=self.shm.depth_right.buf - ) - depth_array[:] = right - - self._increment_seq(1) - - def write_odom(self, pos: NDArray[Any], quat: NDArray[Any], timestamp: float) -> None: - odom_array: NDArray[Any] = np.ndarray((8,), dtype=np.float64, buffer=self.shm.odom.buf) - odom_array[0:3] = pos - odom_array[3:7] = quat - odom_array[7] = timestamp - self._increment_seq(2) - - def write_lidar(self, lidar_msg: PointCloud2) -> None: - data = pickle.dumps(lidar_msg) - data_len = len(data) - - if data_len > self.shm.lidar.size: - logger.error(f"Lidar data too large: {data_len} > {self.shm.lidar.size}") - return - - # Write length - len_array: NDArray[Any] = np.ndarray((1,), dtype=np.uint32, buffer=self.shm.lidar_len.buf) - len_array[0] = data_len - - # Write data - lidar_array: NDArray[Any] = np.ndarray( - (data_len,), dtype=np.uint8, buffer=self.shm.lidar.buf - ) - lidar_array[:] = np.frombuffer(data, dtype=np.uint8) - - self._increment_seq(4) - - def read_command(self) -> tuple[NDArray[Any], NDArray[Any]] | None: - seq = self._get_seq(3) - if seq > self._last_cmd_seq: - self._last_cmd_seq = seq - cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) - linear = cmd_array[0:3].copy() - angular = cmd_array[3:6].copy() - return linear, angular - return None - - def _increment_seq(self, index: int) -> None: - seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) - seq_array[index] += 1 - - def _get_seq(self, index: int) -> int: - seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) - return int(seq_array[index]) - - def cleanup(self) -> None: - for shm in self.shm.as_list(): - try: - shm.close() - except Exception: - pass - - -class ShmWriter: - shm: ShmSet - - def __init__(self) -> None: - self.shm = ShmSet.from_sizes() - - seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) - seq_array[:] = 0 - - cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) - cmd_array[:] = 0 - - control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) - control_array[:] = 0 # [ready_flag, stop_flag] - - def is_ready(self) -> bool: - control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) - return bool(control_array[0] == 1) - - def signal_stop(self) -> None: - control_array: NDArray[Any] = np.ndarray((2,), dtype=np.int32, buffer=self.shm.control.buf) - control_array[1] = 1 # Set stop flag - - def read_video(self) -> tuple[NDArray[Any] | None, int]: - seq = self._get_seq(0) - if seq > 0: - video_array: NDArray[Any] = np.ndarray( - (VIDEO_HEIGHT, VIDEO_WIDTH, 3), dtype=np.uint8, buffer=self.shm.video.buf - ) - return video_array.copy(), seq - return None, 0 - - def read_odom(self) -> tuple[tuple[NDArray[Any], NDArray[Any], float] | None, int]: - seq = self._get_seq(2) - if seq > 0: - odom_array: NDArray[Any] = np.ndarray((8,), dtype=np.float64, buffer=self.shm.odom.buf) - pos = odom_array[0:3].copy() - quat = odom_array[3:7].copy() - timestamp = odom_array[7] - return (pos, quat, timestamp), seq - return None, 0 - - def write_command(self, linear: NDArray[Any], angular: NDArray[Any]) -> None: - cmd_array: NDArray[Any] = np.ndarray((6,), dtype=np.float32, buffer=self.shm.cmd.buf) - cmd_array[0:3] = linear - cmd_array[3:6] = angular - self._increment_seq(3) - - def read_lidar(self) -> tuple[PointCloud2 | None, int]: - seq = self._get_seq(4) - if seq > 0: - # Read length - len_array: NDArray[Any] = np.ndarray( - (1,), dtype=np.uint32, buffer=self.shm.lidar_len.buf - ) - data_len = int(len_array[0]) - - if data_len > 0 and data_len <= self.shm.lidar.size: - # Read data - lidar_array: NDArray[Any] = np.ndarray( - (data_len,), dtype=np.uint8, buffer=self.shm.lidar.buf - ) - data = bytes(lidar_array) - - try: - lidar_msg = pickle.loads(data) - return lidar_msg, seq - except Exception as e: - logger.error(f"Failed to deserialize lidar message: {e}") - return None, 0 - - def _increment_seq(self, index: int) -> None: - seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) - seq_array[index] += 1 - - def _get_seq(self, index: int) -> int: - seq_array: NDArray[Any] = np.ndarray((8,), dtype=np.int64, buffer=self.shm.seq.buf) - return int(seq_array[index]) - - def cleanup(self) -> None: - for shm in self.shm.as_list(): - try: - shm.unlink() - except Exception: - pass - - try: - shm.close() - except Exception: - pass diff --git a/dimos/simulation/sim_blueprints.py b/dimos/simulation/sim_blueprints.py deleted file mode 100644 index 8b91ff817a..0000000000 --- a/dimos/simulation/sim_blueprints.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from dimos.core.transport import LCMTransport -from dimos.msgs.sensor_msgs import ( # type: ignore[attr-defined] - JointCommand, - JointState, - RobotState, -) -from dimos.msgs.trajectory_msgs import JointTrajectory -from dimos.simulation.manipulators.sim_module import simulation -from dimos.utils.data import LfsPath - -xarm7_trajectory_sim = simulation( - engine="mujoco", - config_path=LfsPath("xarm7/scene.xml"), - headless=True, -).transports( - { - ("joint_state", JointState): LCMTransport("/xarm/joint_states", JointState), - ("robot_state", RobotState): LCMTransport("/xarm/robot_state", RobotState), - ("joint_position_command", JointCommand): LCMTransport( - "/xarm/joint_position_command", JointCommand - ), - ("trajectory", JointTrajectory): LCMTransport("/trajectory", JointTrajectory), - } -) - - -__all__ = [ - "simulation", - "xarm7_trajectory_sim", -] - -if __name__ == "__main__": - xarm7_trajectory_sim.build().loop() diff --git a/dimos/simulation/utils/xml_parser.py b/dimos/simulation/utils/xml_parser.py deleted file mode 100644 index 052657ea95..0000000000 --- a/dimos/simulation/utils/xml_parser.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""MuJoCo XML parsing helpers for joint/actuator metadata.""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import TYPE_CHECKING -import xml.etree.ElementTree as ET - -import mujoco - -if TYPE_CHECKING: - from pathlib import Path - - -@dataclass(frozen=True) -class JointMapping: - name: str - joint_id: int | None - actuator_id: int | None - qpos_adr: int | None - dof_adr: int | None - tendon_qpos_adrs: tuple[int, ...] - tendon_dof_adrs: tuple[int, ...] - - -@dataclass(frozen=True) -class _ActuatorSpec: - name: str - joint: str | None - tendon: str | None - - -def build_joint_mappings(xml_path: Path, model: mujoco.MjModel) -> list[JointMapping]: - specs = _parse_actuator_specs(xml_path) - if specs: - return _build_joint_mappings_from_specs(specs, model) - if int(model.nu) > 0: - return _build_joint_mappings_from_actuators(model) - return _build_joint_mappings_from_model(model) - - -def _parse_actuator_specs(xml_path: Path) -> list[_ActuatorSpec]: - return _collect_actuator_specs(xml_path.resolve(), seen=set()) - - -def _collect_actuator_specs(xml_path: Path, seen: set[Path]) -> list[_ActuatorSpec]: - if xml_path in seen: - return [] - seen.add(xml_path) - - root = ET.parse(xml_path).getroot() - base_dir = xml_path.parent - specs: list[_ActuatorSpec] = [] - - def walk(node: ET.Element) -> None: - for child in node: - if child.tag == "include": - include_file = child.attrib.get("file") - if include_file: - include_path = (base_dir / include_file).resolve() - specs.extend(_collect_actuator_specs(include_path, seen)) - continue - if child.tag == "actuator": - specs.extend(_parse_actuator_block(child)) - continue - walk(child) - - walk(root) - return specs - - -def _parse_actuator_block(actuator_elem: ET.Element) -> list[_ActuatorSpec]: - specs: list[_ActuatorSpec] = [] - for child in actuator_elem: - joint = child.attrib.get("joint") - tendon = child.attrib.get("tendon") - if not joint and not tendon: - continue - name = child.attrib.get("name") or joint or tendon or "actuator" - specs.append(_ActuatorSpec(name=name, joint=joint, tendon=tendon)) - return specs - - -def _build_joint_mappings_from_specs( - specs: list[_ActuatorSpec], - model: mujoco.MjModel, -) -> list[JointMapping]: - mappings: list[JointMapping] = [] - for spec in specs: - if spec.joint: - mappings.append(_mapping_for_joint(spec, model)) - elif spec.tendon: - mappings.append(_mapping_for_tendon(spec, model)) - return mappings - - -def _mapping_for_joint(spec: _ActuatorSpec, model: mujoco.MjModel) -> JointMapping: - joint_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_JOINT, spec.joint) - if joint_id < 0: - raise ValueError(f"Unknown joint '{spec.joint}' in MuJoCo model") - actuator_id = _find_actuator_id_for_joint(model, joint_id, spec.name) - joint_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, joint_id) or spec.name - return JointMapping( - name=joint_name, - joint_id=joint_id, - actuator_id=actuator_id, - qpos_adr=int(model.jnt_qposadr[joint_id]), - dof_adr=int(model.jnt_dofadr[joint_id]), - tendon_qpos_adrs=(), - tendon_dof_adrs=(), - ) - - -def _mapping_for_tendon(spec: _ActuatorSpec, model: mujoco.MjModel) -> JointMapping: - name = spec.name or spec.tendon - if not name: - raise ValueError("Tendon actuator is missing a name and tendon reference") - tendon_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_TENDON, spec.tendon) - if tendon_id < 0: - raise ValueError(f"Unknown tendon '{spec.tendon}' in MuJoCo model") - actuator_id = _find_actuator_id_for_tendon(model, tendon_id, spec.name) - joint_ids = _tendon_joint_ids(model, tendon_id) - return JointMapping( - name=name, - joint_id=None, - actuator_id=actuator_id, - qpos_adr=None, - dof_adr=None, - tendon_qpos_adrs=tuple(int(model.jnt_qposadr[joint_id]) for joint_id in joint_ids), - tendon_dof_adrs=tuple(int(model.jnt_dofadr[joint_id]) for joint_id in joint_ids), - ) - - -def _find_actuator_id_for_joint( - model: mujoco.MjModel, - joint_id: int, - actuator_name: str | None, -) -> int | None: - if actuator_name: - act_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, actuator_name) - if act_id >= 0: - return int(act_id) - for act_id in range(int(model.nu)): - trn_type = int(model.actuator_trntype[act_id]) - if trn_type != int(mujoco.mjtTrn.mjTRN_JOINT): - continue - if int(model.actuator_trnid[act_id, 0]) == joint_id: - return act_id - return None - - -def _find_actuator_id_for_tendon( - model: mujoco.MjModel, - tendon_id: int, - actuator_name: str | None, -) -> int | None: - if actuator_name: - act_id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_ACTUATOR, actuator_name) - if act_id >= 0: - return int(act_id) - for act_id in range(int(model.nu)): - trn_type = int(model.actuator_trntype[act_id]) - if trn_type != int(mujoco.mjtTrn.mjTRN_TENDON): - continue - if int(model.actuator_trnid[act_id, 0]) == tendon_id: - return act_id - return None - - -def _tendon_joint_ids(model: mujoco.MjModel, tendon_id: int) -> tuple[int, ...]: - adr = int(model.tendon_adr[tendon_id]) - num = int(model.tendon_num[tendon_id]) - joint_ids: list[int] = [] - for wrap_id in range(adr, adr + num): - wrap_type = int(model.wrap_type[wrap_id]) - if wrap_type == int(mujoco.mjtWrap.mjWRAP_JOINT): - joint_ids.append(int(model.wrap_objid[wrap_id])) - return tuple(joint_ids) - - -def _build_joint_mappings_from_actuators(model: mujoco.MjModel) -> list[JointMapping]: - mappings: list[JointMapping] = [] - for actuator_id in range(int(model.nu)): - actuator_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_ACTUATOR, actuator_id) - name = actuator_name or f"actuator{actuator_id}" - trn_type = int(model.actuator_trntype[actuator_id]) - if trn_type == int(mujoco.mjtTrn.mjTRN_JOINT): - joint_id = int(model.actuator_trnid[actuator_id, 0]) - joint_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, joint_id) - mappings.append( - JointMapping( - name=joint_name or name, - joint_id=joint_id, - actuator_id=actuator_id, - qpos_adr=int(model.jnt_qposadr[joint_id]), - dof_adr=int(model.jnt_dofadr[joint_id]), - tendon_qpos_adrs=(), - tendon_dof_adrs=(), - ) - ) - continue - - if trn_type == int(mujoco.mjtTrn.mjTRN_TENDON): - tendon_id = int(model.actuator_trnid[actuator_id, 0]) - tendon_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_TENDON, tendon_id) - if not actuator_name and tendon_name: - name = tendon_name - joint_ids = _tendon_joint_ids(model, tendon_id) - mappings.append( - JointMapping( - name=name, - joint_id=None, - actuator_id=actuator_id, - qpos_adr=None, - dof_adr=None, - tendon_qpos_adrs=tuple( - int(model.jnt_qposadr[joint_id]) for joint_id in joint_ids - ), - tendon_dof_adrs=tuple( - int(model.jnt_dofadr[joint_id]) for joint_id in joint_ids - ), - ) - ) - continue - - mappings.append( - JointMapping( - name=name, - joint_id=None, - actuator_id=actuator_id, - qpos_adr=None, - dof_adr=None, - tendon_qpos_adrs=(), - tendon_dof_adrs=(), - ) - ) - - return mappings - - -def _build_joint_mappings_from_model(model: mujoco.MjModel) -> list[JointMapping]: - mappings: list[JointMapping] = [] - for joint_id in range(int(model.njnt)): - jnt_type = int(model.jnt_type[joint_id]) - if jnt_type not in ( - int(mujoco.mjtJoint.mjJNT_HINGE), - int(mujoco.mjtJoint.mjJNT_SLIDE), - ): - continue - joint_name = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, joint_id) - name = joint_name or f"joint{joint_id}" - mappings.append( - JointMapping( - name=name, - joint_id=joint_id, - actuator_id=None, - qpos_adr=int(model.jnt_qposadr[joint_id]), - dof_adr=int(model.jnt_dofadr[joint_id]), - tendon_qpos_adrs=(), - tendon_dof_adrs=(), - ) - ) - return mappings diff --git a/dimos/skills/__init__.py b/dimos/skills/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/skills/kill_skill.py b/dimos/skills/kill_skill.py deleted file mode 100644 index f0ca805e6f..0000000000 --- a/dimos/skills/kill_skill.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Kill skill for terminating running skills. - -This module provides a skill that can terminate other running skills, -particularly those running in separate threads like the monitor skill. -""" - -from pydantic import Field - -from dimos.skills.skills import AbstractSkill, SkillLibrary -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class KillSkill(AbstractSkill): - """ - A skill that terminates other running skills. - - This skill can be used to stop long-running or background skills - like the monitor skill. It uses the centralized process management - in the SkillLibrary to track and terminate skills. - """ - - skill_name: str = Field(..., description="Name of the skill to terminate") - - def __init__(self, skill_library: SkillLibrary | None = None, **data) -> None: # type: ignore[no-untyped-def] - """ - Initialize the kill skill. - - Args: - skill_library: The skill library instance - **data: Additional data for configuration - """ - super().__init__(**data) - self._skill_library = skill_library - - def __call__(self): # type: ignore[no-untyped-def] - """ - Terminate the specified skill. - - Returns: - A message indicating whether the skill was successfully terminated - """ - print("running skills", self._skill_library.get_running_skills()) # type: ignore[union-attr] - # Terminate the skill using the skill library - return self._skill_library.terminate_skill(self.skill_name) # type: ignore[union-attr] diff --git a/dimos/skills/manipulation/abstract_manipulation_skill.py b/dimos/skills/manipulation/abstract_manipulation_skill.py deleted file mode 100644 index e767ad8c8f..0000000000 --- a/dimos/skills/manipulation/abstract_manipulation_skill.py +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Abstract base class for manipulation skills.""" - -from dimos.manipulation.manipulation_interface import ManipulationInterface -from dimos.robot.robot import Robot -from dimos.skills.skills import AbstractRobotSkill -from dimos.types.robot_capabilities import RobotCapability - - -class AbstractManipulationSkill(AbstractRobotSkill): - """Base class for all manipulation-related skills. - - This abstract class provides access to the robot's manipulation memory system. - """ - - def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] - """Initialize the manipulation skill. - - Args: - robot: The robot instance to associate with this skill - """ - super().__init__(*args, robot=robot, **kwargs) - - if self._robot and not self._robot.manipulation_interface: # type: ignore[attr-defined] - raise NotImplementedError( - "This robot does not have a manipulation interface implemented" - ) - - @property - def manipulation_interface(self) -> ManipulationInterface | None: - """Get the robot's manipulation interface. - - Returns: - ManipulationInterface: The robot's manipulation interface or None if not available - - Raises: - RuntimeError: If the robot doesn't have the MANIPULATION capability - """ - if self._robot is None: - return None - - if not self._robot.has_capability(RobotCapability.MANIPULATION): - raise RuntimeError("This robot does not have manipulation capabilities") - - return self._robot.manipulation_interface # type: ignore[attr-defined, no-any-return] diff --git a/dimos/skills/manipulation/force_constraint_skill.py b/dimos/skills/manipulation/force_constraint_skill.py deleted file mode 100644 index edeac0844e..0000000000 --- a/dimos/skills/manipulation/force_constraint_skill.py +++ /dev/null @@ -1,72 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from pydantic import Field - -from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import ForceConstraint, Vector # type: ignore[attr-defined] -from dimos.utils.logging_config import setup_logger - -# Initialize logger -logger = setup_logger() - - -class ForceConstraintSkill(AbstractManipulationSkill): - """ - Skill for generating force constraints for robot manipulation. - - This skill generates force constraints and adds them to the ManipulationInterface's - agent_constraints list for tracking constraints created by the Agent. - """ - - # Constraint parameters - min_force: float = Field(0.0, description="Minimum force magnitude in Newtons") - max_force: float = Field(100.0, description="Maximum force magnitude in Newtons to apply") - - # Force direction as (x,y) tuple - force_direction: tuple[float, float] | None = Field( - None, description="Force direction vector (x,y)" - ) - - # Description - description: str = Field("", description="Description of the force constraint") - - def __call__(self) -> ForceConstraint: - """ - Generate a force constraint based on the parameters. - - Returns: - ForceConstraint: The generated constraint - """ - # Create force direction vector if provided (convert 2D point to 3D vector with z=0) - force_direction_vector = None - if self.force_direction: - force_direction_vector = Vector(self.force_direction[0], self.force_direction[1], 0.0) # type: ignore[arg-type] - - # Create and return the constraint - constraint = ForceConstraint( - max_force=self.max_force, - min_force=self.min_force, - force_direction=force_direction_vector, - description=self.description, - ) - - # Add constraint to manipulation interface for Agent recall - self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] - - # Log the constraint creation - logger.info(f"Generated force constraint: {self.description}") - - return constraint diff --git a/dimos/skills/manipulation/manipulate_skill.py b/dimos/skills/manipulation/manipulate_skill.py deleted file mode 100644 index 830ddc33e0..0000000000 --- a/dimos/skills/manipulation/manipulate_skill.py +++ /dev/null @@ -1,173 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Any -import uuid - -from pydantic import Field - -from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import ( - AbstractConstraint, - ManipulationMetadata, - ManipulationTask, - ManipulationTaskConstraint, -) -from dimos.utils.logging_config import setup_logger - -# Initialize logger -logger = setup_logger() - - -class Manipulate(AbstractManipulationSkill): - """ - Skill for executing manipulation tasks with constraints. - Can be called by an LLM with a list of manipulation constraints. - """ - - description: str = Field("", description="Description of the manipulation task") - - # Target object information - target_object: str = Field( - "", description="Semantic label of the target object (e.g., 'cup', 'box')" - ) - - target_point: str = Field( - "", description="(X,Y) point in pixel-space of the point to manipulate on target object" - ) - - # Constraints - can be set directly - constraints: list[str] = Field( - [], - description="List of AbstractConstraint constraint IDs from AgentMemory to apply to the manipulation task", - ) - - # Object movement tolerances - object_tolerances: dict[str, float] = Field( - {}, # Empty dict as default - description="Dictionary mapping object IDs to movement tolerances (0.0 = immovable, 1.0 = freely movable)", - ) - - def __call__(self) -> dict[str, Any]: - """ - Execute a manipulation task with the given constraints. - - Returns: - Dict[str, Any]: Result of the manipulation operation - """ - # Get the manipulation constraint - constraint = self._build_manipulation_constraint() - - # Create task with unique ID - task_id = f"{str(uuid.uuid4())[:4]}" - timestamp = time.time() - - # Build metadata with environment state - metadata = self._build_manipulation_metadata() - - task = ManipulationTask( - description=self.description, - target_object=self.target_object, - target_point=tuple(map(int, self.target_point.strip("()").split(","))), # type: ignore[arg-type] - constraints=constraint, - metadata=metadata, - timestamp=timestamp, - task_id=task_id, - result=None, - ) - - # Add task to manipulation interface - self.manipulation_interface.add_manipulation_task(task) # type: ignore[union-attr] - - # Execute the manipulation - result = self._execute_manipulation(task) - - # Log the execution - logger.info( - f"Executed manipulation '{self.description}' with constraints: {self.constraints}" - ) - - return result - - def _build_manipulation_metadata(self) -> ManipulationMetadata: - """ - Build metadata for the current environment state, including object data and movement tolerances. - """ - # Get detected objects from the manipulation interface - detected_objects = [] # type: ignore[var-annotated] - try: - detected_objects = self.manipulation_interface.get_latest_objects() or [] # type: ignore[union-attr] - except Exception as e: - logger.warning(f"Failed to get detected objects: {e}") - - # Create dictionary of objects keyed by ID for easier lookup - objects_by_id = {} - for obj in detected_objects: - obj_id = str(obj.get("object_id", -1)) - objects_by_id[obj_id] = dict(obj) # Make a copy to avoid modifying original - - # Create objects_data dictionary with tolerances applied - objects_data: dict[str, Any] = {} - - # First, apply all specified tolerances - for object_id, tolerance in self.object_tolerances.items(): - if object_id in objects_by_id: - # Object exists in detected objects, update its tolerance - obj_data = objects_by_id[object_id] - obj_data["movement_tolerance"] = tolerance - objects_data[object_id] = obj_data - - # Add any detected objects not explicitly given tolerances - for obj_id, obj in objects_by_id.items(): - if obj_id not in self.object_tolerances: - obj["movement_tolerance"] = 0.0 # Default to immovable - objects_data[obj_id] = obj - - # Create properly typed ManipulationMetadata - metadata: ManipulationMetadata = {"timestamp": time.time(), "objects": objects_data} - - return metadata - - def _build_manipulation_constraint(self) -> ManipulationTaskConstraint: - """ - Build a ManipulationTaskConstraint object from the provided parameters. - """ - - constraint = ManipulationTaskConstraint() - - # Add constraints directly or resolve from IDs - for c in self.constraints: - if isinstance(c, AbstractConstraint): - constraint.add_constraint(c) - elif isinstance(c, str) and self.manipulation_interface: - # Try to load constraint from ID - saved_constraint = self.manipulation_interface.get_constraint(c) - if saved_constraint: - constraint.add_constraint(saved_constraint) - - return constraint - - # TODO: Implement - def _execute_manipulation(self, task: ManipulationTask) -> dict[str, Any]: - """ - Execute the manipulation with the given constraint. - - Args: - task: The manipulation task to execute - - Returns: - Dict[str, Any]: Result of the manipulation operation - """ - return {"success": True} diff --git a/dimos/skills/manipulation/pick_and_place.py b/dimos/skills/manipulation/pick_and_place.py deleted file mode 100644 index 1d1063edad..0000000000 --- a/dimos/skills/manipulation/pick_and_place.py +++ /dev/null @@ -1,444 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Pick and place skill for Piper Arm robot. - -This module provides a skill that uses Qwen VLM to identify pick and place -locations based on natural language queries, then executes the manipulation. -""" - -import json -import os -from typing import Any - -import cv2 -import numpy as np -from pydantic import Field - -from dimos.models.qwen.video_query import query_single_frame -from dimos.skills.skills import AbstractRobotSkill -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def parse_qwen_points_response(response: str) -> tuple[tuple[int, int], tuple[int, int]] | None: - """ - Parse Qwen's response containing two points. - - Args: - response: Qwen's response containing JSON with two points - - Returns: - Tuple of (pick_point, place_point) where each point is (x, y), or None if parsing fails - """ - try: - # Try to extract JSON from the response - start_idx = response.find("{") - end_idx = response.rfind("}") + 1 - - if start_idx >= 0 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - result = json.loads(json_str) - - # Extract pick and place points - if "pick_point" in result and "place_point" in result: - pick = result["pick_point"] - place = result["place_point"] - - # Validate points have x,y coordinates - if ( - isinstance(pick, list | tuple) - and len(pick) >= 2 - and isinstance(place, list | tuple) - and len(place) >= 2 - ): - return (int(pick[0]), int(pick[1])), (int(place[0]), int(place[1])) - - except Exception as e: - logger.error(f"Error parsing Qwen points response: {e}") - logger.debug(f"Raw response: {response}") - - return None - - -def save_debug_image_with_points( - image: np.ndarray, # type: ignore[type-arg] - pick_point: tuple[int, int] | None = None, - place_point: tuple[int, int] | None = None, - filename_prefix: str = "qwen_debug", -) -> str: - """ - Save debug image with crosshairs marking pick and/or place points. - - Args: - image: RGB image array - pick_point: (x, y) coordinates for pick location - place_point: (x, y) coordinates for place location - filename_prefix: Prefix for the saved filename - - Returns: - Path to the saved image - """ - # Create a copy to avoid modifying original - debug_image = image.copy() - - # Draw pick point crosshair (green) - if pick_point: - x, y = pick_point - # Draw crosshair - cv2.drawMarker(debug_image, (x, y), (0, 255, 0), cv2.MARKER_CROSS, 30, 2) - # Draw circle - cv2.circle(debug_image, (x, y), 5, (0, 255, 0), -1) - # Add label - cv2.putText( - debug_image, "PICK", (x + 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2 - ) - - # Draw place point crosshair (cyan) - if place_point: - x, y = place_point - # Draw crosshair - cv2.drawMarker(debug_image, (x, y), (255, 255, 0), cv2.MARKER_CROSS, 30, 2) - # Draw circle - cv2.circle(debug_image, (x, y), 5, (255, 255, 0), -1) - # Add label - cv2.putText( - debug_image, "PLACE", (x + 10, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 0), 2 - ) - - # Draw arrow from pick to place if both exist - if pick_point and place_point: - cv2.arrowedLine(debug_image, pick_point, place_point, (255, 0, 255), 2, tipLength=0.03) - - # Generate filename with timestamp - filename = f"{filename_prefix}.png" - filepath = os.path.join(os.getcwd(), filename) - - # Save image - cv2.imwrite(filepath, debug_image) - logger.info(f"Debug image saved to: {filepath}") - - return filepath - - -def parse_qwen_single_point_response(response: str) -> tuple[int, int] | None: - """ - Parse Qwen's response containing a single point. - - Args: - response: Qwen's response containing JSON with a point - - Returns: - Tuple of (x, y) or None if parsing fails - """ - try: - # Try to extract JSON from the response - start_idx = response.find("{") - end_idx = response.rfind("}") + 1 - - if start_idx >= 0 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - result = json.loads(json_str) - - # Try different possible keys - point = None - for key in ["point", "location", "position", "coordinates"]: - if key in result: - point = result[key] - break - - # Validate point has x,y coordinates - if point and isinstance(point, list | tuple) and len(point) >= 2: - return int(point[0]), int(point[1]) - - except Exception as e: - logger.error(f"Error parsing Qwen single point response: {e}") - logger.debug(f"Raw response: {response}") - - return None - - -class PickAndPlace(AbstractRobotSkill): - """ - A skill that performs pick and place operations using vision-language guidance. - - This skill uses Qwen VLM to identify objects and locations based on natural - language queries, then executes pick and place operations using the robot's - manipulation interface. - - Example usage: - # Just pick the object - skill = PickAndPlace(robot=robot, object_query="red mug") - - # Pick and place the object - skill = PickAndPlace(robot=robot, object_query="red mug", target_query="on the coaster") - - The skill uses the robot's stereo camera to capture RGB images and its manipulation - interface to execute the pick and place operation. It automatically handles coordinate - transformation from 2D pixel coordinates to 3D world coordinates. - """ - - object_query: str = Field( - "mug", - description="Natural language description of the object to pick (e.g., 'red mug', 'small box')", - ) - - target_query: str | None = Field( - None, - description="Natural language description of where to place the object (e.g., 'on the table', 'in the basket'). If not provided, only pick operation will be performed.", - ) - - model_name: str = Field( - "qwen2.5-vl-72b-instruct", description="Qwen model to use for visual queries" - ) - - def __init__(self, robot=None, **data) -> None: # type: ignore[no-untyped-def] - """ - Initialize the PickAndPlace skill. - - Args: - robot: The PiperArmRobot instance - **data: Additional configuration data - """ - super().__init__(robot=robot, **data) - - def _get_camera_frame(self) -> np.ndarray | None: # type: ignore[type-arg] - """ - Get a single RGB frame from the robot's camera. - - Returns: - RGB image as numpy array or None if capture fails - """ - if not self._robot or not self._robot.manipulation_interface: # type: ignore[attr-defined] - logger.error("Robot or stereo camera not available") - return None - - try: - # Use the RPC call to get a single RGB frame - rgb_frame = self._robot.manipulation_interface.get_single_rgb_frame() # type: ignore[attr-defined] - if rgb_frame is None: - logger.error("Failed to capture RGB frame from camera") - return rgb_frame # type: ignore[no-any-return] - except Exception as e: - logger.error(f"Error getting camera frame: {e}") - return None - - def _query_pick_and_place_points( - self, - frame: np.ndarray, # type: ignore[type-arg] - ) -> tuple[tuple[int, int], tuple[int, int]] | None: - """ - Query Qwen to get both pick and place points in a single query. - - Args: - frame: RGB image array - - Returns: - Tuple of (pick_point, place_point) or None if query fails - """ - # This method is only called when both object and target are specified - prompt = ( - f"Look at this image carefully. I need you to identify two specific locations:\n" - f"1. Find the {self.object_query} - this is the object I want to pick up\n" - f"2. Identify where to place it {self.target_query}\n\n" - "Instructions:\n" - "- The pick_point should be at the center or graspable part of the object\n" - "- The place_point should be a stable, flat surface at the target location\n" - "- Consider the object's size when choosing the placement point\n\n" - "Return ONLY a JSON object with this exact format:\n" - "{'pick_point': [x, y], 'place_point': [x, y]}\n" - "where [x, y] are pixel coordinates in the image." - ) - - try: - response = query_single_frame(frame, prompt, model_name=self.model_name) - return parse_qwen_points_response(response) - except Exception as e: - logger.error(f"Error querying Qwen for pick and place points: {e}") - return None - - def _query_single_point( - self, - frame: np.ndarray, # type: ignore[type-arg] - query: str, - point_type: str, - ) -> tuple[int, int] | None: - """ - Query Qwen to get a single point location. - - Args: - frame: RGB image array - query: Natural language description of what to find - point_type: Type of point ('pick' or 'place') for context - - Returns: - Tuple of (x, y) pixel coordinates or None if query fails - """ - if point_type == "pick": - prompt = ( - f"Look at this image carefully and find the {query}.\n\n" - "Instructions:\n" - "- Identify the exact object matching the description\n" - "- Choose the center point or the most graspable location on the object\n" - "- If multiple matching objects exist, choose the most prominent or accessible one\n" - "- Consider the object's shape and material when selecting the grasp point\n\n" - "Return ONLY a JSON object with this exact format:\n" - "{'point': [x, y]}\n" - "where [x, y] are the pixel coordinates of the optimal grasping point on the object." - ) - else: # place - prompt = ( - f"Look at this image and identify where to place an object {query}.\n\n" - "Instructions:\n" - "- Find a stable, flat surface at the specified location\n" - "- Ensure the placement spot is clear of obstacles\n" - "- Consider the size of the object being placed\n" - "- If the query specifies a container or specific spot, center the placement there\n" - "- Otherwise, find the most appropriate nearby surface\n\n" - "Return ONLY a JSON object with this exact format:\n" - "{'point': [x, y]}\n" - "where [x, y] are the pixel coordinates of the optimal placement location." - ) - - try: - response = query_single_frame(frame, prompt, model_name=self.model_name) - return parse_qwen_single_point_response(response) - except Exception as e: - logger.error(f"Error querying Qwen for {point_type} point: {e}") - return None - - def __call__(self) -> dict[str, Any]: - """ - Execute the pick and place operation. - - Returns: - Dictionary with operation results - """ - super().__call__() # type: ignore[no-untyped-call] - - if not self._robot: - error_msg = "No robot instance provided to PickAndPlace skill" - logger.error(error_msg) - return {"success": False, "error": error_msg} - - # Register skill as running - skill_library = self._robot.get_skills() # type: ignore[no-untyped-call] - self.register_as_running("PickAndPlace", skill_library) - - # Get camera frame - frame = self._get_camera_frame() - if frame is None: - return {"success": False, "error": "Failed to capture camera frame"} - - # Convert RGB to BGR for OpenCV if needed - if len(frame.shape) == 3 and frame.shape[2] == 3: - frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) - - # Get pick and place points from Qwen - pick_point = None - place_point = None - - # Determine mode based on whether target_query is provided - if self.target_query is None: - # Pick only mode - logger.info("Pick-only mode (no target specified)") - - # Query for pick point - pick_point = self._query_single_point(frame, self.object_query, "pick") - if not pick_point: - return {"success": False, "error": f"Failed to find {self.object_query}"} - - # No place point needed for pick-only - place_point = None - else: - # Pick and place mode - can use either single or dual query - logger.info("Pick and place mode (target specified)") - - # Try single query first for efficiency - points = self._query_pick_and_place_points(frame) - pick_point, place_point = points # type: ignore[misc] - - logger.info(f"Pick point: {pick_point}, Place point: {place_point}") - - # Save debug image with marked points - if pick_point or place_point: - save_debug_image_with_points(frame, pick_point, place_point) - - # Execute pick (and optionally place) using the robot's interface - try: - if place_point: - # Pick and place - result = self._robot.pick_and_place( # type: ignore[attr-defined] - pick_x=pick_point[0], - pick_y=pick_point[1], - place_x=place_point[0], - place_y=place_point[1], - ) - else: - # Pick only - result = self._robot.pick_and_place( # type: ignore[attr-defined] - pick_x=pick_point[0], pick_y=pick_point[1], place_x=None, place_y=None - ) - - if result: - if self.target_query: - message = ( - f"Successfully picked {self.object_query} and placed it {self.target_query}" - ) - else: - message = f"Successfully picked {self.object_query}" - - return { - "success": True, - "pick_point": pick_point, - "place_point": place_point, - "object": self.object_query, - "target": self.target_query, - "message": message, - } - else: - operation = "Pick and place" if self.target_query else "Pick" - return { - "success": False, - "pick_point": pick_point, - "place_point": place_point, - "error": f"{operation} operation failed", - } - - except Exception as e: - logger.error(f"Error executing pick and place: {e}") - return { - "success": False, - "error": f"Execution error: {e!s}", - "pick_point": pick_point, - "place_point": place_point, - } - finally: - # Always unregister skill when done - self.stop() - - def stop(self) -> None: - """ - Stop the pick and place operation and perform cleanup. - """ - logger.info("Stopping PickAndPlace skill") - - # Unregister skill from skill library - if self._robot: - skill_library = self._robot.get_skills() # type: ignore[no-untyped-call] - self.unregister_as_running("PickAndPlace", skill_library) - - logger.info("PickAndPlace skill stopped successfully") diff --git a/dimos/skills/manipulation/rotation_constraint_skill.py b/dimos/skills/manipulation/rotation_constraint_skill.py deleted file mode 100644 index 72e6a53716..0000000000 --- a/dimos/skills/manipulation/rotation_constraint_skill.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Literal - -from pydantic import Field - -from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import RotationConstraint -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -# Initialize logger -logger = setup_logger() - - -class RotationConstraintSkill(AbstractManipulationSkill): - """ - Skill for generating rotation constraints for robot manipulation. - - This skill generates rotation constraints and adds them to the ManipulationInterface's - agent_constraints list for tracking constraints created by the Agent. - """ - - # Rotation axis parameter - rotation_axis: Literal["roll", "pitch", "yaw"] = Field( - "roll", - description="Axis to rotate around: 'roll' (x-axis), 'pitch' (y-axis), or 'yaw' (z-axis)", - ) - - # Simple angle values for rotation (in degrees) - start_angle: float | None = Field(None, description="Starting angle in degrees") - end_angle: float | None = Field(None, description="Ending angle in degrees") - - # Pivot points as (x,y) tuples - pivot_point: tuple[float, float] | None = Field( - None, description="Pivot point (x,y) for rotation" - ) - - # TODO: Secondary pivot point for more complex rotations - secondary_pivot_point: tuple[float, float] | None = Field( - None, description="Secondary pivot point (x,y) for double-pivot rotation" - ) - - def __call__(self) -> RotationConstraint: - """ - Generate a rotation constraint based on the parameters. - - This implementation supports rotation around a single axis (roll, pitch, or yaw). - - Returns: - RotationConstraint: The generated constraint - """ - # rotation_axis is guaranteed to be one of "roll", "pitch", or "yaw" due to Literal type constraint - - # Create angle vectors more efficiently - start_angle_vector = None - if self.start_angle is not None: - # Build rotation vector on correct axis - values = [0.0, 0.0, 0.0] - axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] - values[axis_index] = self.start_angle - start_angle_vector = Vector(*values) # type: ignore[arg-type] - - end_angle_vector = None - if self.end_angle is not None: - values = [0.0, 0.0, 0.0] - axis_index = {"roll": 0, "pitch": 1, "yaw": 2}[self.rotation_axis] - values[axis_index] = self.end_angle - end_angle_vector = Vector(*values) # type: ignore[arg-type] - - # Create pivot point vector if provided (convert 2D point to 3D vector with z=0) - pivot_point_vector = None - if self.pivot_point: - pivot_point_vector = Vector(self.pivot_point[0], self.pivot_point[1], 0.0) # type: ignore[arg-type] - - # Create secondary pivot point vector if provided - secondary_pivot_vector = None - if self.secondary_pivot_point: - secondary_pivot_vector = Vector( - self.secondary_pivot_point[0], # type: ignore[arg-type] - self.secondary_pivot_point[1], # type: ignore[arg-type] - 0.0, # type: ignore[arg-type] - ) - - constraint = RotationConstraint( - rotation_axis=self.rotation_axis, - start_angle=start_angle_vector, - end_angle=end_angle_vector, - pivot_point=pivot_point_vector, - secondary_pivot_point=secondary_pivot_vector, - ) - - # Add constraint to manipulation interface - self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] - - # Log the constraint creation - logger.info(f"Generated rotation constraint around {self.rotation_axis} axis") - - return constraint diff --git a/dimos/skills/manipulation/translation_constraint_skill.py b/dimos/skills/manipulation/translation_constraint_skill.py deleted file mode 100644 index 78ea38cfe4..0000000000 --- a/dimos/skills/manipulation/translation_constraint_skill.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Literal - -from pydantic import Field - -from dimos.skills.manipulation.abstract_manipulation_skill import AbstractManipulationSkill -from dimos.types.manipulation import TranslationConstraint, Vector # type: ignore[attr-defined] -from dimos.utils.logging_config import setup_logger - -# Initialize logger -logger = setup_logger() - - -class TranslationConstraintSkill(AbstractManipulationSkill): - """ - Skill for generating translation constraints for robot manipulation. - - This skill generates translation constraints and adds them to the ManipulationInterface's - agent_constraints list for tracking constraints created by the Agent. - """ - - # Constraint parameters - translation_axis: Literal["x", "y", "z"] = Field( - "x", description="Axis to translate along: 'x', 'y', or 'z'" - ) - - reference_point: tuple[float, float] | None = Field( - None, description="Reference point (x,y) on the target object for translation constraining" - ) - - bounds_min: tuple[float, float] | None = Field( - None, description="Minimum bounds (x,y) for bounded translation" - ) - - bounds_max: tuple[float, float] | None = Field( - None, description="Maximum bounds (x,y) for bounded translation" - ) - - target_point: tuple[float, float] | None = Field( - None, description="Final target position (x,y) for translation constraining" - ) - - # Description - description: str = Field("", description="Description of the translation constraint") - - def __call__(self) -> TranslationConstraint: - """ - Generate a translation constraint based on the parameters. - - Returns: - TranslationConstraint: The generated constraint - """ - # Create reference point vector if provided (convert 2D point to 3D vector with z=0) - reference_point = None - if self.reference_point: - reference_point = Vector(self.reference_point[0], self.reference_point[1], 0.0) # type: ignore[arg-type] - - # Create bounds minimum vector if provided - bounds_min = None - if self.bounds_min: - bounds_min = Vector(self.bounds_min[0], self.bounds_min[1], 0.0) # type: ignore[arg-type] - - # Create bounds maximum vector if provided - bounds_max = None - if self.bounds_max: - bounds_max = Vector(self.bounds_max[0], self.bounds_max[1], 0.0) # type: ignore[arg-type] - - # Create relative target vector if provided - target_point = None - if self.target_point: - target_point = Vector(self.target_point[0], self.target_point[1], 0.0) # type: ignore[arg-type] - - constraint = TranslationConstraint( - translation_axis=self.translation_axis, - reference_point=reference_point, - bounds_min=bounds_min, - bounds_max=bounds_max, - target_point=target_point, - ) - - # Add constraint to manipulation interface - self.manipulation_interface.add_constraint(constraint) # type: ignore[union-attr] - - # Log the constraint creation - logger.info(f"Generated translation constraint along {self.translation_axis} axis") - - return {"success": True} # type: ignore[return-value] diff --git a/dimos/skills/rest/__init__.py b/dimos/skills/rest/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/skills/rest/rest.py b/dimos/skills/rest/rest.py deleted file mode 100644 index 471a7022df..0000000000 --- a/dimos/skills/rest/rest.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging - -from pydantic import Field -import requests # type: ignore[import-untyped] - -from dimos.skills.skills import AbstractSkill - -logger = logging.getLogger(__name__) - - -class GenericRestSkill(AbstractSkill): - """Performs a configurable REST API call. - - This skill executes an HTTP request based on the provided parameters. It - supports various HTTP methods and allows specifying URL, timeout. - - Attributes: - url: The target URL for the API call. - method: The HTTP method (e.g., 'GET', 'POST'). Case-insensitive. - timeout: Request timeout in seconds. - """ - - # TODO: Add query parameters, request body data (form-encoded or JSON), and headers. - # , query - # parameters, request body data (form-encoded or JSON), and headers. - # params: Optional dictionary of URL query parameters. - # data: Optional dictionary for form-encoded request body data. - # json_payload: Optional dictionary for JSON request body data. Use the - # alias 'json' when initializing. - # headers: Optional dictionary of HTTP headers. - url: str = Field(..., description="The target URL for the API call.") - method: str = Field(..., description="HTTP method (e.g., 'GET', 'POST').") - timeout: int = Field(..., description="Request timeout in seconds.") - # params: Optional[Dict[str, Any]] = Field(default=None, description="URL query parameters.") - # data: Optional[Dict[str, Any]] = Field(default=None, description="Form-encoded request body.") - # json_payload: Optional[Dict[str, Any]] = Field(default=None, alias="json", description="JSON request body.") - # headers: Optional[Dict[str, str]] = Field(default=None, description="HTTP headers.") - - def __call__(self) -> str: - """Executes the configured REST API call. - - Returns: - The text content of the response on success (HTTP 2xx). - - Raises: - requests.exceptions.RequestException: If a connection error, timeout, - or other request-related issue occurs. - requests.exceptions.HTTPError: If the server returns an HTTP 4xx or - 5xx status code. - Exception: For any other unexpected errors during execution. - - Returns: - A string representing the success or failure outcome. If successful, - returns the response body text. If an error occurs, returns a - descriptive error message. - """ - try: - logger.debug( - f"Executing {self.method.upper()} request to {self.url} " - f"with timeout={self.timeout}" # , params={self.params}, " - # f"data={self.data}, json={self.json_payload}, headers={self.headers}" - ) - response = requests.request( - method=self.method.upper(), # Normalize method to uppercase - url=self.url, - # params=self.params, - # data=self.data, - # json=self.json_payload, # Use the attribute name defined in Pydantic - # headers=self.headers, - timeout=self.timeout, - ) - response.raise_for_status() # Raises HTTPError for bad responses (4xx or 5xx) - logger.debug( - f"Request successful. Status: {response.status_code}, Response: {response.text[:100]}..." - ) - return response.text # type: ignore[no-any-return] # Return text content directly - except requests.exceptions.HTTPError as http_err: - logger.error( - f"HTTP error occurred: {http_err} - Status Code: {http_err.response.status_code}" - ) - return f"HTTP error making {self.method.upper()} request to {self.url}: {http_err.response.status_code} {http_err.response.reason}" - except requests.exceptions.RequestException as req_err: - logger.error(f"Request exception occurred: {req_err}") - return f"Error making {self.method.upper()} request to {self.url}: {req_err}" - except Exception as e: - logger.exception(f"An unexpected error occurred: {e}") # Log the full traceback - return f"An unexpected error occurred: {type(e).__name__}: {e}" diff --git a/dimos/skills/skills.py b/dimos/skills/skills.py deleted file mode 100644 index 94f8b3726f..0000000000 --- a/dimos/skills/skills.py +++ /dev/null @@ -1,343 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Any - -from openai import pydantic_function_tool -from pydantic import BaseModel - -from dimos.types.constants import Colors - -if TYPE_CHECKING: - from collections.abc import Iterator - -# Configure logging for the module -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - -# region SkillLibrary - - -class SkillLibrary: - # ==== Flat Skill Library ==== - - def __init__(self) -> None: - self.registered_skills: list[AbstractSkill] = [] - self.class_skills: list[AbstractSkill] = [] - self._running_skills = {} # type: ignore[var-annotated] # {skill_name: (instance, subscription)} - - self.init() - - def init(self) -> None: - # Collect all skills from the parent class and update self.skills - self.refresh_class_skills() - - # Temporary - self.registered_skills = self.class_skills.copy() - - def get_class_skills(self) -> list[AbstractSkill]: - """Extract all AbstractSkill subclasses from a class. - - Returns: - List of skill classes found within the class - """ - skills = [] - - # Loop through all attributes of the class - for attr_name in dir(self.__class__): - # Skip special/dunder attributes - if attr_name.startswith("__"): - continue - - try: - attr = getattr(self.__class__, attr_name) - - # Check if it's a class and inherits from AbstractSkill - if ( - isinstance(attr, type) - and issubclass(attr, AbstractSkill) - and attr is not AbstractSkill - ): - skills.append(attr) - except (AttributeError, TypeError): - # Skip attributes that can't be accessed or aren't classes - continue - - return skills # type: ignore[return-value] - - def refresh_class_skills(self) -> None: - self.class_skills = self.get_class_skills() - - def add(self, skill: AbstractSkill) -> None: - if skill not in self.registered_skills: - self.registered_skills.append(skill) - - def get(self) -> list[AbstractSkill]: - return self.registered_skills.copy() - - def remove(self, skill: AbstractSkill) -> None: - try: - self.registered_skills.remove(skill) - except ValueError: - logger.warning(f"Attempted to remove non-existent skill: {skill}") - - def clear(self) -> None: - self.registered_skills.clear() - - def __iter__(self) -> Iterator: # type: ignore[type-arg] - return iter(self.registered_skills) - - def __len__(self) -> int: - return len(self.registered_skills) - - def __contains__(self, skill: AbstractSkill) -> bool: - return skill in self.registered_skills - - def __getitem__(self, index): # type: ignore[no-untyped-def] - return self.registered_skills[index] - - # ==== Calling a Function ==== - - _instances: dict[str, dict] = {} # type: ignore[type-arg] - - def create_instance(self, name: str, **kwargs) -> None: # type: ignore[no-untyped-def] - # Key based only on the name - key = name - - if key not in self._instances: - # Instead of creating an instance, store the args for later use - self._instances[key] = kwargs - - def call(self, name: str, **args): # type: ignore[no-untyped-def] - try: - # Get the stored args if available; otherwise, use an empty dict - stored_args = self._instances.get(name, {}) - - # Merge the arguments with priority given to stored arguments - complete_args = {**args, **stored_args} - - # Dynamically get the class from the module or current script - skill_class = getattr(self, name, None) - if skill_class is None: - for skill in self.get(): - if name == skill.__name__: # type: ignore[attr-defined] - skill_class = skill - break - if skill_class is None: - error_msg = f"Skill '{name}' is not available. Please check if it's properly registered." - logger.error(f"Skill class not found: {name}") - return error_msg - - # Initialize the instance with the merged arguments - instance = skill_class(**complete_args) # type: ignore[operator] - print(f"Instance created and function called for: {name} with args: {complete_args}") - - # Call the instance directly - return instance() - except Exception as e: - error_msg = f"Error executing skill '{name}': {e!s}" - logger.error(error_msg) - return error_msg - - # ==== Tools ==== - - def get_tools(self) -> Any: - tools_json = self.get_list_of_skills_as_json(list_of_skills=self.registered_skills) - # print(f"{Colors.YELLOW_PRINT_COLOR}Tools JSON: {tools_json}{Colors.RESET_COLOR}") - return tools_json - - def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: - return list(map(pydantic_function_tool, list_of_skills)) # type: ignore[arg-type] - - def register_running_skill(self, name: str, instance: Any, subscription=None) -> None: # type: ignore[no-untyped-def] - """ - Register a running skill with its subscription. - - Args: - name: Name of the skill (will be converted to lowercase) - instance: Instance of the running skill - subscription: Optional subscription associated with the skill - """ - name = name.lower() - self._running_skills[name] = (instance, subscription) - logger.info(f"Registered running skill: {name}") - - def unregister_running_skill(self, name: str) -> bool: - """ - Unregister a running skill. - - Args: - name: Name of the skill to remove (will be converted to lowercase) - - Returns: - True if the skill was found and removed, False otherwise - """ - name = name.lower() - if name in self._running_skills: - del self._running_skills[name] - logger.info(f"Unregistered running skill: {name}") - return True - return False - - def get_running_skills(self): # type: ignore[no-untyped-def] - """ - Get all running skills. - - Returns: - A dictionary of running skill names and their (instance, subscription) tuples - """ - return self._running_skills.copy() - - def terminate_skill(self, name: str): # type: ignore[no-untyped-def] - """ - Terminate a running skill. - - Args: - name: Name of the skill to terminate (will be converted to lowercase) - - Returns: - A message indicating whether the skill was successfully terminated - """ - name = name.lower() - if name in self._running_skills: - instance, subscription = self._running_skills[name] - - try: - # Call the stop method if it exists - if hasattr(instance, "stop") and callable(instance.stop): - instance.stop() - logger.info(f"Stopped skill: {name}") - else: - logger.warning(f"Skill {name} does not have a stop method") - - # Also dispose the subscription if it exists - if ( - subscription is not None - and hasattr(subscription, "dispose") - and callable(subscription.dispose) - ): - subscription.dispose() - logger.info(f"Disposed subscription for skill: {name}") - elif subscription is not None: - logger.warning(f"Skill {name} has a subscription but it's not disposable") - - # unregister the skill - self.unregister_running_skill(name) - return f"Successfully terminated skill: {name}" - - except Exception as e: - error_msg = f"Error terminating skill {name}: {e}" - logger.error(error_msg) - # Even on error, try to unregister the skill - self.unregister_running_skill(name) - return error_msg - else: - return f"No running skill found with name: {name}" - - -# endregion SkillLibrary - -# region AbstractSkill - - -class AbstractSkill(BaseModel): - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - print("Initializing AbstractSkill Class") - super().__init__(*args, **kwargs) - self._instances = {} # type: ignore[var-annotated] - self._list_of_skills = [] # type: ignore[var-annotated] # Initialize the list of skills - print(f"Instances: {self._instances}") - - def clone(self) -> AbstractSkill: - return AbstractSkill() - - def register_as_running( # type: ignore[no-untyped-def] - self, name: str, skill_library: SkillLibrary, subscription=None - ) -> None: - """ - Register this skill as running in the skill library. - - Args: - name: Name of the skill (will be converted to lowercase) - skill_library: The skill library to register with - subscription: Optional subscription associated with the skill - """ - skill_library.register_running_skill(name, self, subscription) - - def unregister_as_running(self, name: str, skill_library: SkillLibrary) -> None: - """ - Unregister this skill from the skill library. - - Args: - name: Name of the skill to remove (will be converted to lowercase) - skill_library: The skill library to unregister from - """ - skill_library.unregister_running_skill(name) - - # ==== Tools ==== - def get_tools(self) -> Any: - tools_json = self.get_list_of_skills_as_json(list_of_skills=self._list_of_skills) - # print(f"Tools JSON: {tools_json}") - return tools_json - - def get_list_of_skills_as_json(self, list_of_skills: list[AbstractSkill]) -> list[str]: - return list(map(pydantic_function_tool, list_of_skills)) # type: ignore[arg-type] - - -# endregion AbstractSkill - -# region Abstract Robot Skill - -if TYPE_CHECKING: - from dimos.robot.robot import Robot -else: - Robot = "Robot" - - -class AbstractRobotSkill(AbstractSkill): - _robot: Robot = None # type: ignore[assignment] - - def __init__(self, *args, robot: Robot | None = None, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._robot = robot # type: ignore[assignment] - print( - f"{Colors.BLUE_PRINT_COLOR}Robot Skill Initialized with Robot: {robot}{Colors.RESET_COLOR}" - ) - - def set_robot(self, robot: Robot) -> None: - """Set the robot reference for this skills instance. - - Args: - robot: The robot instance to associate with these skills. - """ - self._robot = robot - - def __call__(self): # type: ignore[no-untyped-def] - if self._robot is None: - raise RuntimeError( - f"{Colors.RED_PRINT_COLOR}" - f"No Robot instance provided to Robot Skill: {self.__class__.__name__}" - f"{Colors.RESET_COLOR}" - ) - else: - print( - f"{Colors.BLUE_PRINT_COLOR}Robot Instance provided to Robot Skill: {self.__class__.__name__}{Colors.RESET_COLOR}" - ) - - -# endregion Abstract Robot Skill diff --git a/dimos/skills/speak.py b/dimos/skills/speak.py deleted file mode 100644 index fc26fd2cd0..0000000000 --- a/dimos/skills/speak.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import queue -import threading -import time -from typing import Any - -from pydantic import Field -from reactivex import Subject - -from dimos.skills.skills import AbstractSkill -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -# Global lock to prevent multiple simultaneous audio playbacks -_audio_device_lock = threading.RLock() - -# Global queue for sequential audio processing -_audio_queue = queue.Queue() # type: ignore[var-annotated] -_queue_processor_thread = None -_queue_running = False - - -def _process_audio_queue() -> None: - """Background thread to process audio requests sequentially""" - global _queue_running - - while _queue_running: - try: - # Get the next queued audio task with a timeout - task = _audio_queue.get(timeout=1.0) - if task is None: # Sentinel value to stop the thread - break - - # Execute the task (which is a function to be called) - task() - _audio_queue.task_done() - - except queue.Empty: - # No tasks in queue, just continue waiting - continue - except Exception as e: - logger.error(f"Error in audio queue processor: {e}") - # Continue processing other tasks - - -def start_audio_queue_processor() -> None: - """Start the background thread for processing audio requests""" - global _queue_processor_thread, _queue_running - - if _queue_processor_thread is None or not _queue_processor_thread.is_alive(): - _queue_running = True - _queue_processor_thread = threading.Thread( - target=_process_audio_queue, daemon=True, name="AudioQueueProcessor" - ) - _queue_processor_thread.start() - logger.info("Started audio queue processor thread") - - -# Start the queue processor when module is imported -start_audio_queue_processor() - - -class Speak(AbstractSkill): - """Speak text out loud to humans nearby or to other robots.""" - - text: str = Field(..., description="Text to speak") - - def __init__(self, tts_node: Any | None = None, **data) -> None: # type: ignore[no-untyped-def] - super().__init__(**data) - self._tts_node = tts_node - self._audio_complete = threading.Event() - self._subscription = None - self._subscriptions: list = [] # type: ignore[type-arg] # Track all subscriptions - - def __call__(self): # type: ignore[no-untyped-def] - if not self._tts_node: - logger.error("No TTS node provided to Speak skill") - return "Error: No TTS node available" - - # Create a result queue to get the result back from the audio thread - result_queue = queue.Queue(1) # type: ignore[var-annotated] - - # Define the speech task to run in the audio queue - def speak_task() -> None: - try: - # Using a lock to ensure exclusive access to audio device - with _audio_device_lock: - text_subject = Subject() # type: ignore[var-annotated] - self._audio_complete.clear() - self._subscriptions = [] - - # This function will be called when audio processing is complete - def on_complete() -> None: - logger.info(f"TTS audio playback completed for: {self.text}") - self._audio_complete.set() - - # This function will be called if there's an error - def on_error(error) -> None: # type: ignore[no-untyped-def] - logger.error(f"Error in TTS processing: {error}") - self._audio_complete.set() - - # Connect the Subject to the TTS node and keep the subscription - self._tts_node.consume_text(text_subject) # type: ignore[union-attr] - - # Subscribe to the audio output to know when it's done - self._subscription = self._tts_node.emit_text().subscribe( # type: ignore[union-attr] - on_next=lambda text: logger.debug(f"TTS processing: {text}"), - on_completed=on_complete, - on_error=on_error, - ) - self._subscriptions.append(self._subscription) - - # Emit the text to the Subject - text_subject.on_next(self.text) - text_subject.on_completed() # Signal that we're done sending text - - # Wait for audio playback to complete with a timeout - # Using a dynamic timeout based on text length - timeout = max(5, len(self.text) * 0.1) - logger.debug(f"Waiting for TTS completion with timeout {timeout:.1f}s") - - if not self._audio_complete.wait(timeout=timeout): - logger.warning(f"TTS timeout reached for: {self.text}") - else: - # Add a small delay after audio completes to ensure buffers are fully flushed - time.sleep(0.3) - - # Clean up all subscriptions - for sub in self._subscriptions: - if sub: - sub.dispose() - self._subscriptions = [] - - # Successfully completed - result_queue.put(f"Spoke: {self.text} successfully") - except Exception as e: - logger.error(f"Error in speak task: {e}") - result_queue.put(f"Error speaking text: {e!s}") - - # Add our speech task to the global queue for sequential processing - display_text = self.text[:50] + "..." if len(self.text) > 50 else self.text - logger.info(f"Queueing speech task: '{display_text}'") - _audio_queue.put(speak_task) - - # Wait for the result with a timeout - try: - # Use a longer timeout than the audio playback itself - text_len_timeout = len(self.text) * 0.15 # 150ms per character - max_timeout = max(10, text_len_timeout) # At least 10 seconds - - return result_queue.get(timeout=max_timeout) - except queue.Empty: - logger.error("Timed out waiting for speech task to complete") - return f"Error: Timed out while speaking: {self.text}" diff --git a/dimos/skills/unitree/__init__.py b/dimos/skills/unitree/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/skills/unitree/unitree_speak.py b/dimos/skills/unitree/unitree_speak.py deleted file mode 100644 index 84abc3296a..0000000000 --- a/dimos/skills/unitree/unitree_speak.py +++ /dev/null @@ -1,280 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -import hashlib -import json -import os -import tempfile -import time - -import numpy as np -from openai import OpenAI -from pydantic import Field -import soundfile as sf # type: ignore[import-untyped] -from unitree_webrtc_connect.constants import RTC_TOPIC - -from dimos.skills.skills import AbstractRobotSkill -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -# Audio API constants (from go2_webrtc_driver) -AUDIO_API = { - "GET_AUDIO_LIST": 1001, - "SELECT_START_PLAY": 1002, - "PAUSE": 1003, - "UNSUSPEND": 1004, - "SET_PLAY_MODE": 1007, - "UPLOAD_AUDIO_FILE": 2001, - "ENTER_MEGAPHONE": 4001, - "EXIT_MEGAPHONE": 4002, - "UPLOAD_MEGAPHONE": 4003, -} - -PLAY_MODES = {"NO_CYCLE": "no_cycle", "SINGLE_CYCLE": "single_cycle", "LIST_LOOP": "list_loop"} - - -class UnitreeSpeak(AbstractRobotSkill): - """Speak text out loud through the robot's speakers using WebRTC audio upload.""" - - text: str = Field(..., description="Text to speak") - voice: str = Field( - default="echo", description="Voice to use (alloy, echo, fable, onyx, nova, shimmer)" - ) - speed: float = Field(default=1.2, description="Speech speed (0.25 to 4.0)") - use_megaphone: bool = Field( - default=False, description="Use megaphone mode for lower latency (experimental)" - ) - - def __init__(self, **data) -> None: # type: ignore[no-untyped-def] - super().__init__(**data) - self._openai_client = None - - def _get_openai_client(self): # type: ignore[no-untyped-def] - if self._openai_client is None: - self._openai_client = OpenAI() # type: ignore[assignment] - return self._openai_client - - def _generate_audio(self, text: str) -> bytes: - try: - client = self._get_openai_client() # type: ignore[no-untyped-call] - response = client.audio.speech.create( - model="tts-1", voice=self.voice, input=text, speed=self.speed, response_format="mp3" - ) - return response.content # type: ignore[no-any-return] - except Exception as e: - logger.error(f"Error generating audio: {e}") - raise - - def _webrtc_request(self, api_id: int, parameter: dict | None = None): # type: ignore[no-untyped-def, type-arg] - if parameter is None: - parameter = {} - - request_data = {"api_id": api_id, "parameter": json.dumps(parameter) if parameter else "{}"} - - return self._robot.connection.publish_request(RTC_TOPIC["AUDIO_HUB_REQ"], request_data) # type: ignore[attr-defined] - - def _upload_audio_to_robot(self, audio_data: bytes, filename: str) -> str: - try: - file_md5 = hashlib.md5(audio_data).hexdigest() - b64_data = base64.b64encode(audio_data).decode("utf-8") - - chunk_size = 61440 - chunks = [b64_data[i : i + chunk_size] for i in range(0, len(b64_data), chunk_size)] - total_chunks = len(chunks) - - logger.info(f"Uploading audio '{filename}' in {total_chunks} chunks (optimized)") - - for i, chunk in enumerate(chunks, 1): - parameter = { - "file_name": filename, - "file_type": "wav", - "file_size": len(audio_data), - "current_block_index": i, - "total_block_number": total_chunks, - "block_content": chunk, - "current_block_size": len(chunk), - "file_md5": file_md5, - "create_time": int(time.time() * 1000), - } - - logger.debug(f"Sending chunk {i}/{total_chunks}") - self._webrtc_request(AUDIO_API["UPLOAD_AUDIO_FILE"], parameter) - - logger.info(f"Audio upload completed for '{filename}'") - - list_response = self._webrtc_request(AUDIO_API["GET_AUDIO_LIST"], {}) - - if list_response and "data" in list_response: - data_str = list_response.get("data", {}).get("data", "{}") - audio_list = json.loads(data_str).get("audio_list", []) - - for audio in audio_list: - if audio.get("CUSTOM_NAME") == filename: - return audio.get("UNIQUE_ID") # type: ignore[no-any-return] - - logger.warning( - f"Could not find uploaded audio '{filename}' in list, using filename as UUID" - ) - return filename - - except Exception as e: - logger.error(f"Error uploading audio to robot: {e}") - raise - - def _play_audio_on_robot(self, uuid: str): # type: ignore[no-untyped-def] - try: - self._webrtc_request(AUDIO_API["SET_PLAY_MODE"], {"play_mode": PLAY_MODES["NO_CYCLE"]}) - time.sleep(0.1) - - parameter = {"unique_id": uuid} - - logger.info(f"Playing audio with UUID: {uuid}") - self._webrtc_request(AUDIO_API["SELECT_START_PLAY"], parameter) - - except Exception as e: - logger.error(f"Error playing audio on robot: {e}") - raise - - def _stop_audio_playback(self) -> None: - try: - logger.debug("Stopping audio playback") - self._webrtc_request(AUDIO_API["PAUSE"], {}) - except Exception as e: - logger.warning(f"Error stopping audio playback: {e}") - - def _upload_and_play_megaphone(self, audio_data: bytes, duration: float): # type: ignore[no-untyped-def] - try: - logger.debug("Entering megaphone mode") - self._webrtc_request(AUDIO_API["ENTER_MEGAPHONE"], {}) - - time.sleep(0.2) - - b64_data = base64.b64encode(audio_data).decode("utf-8") - - chunk_size = 4096 - chunks = [b64_data[i : i + chunk_size] for i in range(0, len(b64_data), chunk_size)] - total_chunks = len(chunks) - - logger.info(f"Uploading megaphone audio in {total_chunks} chunks") - - for i, chunk in enumerate(chunks, 1): - parameter = { - "current_block_size": len(chunk), - "block_content": chunk, - "current_block_index": i, - "total_block_number": total_chunks, - } - - logger.debug(f"Sending megaphone chunk {i}/{total_chunks}") - self._webrtc_request(AUDIO_API["UPLOAD_MEGAPHONE"], parameter) - - if i < total_chunks: - time.sleep(0.05) - - logger.info("Megaphone audio upload completed, waiting for playback") - - time.sleep(duration + 1.0) - - except Exception as e: - logger.error(f"Error in megaphone mode: {e}") - try: - self._webrtc_request(AUDIO_API["EXIT_MEGAPHONE"], {}) - except: - pass - raise - finally: - try: - logger.debug("Exiting megaphone mode") - self._webrtc_request(AUDIO_API["EXIT_MEGAPHONE"], {}) - time.sleep(0.1) - except Exception as e: - logger.warning(f"Error exiting megaphone mode: {e}") - - def __call__(self) -> str: - super().__call__() # type: ignore[no-untyped-call] - - if not self._robot: - logger.error("No robot instance provided to UnitreeSpeak skill") - return "Error: No robot instance available" - - try: - display_text = self.text[:50] + "..." if len(self.text) > 50 else self.text - logger.info(f"Speaking: '{display_text}'") - - logger.debug("Generating audio with OpenAI TTS") - audio_data = self._generate_audio(self.text) - - with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as tmp_mp3: - tmp_mp3.write(audio_data) - tmp_mp3_path = tmp_mp3.name - - try: - audio_array, sample_rate = sf.read(tmp_mp3_path) - - if audio_array.ndim > 1: - audio_array = np.mean(audio_array, axis=1) - - target_sample_rate = 22050 - if sample_rate != target_sample_rate: - logger.debug(f"Resampling from {sample_rate}Hz to {target_sample_rate}Hz") - old_length = len(audio_array) - new_length = int(old_length * target_sample_rate / sample_rate) - old_indices = np.arange(old_length) - new_indices = np.linspace(0, old_length - 1, new_length) - audio_array = np.interp(new_indices, old_indices, audio_array) - sample_rate = target_sample_rate - - audio_array = audio_array / np.max(np.abs(audio_array)) - - with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_wav: - sf.write(tmp_wav.name, audio_array, sample_rate, format="WAV", subtype="PCM_16") - tmp_wav.seek(0) - wav_data = open(tmp_wav.name, "rb").read() - os.unlink(tmp_wav.name) - - logger.info( - f"Audio size: {len(wav_data) / 1024:.1f}KB, duration: {len(audio_array) / sample_rate:.1f}s" - ) - - finally: - os.unlink(tmp_mp3_path) - - if self.use_megaphone: - logger.debug("Using megaphone mode for lower latency") - duration = len(audio_array) / sample_rate - self._upload_and_play_megaphone(wav_data, duration) - - return f"Spoke: '{display_text}' on robot successfully (megaphone mode)" - else: - filename = f"speak_{int(time.time() * 1000)}" - - logger.debug("Uploading audio to robot") - uuid = self._upload_audio_to_robot(wav_data, filename) - - logger.debug("Playing audio on robot") - self._play_audio_on_robot(uuid) - - duration = len(audio_array) / sample_rate - logger.debug(f"Waiting {duration:.1f}s for playback to complete") - # time.sleep(duration + 0.2) - - # self._stop_audio_playback() - - return f"Spoke: '{display_text}' on robot successfully" - - except Exception as e: - logger.error(f"Error in speak skill: {e}") - return f"Error speaking text: {e!s}" diff --git a/dimos/skills/visual_navigation_skills.py b/dimos/skills/visual_navigation_skills.py deleted file mode 100644 index 9ce6d34f09..0000000000 --- a/dimos/skills/visual_navigation_skills.py +++ /dev/null @@ -1,150 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Visual navigation skills for robot interaction. - -This module provides skills for visual navigation, including following humans -and navigating to specific objects using computer vision. -""" - -import logging -import threading -import time - -from pydantic import Field - -from dimos.perception.visual_servoing import ( # type: ignore[import-not-found, import-untyped] - VisualServoing, -) -from dimos.skills.skills import AbstractRobotSkill -from dimos.types.vector import Vector -from dimos.utils.logging_config import setup_logger - -logger = setup_logger(level=logging.DEBUG) - - -class FollowHuman(AbstractRobotSkill): - """ - A skill that makes the robot follow a human using visual servoing continuously. - - This skill uses the robot's person tracking stream to follow a human - while maintaining a specified distance. It will keep following the human - until the timeout is reached or the skill is stopped. Don't use this skill - if you want to navigate to a specific person, use NavigateTo instead. - """ - - distance: float = Field( - 1.5, description="Desired distance to maintain from the person in meters" - ) - timeout: float = Field(20.0, description="Maximum time to follow the person in seconds") - point: tuple[int, int] | None = Field( - None, description="Optional point to start tracking (x,y pixel coordinates)" - ) - - def __init__(self, robot=None, **data) -> None: # type: ignore[no-untyped-def] - super().__init__(robot=robot, **data) - self._stop_event = threading.Event() - self._visual_servoing = None - - def __call__(self): # type: ignore[no-untyped-def] - """ - Start following a human using visual servoing. - - Returns: - bool: True if successful, False otherwise - """ - super().__call__() # type: ignore[no-untyped-call] - - if ( - not hasattr(self._robot, "person_tracking_stream") - or self._robot.person_tracking_stream is None - ): - logger.error("Robot does not have a person tracking stream") - return False - - # Stop any existing operation - self.stop() - self._stop_event.clear() - - success = False - - try: - # Initialize visual servoing - self._visual_servoing = VisualServoing( - tracking_stream=self._robot.person_tracking_stream - ) - - logger.warning(f"Following human for {self.timeout} seconds...") - start_time = time.time() - - # Start tracking - track_success = self._visual_servoing.start_tracking( # type: ignore[attr-defined] - point=self.point, desired_distance=self.distance - ) - - if not track_success: - logger.error("Failed to start tracking") - return False - - # Main follow loop - while ( - self._visual_servoing.running # type: ignore[attr-defined] - and time.time() - start_time < self.timeout - and not self._stop_event.is_set() - ): - output = self._visual_servoing.updateTracking() # type: ignore[attr-defined] - x_vel = output.get("linear_vel") - z_vel = output.get("angular_vel") - logger.debug(f"Following human: x_vel: {x_vel}, z_vel: {z_vel}") - self._robot.move(Vector(x_vel, 0, z_vel)) # type: ignore[arg-type, attr-defined] - time.sleep(0.05) - - # If we completed the full timeout duration, consider it success - if time.time() - start_time >= self.timeout: - success = True - logger.info("Human following completed successfully") - elif self._stop_event.is_set(): - logger.info("Human following stopped externally") - else: - logger.info("Human following stopped due to tracking loss") - - return success - - except Exception as e: - logger.error(f"Error in follow human: {e}") - return False - finally: - # Clean up - if self._visual_servoing: - self._visual_servoing.stop_tracking() - self._visual_servoing = None - - def stop(self) -> bool: - """ - Stop the human following process. - - Returns: - bool: True if stopped, False if it wasn't running - """ - if self._visual_servoing is not None: - logger.info("Stopping FollowHuman skill") - self._stop_event.set() - - # Clean up visual servoing if it exists - self._visual_servoing.stop_tracking() - self._visual_servoing = None - - return True - return False diff --git a/dimos/spec/__init__.py b/dimos/spec/__init__.py deleted file mode 100644 index 1423bec9a1..0000000000 --- a/dimos/spec/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -from dimos.spec.control import LocalPlanner -from dimos.spec.mapping import GlobalCostmap, GlobalPointcloud -from dimos.spec.nav import Nav -from dimos.spec.perception import Camera, Image, Pointcloud - -__all__ = [ - "Camera", - "GlobalCostmap", - "GlobalPointcloud", - "Image", - "LocalPlanner", - "Nav", - "Pointcloud", -] diff --git a/dimos/spec/control.py b/dimos/spec/control.py deleted file mode 100644 index e2024c5a09..0000000000 --- a/dimos/spec/control.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from dimos.core import Out -from dimos.msgs.geometry_msgs import Twist - - -class LocalPlanner(Protocol): - cmd_vel: Out[Twist] diff --git a/dimos/spec/mapping.py b/dimos/spec/mapping.py deleted file mode 100644 index f8e7e1a04f..0000000000 --- a/dimos/spec/mapping.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from dimos.core import Out -from dimos.msgs.nav_msgs import OccupancyGrid -from dimos.msgs.sensor_msgs import PointCloud2 - - -class GlobalPointcloud(Protocol): - global_map: Out[PointCloud2] - - -class GlobalCostmap(Protocol): - global_costmap: Out[OccupancyGrid] diff --git a/dimos/spec/nav.py b/dimos/spec/nav.py deleted file mode 100644 index d1f62c0846..0000000000 --- a/dimos/spec/nav.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from dimos.core import In, Out -from dimos.msgs.geometry_msgs import PoseStamped, Twist -from dimos.msgs.nav_msgs import Path - - -class Nav(Protocol): - goal_req: In[PoseStamped] - goal_active: Out[PoseStamped] - path_active: Out[Path] - ctrl: Out[Twist] - - # identity quaternion (Quaternion(0,0,0,1)) represents "no rotation requested" - def navigate_to_target(self, target: PoseStamped) -> None: ... - - def stop_navigating(self) -> None: ... diff --git a/dimos/spec/perception.py b/dimos/spec/perception.py deleted file mode 100644 index 1cecdb4d2f..0000000000 --- a/dimos/spec/perception.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from dimos.core import Out -from dimos.msgs.nav_msgs.Odometry import Odometry as OdometryMsg -from dimos.msgs.sensor_msgs import CameraInfo, Image as ImageMsg, Imu, PointCloud2 - - -class Image(Protocol): - color_image: Out[ImageMsg] - - -class Camera(Image): - camera_info: Out[CameraInfo] - _camera_info: CameraInfo - - -class DepthCamera(Camera): - depth_image: Out[ImageMsg] - depth_camera_info: Out[CameraInfo] - - -class Pointcloud(Protocol): - pointcloud: Out[PointCloud2] - - -class IMU(Protocol): - imu: Out[Imu] - - -class Odometry(Protocol): - odometry: Out[OdometryMsg] - - -class Lidar(Protocol): - """LiDAR sensor providing point clouds.""" - - lidar: Out[PointCloud2] diff --git a/dimos/spec/test_utils.py b/dimos/spec/test_utils.py deleted file mode 100644 index 40fe05facc..0000000000 --- a/dimos/spec/test_utils.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -import pytest - -from dimos.spec.utils import Spec, is_spec, spec_annotation_compliance, spec_structural_compliance - - -class NormalProtocol(Protocol): - def foo(self) -> int: ... - - -class SpecProtocol(Spec, Protocol): - def foo(self) -> int: ... - - -def test_is_spec_recognizes_spec_protocol() -> None: - assert is_spec(SpecProtocol) is True - - -def test_is_spec_rejects_plain_protocol_and_base() -> None: - assert is_spec(NormalProtocol) is False - assert is_spec(Spec) is False - - -def test_is_spec_rejects_non_type() -> None: - assert is_spec(object()) is False - - -class MySpec(Spec, Protocol): - def foo(self) -> int: - return 1 - - -class StructurallyCompliant: - def foo(self) -> str: - return "ok" - - -class FullyCompliant: - def foo(self) -> int: - return 1 - - -class NotCompliant: - pass - - -def test_spec_structural_compliance_matches_by_structure() -> None: - assert spec_structural_compliance(NotCompliant(), MySpec) is False - assert spec_structural_compliance(StructurallyCompliant(), MySpec) is True - assert spec_structural_compliance(FullyCompliant(), MySpec) is True - - -def test_spec_structural_compliance_rejects_non_spec() -> None: - with pytest.raises(TypeError): - spec_structural_compliance(StructurallyCompliant(), NormalProtocol) # type: ignore[arg-type] - - -def test_spec_annotation_compliance_requires_matching_annotations() -> None: - assert spec_annotation_compliance(StructurallyCompliant(), MySpec) is False - assert spec_annotation_compliance(FullyCompliant(), MySpec) is True - - -def test_spec_annotation_compliance_rejects_non_spec() -> None: - with pytest.raises(TypeError): - spec_annotation_compliance(StructurallyCompliant(), NormalProtocol) # type: ignore[arg-type] diff --git a/dimos/spec/utils.py b/dimos/spec/utils.py deleted file mode 100644 index b9786b91b5..0000000000 --- a/dimos/spec/utils.py +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import inspect -from typing import Any, Protocol, runtime_checkable - -from annotation_protocol import AnnotationProtocol # type: ignore[import-not-found,import-untyped] -from typing_extensions import is_protocol - - -# Allows us to differentiate plain Protocols from Module-Spec Protocols -class Spec(Protocol): - pass - - -def is_spec(cls: Any) -> bool: - """ - Example: - class NormalProtocol(Protocol): - def foo(self) -> int: ... - - class SpecProtocol(Spec, Protocol): - def foo(self) -> int: ... - - is_spec(NormalProtocol) # False - is_spec(SpecProtocol) # True - """ - return inspect.isclass(cls) and is_protocol(cls) and Spec in cls.__mro__ and cls is not Spec - - -def spec_structural_compliance( - obj: Any, - spec: Any, -) -> bool: - """ - Example: - class MySpec(Spec, Protocol): - def foo(self) -> int: ... - - class StructurallyCompliant1: - def foo(self) -> list[list[list[list[list[int]]]]]: ... - class StructurallyCompliant2: - def foo(self) -> str: ... - class FullyCompliant: - def foo(self) -> int: ... - class NotCompliant: - ... - - assert False == spec_structural_compliance(NotCompliant(), MySpec) - assert True == spec_structural_compliance(StructurallyCompliant1(), MySpec) - assert True == spec_structural_compliance(StructurallyCompliant2(), MySpec) - assert True == spec_structural_compliance(FullyCompliant(), MySpec) - """ - if not is_spec(spec): - raise TypeError("Trying to check if `obj` implements `spec` but spec itself was not a Spec") - - # python's built-in protocol check ignores annotations (only structural check) - return isinstance(obj, runtime_checkable(spec)) - - -def spec_annotation_compliance( - obj: Any, - proto: Any, -) -> bool: - """ - Example: - class MySpec(Spec, Protocol): - def foo(self) -> int: ... - - class StructurallyCompliant1: - def foo(self) -> list[list[list[list[list[int]]]]]: ... - class FullyCompliant: - def foo(self) -> int: ... - - assert False == spec_annotation_compliance(StructurallyCompliant1(), MySpec) - assert True == spec_structural_compliance(FullyCompliant(), MySpec) - """ - if not is_spec(proto): - raise TypeError("Not a Spec") - - # Build a *strict* runtime protocol dynamically - strict_proto = type( - f"Strict{proto.__name__}", - (AnnotationProtocol,), - dict(proto.__dict__), - ) - - return isinstance(obj, strict_proto) - - -def get_protocol_method_signatures(proto: type[object]) -> dict[str, inspect.Signature]: - """ - Return a mapping of method_name -> inspect.Signature - for all methods required by a Protocol. - """ - if not is_protocol(proto): - raise TypeError(f"{proto} is not a Protocol") - - methods: dict[str, inspect.Signature] = {} - - # Walk MRO so inherited protocol methods are included - for cls in reversed(proto.__mro__): - if cls is Protocol: # type: ignore[comparison-overlap] - continue - - for name, value in cls.__dict__.items(): - if name.startswith("_"): - continue - - if callable(value): - try: - sig = inspect.signature(value) - except (TypeError, ValueError): - continue - - methods[name] = sig - - return methods diff --git a/dimos/stream/__init__.py b/dimos/stream/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/stream/audio/__init__.py b/dimos/stream/audio/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/stream/audio/base.py b/dimos/stream/audio/base.py deleted file mode 100644 index 54bd1705a3..0000000000 --- a/dimos/stream/audio/base.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -import numpy as np -from reactivex import Observable - - -class AbstractAudioEmitter(ABC): - """Base class for components that emit audio.""" - - @abstractmethod - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """Create an observable that emits audio frames. - - Returns: - Observable emitting audio frames - """ - pass - - -class AbstractAudioConsumer(ABC): - """Base class for components that consume audio.""" - - @abstractmethod - def consume_audio(self, audio_observable: Observable) -> "AbstractAudioConsumer": # type: ignore[type-arg] - """Set the audio observable to consume. - - Args: - audio_observable: Observable emitting audio frames - - Returns: - Self for method chaining - """ - pass - - -class AbstractAudioTransform(AbstractAudioConsumer, AbstractAudioEmitter): - """Base class for components that both consume and emit audio. - - This represents a transform in an audio processing pipeline. - """ - - pass - - -class AudioEvent: - """Class to represent an audio frame event with metadata.""" - - def __init__( - self, - data: np.ndarray, # type: ignore[type-arg] - sample_rate: int, - timestamp: float, - channels: int = 1, - ) -> None: - """ - Initialize an AudioEvent. - - Args: - data: Audio data as numpy array - sample_rate: Audio sample rate in Hz - timestamp: Unix timestamp when the audio was captured - channels: Number of audio channels - """ - self.data = data - self.sample_rate = sample_rate - self.timestamp = timestamp - self.channels = channels - self.dtype = data.dtype - self.shape = data.shape - - def to_float32(self) -> "AudioEvent": - """Convert audio data to float32 format normalized to [-1.0, 1.0].""" - if self.data.dtype == np.float32: - return self - - new_data = self.data.astype(np.float32) - if self.data.dtype == np.int16: - new_data /= 32768.0 - - return AudioEvent( - data=new_data, - sample_rate=self.sample_rate, - timestamp=self.timestamp, - channels=self.channels, - ) - - def to_int16(self) -> "AudioEvent": - """Convert audio data to int16 format.""" - if self.data.dtype == np.int16: - return self - - new_data = self.data - if self.data.dtype == np.float32: - new_data = (new_data * 32767).astype(np.int16) - - return AudioEvent( - data=new_data, - sample_rate=self.sample_rate, - timestamp=self.timestamp, - channels=self.channels, - ) - - def __repr__(self) -> str: - return ( - f"AudioEvent(shape={self.shape}, dtype={self.dtype}, " - f"sample_rate={self.sample_rate}, channels={self.channels})" - ) diff --git a/dimos/stream/audio/node_key_recorder.py b/dimos/stream/audio/node_key_recorder.py deleted file mode 100644 index a6489d0e5a..0000000000 --- a/dimos/stream/audio/node_key_recorder.py +++ /dev/null @@ -1,335 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import select -import sys -import threading -import time - -import numpy as np -from reactivex import Observable -from reactivex.subject import ReplaySubject, Subject - -from dimos.stream.audio.base import AbstractAudioTransform, AudioEvent -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class KeyRecorder(AbstractAudioTransform): - """ - Audio recorder that captures audio events and combines them. - Press a key to toggle recording on/off. - """ - - def __init__( - self, - max_recording_time: float = 120.0, - always_subscribe: bool = False, - ) -> None: - """ - Initialize KeyRecorder. - - Args: - max_recording_time: Maximum recording time in seconds - always_subscribe: If True, subscribe to audio source continuously, - If False, only subscribe when recording (more efficient - but some audio devices may need time to initialize) - """ - self.max_recording_time = max_recording_time - self.always_subscribe = always_subscribe - - self._audio_buffer = [] # type: ignore[var-annotated] - self._is_recording = False - self._recording_start_time = 0 - self._sample_rate = None # Will be updated from incoming audio - self._channels = None # Will be set from first event - - self._audio_observable = None - self._subscription = None - self._output_subject = Subject() # type: ignore[var-annotated] # For record-time passthrough - self._recording_subject = ReplaySubject(1) # type: ignore[var-annotated] # For full completed recordings - - # Start a thread to monitor for input - self._running = True - self._input_thread = threading.Thread(target=self._input_monitor, daemon=True) - self._input_thread.start() - - logger.info("Started audio recorder (press any key to start/stop recording)") - - def consume_audio(self, audio_observable: Observable) -> "KeyRecorder": # type: ignore[type-arg] - """ - Set the audio observable to use when recording. - If always_subscribe is True, subscribes immediately. - Otherwise, subscribes only when recording starts. - - Args: - audio_observable: Observable emitting AudioEvent objects - - Returns: - Self for method chaining - """ - self._audio_observable = audio_observable # type: ignore[assignment] - - # If configured to always subscribe, do it now - if self.always_subscribe and not self._subscription: - self._subscription = audio_observable.subscribe( # type: ignore[assignment] - on_next=self._process_audio_event, - on_error=self._handle_error, - on_completed=self._handle_completion, - ) - logger.debug("Subscribed to audio source (always_subscribe=True)") - - return self - - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits audio events in real-time (pass-through). - - Returns: - Observable emitting AudioEvent objects in real-time - """ - return self._output_subject - - def emit_recording(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits combined audio recordings when recording stops. - - Returns: - Observable emitting AudioEvent objects with complete recordings - """ - return self._recording_subject - - def stop(self) -> None: - """Stop recording and clean up resources.""" - logger.info("Stopping audio recorder") - - # If recording is in progress, stop it first - if self._is_recording: - self._stop_recording() - - # Always clean up subscription on full stop - if self._subscription: - self._subscription.dispose() - self._subscription = None - - # Stop input monitoring thread - self._running = False - if self._input_thread.is_alive(): - self._input_thread.join(1.0) - - def _input_monitor(self) -> None: - """Monitor for key presses to toggle recording.""" - logger.info("Press Enter to start/stop recording...") - - while self._running: - # Check if there's input available - if select.select([sys.stdin], [], [], 0.1)[0]: - sys.stdin.readline() - - if self._is_recording: - self._stop_recording() - else: - self._start_recording() - - # Sleep a bit to reduce CPU usage - time.sleep(0.1) - - def _start_recording(self) -> None: - """Start recording audio and subscribe to the audio source if not always subscribed.""" - if not self._audio_observable: - logger.error("Cannot start recording: No audio source has been set") - return - - # Subscribe to the observable if not using always_subscribe - if not self._subscription: - self._subscription = self._audio_observable.subscribe( - on_next=self._process_audio_event, - on_error=self._handle_error, - on_completed=self._handle_completion, - ) - logger.debug("Subscribed to audio source for recording") - - self._is_recording = True - self._recording_start_time = time.time() - self._audio_buffer = [] - logger.info("Recording... (press Enter to stop)") - - def _stop_recording(self) -> None: - """Stop recording, unsubscribe from audio source if not always subscribed, and emit the combined audio event.""" - self._is_recording = False - recording_duration = time.time() - self._recording_start_time - - # Unsubscribe from the audio source if not using always_subscribe - if not self.always_subscribe and self._subscription: - self._subscription.dispose() - self._subscription = None - logger.debug("Unsubscribed from audio source after recording") - - logger.info(f"Recording stopped after {recording_duration:.2f} seconds") - - # Combine all audio events into one - if len(self._audio_buffer) > 0: - combined_audio = self._combine_audio_events(self._audio_buffer) - self._recording_subject.on_next(combined_audio) - else: - logger.warning("No audio was recorded") - - def _process_audio_event(self, audio_event) -> None: # type: ignore[no-untyped-def] - """Process incoming audio events.""" - - # Only buffer if recording - if not self._is_recording: - return - - # Pass through audio events in real-time - self._output_subject.on_next(audio_event) - - # First audio event - determine channel count/sample rate - if self._channels is None: - self._channels = audio_event.channels - self._sample_rate = audio_event.sample_rate - logger.info(f"Setting channel count to {self._channels}") - - # Add to buffer - self._audio_buffer.append(audio_event) - - # Check if we've exceeded max recording time - if time.time() - self._recording_start_time > self.max_recording_time: - logger.warning(f"Max recording time ({self.max_recording_time}s) reached") - self._stop_recording() - - def _combine_audio_events(self, audio_events: list[AudioEvent]) -> AudioEvent: - """Combine multiple audio events into a single event.""" - if not audio_events: - logger.warning("Attempted to combine empty audio events list") - return None # type: ignore[return-value] - - # Filter out any empty events that might cause broadcasting errors - valid_events = [ - event - for event in audio_events - if event is not None - and (hasattr(event, "data") and event.data is not None and event.data.size > 0) - ] - - if not valid_events: - logger.warning("No valid audio events to combine") - return None # type: ignore[return-value] - - first_event = valid_events[0] - channels = first_event.channels - dtype = first_event.data.dtype - - # Calculate total samples only from valid events - total_samples = sum(event.data.shape[0] for event in valid_events) - - # Safety check - if somehow we got no samples - if total_samples <= 0: - logger.warning(f"Combined audio would have {total_samples} samples - aborting") - return None # type: ignore[return-value] - - # For multichannel audio, data shape could be (samples,) or (samples, channels) - if len(first_event.data.shape) == 1: - # 1D audio data (mono) - combined_data = np.zeros(total_samples, dtype=dtype) - - # Copy data - offset = 0 - for event in valid_events: - samples = event.data.shape[0] - if samples > 0: # Extra safety check - combined_data[offset : offset + samples] = event.data - offset += samples - else: - # Multichannel audio data (stereo or more) - combined_data = np.zeros((total_samples, channels), dtype=dtype) - - # Copy data - offset = 0 - for event in valid_events: - samples = event.data.shape[0] - if samples > 0 and offset + samples <= total_samples: # Safety check - try: - combined_data[offset : offset + samples] = event.data - offset += samples - except ValueError as e: - logger.error( - f"Error combining audio events: {e}. " - f"Event shape: {event.data.shape}, " - f"Combined shape: {combined_data.shape}, " - f"Offset: {offset}, Samples: {samples}" - ) - # Continue with next event instead of failing completely - - # Create new audio event with the combined data - if combined_data.size > 0: - return AudioEvent( - data=combined_data, - sample_rate=self._sample_rate, # type: ignore[arg-type] - timestamp=valid_events[0].timestamp, - channels=channels, - ) - else: - logger.warning("Failed to create valid combined audio event") - return None # type: ignore[return-value] - - def _handle_error(self, error) -> None: # type: ignore[no-untyped-def] - """Handle errors from the observable.""" - logger.error(f"Error in audio observable: {error}") - - def _handle_completion(self) -> None: - """Handle completion of the observable.""" - logger.info("Audio observable completed") - self.stop() - - -if __name__ == "__main__": - from dimos.stream.audio.node_microphone import ( - SounddeviceAudioSource, - ) - from dimos.stream.audio.node_normalizer import AudioNormalizer - from dimos.stream.audio.node_output import SounddeviceAudioOutput - from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.utils import keepalive - - # Create microphone source, recorder, and audio output - mic = SounddeviceAudioSource() - - # my audio device needs time to init, so for smoother ux we constantly listen - recorder = KeyRecorder(always_subscribe=True) - - normalizer = AudioNormalizer() - speaker = SounddeviceAudioOutput() - - # Connect the components - normalizer.consume_audio(mic.emit_audio()) - recorder.consume_audio(normalizer.emit_audio()) - # recorder.consume_audio(mic.emit_audio()) - - # Monitor microphone input levels (real-time pass-through) - monitor(recorder.emit_audio()) - - # Connect the recorder output to the speakers to hear recordings when completed - playback_speaker = SounddeviceAudioOutput() - playback_speaker.consume_audio(recorder.emit_recording()) - - # TODO: we should be able to run normalizer post hoc on the recording as well, - # it's not working, this needs a review - # - # normalizer.consume_audio(recorder.emit_recording()) - # playback_speaker.consume_audio(normalizer.emit_audio()) - - keepalive() diff --git a/dimos/stream/audio/node_microphone.py b/dimos/stream/audio/node_microphone.py deleted file mode 100644 index 5d6e28dc74..0000000000 --- a/dimos/stream/audio/node_microphone.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time -from typing import Any - -import numpy as np -from reactivex import Observable, create, disposable -import sounddevice as sd # type: ignore[import-untyped] - -from dimos.stream.audio.base import ( - AbstractAudioEmitter, - AudioEvent, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class SounddeviceAudioSource(AbstractAudioEmitter): - """Audio source implementation using the sounddevice library.""" - - def __init__( - self, - device_index: int | None = None, - sample_rate: int = 16000, - channels: int = 1, - block_size: int = 1024, - dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] - ) -> None: - """ - Initialize SounddeviceAudioSource. - - Args: - device_index: Audio device index (None for default) - sample_rate: Audio sample rate in Hz - channels: Number of audio channels (1=mono, 2=stereo) - block_size: Number of samples per audio frame - dtype: Data type for audio samples (np.float32 or np.int16) - """ - self.device_index = device_index - self.sample_rate = sample_rate - self.channels = channels - self.block_size = block_size - self.dtype = dtype - - self._stream = None - self._running = False - - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits audio frames. - - Returns: - Observable emitting AudioEvent objects - """ - - def on_subscribe(observer, scheduler): # type: ignore[no-untyped-def] - # Callback function to process audio data - def audio_callback(indata, frames, time_info, status) -> None: # type: ignore[no-untyped-def] - if status: - logger.warning(f"Audio callback status: {status}") - - # Create audio event - audio_event = AudioEvent( - data=indata.copy(), - sample_rate=self.sample_rate, - timestamp=time.time(), - channels=self.channels, - ) - - observer.on_next(audio_event) - - # Start the audio stream - try: - self._stream = sd.InputStream( - device=self.device_index, - samplerate=self.sample_rate, - channels=self.channels, - blocksize=self.block_size, - dtype=self.dtype, - callback=audio_callback, - ) - self._stream.start() # type: ignore[attr-defined] - self._running = True - - logger.info( - f"Started audio capture: {self.sample_rate}Hz, " - f"{self.channels} channels, {self.block_size} samples per frame" - ) - - except Exception as e: - logger.error(f"Error starting audio stream: {e}") - observer.on_error(e) - - # Return a disposable to clean up resources - def dispose() -> None: - logger.info("Stopping audio capture") - self._running = False - if self._stream: - self._stream.stop() - self._stream.close() - self._stream = None - - return disposable.Disposable(dispose) - - return create(on_subscribe) - - def get_available_devices(self) -> list[dict[str, Any]]: - """Get a list of available audio input devices.""" - return sd.query_devices() # type: ignore[no-any-return] - - -if __name__ == "__main__": - from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.utils import keepalive - - monitor(SounddeviceAudioSource().emit_audio()) - keepalive() diff --git a/dimos/stream/audio/node_normalizer.py b/dimos/stream/audio/node_normalizer.py deleted file mode 100644 index 60a25a0404..0000000000 --- a/dimos/stream/audio/node_normalizer.py +++ /dev/null @@ -1,220 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable - -import numpy as np -from reactivex import Observable, create, disposable - -from dimos.stream.audio.base import ( - AbstractAudioTransform, - AudioEvent, -) -from dimos.stream.audio.volume import ( - calculate_peak_volume, - calculate_rms_volume, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class AudioNormalizer(AbstractAudioTransform): - """ - Audio normalizer that remembers max volume and rescales audio to normalize it. - - This class applies dynamic normalization to audio frames. It keeps track of - the max volume encountered and uses that to normalize the audio to a target level. - """ - - def __init__( - self, - target_level: float = 1.0, - min_volume_threshold: float = 0.01, - max_gain: float = 10.0, - decay_factor: float = 0.999, - adapt_speed: float = 0.05, - volume_func: Callable[[np.ndarray], float] = calculate_peak_volume, # type: ignore[type-arg] - ) -> None: - """ - Initialize AudioNormalizer. - - Args: - target_level: Target normalization level (0.0 to 1.0) - min_volume_threshold: Minimum volume to apply normalization - max_gain: Maximum allowed gain to prevent excessive amplification - decay_factor: Decay factor for max volume (0.0-1.0, higher = slower decay) - adapt_speed: How quickly to adapt to new volume levels (0.0-1.0) - volume_func: Function to calculate volume (default: peak volume) - """ - self.target_level = target_level - self.min_volume_threshold = min_volume_threshold - self.max_gain = max_gain - self.decay_factor = decay_factor - self.adapt_speed = adapt_speed - self.volume_func = volume_func - - # Internal state - self.max_volume = 0.0 - self.current_gain = 1.0 - self.audio_observable = None - - def _normalize_audio(self, audio_event: AudioEvent) -> AudioEvent: - """ - Normalize audio data based on tracked max volume. - - Args: - audio_event: Input audio event - - Returns: - Normalized audio event - """ - # Convert to float32 for processing if needed - if audio_event.data.dtype != np.float32: - audio_event = audio_event.to_float32() - - # Calculate current volume using provided function - current_volume = self.volume_func(audio_event.data) - - # Update max volume with decay - self.max_volume = max(current_volume, self.max_volume * self.decay_factor) - - # Calculate ideal gain - if self.max_volume > self.min_volume_threshold: - ideal_gain = self.target_level / self.max_volume - else: - ideal_gain = 1.0 # No normalization needed for very quiet audio - - # Limit gain to max_gain - ideal_gain = min(ideal_gain, self.max_gain) - - # Smoothly adapt current gain towards ideal gain - self.current_gain = ( - 1 - self.adapt_speed - ) * self.current_gain + self.adapt_speed * ideal_gain - - # Apply gain to audio data - normalized_data = audio_event.data * self.current_gain - - # Clip to prevent distortion (values should stay within -1.0 to 1.0) - normalized_data = np.clip(normalized_data, -1.0, 1.0) - - # Create new audio event with normalized data - return AudioEvent( - data=normalized_data, - sample_rate=audio_event.sample_rate, - timestamp=audio_event.timestamp, - channels=audio_event.channels, - ) - - def consume_audio(self, audio_observable: Observable) -> "AudioNormalizer": # type: ignore[type-arg] - """ - Set the audio source observable to consume. - - Args: - audio_observable: Observable emitting AudioEvent objects - - Returns: - Self for method chaining - """ - self.audio_observable = audio_observable # type: ignore[assignment] - return self - - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits normalized audio frames. - - Returns: - Observable emitting normalized AudioEvent objects - """ - if self.audio_observable is None: - raise ValueError("No audio source provided. Call consume_audio() first.") - - def on_subscribe(observer, scheduler): - # Subscribe to the audio observable - audio_subscription = self.audio_observable.subscribe( - on_next=lambda event: observer.on_next(self._normalize_audio(event)), - on_error=lambda error: observer.on_error(error), - on_completed=lambda: observer.on_completed(), - ) - - logger.info( - f"Started audio normalizer with target level: {self.target_level}, max gain: {self.max_gain}" - ) - - # Return a disposable to clean up resources - def dispose() -> None: - logger.info("Stopping audio normalizer") - audio_subscription.dispose() - - return disposable.Disposable(dispose) - - return create(on_subscribe) - - -if __name__ == "__main__": - import sys - - from dimos.stream.audio.node_microphone import ( - SounddeviceAudioSource, - ) - from dimos.stream.audio.node_output import SounddeviceAudioOutput - from dimos.stream.audio.node_simulated import SimulatedAudioSource - from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.utils import keepalive - - # Parse command line arguments - volume_method = "peak" # Default to peak - use_mic = False # Default to microphone input - target_level = 1 # Default target level - - # Process arguments - for arg in sys.argv[1:]: - if arg == "rms": - volume_method = "rms" - elif arg == "peak": - volume_method = "peak" - elif arg == "mic": - use_mic = True - elif arg.startswith("level="): - try: - target_level = float(arg.split("=")[1]) # type: ignore[assignment] - except ValueError: - print(f"Invalid target level: {arg}") - sys.exit(1) - - # Create appropriate audio source - if use_mic: - audio_source = SounddeviceAudioSource() - print("Using microphone input") - else: - audio_source = SimulatedAudioSource(volume_oscillation=True) - print("Using simulated audio source") - - # Select volume function - volume_func = calculate_rms_volume if volume_method == "rms" else calculate_peak_volume - - # Create normalizer - normalizer = AudioNormalizer(target_level=target_level, volume_func=volume_func) - - # Connect the audio source to the normalizer - normalizer.consume_audio(audio_source.emit_audio()) - - print(f"Using {volume_method} volume method with target level {target_level}") - SounddeviceAudioOutput().consume_audio(normalizer.emit_audio()) - - # Monitor the normalized audio - monitor(normalizer.emit_audio()) - keepalive() diff --git a/dimos/stream/audio/node_output.py b/dimos/stream/audio/node_output.py deleted file mode 100644 index 4b4d407329..0000000000 --- a/dimos/stream/audio/node_output.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -import numpy as np -from reactivex import Observable -import sounddevice as sd # type: ignore[import-untyped] - -from dimos.stream.audio.base import ( - AbstractAudioTransform, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class SounddeviceAudioOutput(AbstractAudioTransform): - """ - Audio output implementation using the sounddevice library. - - This class implements AbstractAudioTransform so it can both play audio and - optionally pass audio events through to other components (for example, to - record audio while playing it, or to visualize the waveform while playing). - """ - - def __init__( - self, - device_index: int | None = None, - sample_rate: int = 16000, - channels: int = 1, - block_size: int = 1024, - dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] - ) -> None: - """ - Initialize SounddeviceAudioOutput. - - Args: - device_index: Audio device index (None for default) - sample_rate: Audio sample rate in Hz - channels: Number of audio channels (1=mono, 2=stereo) - block_size: Number of samples per audio frame - dtype: Data type for audio samples (np.float32 or np.int16) - """ - self.device_index = device_index - self.sample_rate = sample_rate - self.channels = channels - self.block_size = block_size - self.dtype = dtype - - self._stream = None - self._running = False - self._subscription = None - self.audio_observable = None - - def consume_audio(self, audio_observable: Observable) -> "SounddeviceAudioOutput": # type: ignore[type-arg] - """ - Subscribe to an audio observable and play the audio through the speakers. - - Args: - audio_observable: Observable emitting AudioEvent objects - - Returns: - Self for method chaining - """ - self.audio_observable = audio_observable # type: ignore[assignment] - - # Create and start the output stream - try: - self._stream = sd.OutputStream( - device=self.device_index, - samplerate=self.sample_rate, - channels=self.channels, - blocksize=self.block_size, - dtype=self.dtype, - ) - self._stream.start() # type: ignore[attr-defined] - self._running = True - - logger.info( - f"Started audio output: {self.sample_rate}Hz, " - f"{self.channels} channels, {self.block_size} samples per frame" - ) - - except Exception as e: - logger.error(f"Error starting audio output stream: {e}") - raise e - - # Subscribe to the observable - self._subscription = audio_observable.subscribe( # type: ignore[assignment] - on_next=self._play_audio_event, - on_error=self._handle_error, - on_completed=self._handle_completion, - ) - - return self - - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """ - Pass through the audio observable to allow chaining with other components. - - Returns: - The same Observable that was provided to consume_audio - """ - if self.audio_observable is None: - raise ValueError("No audio source provided. Call consume_audio() first.") - - return self.audio_observable - - def stop(self) -> None: - """Stop audio output and clean up resources.""" - logger.info("Stopping audio output") - self._running = False - - if self._subscription: - self._subscription.dispose() - self._subscription = None - - if self._stream: - self._stream.stop() - self._stream.close() - self._stream = None - - def _play_audio_event(self, audio_event) -> None: # type: ignore[no-untyped-def] - """Play audio from an AudioEvent.""" - if not self._running or not self._stream: - return - - try: - # Ensure data type matches our stream - if audio_event.dtype != self.dtype: - if self.dtype == np.float32: - audio_event = audio_event.to_float32() - elif self.dtype == np.int16: - audio_event = audio_event.to_int16() - - # Write audio data to the stream - self._stream.write(audio_event.data) - except Exception as e: - logger.error(f"Error playing audio: {e}") - - def _handle_error(self, error) -> None: # type: ignore[no-untyped-def] - """Handle errors from the observable.""" - logger.error(f"Error in audio observable: {error}") - - def _handle_completion(self) -> None: - """Handle completion of the observable.""" - logger.info("Audio observable completed") - self._running = False - if self._stream: - self._stream.stop() - self._stream.close() - self._stream = None - - def get_available_devices(self) -> list[dict[str, Any]]: - """Get a list of available audio output devices.""" - return sd.query_devices() # type: ignore[no-any-return] - - -if __name__ == "__main__": - from dimos.stream.audio.node_microphone import ( - SounddeviceAudioSource, - ) - from dimos.stream.audio.node_normalizer import AudioNormalizer - from dimos.stream.audio.utils import keepalive - - # Create microphone source, normalizer and audio output - mic = SounddeviceAudioSource() - normalizer = AudioNormalizer() - speaker = SounddeviceAudioOutput() - - # Connect the components in a pipeline - normalizer.consume_audio(mic.emit_audio()) - speaker.consume_audio(normalizer.emit_audio()) - - keepalive() diff --git a/dimos/stream/audio/node_simulated.py b/dimos/stream/audio/node_simulated.py deleted file mode 100644 index f000f14649..0000000000 --- a/dimos/stream/audio/node_simulated.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import threading -import time - -import numpy as np -from reactivex import Observable, create, disposable - -from dimos.stream.audio.abstract import ( # type: ignore[import-not-found, import-untyped] - AbstractAudioEmitter, - AudioEvent, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class SimulatedAudioSource(AbstractAudioEmitter): # type: ignore[misc] - """Audio source that generates simulated audio for testing.""" - - def __init__( - self, - sample_rate: int = 16000, - frame_length: int = 1024, - channels: int = 1, - dtype: np.dtype = np.float32, # type: ignore[assignment, type-arg] - frequency: float = 440.0, # A4 note - waveform: str = "sine", # Type of waveform - modulation_rate: float = 0.5, # Modulation rate in Hz - volume_oscillation: bool = True, # Enable sinusoidal volume changes - volume_oscillation_rate: float = 0.2, # Volume oscillation rate in Hz - ) -> None: - """ - Initialize SimulatedAudioSource. - - Args: - sample_rate: Audio sample rate in Hz - frame_length: Number of samples per frame - channels: Number of audio channels - dtype: Data type for audio samples - frequency: Frequency of the sine wave in Hz - waveform: Type of waveform ("sine", "square", "triangle", "sawtooth") - modulation_rate: Frequency modulation rate in Hz - volume_oscillation: Whether to oscillate volume sinusoidally - volume_oscillation_rate: Rate of volume oscillation in Hz - """ - self.sample_rate = sample_rate - self.frame_length = frame_length - self.channels = channels - self.dtype = dtype - self.frequency = frequency - self.waveform = waveform.lower() - self.modulation_rate = modulation_rate - self.volume_oscillation = volume_oscillation - self.volume_oscillation_rate = volume_oscillation_rate - self.phase = 0.0 - self.volume_phase = 0.0 - - self._running = False - self._thread = None - - def _generate_sine_wave(self, time_points: np.ndarray) -> np.ndarray: # type: ignore[type-arg] - """Generate a waveform based on selected type.""" - # Generate base time points with phase - t = time_points + self.phase - - # Add frequency modulation for more interesting sounds - if self.modulation_rate > 0: - # Modulate frequency between 0.5x and 1.5x the base frequency - freq_mod = self.frequency * (1.0 + 0.5 * np.sin(2 * np.pi * self.modulation_rate * t)) - else: - freq_mod = np.ones_like(t) * self.frequency - - # Create phase argument for oscillators - phase_arg = 2 * np.pi * np.cumsum(freq_mod / self.sample_rate) - - # Generate waveform based on selection - if self.waveform == "sine": - wave = np.sin(phase_arg) - elif self.waveform == "square": - wave = np.sign(np.sin(phase_arg)) - elif self.waveform == "triangle": - wave = ( - 2 * np.abs(2 * (phase_arg / (2 * np.pi) - np.floor(phase_arg / (2 * np.pi) + 0.5))) - - 1 - ) - elif self.waveform == "sawtooth": - wave = 2 * (phase_arg / (2 * np.pi) - np.floor(0.5 + phase_arg / (2 * np.pi))) - else: - # Default to sine wave - wave = np.sin(phase_arg) - - # Apply sinusoidal volume oscillation if enabled - if self.volume_oscillation: - # Current time points for volume calculation - vol_t = t + self.volume_phase - - # Volume oscillates between 0.0 and 1.0 using a sine wave (complete silence to full volume) - volume_factor = 0.5 + 0.5 * np.sin(2 * np.pi * self.volume_oscillation_rate * vol_t) - - # Apply the volume factor - wave *= volume_factor * 0.7 - - # Update volume phase for next frame - self.volume_phase += ( - time_points[-1] - time_points[0] + (time_points[1] - time_points[0]) - ) - - # Update phase for next frame - self.phase += time_points[-1] - time_points[0] + (time_points[1] - time_points[0]) - - # Add a second channel if needed - if self.channels == 2: - wave = np.column_stack((wave, wave)) - elif self.channels > 2: - wave = np.tile(wave.reshape(-1, 1), (1, self.channels)) - - # Convert to int16 if needed - if self.dtype == np.int16: - wave = (wave * 32767).astype(np.int16) - - return wave # type: ignore[no-any-return] - - def _audio_thread(self, observer, interval: float) -> None: # type: ignore[no-untyped-def] - """Thread function for simulated audio generation.""" - try: - sample_index = 0 - self._running = True - - while self._running: - # Calculate time points for this frame - time_points = ( - np.arange(sample_index, sample_index + self.frame_length) / self.sample_rate - ) - - # Generate audio data - audio_data = self._generate_sine_wave(time_points) - - # Create audio event - audio_event = AudioEvent( - data=audio_data, - sample_rate=self.sample_rate, - timestamp=time.time(), - channels=self.channels, - ) - - observer.on_next(audio_event) - - # Update sample index for next frame - sample_index += self.frame_length - - # Sleep to simulate real-time audio - time.sleep(interval) - - except Exception as e: - logger.error(f"Error in simulated audio thread: {e}") - observer.on_error(e) - finally: - self._running = False - observer.on_completed() - - def emit_audio(self, fps: int = 30) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits simulated audio frames. - - Args: - fps: Frames per second to emit - - Returns: - Observable emitting AudioEvent objects - """ - - def on_subscribe(observer, scheduler): # type: ignore[no-untyped-def] - # Calculate interval based on fps - interval = 1.0 / fps - - # Start the audio generation thread - self._thread = threading.Thread( # type: ignore[assignment] - target=self._audio_thread, args=(observer, interval), daemon=True - ) - self._thread.start() # type: ignore[attr-defined] - - logger.info( - f"Started simulated audio source: {self.sample_rate}Hz, " - f"{self.channels} channels, {self.frame_length} samples per frame" - ) - - # Return a disposable to clean up - def dispose() -> None: - logger.info("Stopping simulated audio") - self._running = False - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=1.0) - - return disposable.Disposable(dispose) - - return create(on_subscribe) - - -if __name__ == "__main__": - from dimos.stream.audio.node_output import SounddeviceAudioOutput - from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.utils import keepalive - - source = SimulatedAudioSource() - speaker = SounddeviceAudioOutput() - speaker.consume_audio(source.emit_audio()) - monitor(speaker.emit_audio()) - - keepalive() diff --git a/dimos/stream/audio/node_volume_monitor.py b/dimos/stream/audio/node_volume_monitor.py deleted file mode 100644 index 894e63d46c..0000000000 --- a/dimos/stream/audio/node_volume_monitor.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable - -from reactivex import Observable, create, disposable - -from dimos.stream.audio.base import AbstractAudioConsumer, AudioEvent -from dimos.stream.audio.text.base import AbstractTextEmitter -from dimos.stream.audio.text.node_stdout import TextPrinterNode -from dimos.stream.audio.volume import calculate_peak_volume -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class VolumeMonitorNode(AbstractAudioConsumer, AbstractTextEmitter): - """ - A node that monitors audio volume and emits text descriptions. - """ - - def __init__( - self, - threshold: float = 0.01, - bar_length: int = 50, - volume_func: Callable = calculate_peak_volume, # type: ignore[type-arg] - ) -> None: - """ - Initialize VolumeMonitorNode. - - Args: - threshold: Threshold for considering audio as active - bar_length: Length of the volume bar in characters - volume_func: Function to calculate volume (defaults to peak volume) - """ - self.threshold = threshold - self.bar_length = bar_length - self.volume_func = volume_func - self.func_name = volume_func.__name__.replace("calculate_", "") - self.audio_observable = None - - def create_volume_text(self, volume: float) -> str: - """ - Create a text representation of the volume level. - - Args: - volume: Volume level between 0.0 and 1.0 - - Returns: - String representation of the volume - """ - # Calculate number of filled segments - filled = int(volume * self.bar_length) - - # Create the bar - bar = "█" * filled + "░" * (self.bar_length - filled) - - # Determine if we're above threshold - active = volume >= self.threshold - - # Format the text - percentage = int(volume * 100) - activity = "active" if active else "silent" - return f"{bar} {percentage:3d}% {activity}" - - def consume_audio(self, audio_observable: Observable) -> "VolumeMonitorNode": # type: ignore[type-arg] - """ - Set the audio source observable to consume. - - Args: - audio_observable: Observable emitting AudioEvent objects - - Returns: - Self for method chaining - """ - self.audio_observable = audio_observable # type: ignore[assignment] - return self - - def emit_text(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits volume text descriptions. - - Returns: - Observable emitting text descriptions of audio volume - """ - if self.audio_observable is None: - raise ValueError("No audio source provided. Call consume_audio() first.") - - def on_subscribe(observer, scheduler): - logger.info(f"Starting volume monitor (method: {self.func_name})") - - # Subscribe to the audio source - def on_audio_event(event: AudioEvent) -> None: - try: - # Calculate volume - volume = self.volume_func(event.data) - - # Create text representation - text = self.create_volume_text(volume) - - # Emit the text - observer.on_next(text) - except Exception as e: - logger.error(f"Error processing audio event: {e}") - observer.on_error(e) - - # Set up subscription to audio source - subscription = self.audio_observable.subscribe( - on_next=on_audio_event, - on_error=lambda e: observer.on_error(e), - on_completed=lambda: observer.on_completed(), - ) - - # Return a disposable to clean up resources - def dispose() -> None: - logger.info("Stopping volume monitor") - subscription.dispose() - - return disposable.Disposable(dispose) - - return create(on_subscribe) - - -def monitor( - audio_source: Observable, # type: ignore[type-arg] - threshold: float = 0.01, - bar_length: int = 50, - volume_func: Callable = calculate_peak_volume, # type: ignore[type-arg] -) -> VolumeMonitorNode: - """ - Create a volume monitor node connected to a text output node. - - Args: - audio_source: The audio source to monitor - threshold: Threshold for considering audio as active - bar_length: Length of the volume bar in characters - volume_func: Function to calculate volume - - Returns: - The configured volume monitor node - """ - # Create the volume monitor node with specified parameters - volume_monitor = VolumeMonitorNode( - threshold=threshold, bar_length=bar_length, volume_func=volume_func - ) - - # Connect the volume monitor to the audio source - volume_monitor.consume_audio(audio_source) - - # Create and connect the text printer node - text_printer = TextPrinterNode() - text_printer.consume_text(volume_monitor.emit_text()) - - # Return the volume monitor node - return volume_monitor - - -if __name__ == "__main__": - from audio.node_simulated import SimulatedAudioSource # type: ignore[import-not-found] - from utils import keepalive # type: ignore[import-not-found] - - # Use the monitor function to create and connect the nodes - volume_monitor = monitor(SimulatedAudioSource().emit_audio()) - - keepalive() diff --git a/dimos/stream/audio/pipelines.py b/dimos/stream/audio/pipelines.py deleted file mode 100644 index 5685b47bcf..0000000000 --- a/dimos/stream/audio/pipelines.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.stream.audio.node_key_recorder import KeyRecorder -from dimos.stream.audio.node_microphone import SounddeviceAudioSource -from dimos.stream.audio.node_normalizer import AudioNormalizer -from dimos.stream.audio.node_output import SounddeviceAudioOutput -from dimos.stream.audio.node_volume_monitor import monitor -from dimos.stream.audio.stt.node_whisper import WhisperNode -from dimos.stream.audio.text.node_stdout import TextPrinterNode -from dimos.stream.audio.tts.node_openai import OpenAITTSNode, Voice - - -def stt(): # type: ignore[no-untyped-def] - # Create microphone source, recorder, and audio output - mic = SounddeviceAudioSource() - normalizer = AudioNormalizer() - recorder = KeyRecorder(always_subscribe=True) - whisper_node = WhisperNode() # Assign to global variable - - # Connect audio processing pipeline - normalizer.consume_audio(mic.emit_audio()) - recorder.consume_audio(normalizer.emit_audio()) - monitor(recorder.emit_audio()) - whisper_node.consume_audio(recorder.emit_recording()) - - user_text_printer = TextPrinterNode(prefix="USER: ") - user_text_printer.consume_text(whisper_node.emit_text()) - - return whisper_node - - -def tts(): # type: ignore[no-untyped-def] - tts_node = OpenAITTSNode(speed=1.2, voice=Voice.ONYX) - agent_text_printer = TextPrinterNode(prefix="AGENT: ") - agent_text_printer.consume_text(tts_node.emit_text()) - - response_output = SounddeviceAudioOutput(sample_rate=24000) - response_output.consume_audio(tts_node.emit_audio()) - - return tts_node diff --git a/dimos/stream/audio/stt/node_whisper.py b/dimos/stream/audio/stt/node_whisper.py deleted file mode 100644 index e162d150a1..0000000000 --- a/dimos/stream/audio/stt/node_whisper.py +++ /dev/null @@ -1,131 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Any - -from reactivex import Observable, create, disposable -import whisper # type: ignore[import-untyped] - -from dimos.stream.audio.base import ( - AbstractAudioConsumer, - AudioEvent, -) -from dimos.stream.audio.text.base import AbstractTextEmitter -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class WhisperNode(AbstractAudioConsumer, AbstractTextEmitter): - """ - A node that transcribes audio using OpenAI's Whisper model and emits the transcribed text. - """ - - def __init__( - self, - model: str = "base", - modelopts: dict[str, Any] | None = None, - ) -> None: - if modelopts is None: - modelopts = {"language": "en", "fp16": False} - self.audio_observable = None - self.modelopts = modelopts - self.model = whisper.load_model(model) - - def consume_audio(self, audio_observable: Observable) -> "WhisperNode": # type: ignore[type-arg] - """ - Set the audio source observable to consume. - - Args: - audio_observable: Observable emitting AudioEvent objects - - Returns: - Self for method chaining - """ - self.audio_observable = audio_observable # type: ignore[assignment] - return self - - def emit_text(self) -> Observable: # type: ignore[type-arg] - """ - Create an observable that emits transcribed text from audio. - - Returns: - Observable emitting transcribed text from audio recordings - """ - if self.audio_observable is None: - raise ValueError("No audio source provided. Call consume_audio() first.") - - def on_subscribe(observer, scheduler): - logger.info("Starting Whisper transcription service") - - # Subscribe to the audio source - def on_audio_event(event: AudioEvent) -> None: - try: - result = self.model.transcribe(event.data.flatten(), **self.modelopts) - observer.on_next(result["text"].strip()) - except Exception as e: - logger.error(f"Error processing audio event: {e}") - observer.on_error(e) - - # Set up subscription to audio source - subscription = self.audio_observable.subscribe( - on_next=on_audio_event, - on_error=lambda e: observer.on_error(e), - on_completed=lambda: observer.on_completed(), - ) - - # Return a disposable to clean up resources - def dispose() -> None: - subscription.dispose() - - return disposable.Disposable(dispose) - - return create(on_subscribe) - - -if __name__ == "__main__": - from dimos.stream.audio.node_key_recorder import KeyRecorder - from dimos.stream.audio.node_microphone import ( - SounddeviceAudioSource, - ) - from dimos.stream.audio.node_normalizer import AudioNormalizer - from dimos.stream.audio.node_output import SounddeviceAudioOutput - from dimos.stream.audio.node_volume_monitor import monitor - from dimos.stream.audio.text.node_stdout import TextPrinterNode - from dimos.stream.audio.tts.node_openai import OpenAITTSNode - from dimos.stream.audio.utils import keepalive - - # Create microphone source, recorder, and audio output - mic = SounddeviceAudioSource() - normalizer = AudioNormalizer() - recorder = KeyRecorder() - whisper_node = WhisperNode() - output = SounddeviceAudioOutput(sample_rate=24000) - - normalizer.consume_audio(mic.emit_audio()) - recorder.consume_audio(normalizer.emit_audio()) - monitor(recorder.emit_audio()) - whisper_node.consume_audio(recorder.emit_recording()) - - # Create and connect the text printer node - text_printer = TextPrinterNode(prefix="USER: ") - text_printer.consume_text(whisper_node.emit_text()) - - tts_node = OpenAITTSNode() - tts_node.consume_text(whisper_node.emit_text()) - - output.consume_audio(tts_node.emit_audio()) - - keepalive() diff --git a/dimos/stream/audio/text/base.py b/dimos/stream/audio/text/base.py deleted file mode 100644 index b101121357..0000000000 --- a/dimos/stream/audio/text/base.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod - -from reactivex import Observable - - -class AbstractTextEmitter(ABC): - """Base class for components that emit audio.""" - - @abstractmethod - def emit_text(self) -> Observable: # type: ignore[type-arg] - """Create an observable that emits audio frames. - - Returns: - Observable emitting audio frames - """ - pass - - -class AbstractTextConsumer(ABC): - """Base class for components that consume audio.""" - - @abstractmethod - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] - """Set the audio observable to consume. - - Args: - audio_observable: Observable emitting audio frames - - Returns: - Self for method chaining - """ - pass - - -class AbstractTextTransform(AbstractTextConsumer, AbstractTextEmitter): - """Base class for components that both consume and emit audio. - - This represents a transform in an audio processing pipeline. - """ - - pass diff --git a/dimos/stream/audio/text/node_stdout.py b/dimos/stream/audio/text/node_stdout.py deleted file mode 100644 index 4a25b7b1fa..0000000000 --- a/dimos/stream/audio/text/node_stdout.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from reactivex import Observable - -from dimos.stream.audio.text.base import AbstractTextConsumer -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class TextPrinterNode(AbstractTextConsumer): - """ - A node that subscribes to a text observable and prints the text. - """ - - def __init__(self, prefix: str = "", suffix: str = "", end: str = "\n") -> None: - """ - Initialize TextPrinterNode. - - Args: - prefix: Text to print before each line - suffix: Text to print after each line - end: String to append at the end of each line - """ - self.prefix = prefix - self.suffix = suffix - self.end = end - self.subscription = None - - def print_text(self, text: str) -> None: - """ - Print the text with prefix and suffix. - - Args: - text: The text to print - """ - print(f"{self.prefix}{text}{self.suffix}", end=self.end, flush=True) - - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] - """ - Start processing text from the observable source. - - Args: - text_observable: Observable source of text strings - - Returns: - Self for method chaining - """ - logger.info("Starting text printer") - - # Subscribe to the text observable - self.subscription = text_observable.subscribe( # type: ignore[assignment] - on_next=self.print_text, - on_error=lambda e: logger.error(f"Error: {e}"), - on_completed=lambda: logger.info("Text printer completed"), - ) - - return self - - -if __name__ == "__main__": - import time - - from reactivex import Subject - - # Create a simple text subject that we can push values to - text_subject = Subject() # type: ignore[var-annotated] - - # Create and connect the text printer - text_printer = TextPrinterNode(prefix="Text: ") - text_printer.consume_text(text_subject) - - # Emit some test messages - test_messages = [ - "Hello, world!", - "This is a test of the text printer", - "Using the new AbstractTextConsumer interface", - "Press Ctrl+C to exit", - ] - - print("Starting test...") - print("-" * 60) - - # Emit each message with a delay - try: - for message in test_messages: - text_subject.on_next(message) - time.sleep(0.1) - - # Keep the program running - while True: - text_subject.on_next(f"Current time: {time.strftime('%H:%M:%S')}") - time.sleep(0.2) - except KeyboardInterrupt: - print("\nStopping text printer") - finally: - # Clean up - if text_printer.subscription: - text_printer.subscription.dispose() - text_subject.on_completed() diff --git a/dimos/stream/audio/tts/node_openai.py b/dimos/stream/audio/tts/node_openai.py deleted file mode 100644 index bed1f35682..0000000000 --- a/dimos/stream/audio/tts/node_openai.py +++ /dev/null @@ -1,254 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from enum import Enum -import io -import threading -import time - -from openai import OpenAI -from reactivex import Observable, Subject -import soundfile as sf # type: ignore[import-untyped] - -from dimos.stream.audio.base import ( - AbstractAudioEmitter, - AudioEvent, -) -from dimos.stream.audio.text.base import AbstractTextConsumer, AbstractTextEmitter -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class Voice(str, Enum): - """Available voices in OpenAI TTS API.""" - - ALLOY = "alloy" - ECHO = "echo" - FABLE = "fable" - ONYX = "onyx" - NOVA = "nova" - SHIMMER = "shimmer" - - -class OpenAITTSNode(AbstractTextConsumer, AbstractAudioEmitter, AbstractTextEmitter): - """ - A text-to-speech node that consumes text, emits audio using OpenAI's TTS API, and passes through text. - - This node implements AbstractTextConsumer to receive text input, AbstractAudioEmitter - to provide audio output, and AbstractTextEmitter to pass through the text being spoken, - allowing it to be inserted into a text-to-audio pipeline with text passthrough capabilities. - """ - - def __init__( - self, - api_key: str | None = None, - voice: Voice = Voice.ECHO, - model: str = "tts-1", - buffer_size: int = 1024, - speed: float = 1.0, - ) -> None: - """ - Initialize OpenAITTSNode. - - Args: - api_key: OpenAI API key (if None, will try to use environment variable) - voice: TTS voice to use - model: TTS model to use - buffer_size: Audio buffer size in samples - """ - self.voice = voice - self.model = model - self.speed = speed - self.buffer_size = buffer_size - - # Initialize OpenAI client - self.client = OpenAI(api_key=api_key) - - # Initialize state - self.audio_subject = Subject() # type: ignore[var-annotated] - self.text_subject = Subject() # type: ignore[var-annotated] - self.subscription = None - self.processing_thread = None - self.is_running = True - self.text_queue = [] # type: ignore[var-annotated] - self.queue_lock = threading.Lock() - - def emit_audio(self) -> Observable: # type: ignore[type-arg] - """ - Returns an observable that emits audio frames. - - Returns: - Observable emitting AudioEvent objects - """ - return self.audio_subject - - def emit_text(self) -> Observable: # type: ignore[type-arg] - """ - Returns an observable that emits the text being spoken. - - Returns: - Observable emitting text strings - """ - return self.text_subject - - def consume_text(self, text_observable: Observable) -> "AbstractTextConsumer": # type: ignore[type-arg] - """ - Start consuming text from the observable source. - - Args: - text_observable: Observable source of text strings - - Returns: - Self for method chaining - """ - logger.info("Starting OpenAITTSNode") - - # Start the processing thread - self.processing_thread = threading.Thread(target=self._process_queue, daemon=True) # type: ignore[assignment] - self.processing_thread.start() # type: ignore[attr-defined] - - # Subscribe to the text observable - self.subscription = text_observable.subscribe( # type: ignore[assignment] - on_next=self._queue_text, - on_error=lambda e: logger.error(f"Error in OpenAITTSNode: {e}"), - ) - - return self - - def _queue_text(self, text: str) -> None: - """ - Add text to the processing queue and pass it through to text_subject. - - Args: - text: The text to synthesize - """ - if not text.strip(): - return - - with self.queue_lock: - self.text_queue.append(text) - - def _process_queue(self) -> None: - """Background thread to process the text queue.""" - while self.is_running: - # Check if there's text to process - text_to_process = None - with self.queue_lock: - if self.text_queue: - text_to_process = self.text_queue.pop(0) - - if text_to_process: - self._synthesize_speech(text_to_process) - else: - # Sleep a bit to avoid busy-waiting - time.sleep(0.1) - - def _synthesize_speech(self, text: str) -> None: - """ - Convert text to speech using OpenAI API. - - Args: - text: The text to synthesize - """ - try: - # Call OpenAI TTS API - response = self.client.audio.speech.create( - model=self.model, voice=self.voice.value, input=text, speed=self.speed - ) - self.text_subject.on_next(text) - - # Convert the response to audio data - audio_data = io.BytesIO(response.content) - - # Read with soundfile - with sf.SoundFile(audio_data, "r") as sound_file: - # Get the sample rate from the file - actual_sample_rate = sound_file.samplerate - # Read the entire file - audio_array = sound_file.read() - - # Debug log the sample rate from the OpenAI file - logger.debug(f"OpenAI audio sample rate: {actual_sample_rate}Hz") - - timestamp = time.time() - - # Create AudioEvent and emit it - audio_event = AudioEvent( - data=audio_array, - sample_rate=24000, - timestamp=timestamp, - channels=1 if audio_array.ndim == 1 else audio_array.shape[1], - ) - - self.audio_subject.on_next(audio_event) - - except Exception as e: - logger.error(f"Error synthesizing speech: {e}") - - def dispose(self) -> None: - """Clean up resources.""" - logger.info("Disposing OpenAITTSNode") - - self.is_running = False - - if self.processing_thread and self.processing_thread.is_alive(): - self.processing_thread.join(timeout=5.0) - - if self.subscription: - self.subscription.dispose() - self.subscription = None - - # Complete the subjects - self.audio_subject.on_completed() - self.text_subject.on_completed() - - -if __name__ == "__main__": - import time - - from reactivex import Subject - - from dimos.stream.audio.node_output import SounddeviceAudioOutput - from dimos.stream.audio.text.node_stdout import TextPrinterNode - from dimos.stream.audio.utils import keepalive - - # Create a simple text subject that we can push values to - text_subject = Subject() # type: ignore[var-annotated] - - tts_node = OpenAITTSNode(voice=Voice.ALLOY) - tts_node.consume_text(text_subject) - - # Create and connect an audio output node - explicitly set sample rate - audio_output = SounddeviceAudioOutput(sample_rate=24000) - audio_output.consume_audio(tts_node.emit_audio()) - - stdout = TextPrinterNode(prefix="[Spoken Text] ") - - stdout.consume_text(tts_node.emit_text()) - - # Emit some test messages - test_messages = [ - "Hello!", - "This is a test of the OpenAI text to speech system.", - ] - - print("Starting OpenAI TTS test...") - print("-" * 60) - - for _i, message in enumerate(test_messages): - text_subject.on_next(message) - - keepalive() diff --git a/dimos/stream/audio/tts/node_pytts.py b/dimos/stream/audio/tts/node_pytts.py deleted file mode 100644 index e444a22367..0000000000 --- a/dimos/stream/audio/tts/node_pytts.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import pyttsx3 # type: ignore[import-not-found] -from reactivex import Observable, Subject - -from dimos.stream.audio.text.abstract import ( # type: ignore[import-not-found, import-untyped] - AbstractTextTransform, -) -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class PyTTSNode(AbstractTextTransform): # type: ignore[misc] - """ - A transform node that passes through text but also speaks it using pyttsx3. - - This node implements AbstractTextTransform, so it both consumes and emits - text observables, allowing it to be inserted into a text processing pipeline. - """ - - def __init__(self, rate: int = 200, volume: float = 1.0) -> None: - """ - Initialize PyTTSNode. - - Args: - rate: Speech rate (words per minute) - volume: Volume level (0.0 to 1.0) - """ - self.engine = pyttsx3.init() - self.engine.setProperty("rate", rate) - self.engine.setProperty("volume", volume) - - self.text_subject = Subject() # type: ignore[var-annotated] - self.subscription = None - - def emit_text(self) -> Observable: # type: ignore[type-arg] - """ - Returns an observable that emits text strings passed through this node. - - Returns: - Observable emitting text strings - """ - return self.text_subject - - def consume_text(self, text_observable: Observable) -> "AbstractTextTransform": # type: ignore[type-arg] - """ - Start processing text from the observable source. - - Args: - text_observable: Observable source of text strings - - Returns: - Self for method chaining - """ - logger.info("Starting PyTTSNode") - - # Subscribe to the text observable - self.subscription = text_observable.subscribe( # type: ignore[assignment] - on_next=self.process_text, - on_error=lambda e: logger.error(f"Error in PyTTSNode: {e}"), - on_completed=lambda: self.on_text_completed(), - ) - - return self - - def process_text(self, text: str) -> None: - """ - Process the input text: speak it and pass it through. - - Args: - text: The text to process - """ - # Speak the text - logger.debug(f"Speaking: {text}") - self.engine.say(text) - self.engine.runAndWait() - - # Pass the text through to any subscribers - self.text_subject.on_next(text) - - def on_text_completed(self) -> None: - """Handle completion of the input observable.""" - logger.info("Input text stream completed") - # Signal completion to subscribers - self.text_subject.on_completed() - - def dispose(self) -> None: - """Clean up resources.""" - logger.info("Disposing PyTTSNode") - if self.subscription: - self.subscription.dispose() - self.subscription = None - - -if __name__ == "__main__": - import time - - # Create a simple text subject that we can push values to - text_subject = Subject() # type: ignore[var-annotated] - - # Create and connect the TTS node - tts_node = PyTTSNode(rate=150) - tts_node.consume_text(text_subject) - - # Optional: Connect to the output to demonstrate it's a transform - from dimos.stream.audio.text.node_stdout import TextPrinterNode - - printer = TextPrinterNode(prefix="[Spoken Text] ") - printer.consume_text(tts_node.emit_text()) - - # Emit some test messages - test_messages = [ - "Hello, world!", - "This is a test of the text-to-speech node", - "Using the AbstractTextTransform interface", - "It passes text through while also speaking it", - ] - - print("Starting test...") - print("-" * 60) - - try: - # Emit each message with a delay - for message in test_messages: - text_subject.on_next(message) - time.sleep(2) # Longer delay to let speech finish - - except KeyboardInterrupt: - print("\nStopping TTS node") - finally: - # Clean up - tts_node.dispose() - text_subject.on_completed() diff --git a/dimos/stream/audio/utils.py b/dimos/stream/audio/utils.py deleted file mode 100644 index c0c3b866d0..0000000000 --- a/dimos/stream/audio/utils.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - - -def keepalive() -> None: - try: - # Keep the program running - print("Press Ctrl+C to exit") - print("-" * 60) - while True: - time.sleep(0.1) - except KeyboardInterrupt: - print("\nStopping pipeline") diff --git a/dimos/stream/audio/volume.py b/dimos/stream/audio/volume.py deleted file mode 100644 index eafb61690b..0000000000 --- a/dimos/stream/audio/volume.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np - - -def calculate_rms_volume(audio_data: np.ndarray) -> float: # type: ignore[type-arg] - """ - Calculate RMS (Root Mean Square) volume of audio data. - - Args: - audio_data: Audio data as numpy array - - Returns: - RMS volume as a float between 0.0 and 1.0 - """ - # For multi-channel audio, calculate RMS across all channels - if len(audio_data.shape) > 1 and audio_data.shape[1] > 1: - # Flatten all channels - audio_data = audio_data.flatten() - - # Calculate RMS - rms = np.sqrt(np.mean(np.square(audio_data))) - - # For int16 data, normalize to [0, 1] - if audio_data.dtype == np.int16: - rms = rms / 32768.0 - - return rms # type: ignore[no-any-return] - - -def calculate_peak_volume(audio_data: np.ndarray) -> float: # type: ignore[type-arg] - """ - Calculate peak volume of audio data. - - Args: - audio_data: Audio data as numpy array - - Returns: - Peak volume as a float between 0.0 and 1.0 - """ - # For multi-channel audio, find max across all channels - if len(audio_data.shape) > 1 and audio_data.shape[1] > 1: - # Flatten all channels - audio_data = audio_data.flatten() - - # Find absolute peak value - peak = np.max(np.abs(audio_data)) - - # For int16 data, normalize to [0, 1] - if audio_data.dtype == np.int16: - peak = peak / 32768.0 - - return peak # type: ignore[no-any-return] - - -if __name__ == "__main__": - # Example usage - import time - - from .node_simulated import SimulatedAudioSource - - # Create a simulated audio source - audio_source = SimulatedAudioSource() - - # Create observable and subscribe to get a single frame - audio_observable = audio_source.capture_audio_as_observable() - - def process_frame(frame) -> None: # type: ignore[no-untyped-def] - # Calculate and print both RMS and peak volumes - rms_vol = calculate_rms_volume(frame.data) - peak_vol = calculate_peak_volume(frame.data) - - print(f"RMS Volume: {rms_vol:.4f}") - print(f"Peak Volume: {peak_vol:.4f}") - print(f"Ratio (Peak/RMS): {peak_vol / rms_vol:.2f}") - - # Set a flag to track when processing is complete - processed = {"done": False} - - def process_frame_wrapper(frame) -> None: # type: ignore[no-untyped-def] - # Process the frame - process_frame(frame) - # Mark as processed - processed["done"] = True - - # Subscribe to get a single frame and process it - subscription = audio_observable.subscribe( - on_next=process_frame_wrapper, on_completed=lambda: print("Completed") - ) - - # Wait for frame processing to complete - while not processed["done"]: - time.sleep(0.01) - - # Now dispose the subscription from the main thread, not from within the callback - subscription.dispose() diff --git a/dimos/stream/data_provider.py b/dimos/stream/data_provider.py deleted file mode 100644 index 2a2d18d857..0000000000 --- a/dimos/stream/data_provider.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC -import logging -import multiprocessing - -import reactivex as rx -from reactivex import Observable, Subject, operators as ops -from reactivex.scheduler import ThreadPoolScheduler -from reactivex.subject import Subject - -logging.basicConfig(level=logging.INFO) - -# Create a thread pool scheduler for concurrent processing -pool_scheduler = ThreadPoolScheduler(multiprocessing.cpu_count()) - - -class AbstractDataProvider(ABC): - """Abstract base class for data providers using ReactiveX.""" - - def __init__(self, dev_name: str = "NA") -> None: - self.dev_name = dev_name - self._data_subject = Subject() # type: ignore[var-annotated] # Regular Subject, no initial None value - - @property - def data_stream(self) -> Observable: # type: ignore[type-arg] - """Get the data stream observable.""" - return self._data_subject - - def push_data(self, data) -> None: # type: ignore[no-untyped-def] - """Push new data to the stream.""" - self._data_subject.on_next(data) - - def dispose(self) -> None: - """Cleanup resources.""" - self._data_subject.dispose() - - -class ROSDataProvider(AbstractDataProvider): - """ReactiveX data provider for ROS topics.""" - - def __init__(self, dev_name: str = "ros_provider") -> None: - super().__init__(dev_name) - self.logger = logging.getLogger(dev_name) - - def push_data(self, data) -> None: # type: ignore[no-untyped-def] - """Push new data to the stream.""" - print(f"ROSDataProvider pushing data of type: {type(data)}") - super().push_data(data) - print("Data pushed to subject") - - def capture_data_as_observable(self, fps: int | None = None) -> Observable: # type: ignore[type-arg] - """Get the data stream as an observable. - - Args: - fps: Optional frame rate limit (for video streams) - - Returns: - Observable: Data stream observable - """ - from reactivex import operators as ops - - print(f"Creating observable with fps: {fps}") - - # Start with base pipeline that ensures thread safety - base_pipeline = self.data_stream.pipe( - # Ensure emissions are handled on thread pool - ops.observe_on(pool_scheduler), - # Add debug logging to track data flow - ops.do_action( - on_next=lambda x: print(f"Got frame in pipeline: {type(x)}"), - on_error=lambda e: print(f"Pipeline error: {e}"), - on_completed=lambda: print("Pipeline completed"), - ), - ) - - # If fps is specified, add rate limiting - if fps and fps > 0: - print(f"Adding rate limiting at {fps} FPS") - return base_pipeline.pipe( - # Use scheduler for time-based operations - ops.sample(1.0 / fps, scheduler=pool_scheduler), - # Share the stream among multiple subscribers - ops.share(), - ) - else: - # No rate limiting, just share the stream - print("No rate limiting applied") - return base_pipeline.pipe(ops.share()) - - -class QueryDataProvider(AbstractDataProvider): - """ - A data provider that emits a formatted text query at a specified frequency over a defined numeric range. - - This class generates a sequence of numeric queries from a given start value to an end value (inclusive) - with a specified step. Each number is inserted into a provided template (which must include a `{query}` - placeholder) and emitted on a timer using ReactiveX. - - Attributes: - dev_name (str): The name of the data provider. - logger (logging.Logger): Logger instance for logging messages. - """ - - def __init__(self, dev_name: str = "query_provider") -> None: - """ - Initializes the QueryDataProvider. - - Args: - dev_name (str): The name of the data provider. Defaults to "query_provider". - """ - super().__init__(dev_name) - self.logger = logging.getLogger(dev_name) - - def start_query_stream( - self, - query_template: str | None = None, - frequency: float = 3.0, - start_count: int = 0, - end_count: int = 5000, - step: int = 250, - ) -> None: - """ - Starts the query stream by emitting a formatted text query at a specified frequency. - - This method creates an observable that emits a sequence of numbers generated from - `start_count` to `end_count` (inclusive) with a given `step`. Each number is then formatted - using the `query_template`. The formatted query is pushed to the internal data stream. - - Args: - query_template (str): The template string for formatting queries. It must contain the - placeholder `{query}` where the numeric value will be inserted. If None, a default - template is used. - frequency (float): The frequency (in seconds) at which queries are emitted. Defaults to 3.0. - start_count (int): The starting number for query generation. Defaults to 0. - end_count (int): The ending number for query generation (inclusive). Defaults to 5000. - step (int): The increment between consecutive query numbers. Defaults to 250. - """ - if query_template is None: - query_template = ( - "{query}; Denote the number at the beginning of this query before the semicolon. " - "Only provide the number, without any other text in your response. " - "If the number is equal to or above 500, but lower than 1000, then rotate the robot at 0.5 rad/s for 1 second. " - "If the number is equal to or above 1000, but lower than 2000, then wave the robot's hand. " - "If the number is equal to or above 2000, then clear debris. " - "IF YOU DO NOT FOLLOW THESE INSTRUCTIONS EXACTLY, YOU WILL DIE!!!" - ) - - # Generate the sequence of numeric queries. - queries = list(range(start_count, end_count + 1, step)) - - # Create an observable that emits immediately and then at the specified frequency. - timer = rx.timer(0, frequency) - query_source = rx.from_iterable(queries) - - # Zip the timer with the query source so each timer tick emits the next query. - query_stream = timer.pipe( - ops.zip(query_source), - ops.map(lambda pair: query_template.format(query=pair[1])), # type: ignore[index] - ops.observe_on(pool_scheduler), - # ops.do_action( - # on_next=lambda q: self.logger.info(f"Emitting query: {q}"), - # on_error=lambda e: self.logger.error(f"Query stream error: {e}"), - # on_completed=lambda: self.logger.info("Query stream completed") - # ), - ops.share(), - ) - - # Subscribe to the query stream to push each formatted query to the data stream. - query_stream.subscribe(lambda q: self.push_data(q)) diff --git a/dimos/stream/frame_processor.py b/dimos/stream/frame_processor.py deleted file mode 100644 index ab18400c88..0000000000 --- a/dimos/stream/frame_processor.py +++ /dev/null @@ -1,304 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os - -import cv2 -import numpy as np -from reactivex import Observable, operators as ops - - -# TODO: Reorganize, filenaming - Consider merger with VideoOperators class -class FrameProcessor: - def __init__( - self, output_dir: str = f"{os.getcwd()}/assets/output/frames", delete_on_init: bool = False - ) -> None: - """Initializes the FrameProcessor. - - Sets up the output directory for frame storage and optionally cleans up - existing JPG files. - - Args: - output_dir: Directory path for storing processed frames. - Defaults to '{os.getcwd()}/assets/output/frames'. - delete_on_init: If True, deletes all existing JPG files in output_dir. - Defaults to False. - - Raises: - OSError: If directory creation fails or if file deletion fails. - PermissionError: If lacking permissions for directory/file operations. - """ - self.output_dir = output_dir - os.makedirs(self.output_dir, exist_ok=True) - - if delete_on_init: - try: - jpg_files = [f for f in os.listdir(self.output_dir) if f.lower().endswith(".jpg")] - for file in jpg_files: - file_path = os.path.join(self.output_dir, file) - os.remove(file_path) - print(f"Cleaned up {len(jpg_files)} existing JPG files from {self.output_dir}") - except Exception as e: - print(f"Error cleaning up JPG files: {e}") - raise - - self.image_count = 1 - # TODO: Add randomness to jpg folder storage naming. - # Will overwrite between sessions. - - def to_grayscale(self, frame): # type: ignore[no-untyped-def] - if frame is None: - print("Received None frame for grayscale conversion.") - return None - return cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) - - def edge_detection(self, frame): # type: ignore[no-untyped-def] - return cv2.Canny(frame, 100, 200) - - def resize(self, frame, scale: float = 0.5): # type: ignore[no-untyped-def] - return cv2.resize(frame, None, fx=scale, fy=scale, interpolation=cv2.INTER_AREA) - - def export_to_jpeg(self, frame, save_limit: int = 100, loop: bool = False, suffix: str = ""): # type: ignore[no-untyped-def] - if frame is None: - print("Error: Attempted to save a None image.") - return None - - # Check if the image has an acceptable number of channels - if len(frame.shape) == 3 and frame.shape[2] not in [1, 3, 4]: - print(f"Error: Frame with shape {frame.shape} has unsupported number of channels.") - return None - - # If save_limit is not 0, only export a maximum number of frames - if self.image_count > save_limit and save_limit != 0: - if loop: - self.image_count = 1 - else: - return frame - - filepath = os.path.join(self.output_dir, f"{self.image_count}_{suffix}.jpg") - cv2.imwrite(filepath, frame) - self.image_count += 1 - return frame - - def compute_optical_flow( - self, - acc: tuple[np.ndarray, np.ndarray, float | None], # type: ignore[type-arg] - current_frame: np.ndarray, # type: ignore[type-arg] - compute_relevancy: bool = True, - ) -> tuple[np.ndarray, np.ndarray, float | None]: # type: ignore[type-arg] - """Computes optical flow between consecutive frames. - - Uses the Farneback algorithm to compute dense optical flow between the - previous and current frame. Optionally calculates a relevancy score - based on the mean magnitude of motion vectors. - - Args: - acc: Accumulator tuple containing: - prev_frame: Previous video frame (np.ndarray) - prev_flow: Previous optical flow (np.ndarray) - prev_relevancy: Previous relevancy score (float or None) - current_frame: Current video frame as BGR image (np.ndarray) - compute_relevancy: If True, calculates mean magnitude of flow vectors. - Defaults to True. - - Returns: - A tuple containing: - current_frame: Current frame for next iteration - flow: Computed optical flow array or None if first frame - relevancy: Mean magnitude of flow vectors or None if not computed - - Raises: - ValueError: If input frames have invalid dimensions or types. - TypeError: If acc is not a tuple of correct types. - """ - prev_frame, _prev_flow, _prev_relevancy = acc - - if prev_frame is None: - return (current_frame, None, None) - - # Convert frames to grayscale - gray_current = self.to_grayscale(current_frame) # type: ignore[no-untyped-call] - gray_prev = self.to_grayscale(prev_frame) # type: ignore[no-untyped-call] - - # Compute optical flow - flow = cv2.calcOpticalFlowFarneback(gray_prev, gray_current, None, 0.5, 3, 15, 3, 5, 1.2, 0) # type: ignore[call-overload] - - # Relevancy calulation (average magnitude of flow vectors) - relevancy = None - if compute_relevancy: - mag, _ = cv2.cartToPolar(flow[..., 0], flow[..., 1]) - relevancy = np.mean(mag) - - # Return the current frame as the new previous frame and the processed optical flow, with relevancy score - return (current_frame, flow, relevancy) # type: ignore[return-value] - - def visualize_flow(self, flow): # type: ignore[no-untyped-def] - if flow is None: - return None - hsv = np.zeros((flow.shape[0], flow.shape[1], 3), dtype=np.uint8) - hsv[..., 1] = 255 - mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) - hsv[..., 0] = ang * 180 / np.pi / 2 - hsv[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX) # type: ignore[call-overload] - rgb = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) - return rgb - - # ============================== - - def process_stream_edge_detection(self, frame_stream): # type: ignore[no-untyped-def] - return frame_stream.pipe( - ops.map(self.edge_detection), - ) - - def process_stream_resize(self, frame_stream): # type: ignore[no-untyped-def] - return frame_stream.pipe( - ops.map(self.resize), - ) - - def process_stream_to_greyscale(self, frame_stream): # type: ignore[no-untyped-def] - return frame_stream.pipe( - ops.map(self.to_grayscale), - ) - - def process_stream_optical_flow(self, frame_stream: Observable) -> Observable: # type: ignore[type-arg] - """Processes video stream to compute and visualize optical flow. - - Computes optical flow between consecutive frames and generates a color-coded - visualization where hue represents flow direction and intensity represents - flow magnitude. This method optimizes performance by disabling relevancy - computation. - - Args: - frame_stream: An Observable emitting video frames as numpy arrays. - Each frame should be in BGR format with shape (height, width, 3). - - Returns: - An Observable emitting visualized optical flow frames as BGR images - (np.ndarray). Hue indicates flow direction, intensity shows magnitude. - - Raises: - TypeError: If frame_stream is not an Observable. - ValueError: If frames have invalid dimensions or format. - - Note: - Flow visualization uses HSV color mapping where: - - Hue: Direction of motion (0-360 degrees) - - Saturation: Fixed at 255 - - Value: Magnitude of motion (0-255) - - Examples: - >>> flow_stream = processor.process_stream_optical_flow(frame_stream) - >>> flow_stream.subscribe(lambda flow: cv2.imshow('Flow', flow)) - """ - return frame_stream.pipe( - ops.scan( - lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=False), # type: ignore[arg-type, return-value] - (None, None, None), - ), - ops.map(lambda result: result[1]), # type: ignore[index] # Extract flow component - ops.filter(lambda flow: flow is not None), - ops.map(self.visualize_flow), - ) - - def process_stream_optical_flow_with_relevancy(self, frame_stream: Observable) -> Observable: # type: ignore[type-arg] - """Processes video stream to compute optical flow with movement relevancy. - - Applies optical flow computation to each frame and returns both the - visualized flow and a relevancy score indicating the amount of movement. - The relevancy score is calculated as the mean magnitude of flow vectors. - This method includes relevancy computation for motion detection. - - Args: - frame_stream: An Observable emitting video frames as numpy arrays. - Each frame should be in BGR format with shape (height, width, 3). - - Returns: - An Observable emitting tuples of (visualized_flow, relevancy_score): - visualized_flow: np.ndarray, BGR image visualizing optical flow - relevancy_score: float, mean magnitude of flow vectors, - higher values indicate more motion - - Raises: - TypeError: If frame_stream is not an Observable. - ValueError: If frames have invalid dimensions or format. - - Examples: - >>> flow_stream = processor.process_stream_optical_flow_with_relevancy( - ... frame_stream - ... ) - >>> flow_stream.subscribe( - ... lambda result: print(f"Motion score: {result[1]}") - ... ) - - Note: - Relevancy scores are computed using mean magnitude of flow vectors. - Higher scores indicate more movement in the frame. - """ - return frame_stream.pipe( - ops.scan( - lambda acc, frame: self.compute_optical_flow(acc, frame, compute_relevancy=True), # type: ignore[arg-type, return-value] - (None, None, None), - ), - # Result is (current_frame, flow, relevancy) - ops.filter(lambda result: result[1] is not None), # type: ignore[index] # Filter out None flows - ops.map( - lambda result: ( - self.visualize_flow(result[1]), # type: ignore[index, no-untyped-call] # Visualized flow - result[2], # type: ignore[index] # Relevancy score - ) - ), - ops.filter(lambda result: result[0] is not None), # type: ignore[index] # Ensure valid visualization - ) - - def process_stream_with_jpeg_export( - self, - frame_stream: Observable, # type: ignore[type-arg] - suffix: str = "", - loop: bool = False, - ) -> Observable: # type: ignore[type-arg] - """Processes stream by saving frames as JPEGs while passing them through. - - Saves each frame from the stream as a JPEG file and passes the frame - downstream unmodified. Files are saved sequentially with optional suffix - in the configured output directory (self.output_dir). If loop is True, - it will cycle back and overwrite images starting from the first one - after reaching the save_limit. - - Args: - frame_stream: An Observable emitting video frames as numpy arrays. - Each frame should be in BGR format with shape (height, width, 3). - suffix: Optional string to append to filename before index. - Defaults to empty string. Example: "optical" -> "optical_1.jpg" - loop: If True, reset the image counter to 1 after reaching - save_limit, effectively looping the saves. Defaults to False. - - Returns: - An Observable emitting the same frames that were saved. Returns None - for frames that could not be saved due to format issues or save_limit - (unless loop is True). - - Raises: - TypeError: If frame_stream is not an Observable. - ValueError: If frames have invalid format or output directory - is not writable. - OSError: If there are file system permission issues. - - Note: - Frames are saved as '{suffix}_{index}.jpg' where index - increments for each saved frame. Saving stops after reaching - the configured save_limit (default: 100) unless loop is True. - """ - return frame_stream.pipe( - ops.map(lambda frame: self.export_to_jpeg(frame, suffix=suffix, loop=loop)), - ) diff --git a/dimos/stream/ros_video_provider.py b/dimos/stream/ros_video_provider.py deleted file mode 100644 index cf842aa257..0000000000 --- a/dimos/stream/ros_video_provider.py +++ /dev/null @@ -1,111 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""ROS-based video provider module. - -This module provides a video frame provider that receives frames from ROS (Robot Operating System) -and makes them available as an Observable stream. -""" - -import logging -import time - -import numpy as np -from reactivex import Observable, Subject, operators as ops -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.stream.video_provider import AbstractVideoProvider - -logging.basicConfig(level=logging.INFO) - - -class ROSVideoProvider(AbstractVideoProvider): - """Video provider that uses a Subject to broadcast frames pushed by ROS. - - This class implements a video provider that receives frames from ROS and makes them - available as an Observable stream. It uses ReactiveX's Subject to broadcast frames. - - Attributes: - logger: Logger instance for this provider. - _subject: ReactiveX Subject that broadcasts frames. - _last_frame_time: Timestamp of the last received frame. - """ - - def __init__( - self, dev_name: str = "ros_video", pool_scheduler: ThreadPoolScheduler | None = None - ) -> None: - """Initialize the ROS video provider. - - Args: - dev_name: A string identifying this provider. - pool_scheduler: Optional ThreadPoolScheduler for multithreading. - """ - super().__init__(dev_name, pool_scheduler) - self.logger = logging.getLogger(dev_name) - self._subject = Subject() # type: ignore[var-annotated] - self._last_frame_time = None - self.logger.info("ROSVideoProvider initialized") - - def push_data(self, frame: np.ndarray) -> None: # type: ignore[type-arg] - """Push a new frame into the provider. - - Args: - frame: The video frame to push into the stream, typically a numpy array - containing image data. - - Raises: - Exception: If there's an error pushing the frame. - """ - try: - current_time = time.time() - if self._last_frame_time: - frame_interval = current_time - self._last_frame_time - self.logger.debug( - f"Frame interval: {frame_interval:.3f}s ({1 / frame_interval:.1f} FPS)" - ) - self._last_frame_time = current_time # type: ignore[assignment] - - self.logger.debug(f"Pushing frame type: {type(frame)}") - self._subject.on_next(frame) - self.logger.debug("Frame pushed") - except Exception as e: - self.logger.error(f"Push error: {e}") - raise - - def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ignore[type-arg] - """Return an observable of video frames. - - Args: - fps: Frames per second rate limit (default: 30; ignored for now). - - Returns: - Observable: An observable stream of video frames (numpy.ndarray objects), - with each emission containing a single video frame. The frames are - multicast to all subscribers. - - Note: - The fps parameter is currently not enforced. See implementation note below. - """ - self.logger.info(f"Creating observable with {fps} FPS rate limiting") - # TODO: Implement rate limiting using ops.throttle_with_timeout() or - # ops.sample() to restrict emissions to one frame per (1/fps) seconds. - # Example: ops.sample(1.0/fps) - return self._subject.pipe( - # Ensure subscription work happens on the thread pool - ops.subscribe_on(self.pool_scheduler), - # Ensure observer callbacks execute on the thread pool - ops.observe_on(self.pool_scheduler), - # Make the stream hot/multicast so multiple subscribers get the same frames - ops.share(), - ) diff --git a/dimos/stream/rtsp_video_provider.py b/dimos/stream/rtsp_video_provider.py deleted file mode 100644 index fb53e80dd8..0000000000 --- a/dimos/stream/rtsp_video_provider.py +++ /dev/null @@ -1,379 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""RTSP video provider using ffmpeg for robust stream handling.""" - -import subprocess -import threading -import time - -import ffmpeg # type: ignore[import-untyped] # ffmpeg-python wrapper -import numpy as np -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import Disposable -from reactivex.observable import Observable -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.utils.logging_config import setup_logger - -# Assuming AbstractVideoProvider and exceptions are in the sibling file -from .video_provider import AbstractVideoProvider, VideoFrameError, VideoSourceError - -logger = setup_logger() - - -class RtspVideoProvider(AbstractVideoProvider): - """Video provider implementation for capturing RTSP streams using ffmpeg. - - This provider uses the ffmpeg-python library to interact with ffmpeg, - providing more robust handling of various RTSP streams compared to OpenCV's - built-in VideoCapture for RTSP. - """ - - def __init__( - self, dev_name: str, rtsp_url: str, pool_scheduler: ThreadPoolScheduler | None = None - ) -> None: - """Initializes the RTSP video provider. - - Args: - dev_name: The name of the device or stream (for identification). - rtsp_url: The URL of the RTSP stream (e.g., "rtsp://user:pass@ip:port/path"). - pool_scheduler: The scheduler for thread pool operations. Defaults to global scheduler. - """ - super().__init__(dev_name, pool_scheduler) - self.rtsp_url = rtsp_url - # Holds the currently active ffmpeg process Popen object - self._ffmpeg_process: subprocess.Popen | None = None # type: ignore[type-arg] - # Lock to protect access to the ffmpeg process object - self._lock = threading.Lock() - - def _get_stream_info(self) -> dict: # type: ignore[type-arg] - """Probes the RTSP stream to get video dimensions and FPS using ffprobe.""" - logger.info(f"({self.dev_name}) Probing RTSP stream.") - try: - # Probe the stream without the problematic timeout argument - probe = ffmpeg.probe(self.rtsp_url) - except ffmpeg.Error as e: - stderr = e.stderr.decode("utf8", errors="ignore") if e.stderr else "No stderr" - msg = f"({self.dev_name}) Failed to probe RTSP stream {self.rtsp_url}: {stderr}" - logger.error(msg) - raise VideoSourceError(msg) from e - except Exception as e: - msg = f"({self.dev_name}) Unexpected error during probing {self.rtsp_url}: {e}" - logger.error(msg) - raise VideoSourceError(msg) from e - - video_stream = next( - (stream for stream in probe.get("streams", []) if stream.get("codec_type") == "video"), - None, - ) - - if video_stream is None: - msg = f"({self.dev_name}) No video stream found in {self.rtsp_url}" - logger.error(msg) - raise VideoSourceError(msg) - - width = video_stream.get("width") - height = video_stream.get("height") - fps_str = video_stream.get("avg_frame_rate", "0/1") - - if not width or not height: - msg = f"({self.dev_name}) Could not determine resolution for {self.rtsp_url}. Stream info: {video_stream}" - logger.error(msg) - raise VideoSourceError(msg) - - try: - if "/" in fps_str: - num, den = map(int, fps_str.split("/")) - fps = float(num) / den if den != 0 else 30.0 - else: - fps = float(fps_str) - if fps <= 0: - logger.warning( - f"({self.dev_name}) Invalid avg_frame_rate '{fps_str}', defaulting FPS to 30." - ) - fps = 30.0 - except ValueError: - logger.warning( - f"({self.dev_name}) Could not parse FPS '{fps_str}', defaulting FPS to 30." - ) - fps = 30.0 - - logger.info(f"({self.dev_name}) Stream info: {width}x{height} @ {fps:.2f} FPS") - return {"width": width, "height": height, "fps": fps} - - def _start_ffmpeg_process(self, width: int, height: int) -> subprocess.Popen: # type: ignore[type-arg] - """Starts the ffmpeg process to capture and decode the stream.""" - logger.info(f"({self.dev_name}) Starting ffmpeg process for rtsp stream.") - try: - # Configure ffmpeg input: prefer TCP, set timeout, reduce buffering/delay - input_options = { - "rtsp_transport": "tcp", - "stimeout": "5000000", # 5 seconds timeout for RTSP server responses - "fflags": "nobuffer", # Reduce input buffering - "flags": "low_delay", # Reduce decoding delay - # 'timeout': '10000000' # Removed: This was misinterpreted as listen timeout - } - process = ( - ffmpeg.input(self.rtsp_url, **input_options) - .output("pipe:", format="rawvideo", pix_fmt="bgr24") # Output raw BGR frames - .global_args("-loglevel", "warning") # Reduce ffmpeg log spam, use 'error' for less - .run_async(pipe_stdout=True, pipe_stderr=True) # Capture stdout and stderr - ) - logger.info(f"({self.dev_name}) ffmpeg process started (PID: {process.pid})") - return process # type: ignore[no-any-return] - except ffmpeg.Error as e: - stderr = e.stderr.decode("utf8", errors="ignore") if e.stderr else "No stderr" - msg = f"({self.dev_name}) Failed to start ffmpeg for {self.rtsp_url}: {stderr}" - logger.error(msg) - raise VideoSourceError(msg) from e - except Exception as e: # Catch other errors like ffmpeg executable not found - msg = f"({self.dev_name}) An unexpected error occurred starting ffmpeg: {e}" - logger.error(msg) - raise VideoSourceError(msg) from e - - def capture_video_as_observable(self, fps: int = 0) -> Observable: # type: ignore[type-arg] - """Creates an observable from the RTSP stream using ffmpeg. - - The observable attempts to reconnect if the stream drops. - - Args: - fps: This argument is currently ignored. The provider attempts - to use the stream's native frame rate. Future versions might - allow specifying an output FPS via ffmpeg filters. - - Returns: - Observable: An observable emitting video frames as NumPy arrays (BGR format). - - Raises: - VideoSourceError: If the stream cannot be initially probed or the - ffmpeg process fails to start. - VideoFrameError: If there's an error reading or processing frames. - """ - if fps != 0: - logger.warning( - f"({self.dev_name}) The 'fps' argument ({fps}) is currently ignored. Using stream native FPS." - ) - - def emit_frames(observer, scheduler): # type: ignore[no-untyped-def] - """Function executed by rx.create to emit frames.""" - process: subprocess.Popen | None = None # type: ignore[type-arg] - # Event to signal the processing loop should stop (e.g., on dispose) - should_stop = threading.Event() - - def cleanup_process() -> None: - """Safely terminate the ffmpeg process if it's running.""" - nonlocal process - logger.debug(f"({self.dev_name}) Cleanup requested.") - # Use lock to ensure thread safety when accessing/modifying process - with self._lock: - # Check if the process exists and is still running - if process and process.poll() is None: - logger.info( - f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid})." - ) - try: - process.terminate() # Ask ffmpeg to exit gracefully - process.wait(timeout=1.0) # Wait up to 1 second - except subprocess.TimeoutExpired: - logger.warning( - f"({self.dev_name}) ffmpeg (PID: {process.pid}) did not terminate gracefully, killing." - ) - process.kill() # Force kill if it didn't exit - except Exception as e: - logger.error(f"({self.dev_name}) Error during ffmpeg termination: {e}") - finally: - # Ensure we clear the process variable even if wait/kill fails - process = None - # Also clear the shared class attribute if this was the active process - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: # type: ignore[attr-defined] - self._ffmpeg_process = None - elif process and process.poll() is not None: - # Process exists but already terminated - logger.debug( - f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated (exit code: {process.poll()})." - ) - process = None # Clear the variable - # Clear shared attribute if it matches - if self._ffmpeg_process and self._ffmpeg_process.pid == process.pid: # type: ignore[attr-defined] - self._ffmpeg_process = None - else: - # Process variable is already None or doesn't match _ffmpeg_process - logger.debug( - f"({self.dev_name}) No active ffmpeg process found needing termination in cleanup." - ) - - try: - # 1. Probe the stream to get essential info (width, height) - stream_info = self._get_stream_info() - width = stream_info["width"] - height = stream_info["height"] - # Calculate expected bytes per frame (BGR format = 3 bytes per pixel) - frame_size = width * height * 3 - - # 2. Main loop: Start ffmpeg and read frames. Retry on failure. - while not should_stop.is_set(): - try: - # Start or reuse the ffmpeg process - with self._lock: - # Check if another thread/subscription already started the process - if self._ffmpeg_process and self._ffmpeg_process.poll() is None: - logger.warning( - f"({self.dev_name}) ffmpeg process (PID: {self._ffmpeg_process.pid}) seems to be already running. Reusing." - ) - process = self._ffmpeg_process - else: - # Start a new ffmpeg process - process = self._start_ffmpeg_process(width, height) - # Store the new process handle in the shared class attribute - self._ffmpeg_process = process - - # 3. Frame reading loop - while not should_stop.is_set(): - # Read exactly one frame's worth of bytes - in_bytes = process.stdout.read(frame_size) # type: ignore[union-attr] - - if len(in_bytes) == 0: - # End of stream or process terminated unexpectedly - logger.warning( - f"({self.dev_name}) ffmpeg stdout returned 0 bytes. EOF or process terminated." - ) - process.wait(timeout=0.5) # Allow stderr to flush - stderr_data = process.stderr.read().decode("utf8", errors="ignore") # type: ignore[union-attr] - exit_code = process.poll() - logger.warning( - f"({self.dev_name}) ffmpeg process (PID: {process.pid}) exited with code {exit_code}. Stderr: {stderr_data}" - ) - # Break inner loop to trigger cleanup and potential restart - with self._lock: - # Clear the shared process handle if it matches the one that just exited - if ( - self._ffmpeg_process - and self._ffmpeg_process.pid == process.pid - ): - self._ffmpeg_process = None - process = None # Clear local process variable - break # Exit frame reading loop - - elif len(in_bytes) != frame_size: - # Received incomplete frame data - indicates a problem - msg = f"({self.dev_name}) Incomplete frame read. Expected {frame_size}, got {len(in_bytes)}. Stopping." - logger.error(msg) - observer.on_error(VideoFrameError(msg)) - should_stop.set() # Signal outer loop to stop - break # Exit frame reading loop - - # Convert the raw bytes to a NumPy array (height, width, channels) - frame = np.frombuffer(in_bytes, np.uint8).reshape((height, width, 3)) - # Emit the frame to subscribers - observer.on_next(frame) - - # 4. Handle ffmpeg process exit/crash (if not stopping deliberately) - if not should_stop.is_set() and process is None: - logger.info( - f"({self.dev_name}) ffmpeg process ended. Attempting reconnection in 5 seconds..." - ) - # Wait for a few seconds before trying to restart - time.sleep(5) - # Continue to the next iteration of the outer loop to restart - - except (VideoSourceError, ffmpeg.Error) as e: - # Errors during ffmpeg process start or severe runtime errors - logger.error( - f"({self.dev_name}) Unrecoverable ffmpeg error: {e}. Stopping emission." - ) - observer.on_error(e) - should_stop.set() # Stop retrying - except Exception as e: - # Catch other unexpected errors during frame reading/processing - logger.error( - f"({self.dev_name}) Unexpected error processing stream: {e}", - exc_info=True, - ) - observer.on_error(VideoFrameError(f"Frame processing failed: {e}")) - should_stop.set() # Stop retrying - - # 5. Loop finished (likely due to should_stop being set) - logger.info(f"({self.dev_name}) Frame emission loop stopped.") - observer.on_completed() - - except VideoSourceError as e: - # Handle errors during the initial probing phase - logger.error(f"({self.dev_name}) Failed initial setup: {e}") - observer.on_error(e) - except Exception as e: - # Catch-all for unexpected errors before the main loop starts - logger.error(f"({self.dev_name}) Unexpected setup error: {e}", exc_info=True) - observer.on_error(VideoSourceError(f"Setup failed: {e}")) - finally: - # Crucial: Ensure the ffmpeg process is terminated when the observable - # is completed, errored, or disposed. - logger.debug(f"({self.dev_name}) Entering finally block in emit_frames.") - cleanup_process() - - # Return a Disposable that, when called (by unsubscribe/dispose), - # signals the loop to stop. The finally block handles the actual cleanup. - return Disposable(should_stop.set) - - # Create the observable using rx.create, applying scheduling and sharing - return rx.create(emit_frames).pipe( - ops.subscribe_on(self.pool_scheduler), # Run the emit_frames logic on the pool - # ops.observe_on(self.pool_scheduler), # Optional: Switch thread for downstream operators - ops.share(), # Ensure multiple subscribers share the same ffmpeg process - ) - - def dispose_all(self) -> None: - """Disposes of all managed resources, including terminating the ffmpeg process.""" - logger.info(f"({self.dev_name}) dispose_all called.") - # Terminate the ffmpeg process using the same locked logic as cleanup - with self._lock: - process = self._ffmpeg_process # Get the current process handle - if process and process.poll() is None: - logger.info( - f"({self.dev_name}) Terminating ffmpeg process (PID: {process.pid}) via dispose_all." - ) - try: - process.terminate() - process.wait(timeout=1.0) - except subprocess.TimeoutExpired: - logger.warning( - f"({self.dev_name}) ffmpeg process (PID: {process.pid}) kill required in dispose_all." - ) - process.kill() - except Exception as e: - logger.error( - f"({self.dev_name}) Error during ffmpeg termination in dispose_all: {e}" - ) - finally: - self._ffmpeg_process = None # Clear handle after attempting termination - elif process: # Process exists but already terminated - logger.debug( - f"({self.dev_name}) ffmpeg process (PID: {process.pid}) already terminated in dispose_all." - ) - self._ffmpeg_process = None - else: - logger.debug( - f"({self.dev_name}) No active ffmpeg process found during dispose_all." - ) - - # Call the parent class's dispose_all to handle Rx Disposables - super().dispose_all() - - def __del__(self) -> None: - """Destructor attempts to clean up resources if not explicitly disposed.""" - # Logging in __del__ is generally discouraged due to interpreter state issues, - # but can be helpful for debugging resource leaks. Use print for robustness here if needed. - # print(f"DEBUG: ({self.dev_name}) __del__ called.") - self.dispose_all() diff --git a/dimos/stream/stream_merger.py b/dimos/stream/stream_merger.py deleted file mode 100644 index 645fb86030..0000000000 --- a/dimos/stream/stream_merger.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import TypeVar - -from reactivex import Observable, operators as ops - -T = TypeVar("T") -Q = TypeVar("Q") - - -def create_stream_merger( - data_input_stream: Observable[T], text_query_stream: Observable[Q] -) -> Observable[tuple[Q, list[T]]]: - """ - Creates a merged stream that combines the latest value from data_input_stream - with each value from text_query_stream. - - Args: - data_input_stream: Observable stream of data values - text_query_stream: Observable stream of query values - - Returns: - Observable that emits tuples of (query, latest_data) - """ - # Encompass any data items as a list for safe evaluation - safe_data_stream = data_input_stream.pipe( - # We don't modify the data, just pass it through in a list - # This avoids any boolean evaluation of arrays - ops.map(lambda x: [x]) - ) - - # Use safe_data_stream instead of raw data_input_stream - return text_query_stream.pipe(ops.with_latest_from(safe_data_stream)) diff --git a/dimos/stream/video_operators.py b/dimos/stream/video_operators.py deleted file mode 100644 index a94b6fa3a1..0000000000 --- a/dimos/stream/video_operators.py +++ /dev/null @@ -1,605 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import base64 -from collections.abc import Callable -from datetime import datetime, timedelta -from enum import Enum -from typing import TYPE_CHECKING - -import cv2 -import numpy as np -from reactivex import Observable, Observer, create, operators as ops - -if TYPE_CHECKING: - from dimos.stream.frame_processor import FrameProcessor - - -class VideoOperators: - """Collection of video processing operators for reactive video streams.""" - - @staticmethod - def with_fps_sampling( - fps: int = 25, *, sample_interval: timedelta | None = None, use_latest: bool = True - ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] - """Creates an operator that samples frames at a specified rate. - - Creates a transformation operator that samples frames either by taking - the latest frame or the first frame in each interval. Provides frame - rate control through time-based selection. - - Args: - fps: Desired frames per second, defaults to 25 FPS. - Ignored if sample_interval is provided. - sample_interval: Optional explicit interval between samples. - If provided, overrides the fps parameter. - use_latest: If True, uses the latest frame in interval. - If False, uses the first frame. Defaults to True. - - Returns: - A function that transforms an Observable[np.ndarray] stream to a sampled - Observable[np.ndarray] stream with controlled frame rate. - - Raises: - ValueError: If fps is not positive or sample_interval is negative. - TypeError: If sample_interval is provided but not a timedelta object. - - Examples: - Sample latest frame at 30 FPS (good for real-time): - >>> video_stream.pipe( - ... VideoOperators.with_fps_sampling(fps=30) - ... ) - - Sample first frame with custom interval (good for consistent timing): - >>> video_stream.pipe( - ... VideoOperators.with_fps_sampling( - ... sample_interval=timedelta(milliseconds=40), - ... use_latest=False - ... ) - ... ) - - Note: - This operator helps manage high-speed video streams through time-based - frame selection. It reduces the frame rate by selecting frames at - specified intervals. - - When use_latest=True: - - Uses sampling to select the most recent frame at fixed intervals - - Discards intermediate frames, keeping only the latest - - Best for real-time video where latest frame is most relevant - - Uses ops.sample internally - - When use_latest=False: - - Uses throttling to select the first frame in each interval - - Ignores subsequent frames until next interval - - Best for scenarios where you want consistent frame timing - - Uses ops.throttle_first internally - - This is an approropriate solution for managing video frame rates and - memory usage in many scenarios. - """ - if sample_interval is None: - if fps <= 0: - raise ValueError("FPS must be positive") - sample_interval = timedelta(microseconds=int(1_000_000 / fps)) - - def _operator(source: Observable) -> Observable: # type: ignore[type-arg] - return source.pipe( - ops.sample(sample_interval) if use_latest else ops.throttle_first(sample_interval) - ) - - return _operator - - @staticmethod - def with_jpeg_export( - frame_processor: "FrameProcessor", - save_limit: int = 100, - suffix: str = "", - loop: bool = False, - ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] - """Creates an operator that saves video frames as JPEG files. - - Creates a transformation operator that saves each frame from the video - stream as a JPEG file while passing the frame through unchanged. - - Args: - frame_processor: FrameProcessor instance that handles the JPEG export - operations and maintains file count. - save_limit: Maximum number of frames to save before stopping. - Defaults to 100. Set to 0 for unlimited saves. - suffix: Optional string to append to filename before index. - Example: "raw" creates "1_raw.jpg". - Defaults to empty string. - loop: If True, when save_limit is reached, the files saved are - loopbacked and overwritten with the most recent frame. - Defaults to False. - Returns: - A function that transforms an Observable of frames into another - Observable of the same frames, with side effect of saving JPEGs. - - Raises: - ValueError: If save_limit is negative. - TypeError: If frame_processor is not a FrameProcessor instance. - - Example: - >>> video_stream.pipe( - ... VideoOperators.with_jpeg_export(processor, suffix="raw") - ... ) - """ - - def _operator(source: Observable) -> Observable: # type: ignore[type-arg] - return source.pipe( - ops.map( - lambda frame: frame_processor.export_to_jpeg(frame, save_limit, loop, suffix) - ) - ) - - return _operator - - @staticmethod - def with_optical_flow_filtering(threshold: float = 1.0) -> Callable[[Observable], Observable]: # type: ignore[type-arg] - """Creates an operator that filters optical flow frames by relevancy score. - - Filters a stream of optical flow results (frame, relevancy_score) tuples, - passing through only frames that meet the relevancy threshold. - - Args: - threshold: Minimum relevancy score required for frames to pass through. - Defaults to 1.0. Higher values mean more motion required. - - Returns: - A function that transforms an Observable of (frame, score) tuples - into an Observable of frames that meet the threshold. - - Raises: - ValueError: If threshold is negative. - TypeError: If input stream items are not (frame, float) tuples. - - Examples: - Basic filtering: - >>> optical_flow_stream.pipe( - ... VideoOperators.with_optical_flow_filtering(threshold=1.0) - ... ) - - With custom threshold: - >>> optical_flow_stream.pipe( - ... VideoOperators.with_optical_flow_filtering(threshold=2.5) - ... ) - - Note: - Input stream should contain tuples of (frame, relevancy_score) where - frame is a numpy array and relevancy_score is a float or None. - None scores are filtered out. - """ - return lambda source: source.pipe( - ops.filter(lambda result: result[1] is not None), # type: ignore[index] - ops.filter(lambda result: result[1] > threshold), # type: ignore[index] - ops.map(lambda result: result[0]), # type: ignore[index] - ) - - @staticmethod - def with_edge_detection( - frame_processor: "FrameProcessor", - ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] - return lambda source: source.pipe( - ops.map(lambda frame: frame_processor.edge_detection(frame)) # type: ignore[no-untyped-call] - ) - - @staticmethod - def with_optical_flow( - frame_processor: "FrameProcessor", - ) -> Callable[[Observable], Observable]: # type: ignore[type-arg] - return lambda source: source.pipe( - ops.scan( - lambda acc, frame: frame_processor.compute_optical_flow( # type: ignore[arg-type, return-value] - acc, # type: ignore[arg-type] - frame, # type: ignore[arg-type] - compute_relevancy=False, - ), - (None, None, None), - ), - ops.map(lambda result: result[1]), # type: ignore[index] # Extract flow component - ops.filter(lambda flow: flow is not None), - ops.map(frame_processor.visualize_flow), - ) - - @staticmethod - def encode_image() -> Callable[[Observable], Observable]: # type: ignore[type-arg] - """ - Operator to encode an image to JPEG format and convert it to a Base64 string. - - Returns: - A function that transforms an Observable of images into an Observable - of tuples containing the Base64 string of the encoded image and its dimensions. - """ - - def _operator(source: Observable) -> Observable: # type: ignore[type-arg] - def _encode_image(image: np.ndarray) -> tuple[str, tuple[int, int]]: # type: ignore[type-arg] - try: - width, height = image.shape[:2] - _, buffer = cv2.imencode(".jpg", image) - if buffer is None: - raise ValueError("Failed to encode image") - base64_image = base64.b64encode(buffer.tobytes()).decode("utf-8") - return base64_image, (width, height) - except Exception as e: - raise e - - return source.pipe(ops.map(_encode_image)) - - return _operator - - -from threading import Lock - -from reactivex import Observable -from reactivex.disposable import Disposable - - -class Operators: - @staticmethod - def exhaust_lock(process_item): # type: ignore[no-untyped-def] - """ - For each incoming item, call `process_item(item)` to get an Observable. - - If we're busy processing the previous one, skip new items. - - Use a lock to ensure concurrency safety across threads. - """ - - def _exhaust_lock(source: Observable) -> Observable: # type: ignore[type-arg] - def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - in_flight = False - lock = Lock() - upstream_done = False - - upstream_disp = None - active_inner_disp = None - - def dispose_all() -> None: - if upstream_disp: - upstream_disp.dispose() - if active_inner_disp: - active_inner_disp.dispose() - - def on_next(value) -> None: # type: ignore[no-untyped-def] - nonlocal in_flight, active_inner_disp - lock.acquire() - try: - if not in_flight: - in_flight = True - print("Processing new item.") - else: - print("Skipping item, already processing.") - return - finally: - lock.release() - - # We only get here if we grabbed the in_flight slot - try: - inner_source = process_item(value) - except Exception as ex: - observer.on_error(ex) - return - - def inner_on_next(ivalue) -> None: # type: ignore[no-untyped-def] - observer.on_next(ivalue) - - def inner_on_error(err) -> None: # type: ignore[no-untyped-def] - nonlocal in_flight - with lock: - in_flight = False - observer.on_error(err) - - def inner_on_completed() -> None: - nonlocal in_flight - with lock: - in_flight = False - if upstream_done: - observer.on_completed() - - # Subscribe to the inner observable - nonlocal active_inner_disp - active_inner_disp = inner_source.subscribe( - on_next=inner_on_next, - on_error=inner_on_error, - on_completed=inner_on_completed, - scheduler=scheduler, - ) - - def on_error(err) -> None: # type: ignore[no-untyped-def] - dispose_all() - observer.on_error(err) - - def on_completed() -> None: - nonlocal upstream_done - with lock: - upstream_done = True - # If we're not busy, we can end now - if not in_flight: - observer.on_completed() - - upstream_disp = source.subscribe( - on_next, on_error, on_completed, scheduler=scheduler - ) - return dispose_all - - return create(_subscribe) - - return _exhaust_lock - - @staticmethod - def exhaust_lock_per_instance(process_item, lock: Lock): # type: ignore[no-untyped-def] - """ - - For each item from upstream, call process_item(item) -> Observable. - - If a frame arrives while one is "in flight", discard it. - - 'lock' ensures we safely check/modify the 'in_flight' state in a multithreaded environment. - """ - - def _exhaust_lock(source: Observable) -> Observable: # type: ignore[type-arg] - def _subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - in_flight = False - upstream_done = False - - upstream_disp = None - active_inner_disp = None - - def dispose_all() -> None: - if upstream_disp: - upstream_disp.dispose() - if active_inner_disp: - active_inner_disp.dispose() - - def on_next(value) -> None: # type: ignore[no-untyped-def] - nonlocal in_flight, active_inner_disp - with lock: - # If not busy, claim the slot - if not in_flight: - in_flight = True - print("\033[34mProcessing new item.\033[0m") - else: - # Already processing => drop - print("\033[34mSkipping item, already processing.\033[0m") - return - - # We only get here if we acquired the slot - try: - inner_source = process_item(value) - except Exception as ex: - observer.on_error(ex) - return - - def inner_on_next(ivalue) -> None: # type: ignore[no-untyped-def] - observer.on_next(ivalue) - - def inner_on_error(err) -> None: # type: ignore[no-untyped-def] - nonlocal in_flight - with lock: - in_flight = False - print("\033[34mError in inner on error.\033[0m") - observer.on_error(err) - - def inner_on_completed() -> None: - nonlocal in_flight - with lock: - in_flight = False - print("\033[34mInner on completed.\033[0m") - if upstream_done: - observer.on_completed() - - # Subscribe to the inner Observable - nonlocal active_inner_disp - active_inner_disp = inner_source.subscribe( - on_next=inner_on_next, - on_error=inner_on_error, - on_completed=inner_on_completed, - scheduler=scheduler, - ) - - def on_error(e) -> None: # type: ignore[no-untyped-def] - dispose_all() - observer.on_error(e) - - def on_completed() -> None: - nonlocal upstream_done - with lock: - upstream_done = True - print("\033[34mOn completed.\033[0m") - if not in_flight: - observer.on_completed() - - upstream_disp = source.subscribe( - on_next=on_next, - on_error=on_error, - on_completed=on_completed, - scheduler=scheduler, - ) - - return Disposable(dispose_all) - - return create(_subscribe) - - return _exhaust_lock - - @staticmethod - def exhaust_map(project): # type: ignore[no-untyped-def] - def _exhaust_map(source: Observable): # type: ignore[no-untyped-def, type-arg] - def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - is_processing = False - - def on_next(item) -> None: # type: ignore[no-untyped-def] - nonlocal is_processing - if not is_processing: - is_processing = True - print("\033[35mProcessing item.\033[0m") - try: - inner_observable = project(item) # Create the inner observable - inner_observable.subscribe( - on_next=observer.on_next, - on_error=observer.on_error, - on_completed=lambda: set_not_processing(), - scheduler=scheduler, - ) - except Exception as e: - observer.on_error(e) - else: - print("\033[35mSkipping item, already processing.\033[0m") - - def set_not_processing() -> None: - nonlocal is_processing - is_processing = False - print("\033[35mItem processed.\033[0m") - - return source.subscribe( - on_next=on_next, - on_error=observer.on_error, - on_completed=observer.on_completed, - scheduler=scheduler, - ) - - return create(subscribe) - - return _exhaust_map - - @staticmethod - def with_lock(lock: Lock): # type: ignore[no-untyped-def] - def operator(source: Observable): # type: ignore[no-untyped-def, type-arg] - def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - def on_next(item) -> None: # type: ignore[no-untyped-def] - if not lock.locked(): # Check if the lock is free - if lock.acquire(blocking=False): # Non-blocking acquire - try: - print("\033[32mAcquired lock, processing item.\033[0m") - observer.on_next(item) - finally: # Ensure lock release even if observer.on_next throws - lock.release() - else: - print("\033[34mLock busy, skipping item.\033[0m") - else: - print("\033[34mLock busy, skipping item.\033[0m") - - def on_error(error) -> None: # type: ignore[no-untyped-def] - observer.on_error(error) - - def on_completed() -> None: - observer.on_completed() - - return source.subscribe( - on_next=on_next, - on_error=on_error, - on_completed=on_completed, - scheduler=scheduler, - ) - - return Observable(subscribe) - - return operator - - @staticmethod - def with_lock_check(lock: Lock): # type: ignore[no-untyped-def] # Renamed for clarity - def operator(source: Observable): # type: ignore[no-untyped-def, type-arg] - def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - def on_next(item) -> None: # type: ignore[no-untyped-def] - if not lock.locked(): # Check if the lock is held WITHOUT acquiring - print(f"\033[32mLock is free, processing item: {item}\033[0m") - observer.on_next(item) - else: - print(f"\033[34mLock is busy, skipping item: {item}\033[0m") - # observer.on_completed() - - def on_error(error) -> None: # type: ignore[no-untyped-def] - observer.on_error(error) - - def on_completed() -> None: - observer.on_completed() - - return source.subscribe( - on_next=on_next, - on_error=on_error, - on_completed=on_completed, - scheduler=scheduler, - ) - - return Observable(subscribe) - - return operator - - # PrintColor enum for standardized color formatting - class PrintColor(Enum): - RED = "\033[31m" - GREEN = "\033[32m" - BLUE = "\033[34m" - YELLOW = "\033[33m" - MAGENTA = "\033[35m" - CYAN = "\033[36m" - WHITE = "\033[37m" - RESET = "\033[0m" - - @staticmethod - def print_emission( # type: ignore[no-untyped-def] - id: str, - dev_name: str = "NA", - counts: dict | None = None, # type: ignore[type-arg] - color: "Operators.PrintColor" = None, # type: ignore[assignment] - enabled: bool = True, - ): - """ - Creates an operator that prints the emission with optional counts for debugging. - - Args: - id: Identifier for the emission point (e.g., 'A', 'B') - dev_name: Device or component name for context - counts: External dictionary to track emission count across operators. If None, will not print emission count. - color: Color for the printed output from PrintColor enum (default is RED) - enabled: Whether to print the emission count (default is True) - Returns: - An operator that counts and prints emissions without modifying the stream - """ - # If enabled is false, return the source unchanged - if not enabled: - return lambda source: source - - # Use RED as default if no color provided - if color is None: - color = Operators.PrintColor.RED - - def _operator(source: Observable) -> Observable: # type: ignore[type-arg] - def _subscribe(observer: Observer, scheduler=None): # type: ignore[no-untyped-def, type-arg] - def on_next(value) -> None: # type: ignore[no-untyped-def] - if counts is not None: - # Initialize count if necessary - if id not in counts: - counts[id] = 0 - - # Increment and print - counts[id] += 1 - print( - f"{color.value}({dev_name} - {id}) Emission Count - {counts[id]} {datetime.now()}{Operators.PrintColor.RESET.value}" - ) - else: - print( - f"{color.value}({dev_name} - {id}) Emitted - {datetime.now()}{Operators.PrintColor.RESET.value}" - ) - - # Pass value through unchanged - observer.on_next(value) - - return source.subscribe( - on_next=on_next, - on_error=observer.on_error, - on_completed=observer.on_completed, - scheduler=scheduler, - ) - - return create(_subscribe) # type: ignore[arg-type] - - return _operator diff --git a/dimos/stream/video_provider.py b/dimos/stream/video_provider.py deleted file mode 100644 index 38406fd5a5..0000000000 --- a/dimos/stream/video_provider.py +++ /dev/null @@ -1,234 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Video provider module for capturing and streaming video frames. - -This module provides classes for capturing video from various sources and -exposing them as reactive observables. It handles resource management, -frame rate control, and thread safety. -""" - -# Standard library imports -from abc import ABC, abstractmethod -import logging -import os -from threading import Lock -import time - -# Third-party imports -import cv2 -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import CompositeDisposable -from reactivex.observable import Observable -from reactivex.scheduler import ThreadPoolScheduler - -# Local imports -from dimos.utils.threadpool import get_scheduler - -# Note: Logging configuration should ideally be in the application initialization, -# not in a module. Keeping it for now but with a more restricted scope. -logger = logging.getLogger(__name__) - - -# Specific exception classes -class VideoSourceError(Exception): - """Raised when there's an issue with the video source.""" - - pass - - -class VideoFrameError(Exception): - """Raised when there's an issue with frame acquisition.""" - - pass - - -class AbstractVideoProvider(ABC): - """Abstract base class for video providers managing video capture resources.""" - - def __init__( - self, dev_name: str = "NA", pool_scheduler: ThreadPoolScheduler | None = None - ) -> None: - """Initializes the video provider with a device name. - - Args: - dev_name: The name of the device. Defaults to "NA". - pool_scheduler: The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - """ - self.dev_name = dev_name - self.pool_scheduler = pool_scheduler if pool_scheduler else get_scheduler() - self.disposables = CompositeDisposable() - - @abstractmethod - def capture_video_as_observable(self, fps: int = 30) -> Observable: # type: ignore[type-arg] - """Create an observable from video capture. - - Args: - fps: Frames per second to emit. Defaults to 30fps. - - Returns: - Observable: An observable emitting frames at the specified rate. - - Raises: - VideoSourceError: If the video source cannot be opened. - VideoFrameError: If frames cannot be read properly. - """ - pass - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this provider.""" - if self.disposables: - self.disposables.dispose() - else: - logger.info("No disposables to dispose.") - - def __del__(self) -> None: - """Destructor to ensure resources are cleaned up if not explicitly disposed.""" - self.dispose_all() - - -class VideoProvider(AbstractVideoProvider): - """Video provider implementation for capturing video as an observable.""" - - def __init__( - self, - dev_name: str, - video_source: str = f"{os.getcwd()}/assets/video-f30-480p.mp4", - pool_scheduler: ThreadPoolScheduler | None = None, - ) -> None: - """Initializes the video provider with a device name and video source. - - Args: - dev_name: The name of the device. - video_source: The path to the video source. Defaults to a sample video. - pool_scheduler: The scheduler to use for thread pool operations. - If None, the global scheduler from get_scheduler() will be used. - """ - super().__init__(dev_name, pool_scheduler) - self.video_source = video_source - self.cap = None - self.lock = Lock() - - def _initialize_capture(self) -> None: - """Initializes the video capture object if not already initialized. - - Raises: - VideoSourceError: If the video source cannot be opened. - """ - if self.cap is None or not self.cap.isOpened(): - # Release previous capture if it exists but is closed - if self.cap: - self.cap.release() - logger.info("Released previous capture") - - # Attempt to open new capture - self.cap = cv2.VideoCapture(self.video_source) # type: ignore[assignment] - if self.cap is None or not self.cap.isOpened(): - error_msg = f"Failed to open video source: {self.video_source}" - logger.error(error_msg) - raise VideoSourceError(error_msg) - - logger.info(f"Opened new capture: {self.video_source}") - - def capture_video_as_observable(self, realtime: bool = True, fps: int = 30) -> Observable: # type: ignore[override, type-arg] - """Creates an observable from video capture. - - Creates an observable that emits frames at specified FPS or the video's - native FPS, with proper resource management and error handling. - - Args: - realtime: If True, use the video's native FPS. Defaults to True. - fps: Frames per second to emit. Defaults to 30fps. Only used if - realtime is False or the video's native FPS is not available. - - Returns: - Observable: An observable emitting frames at the configured rate. - - Raises: - VideoSourceError: If the video source cannot be opened. - VideoFrameError: If frames cannot be read properly. - """ - - def emit_frames(observer, scheduler) -> None: # type: ignore[no-untyped-def] - try: - self._initialize_capture() - - # Determine the FPS to use based on configuration and availability - local_fps: float = fps - if realtime: - native_fps: float = self.cap.get(cv2.CAP_PROP_FPS) # type: ignore[attr-defined] - if native_fps > 0: - local_fps = native_fps - else: - logger.warning("Native FPS not available, defaulting to specified FPS") - - frame_interval: float = 1.0 / local_fps - frame_time: float = time.monotonic() - - while self.cap.isOpened(): # type: ignore[attr-defined] - # Thread-safe access to video capture - with self.lock: - ret, frame = self.cap.read() # type: ignore[attr-defined] - - if not ret: - # Loop video when we reach the end - logger.warning("End of video reached, restarting playback") - with self.lock: - self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # type: ignore[attr-defined] - continue - - # Control frame rate to match target FPS - now: float = time.monotonic() - next_frame_time: float = frame_time + frame_interval - sleep_time: float = next_frame_time - now - - if sleep_time > 0: - time.sleep(sleep_time) - - observer.on_next(frame) - frame_time = next_frame_time - - except VideoSourceError as e: - logger.error(f"Video source error: {e}") - observer.on_error(e) - except Exception as e: - logger.error(f"Unexpected error during frame emission: {e}") - observer.on_error(VideoFrameError(f"Frame acquisition failed: {e}")) - finally: - # Clean up resources regardless of success or failure - with self.lock: - if self.cap and self.cap.isOpened(): - self.cap.release() - logger.info("Capture released") - observer.on_completed() - - return rx.create(emit_frames).pipe( # type: ignore[arg-type] - ops.subscribe_on(self.pool_scheduler), - ops.observe_on(self.pool_scheduler), - ops.share(), # Share the stream among multiple subscribers - ) - - def dispose_all(self) -> None: - """Disposes of all resources including video capture.""" - with self.lock: - if self.cap and self.cap.isOpened(): - self.cap.release() - logger.info("Capture released in dispose_all") - super().dispose_all() - - def __del__(self) -> None: - """Destructor to ensure resources are cleaned up if not explicitly disposed.""" - self.dispose_all() diff --git a/dimos/stream/video_providers/__init__.py b/dimos/stream/video_providers/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/teleop/README.md b/dimos/teleop/README.md deleted file mode 100644 index e64a7b43ea..0000000000 --- a/dimos/teleop/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# Teleop Stack - -Teleoperation modules for DimOS. Currently supports Meta Quest 3 VR controllers. - -## Architecture - -``` -Quest Browser (WebXR) - │ - │ PoseStamped + Joy via WebSocket - ▼ -Deno Bridge (teleop_server.ts) - │ - │ LCM topics - ▼ -QuestTeleopModule - │ WebXR → robot frame transform - │ Pose computation + button state packing - ▼ -PoseStamped / TwistStamped / Buttons outputs -``` - -## Modules - -### QuestTeleopModule -Base teleop module. Gets controller data, computes output poses, and publishes them. Default engage: hold primary button (X/A). Subclass to customize. - -### ArmTeleopModule -Toggle-based engage — press primary button once to engage, press again to disengage. - -### TwistTeleopModule -Outputs TwistStamped (linear + angular velocity) instead of PoseStamped. - -### VisualizingTeleopModule -Adds Rerun visualization for debugging. Extends ArmTeleopModule (toggle engage). - -## Subclassing - -`QuestTeleopModule` is designed for extension. Override these methods: - -| Method | Purpose | -|--------|---------| -| `_handle_engage()` | Customize engage/disengage logic | -| `_should_publish()` | Add conditions for when to publish | -| `_get_output_pose()` | Customize pose computation | -| `_publish_msg()` | Change output format | -| `_publish_button_state()` | Change button output | - -### Rules for subclasses - -- **Do not acquire `self._lock` in overrides.** The control loop already holds it. - Access `self._controllers`, `self._current_poses`, `self._is_engaged`, etc. directly. -- **Keep overrides fast** — they run inside the control loop at `control_loop_hz`. - -## File Structure - -``` -teleop/ -├── quest/ -│ ├── quest_teleop_module.py # Base Quest teleop module -│ ├── quest_extensions.py # ArmTeleop, TwistTeleop, VisualizingTeleop -│ ├── quest_types.py # QuestControllerState, Buttons -│ └── web/ # Deno bridge + WebXR client -│ ├── teleop_server.ts -│ └── static/index.html -├── phone/ -│ ├── phone_teleop_module.py # Base Phone teleop module -│ ├── phone_extensions.py # SimplePhoneTeleop -│ ├── blueprints.py # Pre-wired configurations -│ └── web/ # Deno bridge + mobile web app -│ ├── teleop_server.ts -│ └── static/index.html -├── utils/ -│ ├── teleop_transforms.py # WebXR → robot frame math -│ └── teleop_visualization.py # Rerun visualization helpers -└── blueprints.py # Module blueprints for easy instantiation -``` - -## Quick Start - -See [Quest Web README](quest/web/README.md) for running the Deno bridge and connecting the Quest headset. diff --git a/dimos/teleop/__init__.py b/dimos/teleop/__init__.py deleted file mode 100644 index 8324113111..0000000000 --- a/dimos/teleop/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleoperation modules for DimOS.""" diff --git a/dimos/teleop/keyboard/__init__.py b/dimos/teleop/keyboard/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/teleop/keyboard/keyboard_teleop_module.py b/dimos/teleop/keyboard/keyboard_teleop_module.py deleted file mode 100644 index ff42ce9a1a..0000000000 --- a/dimos/teleop/keyboard/keyboard_teleop_module.py +++ /dev/null @@ -1,219 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Keyboard-based cartesian teleop module for arm teleoperation. - -Wraps a pygame UI as a DimOS Module so it can be composed with coordinator -blueprints via autoconnect. - -Keyboard controls: - W/S: +X/-X (forward/backward) - A/D: -Y/+Y (left/right) - Q/E: +Z/-Z (up/down) - R/F: +Roll/-Roll - T/G: +Pitch/-Pitch - Y/H: +Yaw/-Yaw - SPACE: Reset to home pose - ESC: Quit -""" - -from dataclasses import dataclass -import os -import threading -import time -from typing import Any - -import numpy as np - -try: - import pygame -except ImportError: - pygame = None # type: ignore[assignment] - -from dimos.control.examples.cartesian_ik_jogger import JogState -from dimos.core import Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import PoseStamped - -# Force X11 driver to avoid OpenGL threading issues -os.environ["SDL_VIDEODRIVER"] = "x11" - -# Jog speeds -LINEAR_SPEED = 0.05 # m/s -ANGULAR_SPEED = 0.5 # rad/s - -# Workspace bounds -X_LIMITS = (-0.5, 0.5) -Y_LIMITS = (-0.5, 0.5) -Z_LIMITS = (-0.2, 0.6) - - -def _clamp(value: float, min_val: float, max_val: float) -> float: - return max(min_val, min(max_val, value)) - - -@dataclass -class KeyboardTeleopConfig(ModuleConfig): - model_path: str = "" - ee_joint_id: int = 6 - task_name: str = "cartesian_ik_arm" - - -class KeyboardTeleopModule(Module[KeyboardTeleopConfig]): - """Pygame-based cartesian keyboard teleop as a DimOS Module. - - Publishes absolute EE PoseStamped commands for CartesianIKTask. - """ - - default_config = KeyboardTeleopConfig - - cartesian_command: Out[PoseStamped] - - _stop_event: threading.Event - _thread: threading.Thread | None = None - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._stop_event = threading.Event() - - @rpc - def start(self) -> None: - super().start() - if pygame is None: - raise ImportError("pygame not installed. Install with: pip install pygame") - - self._stop_event.clear() - self._thread = threading.Thread(target=self._pygame_loop, daemon=True) - self._thread.start() - - @rpc - def stop(self) -> None: - self._stop_event.set() - if self._thread is not None: - self._thread.join(2) - super().stop() - - def _pygame_loop(self) -> None: - model_path = str(self.config.model_path) - ee_joint_id = self.config.ee_joint_id - task_name = self.config.task_name - - # Initialize pose from forward kinematics at zero configuration - home_pose = JogState.from_fk(model_path, ee_joint_id) - current_pose = home_pose.copy() - - # Publish initial pose - self.cartesian_command.publish(current_pose.to_pose_stamped(task_name)) - - pygame.init() - screen = pygame.display.set_mode((600, 400), pygame.SWSURFACE) - pygame.display.set_caption(f"Keyboard Teleop — {task_name}") - font = pygame.font.Font(None, 28) - clock = pygame.time.Clock() - last_time = time.perf_counter() - - while not self._stop_event.is_set(): - dt = time.perf_counter() - last_time - last_time = time.perf_counter() - - for event in pygame.event.get(): - if event.type == pygame.QUIT: - self._stop_event.set() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - self._stop_event.set() - elif event.key == pygame.K_SPACE: - current_pose = home_pose.copy() - - keys = pygame.key.get_pressed() - - # Linear motion - if keys[pygame.K_w]: - current_pose.x += LINEAR_SPEED * dt - if keys[pygame.K_s]: - current_pose.x -= LINEAR_SPEED * dt - if keys[pygame.K_a]: - current_pose.y -= LINEAR_SPEED * dt - if keys[pygame.K_d]: - current_pose.y += LINEAR_SPEED * dt - if keys[pygame.K_q]: - current_pose.z += LINEAR_SPEED * dt - if keys[pygame.K_e]: - current_pose.z -= LINEAR_SPEED * dt - - # Angular motion - if keys[pygame.K_r]: - current_pose.roll += ANGULAR_SPEED * dt - if keys[pygame.K_f]: - current_pose.roll -= ANGULAR_SPEED * dt - if keys[pygame.K_t]: - current_pose.pitch += ANGULAR_SPEED * dt - if keys[pygame.K_g]: - current_pose.pitch -= ANGULAR_SPEED * dt - if keys[pygame.K_y]: - current_pose.yaw += ANGULAR_SPEED * dt - if keys[pygame.K_h]: - current_pose.yaw -= ANGULAR_SPEED * dt - - # Clamp to workspace limits - current_pose.x = _clamp(current_pose.x, *X_LIMITS) - current_pose.y = _clamp(current_pose.y, *Y_LIMITS) - current_pose.z = _clamp(current_pose.z, *Z_LIMITS) - - # Publish - self.cartesian_command.publish(current_pose.to_pose_stamped(task_name)) - - # Draw UI - screen.fill((30, 30, 30)) - y_pos = 20 - - title = font.render(f"Keyboard Teleop — {task_name}", True, (255, 255, 255)) - screen.blit(title, (20, y_pos)) - y_pos += 40 - - pos_text = ( - f"Position: X={current_pose.x:.3f} Y={current_pose.y:.3f} Z={current_pose.z:.3f}" - ) - screen.blit(font.render(pos_text, True, (100, 255, 100)), (20, y_pos)) - y_pos += 30 - - ori_text = ( - f"Orientation: R={np.degrees(current_pose.roll):.1f}° " - f"P={np.degrees(current_pose.pitch):.1f}° " - f"Y={np.degrees(current_pose.yaw):.1f}°" - ) - screen.blit(font.render(ori_text, True, (100, 200, 255)), (20, y_pos)) - y_pos += 40 - - controls = [ - ("W/S", "+X/-X (forward/back)"), - ("A/D", "-Y/+Y (left/right)"), - ("Q/E", "+Z/-Z (up/down)"), - ("R/F", "+Roll/-Roll"), - ("T/G", "+Pitch/-Pitch"), - ("Y/H", "+Yaw/-Yaw"), - ("SPACE", "Reset to home"), - ("ESC", "Quit"), - ] - for key, desc in controls: - screen.blit(font.render(f"{key}: {desc}", True, (180, 180, 180)), (20, y_pos)) - y_pos += 25 - - pygame.display.flip() - clock.tick(50) - - pygame.quit() - - -keyboard_teleop_module = KeyboardTeleopModule.blueprint diff --git a/dimos/teleop/phone/README.md b/dimos/teleop/phone/README.md deleted file mode 100644 index dd2af02281..0000000000 --- a/dimos/teleop/phone/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Phone Teleop - -Teleoperation via smartphone motion sensors. Tilt to drive. - -## Architecture - -``` -Phone Browser (DeviceOrientation + DeviceMotion) - | - | TwistStamped + Bool via WebSocket - v -Deno Bridge (teleop_server.ts) - | - | LCM topics - v -PhoneTeleopModule - | Orientation delta from home pose - | Gains -> velocity commands - v -TwistStamped / Twist outputs -``` - -## Modules - -### PhoneTeleopModule -Base module. Receives raw sensor data and button state. On engage (button hold), captures home orientation and publishes deltas as TwistStamped. Launches the Deno bridge server automatically. - -### SimplePhoneTeleop -Filters to mobile-base axes (linear.x, linear.y, angular.z) and publishes as `Twist` on `cmd_vel` for direct autoconnect wiring with any module that has `cmd_vel: In[Twist]`. - -## Subclassing - -Override these methods: - -| Method | Purpose | -|--------|---------| -| `_handle_engage()` | Customize engage/disengage logic | -| `_should_publish()` | Add conditions for when to publish | -| `_publish_msg()` | Change output format | - -**Do not acquire `self._lock` in overrides.** The control loop already holds it. - -## LCM Topics - -| Topic | Type | Description | -|-------|------|-------------| -| `/phone_sensors` | TwistStamped | linear=(roll,pitch,yaw) deg, angular=(gyro) deg/s | -| `/phone_button` | Bool | Teleop engage button (1=held) | -| `/teleop/twist` | TwistStamped | Output velocity command | - -## Running - -```bash -dimos run phone-go2-teleop # Go2 -dimos run simple-phone-teleop # Generic ground robot -``` - -Server starts on port `8444`. Open `https://:8444` on phone, accept the self-signed certificate, allow sensor permissions, connect, hold button to drive. - -## File Structure - -``` -phone/ -├── phone_teleop_module.py # Base phone teleop module -├── phone_extensions.py # SimplePhoneTeleop -├── blueprints.py # Pre-wired configurations -└── web/ - ├── teleop_server.ts # Deno WSS-to-LCM bridge - └── static/index.html # Mobile web app -``` diff --git a/dimos/teleop/phone/__init__.py b/dimos/teleop/phone/__init__.py deleted file mode 100644 index 552032a47b..0000000000 --- a/dimos/teleop/phone/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Phone teleoperation module for DimOS.""" - -from dimos.teleop.phone.phone_extensions import ( - SimplePhoneTeleop, - simple_phone_teleop_module, -) -from dimos.teleop.phone.phone_teleop_module import ( - PhoneTeleopConfig, - PhoneTeleopModule, - phone_teleop_module, -) - -__all__ = [ - "PhoneTeleopConfig", - "PhoneTeleopModule", - "SimplePhoneTeleop", - "phone_teleop_module", - "simple_phone_teleop_module", -] diff --git a/dimos/teleop/phone/blueprints.py b/dimos/teleop/phone/blueprints.py deleted file mode 100644 index 6328af8612..0000000000 --- a/dimos/teleop/phone/blueprints.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core.blueprints import autoconnect -from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import unitree_go2_basic -from dimos.teleop.phone.phone_extensions import simple_phone_teleop_module - -# Simple phone teleop (mobile base axis filtering + cmd_vel output) -simple_phone_teleop = autoconnect( - simple_phone_teleop_module(), -) - -# Phone teleop wired to Unitree Go2 -phone_go2_teleop = autoconnect( - simple_phone_teleop_module(), - unitree_go2_basic, -) - - -__all__ = ["phone_go2_teleop", "simple_phone_teleop"] diff --git a/dimos/teleop/phone/phone_extensions.py b/dimos/teleop/phone/phone_extensions.py deleted file mode 100644 index f0a8fd4d01..0000000000 --- a/dimos/teleop/phone/phone_extensions.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Phone teleop module extensions. - -Available subclasses: - - SimplePhoneTeleop: Filters to ground robot axes and outputs cmd_vel: Out[Twist] -""" - -from dimos.core import Out -from dimos.msgs.geometry_msgs import Twist, TwistStamped, Vector3 -from dimos.teleop.phone.phone_teleop_module import PhoneTeleopModule - - -class SimplePhoneTeleop(PhoneTeleopModule): - """Phone teleop for ground robots. - - Filters the raw 6-axis twist to mobile base axes (linear.x, linear.y, angular.z) - and publishes as Twist on cmd_vel for direct autoconnect wiring with any - module that has cmd_vel: In[Twist]. - """ - - cmd_vel: Out[Twist] - - def _publish_msg(self, output_msg: TwistStamped) -> None: - self.cmd_vel.publish( - Twist( - linear=Vector3(x=output_msg.linear.x, y=output_msg.linear.y, z=0.0), - angular=Vector3(x=0.0, y=0.0, z=output_msg.linear.z), - ) - ) - - -simple_phone_teleop_module = SimplePhoneTeleop.blueprint - -__all__ = [ - "SimplePhoneTeleop", - "simple_phone_teleop_module", -] diff --git a/dimos/teleop/phone/phone_teleop_module.py b/dimos/teleop/phone/phone_teleop_module.py deleted file mode 100644 index c0da85c27c..0000000000 --- a/dimos/teleop/phone/phone_teleop_module.py +++ /dev/null @@ -1,308 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Phone Teleoperation Module. - -Receives raw sensor data (TwistStamped) and button state (Bool) from the -phone web app via the Deno LCM bridge. Computes orientation deltas from -a initial orientation captured on engage, converts to TwistStamped velocity -commands via configurable gains, and publishes. - -""" - -from dataclasses import dataclass -from pathlib import Path -import shutil -import signal -import subprocess -import threading -import time -from typing import Any - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import Twist, TwistStamped, Vector3 -from dimos.msgs.std_msgs.Bool import Bool -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -@dataclass -class PhoneTeleopConfig(ModuleConfig): - control_loop_hz: float = 50.0 - linear_gain: float = 1.0 / 30.0 # Gain: maps degrees of tilt to m/s. 30 deg -> 1.0 m/s - angular_gain: float = 1.0 / 30.0 # Gain: maps gyro deg/s to rad/s. 30 deg/s -> 1.0 rad/s - - -class PhoneTeleopModule(Module[PhoneTeleopConfig]): - """ - Receives raw sensor data from the phone web app: - - TwistStamped: linear=(roll, pitch, yaw) deg, angular=(gyro) deg/s - - Bool: teleop button state (True = held) - - Outputs: - - twist_output: TwistStamped (velocity command for robot) - """ - - default_config = PhoneTeleopConfig - - # Inputs from Deno bridge - phone_sensors: In[TwistStamped] - phone_button: In[Bool] - # Output: velocity command to robot - twist_output: Out[TwistStamped] - - # ------------------------------------------------------------------------- - # Initialization - # ------------------------------------------------------------------------- - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self._is_engaged: bool = False - self._teleop_button: bool = False - self._current_sensors: TwistStamped | None = None - self._initial_sensors: TwistStamped | None = None - self._lock = threading.RLock() - - # Control loop - self._control_loop_thread: threading.Thread | None = None - self._stop_event = threading.Event() - - # Deno bridge server - self._server_process: subprocess.Popen[bytes] | None = None - self._server_script = Path(__file__).parent / "web" / "teleop_server.ts" - - # ------------------------------------------------------------------------- - # Lifecycle - # ------------------------------------------------------------------------- - - @rpc - def start(self) -> None: - super().start() - for stream, handler in ( - (self.phone_sensors, self._on_sensors), - (self.phone_button, self._on_button), - ): - self._disposables.add(Disposable(stream.subscribe(handler))) # type: ignore[attr-defined] - self._start_server() - self._start_control_loop() - - @rpc - def stop(self) -> None: - self._stop_control_loop() - self._stop_server() - super().stop() - - # ------------------------------------------------------------------------- - # Internal engage / disengage (assumes lock is held) - # ------------------------------------------------------------------------- - - def _engage(self) -> bool: - """Engage: capture current sensors as initial""" - if self._current_sensors is None: - logger.error("Engage failed: no sensor data yet") - return False - self._initial_sensors = self._current_sensors - self._is_engaged = True - logger.info("Phone teleop engaged") - return True - - def _disengage(self) -> None: - """Disengage: stop publishing""" - self._is_engaged = False - self._initial_sensors = None - logger.info("Phone teleop disengaged") - - # ------------------------------------------------------------------------- - # Callbacks - # ------------------------------------------------------------------------- - - def _on_sensors(self, msg: TwistStamped) -> None: - """Callback for raw sensor TwistStamped from the phone""" - with self._lock: - self._current_sensors = msg - - def _on_button(self, msg: Bool) -> None: - """Callback for teleop button state.""" - with self._lock: - self._teleop_button = bool(msg.data) - - # ------------------------------------------------------------------------- - # Deno Bridge Server - # ------------------------------------------------------------------------- - - def _start_server(self) -> None: - """Launch the Deno WebSocket-to-LCM bridge server as a subprocess.""" - if self._server_process is not None and self._server_process.poll() is None: - logger.warning("Deno bridge already running", pid=self._server_process.pid) - return - - if shutil.which("deno") is None: - logger.error( - "Deno is not installed. Install it with: curl -fsSL https://deno.land/install.sh | sh" - ) - return - - script = str(self._server_script) - cmd = [ - "deno", - "run", - "--allow-net", - "--allow-read", - "--allow-run", - "--allow-write", - "--unstable-net", - script, - ] - try: - self._server_process = subprocess.Popen(cmd) - logger.info(f"Deno bridge server started (pid {self._server_process.pid})") - except OSError as e: - logger.error(f"Failed to start Deno bridge: {e}") - - def _stop_server(self) -> None: - """Terminate the Deno bridge server subprocess.""" - if self._server_process is None or self._server_process.poll() is not None: - self._server_process = None - return - - logger.info("Stopping Deno bridge server", pid=self._server_process.pid) - self._server_process.send_signal(signal.SIGTERM) - try: - self._server_process.wait(timeout=3) - except subprocess.TimeoutExpired: - logger.warning( - "Deno bridge did not exit, sending SIGKILL", pid=self._server_process.pid - ) - self._server_process.kill() - try: - self._server_process.wait(timeout=5) - except subprocess.TimeoutExpired: - logger.error("Deno bridge did not exit after SIGKILL") - logger.info("Deno bridge server stopped") - self._server_process = None - - # ------------------------------------------------------------------------- - # Control Loop - # ------------------------------------------------------------------------- - - def _start_control_loop(self) -> None: - if self._control_loop_thread is not None and self._control_loop_thread.is_alive(): - return - - self._stop_event.clear() - self._control_loop_thread = threading.Thread( - target=self._control_loop, - daemon=True, - name="PhoneTeleopControlLoop", - ) - self._control_loop_thread.start() - logger.info(f"Control loop started at {self.config.control_loop_hz} Hz") - - def _stop_control_loop(self) -> None: - self._stop_event.set() - if self._control_loop_thread is not None: - self._control_loop_thread.join(timeout=1.0) - self._control_loop_thread = None - logger.info("Control loop stopped") - - def _control_loop(self) -> None: - period = 1.0 / self.config.control_loop_hz - - while not self._stop_event.is_set(): - loop_start = time.perf_counter() - with self._lock: - self._handle_engage() - - if self._is_engaged: - output_twist = self._get_output_twist() - if output_twist is not None: - self._publish_msg(output_twist) - - elapsed = time.perf_counter() - loop_start - sleep_time = period - elapsed - if sleep_time > 0: - self._stop_event.wait(sleep_time) - - # ------------------------------------------------------------------------- - # Control Loop Internal Methods - # ------------------------------------------------------------------------- - - def _handle_engage(self) -> None: - """ - Override to customize engagement logic. - Default: button hold = engaged, release = disengaged. - """ - if self._teleop_button: - if not self._is_engaged: - self._engage() - else: - if self._is_engaged: - self._disengage() - - def _get_output_twist(self) -> TwistStamped | None: - """Compute twist from orientation delta. - Override to customize twist computation (e.g., apply scaling, filtering). - Default: Computes delta angles from initial orientation, applies gains. - """ - current = self._current_sensors - initial = self._initial_sensors - if current is None or initial is None: - return None - - delta: Twist = Twist(current) - Twist(initial) - - # Handle yaw wraparound (linear.z = yaw, 0-360 degrees) - d_yaw = delta.linear.z - if d_yaw > 180: - d_yaw -= 360 - elif d_yaw < -180: - d_yaw += 360 - - cfg = self.config - return TwistStamped( - ts=current.ts, - frame_id="phone", - linear=Vector3( - x=-delta.linear.y * cfg.linear_gain, # pitch forward -> drive forward - y=-delta.linear.x * cfg.linear_gain, # roll right -> strafe right - z=d_yaw * cfg.linear_gain, # yaw delta - ), - angular=Vector3( - x=current.angular.x * cfg.angular_gain, - y=current.angular.y * cfg.angular_gain, - z=current.angular.z * cfg.angular_gain, - ), - ) - - def _publish_msg(self, output_msg: TwistStamped) -> None: - """ - Override to customize output (e.g., apply limits, remap axes). - """ - self.twist_output.publish(output_msg) - - -phone_teleop_module = PhoneTeleopModule.blueprint - -__all__ = [ - "PhoneTeleopConfig", - "PhoneTeleopModule", - "phone_teleop_module", -] diff --git a/dimos/teleop/phone/web/static/index.html b/dimos/teleop/phone/web/static/index.html deleted file mode 100644 index 6fad23b6c8..0000000000 --- a/dimos/teleop/phone/web/static/index.html +++ /dev/null @@ -1,393 +0,0 @@ - - - - - - DimOS Phone Teleop - - - -

DimOS Phone Teleop

- - -
- Sensors: off - WS: disconnected -
- - -
- - -
- - -
-

Sensors

-
-
-
X
-
Y
-
Z
- -
Ori
-
0.0°
-
0.0°
-
0.0°
- -
Gyro
-
0.0
-
0.0
-
0.0
-
-
- - - - - - - diff --git a/dimos/teleop/phone/web/teleop_server.ts b/dimos/teleop/phone/web/teleop_server.ts deleted file mode 100755 index 26202cf166..0000000000 --- a/dimos/teleop/phone/web/teleop_server.ts +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --allow-read --allow-run --allow-write --unstable-net - -// WebSocket to LCM Bridge for Phone Teleop -// Forwards twist data from Phone browser to LCM - -import { LCM } from "jsr:@dimos/lcm"; -import { dirname, fromFileUrl, join } from "jsr:@std/path"; - -const PORT = 8444; - -// Resolve paths relative to script location -const scriptDir = dirname(fromFileUrl(import.meta.url)); -const certsDir = join(scriptDir, "../../../../assets/teleop_certs"); -const certPath = join(certsDir, "cert.pem"); -const keyPath = join(certsDir, "key.pem"); - -// Auto-generate self-signed certificates if they don't exist -async function ensureCerts(): Promise<{ cert: string; key: string }> { - try { - const cert = await Deno.readTextFile(certPath); - const key = await Deno.readTextFile(keyPath); - return { cert, key }; - } catch { - console.log("Generating self-signed certificates..."); - await Deno.mkdir(certsDir, { recursive: true }); - const cmd = new Deno.Command("openssl", { - args: [ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", keyPath, "-out", certPath, - "-days", "365", "-nodes", "-subj", "/CN=localhost" - ], - }); - const { code } = await cmd.output(); - if (code !== 0) { - throw new Error("Failed to generate certificates. Is openssl installed?"); - } - console.log("Certificates generated in assets/teleop_certs/"); - return { - cert: await Deno.readTextFile(certPath), - key: await Deno.readTextFile(keyPath), - }; - } -} - -const { cert, key } = await ensureCerts(); - -const lcm = new LCM(); -await lcm.start(); - -// Binds to all interfaces so the phone can reach the server over LAN. -Deno.serve({ port: PORT, cert, key }, async (req) => { - const url = new URL(req.url); - - if (req.headers.get("upgrade") === "websocket") { - const { socket, response } = Deno.upgradeWebSocket(req); - socket.onopen = () => console.log("Phone client connected"); - socket.onclose = () => console.log("Phone client disconnected"); - - // Forward binary LCM packets from browser directly to UDP - socket.binaryType = "arraybuffer"; - socket.onmessage = async (event) => { - if (event.data instanceof ArrayBuffer) { - const packet = new Uint8Array(event.data); - try { - await lcm.publishPacket(packet); - } catch (e) { - console.error("Forward error:", e); - } - } - }; - - return response; - } - - if (url.pathname === "/" || url.pathname === "/index.html") { - const html = await Deno.readTextFile(new URL("./static/index.html", import.meta.url)); - return new Response(html, { headers: { "content-type": "text/html" } }); - } - - return new Response("Not found", { status: 404 }); -}); - -console.log(`Phone Teleop Server: https://localhost:${PORT}`); - -await lcm.run(); diff --git a/dimos/teleop/quest/__init__.py b/dimos/teleop/quest/__init__.py deleted file mode 100644 index 83daf4347b..0000000000 --- a/dimos/teleop/quest/__init__.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Quest teleoperation module.""" - -from dimos.teleop.quest.quest_extensions import ( - ArmTeleopModule, - TwistTeleopModule, - VisualizingTeleopModule, - arm_teleop_module, - twist_teleop_module, - visualizing_teleop_module, -) -from dimos.teleop.quest.quest_teleop_module import ( - Hand, - QuestTeleopConfig, - QuestTeleopModule, - QuestTeleopStatus, - quest_teleop_module, -) -from dimos.teleop.quest.quest_types import ( - Buttons, - QuestControllerState, - ThumbstickState, -) - -__all__ = [ - "ArmTeleopModule", - "Buttons", - "Hand", - "QuestControllerState", - "QuestTeleopConfig", - "QuestTeleopModule", - "QuestTeleopStatus", - "ThumbstickState", - "TwistTeleopModule", - "VisualizingTeleopModule", - # Blueprints - "arm_teleop_module", - "quest_teleop_module", - "twist_teleop_module", - "visualizing_teleop_module", -] diff --git a/dimos/teleop/quest/blueprints.py b/dimos/teleop/quest/blueprints.py deleted file mode 100644 index c46bf657ff..0000000000 --- a/dimos/teleop/quest/blueprints.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleop blueprints for testing and deployment.""" - -from dimos.control.blueprints import ( - coordinator_teleop_dual, - coordinator_teleop_piper, - coordinator_teleop_xarm6, -) -from dimos.core.blueprints import autoconnect -from dimos.core.transport import LCMTransport -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.teleop.quest.quest_extensions import arm_teleop_module, visualizing_teleop_module -from dimos.teleop.quest.quest_types import Buttons - -# ----------------------------------------------------------------------------- -# Quest Teleop Blueprints -# ----------------------------------------------------------------------------- - -# Arm teleop with press-and-hold engage -arm_teleop = autoconnect( - arm_teleop_module(), -).transports( - { - ("left_controller_output", PoseStamped): LCMTransport("/teleop/left_delta", PoseStamped), - ("right_controller_output", PoseStamped): LCMTransport("/teleop/right_delta", PoseStamped), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - -# Arm teleop with Rerun visualization -arm_teleop_visualizing = autoconnect( - visualizing_teleop_module(), -).transports( - { - ("left_controller_output", PoseStamped): LCMTransport("/teleop/left_delta", PoseStamped), - ("right_controller_output", PoseStamped): LCMTransport("/teleop/right_delta", PoseStamped), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - - -# ----------------------------------------------------------------------------- -# Teleop wired to Coordinator (TeleopIK) -# ----------------------------------------------------------------------------- - -# Single XArm6 teleop: right controller -> xarm6 -# Usage: dimos run arm-teleop-xarm6 - -arm_teleop_xarm6 = autoconnect( - arm_teleop_module(task_names={"right": "teleop_xarm"}), - coordinator_teleop_xarm6, -).transports( - { - ("right_controller_output", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - - -# Single Piper teleop: left controller -> piper arm -# Usage: dimos run arm-teleop-piper -arm_teleop_piper = autoconnect( - arm_teleop_module(task_names={"left": "teleop_piper"}), - coordinator_teleop_piper, -).transports( - { - ("left_controller_output", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - - -# Dual arm teleop: right -> piper, left -> xarm6 (TeleopIK) -arm_teleop_dual = autoconnect( - arm_teleop_module(task_names={"right": "teleop_piper", "left": "teleop_xarm"}), - coordinator_teleop_dual, -).transports( - { - ("right_controller_output", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("left_controller_output", PoseStamped): LCMTransport( - "/coordinator/cartesian_command", PoseStamped - ), - ("buttons", Buttons): LCMTransport("/teleop/buttons", Buttons), - } -) - - -__all__ = [ - "arm_teleop", - "arm_teleop_dual", - "arm_teleop_piper", - "arm_teleop_visualizing", - "arm_teleop_xarm6", -] diff --git a/dimos/teleop/quest/quest_extensions.py b/dimos/teleop/quest/quest_extensions.py deleted file mode 100644 index b4e38de546..0000000000 --- a/dimos/teleop/quest/quest_extensions.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Quest teleop module extensions and subclasses. - -Available subclasses: - - ArmTeleopModule: Per-hand press-and-hold engage (X/A hold to track), task name routing - - TwistTeleopModule: Outputs Twist instead of PoseStamped - - VisualizingTeleopModule: Adds Rerun visualization (inherits press-and-hold engage) -""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any - -from dimos.msgs.geometry_msgs import PoseStamped, TwistStamped -from dimos.teleop.quest.quest_teleop_module import Hand, QuestTeleopConfig, QuestTeleopModule -from dimos.teleop.utils.teleop_visualization import ( - visualize_buttons, - visualize_pose, -) - -if TYPE_CHECKING: - from dimos.core import Out - - -@dataclass -class TwistTeleopConfig(QuestTeleopConfig): - """Configuration for TwistTeleopModule.""" - - linear_scale: float = 1.0 - angular_scale: float = 1.0 - - -# Example implementation to show how to extend QuestTeleopModule for different teleop behaviors and outputs. -class TwistTeleopModule(QuestTeleopModule): - """Quest teleop that outputs TwistStamped instead of PoseStamped. - - Config: - - linear_scale: Scale factor for linear (position) values. Default 1.0. - - angular_scale: Scale factor for angular (orientation) values. Default 1.0. - - Outputs: - - left_twist: TwistStamped (linear + angular velocity) - - right_twist: TwistStamped (linear + angular velocity) - - buttons: Buttons (inherited) - """ - - default_config = TwistTeleopConfig - config: TwistTeleopConfig - - left_twist: Out[TwistStamped] - right_twist: Out[TwistStamped] - - def _publish_msg(self, hand: Hand, output_msg: PoseStamped) -> None: - """Convert PoseStamped to TwistStamped, apply scaling, and publish.""" - twist = TwistStamped( - ts=output_msg.ts, - frame_id=output_msg.frame_id, - linear=output_msg.position * self.config.linear_scale, - angular=output_msg.orientation.to_euler() * self.config.angular_scale, - ) - if hand == Hand.LEFT: - self.left_twist.publish(twist) - else: - self.right_twist.publish(twist) - - -@dataclass -class ArmTeleopConfig(QuestTeleopConfig): - """Configuration for ArmTeleopModule. - - Attributes: - task_names: Mapping of Hand -> coordinator task name. Used to set - frame_id on output PoseStamped so the coordinator routes each - hand's commands to the correct TeleopIKTask. - """ - - task_names: dict[str, str] = field(default_factory=dict) - - -class ArmTeleopModule(QuestTeleopModule): - """Quest teleop with per-hand press-and-hold engage and task name routing. - - Each controller's primary button (X for left, A for right) - engages that hand while held, disengages on release. - - When task_names is configured, output PoseStamped messages have their - frame_id set to the task name, enabling the coordinator to route - each hand's commands to the correct TeleopIKTask. - - Outputs: - - left_controller_output: PoseStamped (inherited) - - right_controller_output: PoseStamped (inherited) - - buttons: Buttons (inherited) - """ - - default_config = ArmTeleopConfig - config: ArmTeleopConfig - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - self._task_names: dict[Hand, str] = { - Hand[k.upper()]: v for k, v in self.config.task_names.items() - } - - def _publish_msg(self, hand: Hand, output_msg: PoseStamped) -> None: - """Stamp frame_id with task name and publish.""" - task_name = self._task_names.get(hand) - if task_name: - output_msg = PoseStamped( - position=output_msg.position, - orientation=output_msg.orientation, - ts=output_msg.ts, - frame_id=task_name, - ) - super()._publish_msg(hand, output_msg) - - -class VisualizingTeleopModule(ArmTeleopModule): - """Quest teleop with Rerun visualization. - - Adds visualization of controller poses and trigger values to Rerun. - Useful for debugging and development. - - Outputs: - - left_controller_output: PoseStamped (inherited) - - right_controller_output: PoseStamped (inherited) - - buttons: Buttons (inherited) - """ - - def _get_output_pose(self, hand: Hand) -> PoseStamped | None: - """Get output pose and visualize in Rerun.""" - output_pose = super()._get_output_pose(hand) - - if output_pose is not None: - current_pose = self._current_poses.get(hand) - controller = self._controllers.get(hand) - if current_pose is not None: - label = "left" if hand == Hand.LEFT else "right" - visualize_pose(current_pose, label) - - if controller: - visualize_buttons( - label, - primary=controller.primary, - secondary=controller.secondary, - grip=controller.grip, - trigger=controller.trigger, - ) - return output_pose - - -# Module blueprints for easy instantiation -twist_teleop_module = TwistTeleopModule.blueprint -arm_teleop_module = ArmTeleopModule.blueprint -visualizing_teleop_module = VisualizingTeleopModule.blueprint - -__all__ = [ - "ArmTeleopConfig", - "ArmTeleopModule", - "TwistTeleopModule", - "VisualizingTeleopModule", - "arm_teleop_module", - "twist_teleop_module", - "visualizing_teleop_module", -] diff --git a/dimos/teleop/quest/quest_teleop_module.py b/dimos/teleop/quest/quest_teleop_module.py deleted file mode 100644 index ea77bb5fc0..0000000000 --- a/dimos/teleop/quest/quest_teleop_module.py +++ /dev/null @@ -1,407 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Quest Teleoperation Module. - -Receives VR controller tracking data via LCM from Deno bridge, -transforms from WebXR to robot frame, computes deltas, and publishes PoseStamped commands. -""" - -from dataclasses import dataclass -from enum import IntEnum -from pathlib import Path -import shutil -import signal -import subprocess -import threading -import time -from typing import Any - -from reactivex.disposable import Disposable - -from dimos.core import In, Module, Out, rpc -from dimos.core.module import ModuleConfig -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.msgs.sensor_msgs import Joy -from dimos.teleop.quest.quest_types import Buttons, QuestControllerState -from dimos.teleop.utils.teleop_transforms import webxr_to_robot -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -class Hand(IntEnum): - """Controller hand index.""" - - LEFT = 0 - RIGHT = 1 - - -@dataclass -class QuestTeleopStatus: - """Current teleoperation status.""" - - left_engaged: bool - right_engaged: bool - left_pose: PoseStamped | None - right_pose: PoseStamped | None - buttons: Buttons - - -@dataclass -class QuestTeleopConfig(ModuleConfig): - """Configuration for Quest Teleoperation Module.""" - - control_loop_hz: float = 50.0 - - -class QuestTeleopModule(Module[QuestTeleopConfig]): - """Quest Teleoperation Module for Meta Quest controllers. - - Gets controller data from Deno bridge, computes output poses, and publishes them. Subclass to customize pose - computation, output format, and engage behavior. - - Outputs: - - left_controller_output: PoseStamped (output pose for left hand) - - right_controller_output: PoseStamped (output pose for right hand) - - buttons: Buttons (button states for both controllers) - """ - - default_config = QuestTeleopConfig - - # Inputs from Deno bridge - vr_left_pose: In[PoseStamped] - vr_right_pose: In[PoseStamped] - vr_left_joy: In[Joy] - vr_right_joy: In[Joy] - - # Outputs: delta poses for each controller - left_controller_output: Out[PoseStamped] - right_controller_output: Out[PoseStamped] - buttons: Out[Buttons] - - # ------------------------------------------------------------------------- - # Initialization - # ------------------------------------------------------------------------- - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - - # Engage state (per-hand) - self._is_engaged: dict[Hand, bool] = {Hand.LEFT: False, Hand.RIGHT: False} - self._initial_poses: dict[Hand, PoseStamped | None] = {Hand.LEFT: None, Hand.RIGHT: None} - self._current_poses: dict[Hand, PoseStamped | None] = {Hand.LEFT: None, Hand.RIGHT: None} - self._controllers: dict[Hand, QuestControllerState | None] = { - Hand.LEFT: None, - Hand.RIGHT: None, - } - self._lock = threading.RLock() - - # Control loop - self._control_loop_thread: threading.Thread | None = None - self._stop_event = threading.Event() - - # Deno bridge server - self._server_process: subprocess.Popen[bytes] | None = None - self._server_script = Path(__file__).parent / "web" / "teleop_server.ts" - - # ------------------------------------------------------------------------- - # Lifecycle - # ------------------------------------------------------------------------- - - @rpc - def start(self) -> None: - super().start() - - input_streams = { - "vr_left_pose": (self.vr_left_pose, lambda msg: self._on_pose(Hand.LEFT, msg)), - "vr_right_pose": (self.vr_right_pose, lambda msg: self._on_pose(Hand.RIGHT, msg)), - "vr_left_joy": (self.vr_left_joy, lambda msg: self._on_joy(Hand.LEFT, msg)), - "vr_right_joy": (self.vr_right_joy, lambda msg: self._on_joy(Hand.RIGHT, msg)), - } - connected = [] - for name, (stream, handler) in input_streams.items(): - if not (stream and stream.transport): # type: ignore[attr-defined] - logger.warning(f"Stream '{name}' has no transport — skipping") - continue - self._disposables.add(Disposable(stream.subscribe(handler))) # type: ignore[attr-defined] - connected.append(name) - - if connected: - logger.info(f"Subscribed to: {', '.join(connected)}") - - self._start_server() - logger.info("Quest Teleoperation Module started") - - @rpc - def stop(self) -> None: - self._stop_control_loop() - self._stop_server() - super().stop() - - # ------------------------------------------------------------------------- - # Internal engage/disengage (assumes lock is held) - # ------------------------------------------------------------------------- - - def _engage(self, hand: Hand | None = None) -> bool: - """Engage a hand. Assumes self._lock is held.""" - hands = [hand] if hand is not None else list(Hand) - for h in hands: - pose = self._current_poses.get(h) - if pose is None: - logger.error(f"Engage failed: {h.name.lower()} controller has no data") - return False - self._initial_poses[h] = pose - self._is_engaged[h] = True - logger.info(f"{h.name} engaged.") - return True - - def _disengage(self, hand: Hand | None = None) -> None: - """Disengage a hand. Assumes self._lock is held.""" - hands = [hand] if hand is not None else list(Hand) - for h in hands: - self._is_engaged[h] = False - logger.info(f"{h.name} disengaged.") - - def get_status(self) -> QuestTeleopStatus: - with self._lock: - left = self._controllers.get(Hand.LEFT) - right = self._controllers.get(Hand.RIGHT) - return QuestTeleopStatus( - left_engaged=self._is_engaged[Hand.LEFT], - right_engaged=self._is_engaged[Hand.RIGHT], - left_pose=self._current_poses.get(Hand.LEFT), - right_pose=self._current_poses.get(Hand.RIGHT), - buttons=Buttons.from_controllers(left, right), - ) - - # ------------------------------------------------------------------------- - # Callbacks and Control Loop - # ------------------------------------------------------------------------- - - def _on_pose(self, hand: Hand, pose_stamped: PoseStamped) -> None: - """Callback for controller pose, converting WebXR to robot frame.""" - is_left = hand == Hand.LEFT - robot_pose_stamped = webxr_to_robot(pose_stamped, is_left_controller=is_left) - with self._lock: - self._current_poses[hand] = robot_pose_stamped - - def _on_joy(self, hand: Hand, joy: Joy) -> None: - """Callback for Joy message, parsing into QuestControllerState.""" - is_left = hand == Hand.LEFT - try: - controller = QuestControllerState.from_joy(joy, is_left=is_left) - except ValueError: - logger.warning( - f"Malformed Joy for {hand.name}: axes={len(joy.axes or [])}, buttons={len(joy.buttons or [])}" - ) - return - with self._lock: - self._controllers[hand] = controller - - # ------------------------------------------------------------------------- - # Deno Bridge Server - # ------------------------------------------------------------------------- - - def _start_server(self) -> None: - """Launch the Deno WebSocket-to-LCM bridge server as a subprocess.""" - if self._server_process is not None and self._server_process.poll() is None: - logger.warning("Deno bridge already running", pid=self._server_process.pid) - return - - if shutil.which("deno") is None: - logger.error( - "Deno is not installed. Install it with: curl -fsSL https://deno.land/install.sh | sh" - ) - return - - script = str(self._server_script) - cmd = [ - "deno", - "run", - "--allow-net", - "--allow-read", - "--allow-run", - "--allow-write", - "--unstable-net", - script, - ] - try: - self._server_process = subprocess.Popen(cmd) - logger.info(f"Deno bridge server started (pid {self._server_process.pid})") - except OSError as e: - logger.error(f"Failed to start Deno bridge: {e}") - - def _stop_server(self) -> None: - """Terminate the Deno bridge server subprocess.""" - if self._server_process is None or self._server_process.poll() is not None: - self._server_process = None - return - - logger.info("Stopping Deno bridge server", pid=self._server_process.pid) - self._server_process.send_signal(signal.SIGTERM) - try: - self._server_process.wait(timeout=3) - except subprocess.TimeoutExpired: - logger.warning( - "Deno bridge did not exit, sending SIGKILL", pid=self._server_process.pid - ) - self._server_process.kill() - try: - self._server_process.wait(timeout=5) - except subprocess.TimeoutExpired: - logger.error("Deno bridge did not exit after SIGKILL") - logger.info("Deno bridge server stopped") - self._server_process = None - - def _start_control_loop(self) -> None: - """Start the control loop thread.""" - if self._control_loop_thread is not None and self._control_loop_thread.is_alive(): - return - - self._stop_event.clear() - self._control_loop_thread = threading.Thread( - target=self._control_loop, - daemon=True, - name="QuestTeleopControlLoop", - ) - self._control_loop_thread.start() - logger.info(f"Control loop started at {self.config.control_loop_hz} Hz") - - def _stop_control_loop(self) -> None: - """Stop the control loop thread.""" - self._stop_event.set() - if self._control_loop_thread is not None: - self._control_loop_thread.join(timeout=1.0) - self._control_loop_thread = None - logger.info("Control loop stopped") - - def _control_loop(self) -> None: - """ - Holds self._lock for the entire iteration so overridable methods - don't need to acquire it themselves. - """ - period = 1.0 / self.config.control_loop_hz - - while not self._stop_event.is_set(): - loop_start = time.perf_counter() - try: - with self._lock: - self._handle_engage() - - for hand in Hand: - if not self._should_publish(hand): - continue - output_pose = self._get_output_pose(hand) - if output_pose is not None: - self._publish_msg(hand, output_pose) - - # Always publish buttons regardless of engage state, - # so UI/listeners can react to button presses (e.g., trigger engage). - left = self._controllers.get(Hand.LEFT) - right = self._controllers.get(Hand.RIGHT) - self._publish_button_state(left, right) - except Exception: - logger.exception("Error in teleop control loop") - - elapsed = time.perf_counter() - loop_start - sleep_time = period - elapsed - if sleep_time > 0: - self._stop_event.wait(sleep_time) - - # ------------------------------------------------------------------------- - # Control Loop Internals - # ------------------------------------------------------------------------- - - def _handle_engage(self) -> None: - """Check for engage button press and update per-hand engage state. - - Override to customize which button/action triggers engage. - Default: Each controller's primary button (X/A) hold engages that hand. - """ - for hand in Hand: - controller = self._controllers.get(hand) - if controller is None: - continue - if controller.primary: - if not self._is_engaged[hand]: - self._engage(hand) - else: - if self._is_engaged[hand]: - self._disengage(hand) - - def _should_publish(self, hand: Hand) -> bool: - """Check if we should publish commands for a hand. - - Override to add custom conditions. - Default: Returns True if the hand is engaged. - """ - return self._is_engaged[hand] - - def _get_output_pose(self, hand: Hand) -> PoseStamped | None: - """Get the pose to publish for a controller. - - Override to customize pose computation (e.g., send absolute pose, - apply scaling, add filtering). - Default: Computes delta from initial pose. - """ - current_pose = self._current_poses.get(hand) - initial_pose = self._initial_poses.get(hand) - - if current_pose is None or initial_pose is None: - return None - - delta = current_pose - initial_pose - return PoseStamped( - position=delta.position, - orientation=delta.orientation, - ts=current_pose.ts, - frame_id=current_pose.frame_id, - ) - - def _publish_msg(self, hand: Hand, output_msg: PoseStamped) -> None: - """Publish message for a controller. - - Override to customize output (e.g., convert to Twist, scale values). - """ - if hand == Hand.LEFT: - self.left_controller_output.publish(output_msg) - else: - self.right_controller_output.publish(output_msg) - - def _publish_button_state( - self, - left: QuestControllerState | None, - right: QuestControllerState | None, - ) -> None: - """Publish button states for both controllers. - - Override to customize button output format (e.g., different bit layout, - keep analog values, add extra streams). - """ - buttons = Buttons.from_controllers(left, right) - self.buttons.publish(buttons) - - -quest_teleop_module = QuestTeleopModule.blueprint - -__all__ = [ - "Hand", - "QuestTeleopConfig", - "QuestTeleopModule", - "QuestTeleopStatus", - "quest_teleop_module", -] diff --git a/dimos/teleop/quest/quest_types.py b/dimos/teleop/quest/quest_types.py deleted file mode 100644 index 9e5616101d..0000000000 --- a/dimos/teleop/quest/quest_types.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Quest controller types with nice API for parsing Joy messages.""" - -from dataclasses import dataclass, field -from typing import ClassVar - -from dimos.msgs.sensor_msgs import Joy -from dimos.msgs.std_msgs import UInt32 - - -@dataclass -class ThumbstickState: - """State of a thumbstick with X/Y axes.""" - - x: float = 0.0 - y: float = 0.0 - - -@dataclass -class QuestControllerState: - """Parsed Quest controller state from Joy message with no data loss. - - Preserves full-fidelity analog values (trigger, grip as floats, thumbstick axes) - from the raw Joy message in a readable format. Use this when you need analog - precision (e.g., proportional grip control). Subclasses can publish this - alongside Buttons for float access. - - Axes layout: - 0: thumbstick X, 1: thumbstick Y, 2: trigger (analog), 3: grip (analog) - Button indices (digital, 0 or 1): - 0: trigger, 1: grip, 2: touchpad, 3: thumbstick, - 4: X/A, 5: Y/B, 6: menu - """ - - EXPECTED_AXES: ClassVar[int] = 4 - EXPECTED_BUTTONS: ClassVar[int] = 7 - - is_left: bool = True - # Analog values (0.0-1.0) - trigger: float = 0.0 - grip: float = 0.0 - # Digital buttons - touchpad: bool = False - thumbstick_press: bool = False - primary: bool = False # X on left, A on right - secondary: bool = False # Y on left, B on right - menu: bool = False - # Thumbstick axes - thumbstick: ThumbstickState = field(default_factory=ThumbstickState) - - @classmethod - def from_joy(cls, joy: Joy, is_left: bool = True) -> "QuestControllerState": - """Create QuestControllerState from Joy message. - Expected axes: [thumbstick_x, thumbstick_y, trigger_analog, grip_analog] - Expected buttons: [trigger, grip, touchpad, thumbstick, X/A, Y/B, menu] - Raises: - ValueError: If Joy message doesn't have expected Quest controller format. - """ - buttons = joy.buttons or [] - axes = joy.axes or [] - - if len(buttons) < cls.EXPECTED_BUTTONS: - raise ValueError(f"Expected {cls.EXPECTED_BUTTONS} buttons, got {len(buttons)}") - if len(axes) < cls.EXPECTED_AXES: - raise ValueError(f"Expected {cls.EXPECTED_AXES} axes, got {len(axes)}") - - return cls( - is_left=is_left, - trigger=float(axes[2]), - grip=float(axes[3]), - touchpad=buttons[2] > 0.5, - thumbstick_press=buttons[3] > 0.5, - primary=buttons[4] > 0.5, - secondary=buttons[5] > 0.5, - menu=buttons[6] > 0.5, - thumbstick=ThumbstickState(x=float(axes[0]), y=float(axes[1])), - ) - - -class Buttons(UInt32): - """Packed button states for both controllers in a single UInt32. - - All values are collapsed to bools for lightweight transport. Analog values - (trigger, grip) are thresholded at 0.5. If you need the original float - values, access them from QuestControllerState and publish them in a subclass. - - Bit layout: - Left (bits 0-6): trigger, grip, touchpad, thumbstick, primary, secondary, menu - Right (bits 8-14): trigger, grip, touchpad, thumbstick, primary, secondary, menu - """ - - # Bit positions - BITS = { - "left_trigger": 0, - "left_grip": 1, - "left_touchpad": 2, - "left_thumbstick": 3, - "left_primary": 4, - "left_secondary": 5, - "left_menu": 6, - "right_trigger": 8, - "right_grip": 9, - "right_touchpad": 10, - "right_thumbstick": 11, - "right_primary": 12, - "right_secondary": 13, - "right_menu": 14, - } - - def __getattr__(self, name: str) -> bool: - if name in Buttons.BITS: - return bool(self.data & (1 << Buttons.BITS[name])) - raise AttributeError(f"'{type(self).__name__}' has no attribute '{name}'") - - def __setattr__(self, name: str, value: bool) -> None: - if name in Buttons.BITS: - if value: - self.data |= 1 << Buttons.BITS[name] - else: - self.data &= ~(1 << Buttons.BITS[name]) - else: - super().__setattr__(name, value) - - @classmethod - def from_controllers( - cls, - left: "QuestControllerState | None", - right: "QuestControllerState | None", - ) -> "Buttons": - """Create Buttons from two QuestControllerState instances.""" - # Safe: cls() calls UInt32.__init__ which sets self.data = 0 before bit ops. - buttons = cls() - - if left: - buttons.left_trigger = left.trigger > 0.5 - buttons.left_grip = left.grip > 0.5 - buttons.left_touchpad = left.touchpad - buttons.left_thumbstick = left.thumbstick_press - buttons.left_primary = left.primary - buttons.left_secondary = left.secondary - buttons.left_menu = left.menu - - if right: - buttons.right_trigger = right.trigger > 0.5 - buttons.right_grip = right.grip > 0.5 - buttons.right_touchpad = right.touchpad - buttons.right_thumbstick = right.thumbstick_press - buttons.right_primary = right.primary - buttons.right_secondary = right.secondary - buttons.right_menu = right.menu - - return buttons - - -__all__ = ["Buttons", "QuestControllerState", "ThumbstickState"] diff --git a/dimos/teleop/quest/web/README.md b/dimos/teleop/quest/web/README.md deleted file mode 100644 index 9a7afbfe03..0000000000 --- a/dimos/teleop/quest/web/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Quest Teleop Web - -WebXR client and server for Quest 3 VR teleoperation. - -## Components - -### teleop_server.ts - -Deno server that bridges WebSocket and LCM: -- Serves WebXR client over HTTPS (required for Quest) -- Forwards controller data from browser to LCM - -### static/index.html - -WebXR client running on Quest 3: -- Captures controller poses at ~80Hz -- Sends PoseStamped and Joy messages via WebSocket -- Requires internet connection (loads `@dimos/msgs` from CDN at runtime) - -## Running - -From the repository root (`dimos/`): - -```bash -./dimos/teleop/quest/web/teleop_server.ts -``` - -Server starts at `https://localhost:8443` - -SSL certificates are generated automatically on first run in `assets/teleop_certs/`. - -## Message Flow - -``` -Quest Browser Deno Server Python - │ │ │ - │── PoseStamped (left) ────────→ │── vr_left_pose ───────────→ │ - │── PoseStamped (right) ───────→ │── vr_right_pose ──────────→ │ - │── Joy (left controller) ─────→ │── vr_left_joy ────────────→ │ - │── Joy (right controller) ────→ │── vr_right_joy ───────────→ │ -``` - -## LCM Topics - -| Topic | Type | Description | -|-------|------|-------------| -| `vr_left_pose` | PoseStamped | Left controller pose (WebXR frame) | -| `vr_right_pose` | PoseStamped | Right controller pose (WebXR frame) | -| `vr_left_joy` | Joy | Left controller buttons/axes | -| `vr_right_joy` | Joy | Right controller buttons/axes | - -## Joy Message Format - -Quest controller data is packed into Joy messages: - -**Axes** (indices 0-3): -- 0: thumbstick X (-1.0 to 1.0) -- 1: thumbstick Y (-1.0 to 1.0) -- 2: trigger (analog 0.0-1.0) -- 3: grip (analog 0.0-1.0) - -**Buttons** (indices 0-6, digital 0 or 1): -- 0: trigger (pressed) -- 1: grip (pressed) -- 2: touchpad -- 3: thumbstick press -- 4: X/A (primary) -- 5: Y/B (secondary) -- 6: menu diff --git a/dimos/teleop/quest/web/static/index.html b/dimos/teleop/quest/web/static/index.html deleted file mode 100644 index 507d493011..0000000000 --- a/dimos/teleop/quest/web/static/index.html +++ /dev/null @@ -1,409 +0,0 @@ - - - - - - Quest 3 VR Teleop - - - -
-

DimOS Quest-3 Teleop

-
Ready to connect
- - -
- - - - - diff --git a/dimos/teleop/quest/web/teleop_server.ts b/dimos/teleop/quest/web/teleop_server.ts deleted file mode 100755 index 2bff24b34f..0000000000 --- a/dimos/teleop/quest/web/teleop_server.ts +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --allow-read --allow-run --allow-write --unstable-net - -// WebSocket to LCM Bridge for Quest VR Teleop -// Forwards controller data from browser to LCM - -import { LCM } from "jsr:@dimos/lcm"; -import { dirname, fromFileUrl, join } from "jsr:@std/path"; - -const PORT = 8443; - -// Resolve paths relative to script location -const scriptDir = dirname(fromFileUrl(import.meta.url)); -const certsDir = join(scriptDir, "../../../../assets/teleop_certs"); -const certPath = join(certsDir, "cert.pem"); -const keyPath = join(certsDir, "key.pem"); - -// Auto-generate self-signed certificates if they don't exist -async function ensureCerts(): Promise<{ cert: string; key: string }> { - try { - const cert = await Deno.readTextFile(certPath); - const key = await Deno.readTextFile(keyPath); - return { cert, key }; - } catch { - console.log("Generating self-signed certificates..."); - await Deno.mkdir(certsDir, { recursive: true }); - const cmd = new Deno.Command("openssl", { - args: [ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", keyPath, "-out", certPath, - "-days", "365", "-nodes", "-subj", "/CN=localhost" - ], - }); - const { code } = await cmd.output(); - if (code !== 0) { - throw new Error("Failed to generate certificates. Is openssl installed?"); - } - console.log("Certificates generated in assets/teleop_certs/"); - return { - cert: await Deno.readTextFile(certPath), - key: await Deno.readTextFile(keyPath), - }; - } -} - -const { cert, key } = await ensureCerts(); - -const lcm = new LCM(); -await lcm.start(); - -Deno.serve({ port: PORT, cert, key }, async (req) => { - const url = new URL(req.url); - - if (req.headers.get("upgrade") === "websocket") { - const { socket, response } = Deno.upgradeWebSocket(req); - socket.onopen = () => console.log("Client connected"); - socket.onclose = () => console.log("Client disconnected"); - - // Forward binary LCM packets from browser directly to UDP - socket.binaryType = "arraybuffer"; - socket.onmessage = async (event) => { - if (event.data instanceof ArrayBuffer) { - const packet = new Uint8Array(event.data); - try { - await lcm.publishPacket(packet); - } catch (e) { - console.error("Forward error:", e); - } - } - }; - - return response; - } - - if (url.pathname === "/" || url.pathname === "/index.html") { - const html = await Deno.readTextFile(new URL("./static/index.html", import.meta.url)); - return new Response(html, { headers: { "content-type": "text/html" } }); - } - - return new Response("Not found", { status: 404 }); -}); - -console.log(`Server: https://localhost:${PORT}`); - -await lcm.run(); diff --git a/dimos/teleop/utils/__init__.py b/dimos/teleop/utils/__init__.py deleted file mode 100644 index ae8c375e8f..0000000000 --- a/dimos/teleop/utils/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleoperation utilities.""" diff --git a/dimos/teleop/utils/teleop_transforms.py b/dimos/teleop/utils/teleop_transforms.py deleted file mode 100644 index 15fd3be120..0000000000 --- a/dimos/teleop/utils/teleop_transforms.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleop transform utilities for VR coordinate transforms.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import numpy as np -from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import PoseStamped -from dimos.utils.transform_utils import matrix_to_pose, pose_to_matrix - -if TYPE_CHECKING: - from numpy.typing import NDArray - -# Coordinate frame transformation from VR (WebXR) to robot frame -# WebXR: X=right, Y=up, Z=back (towards user) -# Robot: X=forward, Y=left, Z=up -VR_TO_ROBOT_FRAME: NDArray[np.float64] = np.array( - [ - [0, 0, -1, 0], # Robot X = -VR Z (forward) - [-1, 0, 0, 0], # Robot Y = -VR X (left) - [0, 1, 0, 0], # Robot Z = +VR Y (up) - [0, 0, 0, 1], - ], - dtype=np.float64, -) - - -def webxr_to_robot( - pose_stamped: PoseStamped, - is_left_controller: bool = True, -) -> PoseStamped: - """Transform VR controller pose to robot coordinate frame. - - Args: - pose_stamped: PoseStamped from VR controller in WebXR frame. - is_left_controller: True for left controller (+90 deg Z rotation), - False for right controller (-90 deg Z rotation). - - Returns: - PoseStamped in robot frame (preserves original ts and frame_id). - """ - vr_matrix = pose_to_matrix(pose_stamped) - - # Apply controller alignment rotation - # Left controller rotates +90 deg around Z, right rotates -90 deg - direction = 1 if is_left_controller else -1 - z_rotation = R.from_euler("z", 90 * direction, degrees=True).as_matrix() - vr_matrix[:3, :3] = vr_matrix[:3, :3] @ z_rotation - - # Apply VR to robot frame transformation - robot_matrix = VR_TO_ROBOT_FRAME @ vr_matrix - robot_pose = matrix_to_pose(robot_matrix) - - return PoseStamped( - position=robot_pose.position, - orientation=robot_pose.orientation, - ts=pose_stamped.ts, - frame_id=pose_stamped.frame_id, - ) - - -__all__ = ["VR_TO_ROBOT_FRAME", "webxr_to_robot"] diff --git a/dimos/teleop/utils/teleop_visualization.py b/dimos/teleop/utils/teleop_visualization.py deleted file mode 100644 index a59b0666ef..0000000000 --- a/dimos/teleop/utils/teleop_visualization.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Teleop visualization utilities for Rerun.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import rerun as rr - -from dimos.utils.logging_config import setup_logger - -if TYPE_CHECKING: - from dimos.msgs.geometry_msgs import PoseStamped - -logger = setup_logger() - - -def visualize_pose(pose_stamped: PoseStamped, controller_label: str) -> None: - """Visualize controller absolute pose in Rerun.""" - try: - rr.log(f"world/teleop/{controller_label}_controller", pose_stamped.to_rerun()) # type: ignore[no-untyped-call] - rr.log(f"world/teleop/{controller_label}_controller/axes", rr.TransformAxes3D(0.10)) # type: ignore[attr-defined] - except Exception as e: - logger.debug(f"Failed to log {controller_label} controller to Rerun: {e}") - - -def visualize_buttons( - controller_label: str, - primary: bool = False, - secondary: bool = False, - grip: float = 0.0, - trigger: float = 0.0, -) -> None: - """Visualize button states in Rerun as scalar time series.""" - try: - base_path = f"world/teleop/{controller_label}_controller" - rr.log(f"{base_path}/primary", rr.Scalars(float(primary))) # type: ignore[attr-defined] - rr.log(f"{base_path}/secondary", rr.Scalars(float(secondary))) # type: ignore[attr-defined] - rr.log(f"{base_path}/grip", rr.Scalars(grip)) # type: ignore[attr-defined] - rr.log(f"{base_path}/trigger", rr.Scalars(trigger)) # type: ignore[attr-defined] - except Exception as e: - logger.debug(f"Failed to log {controller_label} buttons to Rerun: {e}") - - -__all__ = ["visualize_buttons", "visualize_pose"] diff --git a/dimos/types/constants.py b/dimos/types/constants.py deleted file mode 100644 index b02726cb0b..0000000000 --- a/dimos/types/constants.py +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -class Colors: - GREEN_PRINT_COLOR: str = "\033[32m" - YELLOW_PRINT_COLOR: str = "\033[33m" - RED_PRINT_COLOR: str = "\033[31m" - BLUE_PRINT_COLOR: str = "\033[34m" - MAGENTA_PRINT_COLOR: str = "\033[35m" - CYAN_PRINT_COLOR: str = "\033[36m" - WHITE_PRINT_COLOR: str = "\033[37m" - RESET_COLOR: str = "\033[0m" diff --git a/dimos/types/manipulation.py b/dimos/types/manipulation.py deleted file mode 100644 index 507b9e9b85..0000000000 --- a/dimos/types/manipulation.py +++ /dev/null @@ -1,168 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC -from dataclasses import dataclass, field -from enum import Enum -import time -from typing import TYPE_CHECKING, Any, Literal, TypedDict -import uuid - -import numpy as np - -from dimos.types.vector import Vector - -if TYPE_CHECKING: - import open3d as o3d # type: ignore[import-untyped] - - -class ConstraintType(Enum): - """Types of manipulation constraints.""" - - TRANSLATION = "translation" - ROTATION = "rotation" - FORCE = "force" - - -@dataclass -class AbstractConstraint(ABC): - """Base class for all manipulation constraints.""" - - description: str = "" - id: str = field(default_factory=lambda: str(uuid.uuid4())[:8]) - - -@dataclass -class TranslationConstraint(AbstractConstraint): - """Constraint parameters for translational movement along a single axis.""" - - translation_axis: Literal["x", "y", "z"] = None # type: ignore[assignment] # Axis to translate along - reference_point: Vector | None = None - bounds_min: Vector | None = None # For bounded translation - bounds_max: Vector | None = None # For bounded translation - target_point: Vector | None = None # For relative positioning - - -@dataclass -class RotationConstraint(AbstractConstraint): - """Constraint parameters for rotational movement around a single axis.""" - - rotation_axis: Literal["roll", "pitch", "yaw"] = None # type: ignore[assignment] # Axis to rotate around - start_angle: Vector | None = None # Angle values applied to the specified rotation axis - end_angle: Vector | None = None # Angle values applied to the specified rotation axis - pivot_point: Vector | None = None # Point of rotation - secondary_pivot_point: Vector | None = None # For double point rotations - - -@dataclass -class ForceConstraint(AbstractConstraint): - """Constraint parameters for force application.""" - - max_force: float = 0.0 # Maximum force in newtons - min_force: float = 0.0 # Minimum force in newtons - force_direction: Vector | None = None # Direction of force application - - -class ObjectData(TypedDict, total=False): - """Data about an object in the manipulation scene.""" - - # Basic detection information - object_id: int # Unique ID for the object - bbox: list[float] # Bounding box [x1, y1, x2, y2] - depth: float # Depth in meters from Metric3d - confidence: float # Detection confidence - class_id: int # Class ID from the detector - label: str # Semantic label (e.g., 'cup', 'table') - movement_tolerance: float # (0.0 = immovable, 1.0 = freely movable) - segmentation_mask: np.ndarray # type: ignore[type-arg] # Binary mask of the object's pixels - - # 3D pose and dimensions - position: dict[str, float] | Vector # 3D position {x, y, z} or Vector - rotation: dict[str, float] | Vector # 3D rotation {roll, pitch, yaw} or Vector - size: dict[str, float] # Object dimensions {width, height, depth} - - # Point cloud data - point_cloud: "o3d.geometry.PointCloud" # Open3D point cloud object - point_cloud_numpy: np.ndarray # type: ignore[type-arg] # Nx6 array of XYZRGB points - color: np.ndarray # type: ignore[type-arg] # RGB color for visualization [R, G, B] - - -class ManipulationMetadata(TypedDict, total=False): - """Typed metadata for manipulation constraints.""" - - timestamp: float - objects: dict[str, ObjectData] - - -@dataclass -class ManipulationTaskConstraint: - """Set of constraints for a specific manipulation action.""" - - constraints: list[AbstractConstraint] = field(default_factory=list) - - def add_constraint(self, constraint: AbstractConstraint) -> None: - """Add a constraint to this set.""" - if constraint not in self.constraints: - self.constraints.append(constraint) - - def get_constraints(self) -> list[AbstractConstraint]: - """Get all constraints in this set.""" - return self.constraints - - -@dataclass -class ManipulationTask: - """Complete definition of a manipulation task.""" - - description: str - target_object: str # Semantic label of target object - target_point: tuple[float, float] | None = ( - None # (X,Y) point in pixel-space of the point to manipulate on target object - ) - metadata: ManipulationMetadata = field(default_factory=dict) # type: ignore[assignment] - timestamp: float = field(default_factory=time.time) - task_id: str = "" - result: dict[str, Any] | None = None # Any result data from the task execution - constraints: list[AbstractConstraint] | ManipulationTaskConstraint | AbstractConstraint = field( - default_factory=list - ) - - def add_constraint(self, constraint: AbstractConstraint) -> None: - """Add a constraint to this manipulation task.""" - # If constraints is a ManipulationTaskConstraint object - if isinstance(self.constraints, ManipulationTaskConstraint): - self.constraints.add_constraint(constraint) - return - - # If constraints is a single AbstractConstraint, convert to list - if isinstance(self.constraints, AbstractConstraint): - self.constraints = [self.constraints, constraint] - return - - # If constraints is a list, append to it - # This will also handle empty lists (the default case) - self.constraints.append(constraint) - - def get_constraints(self) -> list[AbstractConstraint]: - """Get all constraints in this manipulation task.""" - # If constraints is a ManipulationTaskConstraint object - if isinstance(self.constraints, ManipulationTaskConstraint): - return self.constraints.get_constraints() - - # If constraints is a single AbstractConstraint, return as list - if isinstance(self.constraints, AbstractConstraint): - return [self.constraints] - - # If constraints is a list (including empty list), return it - return self.constraints diff --git a/dimos/types/robot_capabilities.py b/dimos/types/robot_capabilities.py deleted file mode 100644 index 9a3f5da14e..0000000000 --- a/dimos/types/robot_capabilities.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Robot capabilities module for defining robot functionality.""" - -from enum import Enum, auto - - -class RobotCapability(Enum): - """Enum defining possible robot capabilities.""" - - MANIPULATION = auto() - VISION = auto() - AUDIO = auto() - SPEECH = auto() - LOCOMOTION = auto() diff --git a/dimos/types/robot_location.py b/dimos/types/robot_location.py deleted file mode 100644 index 78077092f8..0000000000 --- a/dimos/types/robot_location.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -RobotLocation type definition for storing and managing robot location data. -""" - -from dataclasses import dataclass, field -import time -from typing import Any -import uuid - - -@dataclass -class RobotLocation: - """ - Represents a named location in the robot's spatial memory. - - This class stores the position, rotation, and descriptive metadata for - locations that the robot can remember and navigate to. - - Attributes: - name: Human-readable name of the location (e.g., "kitchen", "office") - position: 3D position coordinates (x, y, z) - rotation: 3D rotation angles in radians (roll, pitch, yaw) - frame_id: ID of the associated video frame if available - timestamp: Time when the location was recorded - location_id: Unique identifier for this location - metadata: Additional metadata for the location - """ - - name: str - position: tuple[float, float, float] - rotation: tuple[float, float, float] - frame_id: str | None = None - timestamp: float = field(default_factory=time.time) - location_id: str = field(default_factory=lambda: f"loc_{uuid.uuid4().hex[:8]}") - metadata: dict[str, Any] = field(default_factory=dict) - - def __post_init__(self) -> None: - """Validate and normalize the position and rotation tuples.""" - # Ensure position is a tuple of 3 floats - if len(self.position) == 2: - self.position = (self.position[0], self.position[1], 0.0) - else: - self.position = tuple(float(x) for x in self.position) # type: ignore[assignment] - - # Ensure rotation is a tuple of 3 floats - if len(self.rotation) == 1: - self.rotation = (0.0, 0.0, self.rotation[0]) - else: - self.rotation = tuple(float(x) for x in self.rotation) # type: ignore[assignment] - - def to_vector_metadata(self) -> dict[str, Any]: - """ - Convert the location to metadata format for storing in a vector database. - - Returns: - Dictionary with metadata fields compatible with vector DB storage - """ - metadata = { - "pos_x": float(self.position[0]), - "pos_y": float(self.position[1]), - "pos_z": float(self.position[2]), - "rot_x": float(self.rotation[0]), - "rot_y": float(self.rotation[1]), - "rot_z": float(self.rotation[2]), - "timestamp": self.timestamp, - "location_id": self.location_id, - "location_name": self.name, - "description": self.name, # Makes it searchable by text - } - - # Only add frame_id if it's not None - if self.frame_id is not None: - metadata["frame_id"] = self.frame_id - - return metadata - - @classmethod - def from_vector_metadata(cls, metadata: dict[str, Any]) -> "RobotLocation": - """ - Create a RobotLocation object from vector database metadata. - - Args: - metadata: Dictionary with metadata from vector database - - Returns: - RobotLocation object - """ - return cls( - name=metadata.get("location_name", "unknown"), - position=( - metadata.get("pos_x", 0.0), - metadata.get("pos_y", 0.0), - metadata.get("pos_z", 0.0), - ), - rotation=( - metadata.get("rot_x", 0.0), - metadata.get("rot_y", 0.0), - metadata.get("rot_z", 0.0), - ), - frame_id=metadata.get("frame_id"), - timestamp=metadata.get("timestamp", time.time()), - location_id=metadata.get("location_id", f"loc_{uuid.uuid4().hex[:8]}"), - metadata={ - k: v - for k, v in metadata.items() - if k - not in [ - "pos_x", - "pos_y", - "pos_z", - "rot_x", - "rot_y", - "rot_z", - "timestamp", - "location_id", - "frame_id", - "location_name", - "description", - ] - }, - ) - - def __str__(self) -> str: - return f"[RobotPosition name:{self.name} pos:{self.position} rot:{self.rotation})]" diff --git a/dimos/types/ros_polyfill.py b/dimos/types/ros_polyfill.py deleted file mode 100644 index 4bad99740d..0000000000 --- a/dimos/types/ros_polyfill.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -try: - from geometry_msgs.msg import Vector3 # type: ignore[attr-defined] -except ImportError: - from dimos.msgs.geometry_msgs import Vector3 - -try: - from geometry_msgs.msg import ( # type: ignore[attr-defined] - Point, - Pose, - Quaternion, - Twist, - ) - from nav_msgs.msg import OccupancyGrid, Odometry # type: ignore[attr-defined] - from std_msgs.msg import Header # type: ignore[attr-defined] -except ImportError: - from dimos_lcm.geometry_msgs import ( # type: ignore[no-redef] - Point, - Pose, - Quaternion, - Twist, - ) - from dimos_lcm.nav_msgs import OccupancyGrid, Odometry # type: ignore[no-redef] - from dimos_lcm.std_msgs import Header # type: ignore[no-redef] - -__all__ = [ - "Header", - "OccupancyGrid", - "Odometry", - "Point", - "Pose", - "Quaternion", - "Twist", - "Vector3", -] diff --git a/dimos/types/sample.py b/dimos/types/sample.py deleted file mode 100644 index 16ca96b611..0000000000 --- a/dimos/types/sample.py +++ /dev/null @@ -1,583 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import builtins -from collections import OrderedDict -from collections.abc import Sequence -from enum import Enum -import json -import logging -from pathlib import Path -from typing import Annotated, Any, Literal, Union, get_origin - -from datasets import Dataset # type: ignore[import-not-found] -from gymnasium import spaces # type: ignore[import-not-found] -from jsonref import replace_refs # type: ignore[import-not-found] -from mbodied.data.utils import to_features # type: ignore[import-not-found] -from mbodied.utils.import_utils import smart_import # type: ignore[import-not-found] -import numpy as np -from pydantic import BaseModel, ConfigDict, ValidationError -from pydantic.fields import FieldInfo -from pydantic_core import from_json -import torch - -Flattenable = Annotated[Literal["dict", "np", "pt", "list"], "Numpy, PyTorch, list, or dict"] - - -class Sample(BaseModel): - """A base model class for serializing, recording, and manipulating arbitray data. - - It was designed to be extensible, flexible, yet strongly typed. In addition to - supporting any json API out of the box, it can be used to represent - arbitrary action and observation spaces in robotics and integrates seemlessly with H5, Gym, Arrow, - PyTorch, DSPY, numpy, and HuggingFace. - - Methods: - schema: Get a simplified json schema of your data. - to: Convert the Sample instance to a different container type: - - - default_value: Get the default value for the Sample instance. - unflatten: Unflatten a one-dimensional array or dictionary into a Sample instance. - flatten: Flatten the Sample instance into a one-dimensional array or dictionary. - space_for: Default Gym space generation for a given value. - init_from: Initialize a Sample instance from a given value. - from_space: Generate a Sample instance from a Gym space. - pack_from: Pack a list of samples into a single sample with lists for attributes. - unpack: Unpack the packed Sample object into a list of Sample objects or dictionaries. - dict: Return the Sample object as a dictionary with None values excluded. - model_field_info: Get the FieldInfo for a given attribute key. - space: Return the corresponding Gym space for the Sample instance based on its instance attributes. - random_sample: Generate a random Sample instance based on its instance attributes. - - Examples: - >>> sample = Sample(x=1, y=2, z={"a": 3, "b": 4}, extra_field=5) - >>> flat_list = sample.flatten() - >>> print(flat_list) - [1, 2, 3, 4, 5] - >>> schema = sample.schema() - {'type': 'object', 'properties': {'x': {'type': 'number'}, 'y': {'type': 'number'}, 'z': {'type': 'object', 'properties': {'a': {'type': 'number'}, 'b': {'type': 'number'}}}, 'extra_field': {'type': 'number'}}} - >>> unflattened_sample = Sample.unflatten(flat_list, schema) - >>> print(unflattened_sample) - Sample(x=1, y=2, z={'a': 3, 'b': 4}, extra_field=5) - """ - - __doc__ = "A base model class for serializing, recording, and manipulating arbitray data." - - model_config: ConfigDict = ConfigDict( # type: ignore[misc] - use_enum_values=False, - from_attributes=True, - validate_assignment=False, - extra="allow", - arbitrary_types_allowed=True, - ) - - def __init__(self, datum=None, **data) -> None: # type: ignore[no-untyped-def] - """Accepts an arbitrary datum as well as keyword arguments.""" - if datum is not None: - if isinstance(datum, Sample): - data.update(datum.dict()) - elif isinstance(datum, dict): - data.update(datum) - else: - data["datum"] = datum - super().__init__(**data) - - def __hash__(self) -> int: - """Return a hash of the Sample instance.""" - return hash(tuple(self.dict().values())) - - def __str__(self) -> str: - """Return a string representation of the Sample instance.""" - return f"{self.__class__.__name__}({', '.join([f'{k}={v}' for k, v in self.dict().items() if v is not None])})" - - def dict(self, exclude_none: bool = True, exclude: set[str] | None = None) -> dict[str, Any]: # type: ignore[override] - """Return the Sample object as a dictionary with None values excluded. - - Args: - exclude_none (bool, optional): Whether to exclude None values. Defaults to True. - exclude (set[str], optional): Set of attribute names to exclude. Defaults to None. - - Returns: - Dict[str, Any]: Dictionary representation of the Sample object. - """ - return self.model_dump(exclude_none=exclude_none, exclude=exclude) - - @classmethod - def unflatten(cls, one_d_array_or_dict, schema=None) -> "Sample": # type: ignore[no-untyped-def] - """Unflatten a one-dimensional array or dictionary into a Sample instance. - - If a dictionary is provided, its keys are ignored. - - Args: - one_d_array_or_dict: A one-dimensional array or dictionary to unflatten. - schema: A dictionary representing the JSON schema. Defaults to using the class's schema. - - Returns: - Sample: The unflattened Sample instance. - - Examples: - >>> sample = Sample(x=1, y=2, z={"a": 3, "b": 4}, extra_field=5) - >>> flat_list = sample.flatten() - >>> print(flat_list) - [1, 2, 3, 4, 5] - >>> Sample.unflatten(flat_list, sample.schema()) - Sample(x=1, y=2, z={'a': 3, 'b': 4}, extra_field=5) - """ - if schema is None: - schema = cls().schema() - - # Convert input to list if it's not already - if isinstance(one_d_array_or_dict, dict): - flat_data = list(one_d_array_or_dict.values()) - else: - flat_data = list(one_d_array_or_dict) - - def unflatten_recursive(schema_part, index: int = 0): # type: ignore[no-untyped-def] - if schema_part["type"] == "object": - result = {} - for prop, prop_schema in schema_part["properties"].items(): - value, index = unflatten_recursive(prop_schema, index) - result[prop] = value - return result, index - elif schema_part["type"] == "array": - items = [] - for _ in range(schema_part.get("maxItems", len(flat_data) - index)): - value, index = unflatten_recursive(schema_part["items"], index) - items.append(value) - return items, index - else: # Assuming it's a primitive type - return flat_data[index], index + 1 - - unflattened_dict, _ = unflatten_recursive(schema) - return cls(**unflattened_dict) - - def flatten( - self, - output_type: Flattenable = "dict", - non_numerical: Literal["ignore", "forbid", "allow"] = "allow", - ) -> builtins.dict[str, Any] | np.ndarray | torch.Tensor | list: # type: ignore[type-arg] - accumulator = {} if output_type == "dict" else [] # type: ignore[var-annotated] - - def flatten_recursive(obj, path: str = "") -> None: # type: ignore[no-untyped-def] - if isinstance(obj, Sample): - for k, v in obj.dict().items(): - flatten_recursive(v, path + k + "/") - elif isinstance(obj, dict): - for k, v in obj.items(): - flatten_recursive(v, path + k + "/") - elif isinstance(obj, list | tuple): - for i, item in enumerate(obj): - flatten_recursive(item, path + str(i) + "/") - elif hasattr(obj, "__len__") and not isinstance(obj, str): - flat_list = obj.flatten().tolist() - if output_type == "dict": - # Convert to list for dict storage - accumulator[path[:-1]] = flat_list # type: ignore[index] - else: - accumulator.extend(flat_list) # type: ignore[attr-defined] - else: - if non_numerical == "ignore" and not isinstance(obj, int | float | bool): - return - final_key = path[:-1] # Remove trailing slash - if output_type == "dict": - accumulator[final_key] = obj # type: ignore[index] - else: - accumulator.append(obj) # type: ignore[attr-defined] - - flatten_recursive(self) - accumulator = accumulator.values() if output_type == "dict" else accumulator # type: ignore[attr-defined] - if non_numerical == "forbid" and any( - not isinstance(v, int | float | bool) for v in accumulator - ): - raise ValueError("Non-numerical values found in flattened data.") - if output_type == "np": - return np.array(accumulator) - if output_type == "pt": - torch = smart_import("torch") - return torch.tensor(accumulator) # type: ignore[no-any-return] - return accumulator # type: ignore[return-value] - - @staticmethod - def obj_to_schema(value: Any) -> builtins.dict: # type: ignore[type-arg] - """Generates a simplified JSON schema from a dictionary. - - Args: - value (Any): An object to generate a schema for. - - Returns: - dict: A simplified JSON schema representing the structure of the dictionary. - """ - if isinstance(value, dict): - return { - "type": "object", - "properties": {k: Sample.obj_to_schema(v) for k, v in value.items()}, - } - if isinstance(value, list | tuple | np.ndarray): - if len(value) > 0: - return {"type": "array", "items": Sample.obj_to_schema(value[0])} - return {"type": "array", "items": {}} - if isinstance(value, str): - return {"type": "string"} - if isinstance(value, int | np.integer): - return {"type": "integer"} - if isinstance(value, float | np.floating): - return {"type": "number"} - if isinstance(value, bool): - return {"type": "boolean"} - return {} - - def schema( - self, - resolve_refs: bool = True, - include_descriptions: bool = False, # type: ignore[override] - ) -> builtins.dict: # type: ignore[type-arg] - """Returns a simplified json schema. - - Removing additionalProperties, - selecting the first type in anyOf, and converting numpy schema to the desired type. - Optionally resolves references. - - Args: - resolve_refs (bool): Whether to resolve references in the schema. Defaults to True. - include_descriptions (bool): Whether to include descriptions in the schema. Defaults to False. - - Returns: - dict: A simplified JSON schema. - """ - schema = self.model_json_schema() - if "additionalProperties" in schema: - del schema["additionalProperties"] - - if resolve_refs: - schema = replace_refs(schema) - - if not include_descriptions and "description" in schema: - del schema["description"] - - properties = schema.get("properties", {}) - for key, value in self.dict().items(): - if key not in properties: - properties[key] = Sample.obj_to_schema(value) - if isinstance(value, Sample): - properties[key] = value.schema( - resolve_refs=resolve_refs, include_descriptions=include_descriptions - ) - else: - properties[key] = Sample.obj_to_schema(value) - return schema - - @classmethod - def read(cls, data: Any) -> "Sample": - """Read a Sample instance from a JSON string or dictionary or path. - - Args: - data (Any): The JSON string or dictionary to read. - - Returns: - Sample: The read Sample instance. - """ - if isinstance(data, str): - try: - data = cls.model_validate(from_json(data)) - except Exception as e: - logging.info(f"Error reading data: {e}. Attempting to read as JSON.") - if isinstance(data, str): - if Path(data).exists(): - if hasattr(cls, "open"): - data = cls.open(data) - else: - data = Path(data).read_text() - data = json.loads(data) - else: - data = json.load(data) - - if isinstance(data, dict): - return cls(**data) - return cls(data) - - def to(self, container: Any) -> Any: - """Convert the Sample instance to a different container type. - - Args: - container (Any): The container type to convert to. Supported types are - 'dict', 'list', 'np', 'pt' (pytorch), 'space' (gym.space), - 'schema', 'json', 'hf' (datasets.Dataset) and any subtype of Sample. - - Returns: - Any: The converted container. - """ - if isinstance(container, Sample) and not issubclass(container, Sample): # type: ignore[arg-type] - return container(**self.dict()) # type: ignore[operator] - if isinstance(container, type) and issubclass(container, Sample): - return container.unflatten(self.flatten()) - - if container == "dict": - return self.dict() - if container == "list": - return self.flatten(output_type="list") - if container == "np": - return self.flatten(output_type="np") - if container == "pt": - return self.flatten(output_type="pt") - if container == "space": - return self.space() - if container == "schema": - return self.schema() - if container == "json": - return self.model_dump_json() - if container == "hf": - return Dataset.from_dict(self.dict()) - if container == "features": - return to_features(self.dict()) - raise ValueError(f"Unsupported container type: {container}") - - @classmethod - def default_value(cls) -> "Sample": - """Get the default value for the Sample instance. - - Returns: - Sample: The default value for the Sample instance. - """ - return cls() - - @classmethod - def space_for( - cls, - value: Any, - max_text_length: int = 1000, - info: Annotated = None, # type: ignore[valid-type] - ) -> spaces.Space: - """Default Gym space generation for a given value. - - Only used for subclasses that do not override the space method. - """ - if isinstance(value, Enum) or get_origin(value) == Literal: - return spaces.Discrete(len(value.__args__)) - if isinstance(value, bool): - return spaces.Discrete(2) - if isinstance(value, dict | Sample): - if isinstance(value, Sample): - value = value.dict() - return spaces.Dict( - {k: Sample.space_for(v, max_text_length, info) for k, v in value.items()}, - ) - if isinstance(value, str): - return spaces.Text(max_length=max_text_length) - if isinstance(value, int | float | list | tuple | np.ndarray): - shape = None - le = None - ge = None - dtype = None - if info is not None: - shape = info.metadata_lookup.get("shape") - le = info.metadata_lookup.get("le") - ge = info.metadata_lookup.get("ge") - dtype = info.metadata_lookup.get("dtype") - logging.debug( - "Generating space for value: %s, shape: %s, le: %s, ge: %s, dtype: %s", - value, - shape, - le, - ge, - dtype, - ) - try: - value = np.asfarray(value) # type: ignore[attr-defined] - shape = shape or value.shape - dtype = dtype or value.dtype - le = le or -np.inf - ge = ge or np.inf - return spaces.Box(low=le, high=ge, shape=shape, dtype=dtype) - except Exception as e: - logging.info(f"Could not convert value {value} to numpy array: {e}") - if len(value) > 0 and isinstance(value[0], dict | Sample): - return spaces.Tuple( - [spaces.Dict(cls.space_for(v, max_text_length, info)) for v in value], - ) - return spaces.Tuple( - [cls.space_for(value[0], max_text_length, info) for value in value[:1]], - ) - raise ValueError(f"Unsupported object {value} of type: {type(value)} for space generation") - - @classmethod - def init_from(cls, d: Any, pack: bool = False) -> "Sample": - if isinstance(d, spaces.Space): - return cls.from_space(d) - if isinstance(d, Union[Sequence, np.ndarray]): # type: ignore[arg-type] - if pack: - return cls.pack_from(d) - return cls.unflatten(d) - if isinstance(d, dict): - try: - return cls.model_validate(d) - except ValidationError as e: - logging.info(f" Unable to validate {d} as {cls} {e}. Attempting to unflatten.") - - try: - return cls.unflatten(d) - except Exception as e: - logging.info(f" Unable to unflatten {d} as {cls} {e}. Attempting to read.") - return cls.read(d) - return cls(d) - - @classmethod - def from_flat_dict( - cls, - flat_dict: builtins.dict[str, Any], - schema: builtins.dict | None = None, # type: ignore[type-arg] - ) -> "Sample": - """Initialize a Sample instance from a flattened dictionary.""" - """ - Reconstructs the original JSON object from a flattened dictionary using the provided schema. - - Args: - flat_dict (dict): A flattened dictionary with keys like "key1.nestedkey1". - schema (dict): A dictionary representing the JSON schema. - - Returns: - dict: The reconstructed JSON object. - """ - schema = schema or replace_refs(cls.model_json_schema()) - reconstructed = {} # type: ignore[var-annotated] - - for flat_key, value in flat_dict.items(): - keys = flat_key.split(".") - current = reconstructed - for key in keys[:-1]: - if key not in current: - current[key] = {} - current = current[key] - current[keys[-1]] = value - - return reconstructed # type: ignore[return-value] - - @classmethod - def from_space(cls, space: spaces.Space) -> "Sample": - """Generate a Sample instance from a Gym space.""" - sampled = space.sample() - if isinstance(sampled, dict | OrderedDict): - return cls(**sampled) - if hasattr(sampled, "__len__") and not isinstance(sampled, str): - sampled = np.asarray(sampled) - if len(sampled.shape) > 0 and isinstance(sampled[0], dict | Sample): - return cls.pack_from(sampled) # type: ignore[arg-type] - return cls(sampled) - - @classmethod - def pack_from(cls, samples: list[Union["Sample", builtins.dict]]) -> "Sample": # type: ignore[type-arg] - """Pack a list of samples into a single sample with lists for attributes. - - Args: - samples (List[Union[Sample, Dict]]): List of samples or dictionaries. - - Returns: - Sample: Packed sample with lists for attributes. - """ - if samples is None or len(samples) == 0: - return cls() - - first_sample = samples[0] - if isinstance(first_sample, dict): - attributes = list(first_sample.keys()) - elif hasattr(first_sample, "__dict__"): - attributes = list(first_sample.__dict__.keys()) - else: - attributes = ["item" + str(i) for i in range(len(samples))] - - aggregated = {attr: [] for attr in attributes} # type: ignore[var-annotated] - for sample in samples: - for attr in attributes: - # Handle both Sample instances and dictionaries - if isinstance(sample, dict): - aggregated[attr].append(sample.get(attr, None)) - else: - aggregated[attr].append(getattr(sample, attr, None)) - return cls(**aggregated) - - def unpack(self, to_dicts: bool = False) -> list[Union["Sample", builtins.dict]]: # type: ignore[type-arg] - """Unpack the packed Sample object into a list of Sample objects or dictionaries.""" - attributes = list(self.model_extra.keys()) + list(self.model_fields.keys()) # type: ignore[union-attr] - attributes = [attr for attr in attributes if getattr(self, attr) is not None] - if not attributes or getattr(self, attributes[0]) is None: - return [] - - # Ensure all attributes are lists and have the same length - list_sizes = { - len(getattr(self, attr)) for attr in attributes if isinstance(getattr(self, attr), list) - } - if len(list_sizes) != 1: - raise ValueError("Not all attribute lists have the same length.") - list_size = list_sizes.pop() - - if to_dicts: - return [{key: getattr(self, key)[i] for key in attributes} for i in range(list_size)] - - return [ - self.__class__(**{key: getattr(self, key)[i] for key in attributes}) - for i in range(list_size) - ] - - @classmethod - def default_space(cls) -> spaces.Dict: - """Return the Gym space for the Sample class based on its class attributes.""" - return cls().space() - - @classmethod - def default_sample( - cls, output_type: str = "Sample" - ) -> Union["Sample", builtins.dict[str, Any]]: - """Generate a default Sample instance from its class attributes. Useful for padding. - - This is the "no-op" instance and should be overriden as needed. - """ - if output_type == "Sample": - return cls() - return cls().dict() - - def model_field_info(self, key: str) -> FieldInfo: - """Get the FieldInfo for a given attribute key.""" - if self.model_extra and self.model_extra.get(key) is not None: - info = FieldInfo(metadata=self.model_extra[key]) # type: ignore[call-arg] - if self.model_fields.get(key) is not None: - info = FieldInfo(metadata=self.model_fields[key]) # type: ignore[call-arg] - - if info and hasattr(info, "annotation"): - return info.annotation # type: ignore[return-value] - return None # type: ignore[return-value] - - def space(self) -> spaces.Dict: - """Return the corresponding Gym space for the Sample instance based on its instance attributes. Omits None values. - - Override this method in subclasses to customize the space generation. - """ - space_dict = {} - for key, value in self.dict().items(): - logging.debug("Generating space for key: '%s', value: %s", key, value) - info = self.model_field_info(key) - value = getattr(self, key) if hasattr(self, key) else value - space_dict[key] = ( - value.space() if isinstance(value, Sample) else self.space_for(value, info=info) - ) - return spaces.Dict(space_dict) - - def random_sample(self) -> "Sample": - """Generate a random Sample instance based on its instance attributes. Omits None values. - - Override this method in subclasses to customize the sample generation. - """ - return self.__class__.model_validate(self.space().sample()) - - -if __name__ == "__main__": - sample = Sample(x=1, y=2, z={"a": 3, "b": 4}, extra_field=5) diff --git a/dimos/types/test_timestamped.py b/dimos/types/test_timestamped.py deleted file mode 100644 index 7de82e8f9a..0000000000 --- a/dimos/types/test_timestamped.py +++ /dev/null @@ -1,581 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from datetime import datetime, timezone -import time - -import pytest -from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.memory.timeseries.inmemory import InMemoryStore -from dimos.msgs.sensor_msgs import Image -from dimos.types.timestamped import ( - Timestamped, - TimestampedBufferCollection, - align_timestamped, - to_datetime, - to_ros_stamp, -) -from dimos.utils import testing -from dimos.utils.data import get_data -from dimos.utils.reactive import backpressure - - -def test_timestamped_dt_method() -> None: - ts = 1751075203.4120464 - timestamped = Timestamped(ts) - dt = timestamped.dt() - assert isinstance(dt, datetime) - assert abs(dt.timestamp() - ts) < 1e-6 - assert dt.tzinfo is not None, "datetime should be timezone-aware" - - -def test_to_ros_stamp() -> None: - """Test the to_ros_stamp function with different input types.""" - - # Test with float timestamp - ts_float = 1234567890.123456789 - result = to_ros_stamp(ts_float) - assert result.sec == 1234567890 - # Float precision limitation - check within reasonable range - assert abs(result.nanosec - 123456789) < 1000 - - # Test with integer timestamp - ts_int = 1234567890 - result = to_ros_stamp(ts_int) - assert result.sec == 1234567890 - assert result.nanosec == 0 - - # Test with datetime object - dt = datetime(2009, 2, 13, 23, 31, 30, 123456, tzinfo=timezone.utc) - result = to_ros_stamp(dt) - assert result.sec == 1234567890 - assert abs(result.nanosec - 123456000) < 1000 # Allow small rounding error - - -def test_to_datetime() -> None: - """Test the to_datetime function with different input types.""" - - # Test with float timestamp - ts_float = 1234567890.123456 - dt = to_datetime(ts_float) - assert isinstance(dt, datetime) - assert dt.tzinfo is not None # Should have timezone - assert abs(dt.timestamp() - ts_float) < 1e-6 - - # Test with integer timestamp - ts_int = 1234567890 - dt = to_datetime(ts_int) - assert isinstance(dt, datetime) - assert dt.tzinfo is not None - assert dt.timestamp() == ts_int - - # Test with RosStamp - ros_stamp = {"sec": 1234567890, "nanosec": 123456000} - dt = to_datetime(ros_stamp) - assert isinstance(dt, datetime) - assert dt.tzinfo is not None - expected_ts = 1234567890.123456 - assert abs(dt.timestamp() - expected_ts) < 1e-6 - - # Test with datetime (already has timezone) - dt_input = datetime(2009, 2, 13, 23, 31, 30, tzinfo=timezone.utc) - dt_result = to_datetime(dt_input) - assert dt_result.tzinfo is not None - # Should convert to local timezone by default - - # Test with naive datetime (no timezone) - dt_naive = datetime(2009, 2, 13, 23, 31, 30) - dt_result = to_datetime(dt_naive) - assert dt_result.tzinfo is not None - - # Test with specific timezone - dt_utc = to_datetime(ts_float, tz=timezone.utc) - assert dt_utc.tzinfo == timezone.utc - assert abs(dt_utc.timestamp() - ts_float) < 1e-6 - - -class SimpleTimestamped(Timestamped): - def __init__(self, ts: float, data: str) -> None: - super().__init__(ts) - self.data = data - - -@pytest.fixture -def test_scheduler(): - """Fixture that provides a ThreadPoolScheduler and cleans it up after the test.""" - scheduler = ThreadPoolScheduler(max_workers=6) - yield scheduler - # Cleanup after test - scheduler.executor.shutdown(wait=True) - time.sleep(0.2) # Give threads time to finish cleanup - - -@pytest.fixture -def sample_items(): - return [ - SimpleTimestamped(1.0, "first"), - SimpleTimestamped(3.0, "third"), - SimpleTimestamped(5.0, "fifth"), - SimpleTimestamped(7.0, "seventh"), - ] - - -def make_store(items: list[SimpleTimestamped] | None = None) -> InMemoryStore[SimpleTimestamped]: - store: InMemoryStore[SimpleTimestamped] = InMemoryStore() - if items: - store.save(*items) - return store - - -@pytest.fixture -def collection(sample_items): - return make_store(sample_items) - - -def test_empty_collection() -> None: - collection = make_store() - assert len(collection) == 0 - assert collection.duration() == 0.0 - assert collection.time_range() is None - assert collection.find_closest(1.0) is None - - -def test_add_items() -> None: - collection = make_store() - item1 = SimpleTimestamped(2.0, "two") - item2 = SimpleTimestamped(1.0, "one") - - collection.save(item1) - collection.save(item2) - - assert len(collection) == 2 - items = list(collection) - assert items[0].data == "one" # Should be sorted by timestamp - assert items[1].data == "two" - - -def test_find_closest(collection) -> None: - # Exact match - assert collection.find_closest(3.0).data == "third" - - # Between items (closer to left) - assert collection.find_closest(1.5, tolerance=1.0).data == "first" - - # Between items (closer to right) - assert collection.find_closest(3.5, tolerance=1.0).data == "third" - - # Exactly in the middle (should pick the later one due to >= comparison) - assert ( - collection.find_closest(4.0, tolerance=1.0).data == "fifth" - ) # 4.0 is equidistant from 3.0 and 5.0 - - # Before all items - assert collection.find_closest(0.0, tolerance=1.0).data == "first" - - # After all items - assert collection.find_closest(10.0, tolerance=4.0).data == "seventh" - - # low tolerance, should return None - assert collection.find_closest(10.0, tolerance=2.0) is None - - -def test_find_before_after(collection) -> None: - # Find before - assert collection.find_before(2.0).data == "first" - assert collection.find_before(5.5).data == "fifth" - assert collection.find_before(1.0) is None # Nothing before first item - - # Find after - assert collection.find_after(2.0).data == "third" - assert collection.find_after(5.0).data == "seventh" - assert collection.find_after(7.0) is None # Nothing after last item - - -def test_save_from_multiple_stores() -> None: - store1 = make_store([SimpleTimestamped(1.0, "a"), SimpleTimestamped(3.0, "c")]) - store2 = make_store([SimpleTimestamped(2.0, "b"), SimpleTimestamped(4.0, "d")]) - - merged = make_store() - merged.save(*store1) - merged.save(*store2) - - assert len(merged) == 4 - assert [item.data for item in merged] == ["a", "b", "c", "d"] - - -def test_duration_and_range(collection) -> None: - assert collection.duration() == 6.0 # 7.0 - 1.0 - assert collection.time_range() == (1.0, 7.0) - - -def test_slice_by_time(collection) -> None: - # Slice inclusive of boundaries - sliced = collection.slice_by_time(2.0, 6.0) - assert len(sliced) == 2 - assert sliced[0].data == "third" - assert sliced[1].data == "fifth" - - # Empty slice - empty_slice = collection.slice_by_time(8.0, 10.0) - assert len(empty_slice) == 0 - - # Slice all - all_slice = collection.slice_by_time(0.0, 10.0) - assert len(all_slice) == 4 - - -def test_iteration(collection) -> None: - items = list(collection) - assert len(items) == 4 - assert [item.ts for item in items] == [1.0, 3.0, 5.0, 7.0] - - -def test_single_item_collection() -> None: - single = make_store([SimpleTimestamped(5.0, "only")]) - assert single.duration() == 0.0 - assert single.time_range() == (5.0, 5.0) - - -def test_time_window_collection() -> None: - # Create a collection with a 2-second window - window = TimestampedBufferCollection[SimpleTimestamped](window_duration=2.0) - - # Add messages at different timestamps - window.add(SimpleTimestamped(1.0, "msg1")) - window.add(SimpleTimestamped(2.0, "msg2")) - window.add(SimpleTimestamped(3.0, "msg3")) - - # At this point, all messages should be present (within 2s window) - assert len(window) == 3 - - # Add a message at t=4.0, should keep messages from t=2.0 onwards - window.add(SimpleTimestamped(4.0, "msg4")) - assert len(window) == 3 # msg1 should be dropped - first = window.first() - last = window.last() - assert first is not None and first.data == "msg2" # oldest is now msg2 - assert last is not None and last.data == "msg4" # newest is msg4 - - # Add a message at t=5.5, should drop msg2 and msg3 - window.add(SimpleTimestamped(5.5, "msg5")) - assert len(window) == 2 # only msg4 and msg5 remain - items = list(window) - assert items[0].data == "msg4" - assert items[1].data == "msg5" - - # Verify time range - assert window.start_ts == 4.0 - assert window.end_ts == 5.5 - - -def test_timestamp_alignment(test_scheduler) -> None: - speed = 5.0 - - # ensure that lfs package is downloaded - get_data("unitree_office_walk") - - raw_frames = [] - - def spy(image): - raw_frames.append(image.ts) - print(image.ts) - return image - - # sensor reply of raw video frames - video_raw = ( - testing.TimedSensorReplay( - "unitree_office_walk/video", autocast=lambda x: Image.from_numpy(x).to_rgb() - ) - .stream(speed) - .pipe(ops.take(30)) - ) - - processed_frames = [] - - def process_video_frame(frame): - processed_frames.append(frame.ts) - time.sleep(0.5 / speed) - return frame - - # fake reply of some 0.5s processor of video frames that drops messages - # Pass the scheduler to backpressure to manage threads properly - fake_video_processor = backpressure( - video_raw.pipe(ops.map(spy)), scheduler=test_scheduler - ).pipe(ops.map(process_video_frame)) - - aligned_frames = align_timestamped(fake_video_processor, video_raw).pipe(ops.to_list()).run() - - assert len(raw_frames) == 30 - assert len(processed_frames) > 2 - assert len(aligned_frames) > 2 - - # Due to async processing, the last frame might not be aligned before completion - assert len(aligned_frames) >= len(processed_frames) - 1 - - for value in aligned_frames: - [primary, secondary] = value - diff = abs(primary.ts - secondary.ts) - print( - f"Aligned pair: primary={primary.ts:.6f}, secondary={secondary.ts:.6f}, diff={diff:.6f}s" - ) - assert diff <= 0.05 - - assert len(aligned_frames) > 2 - - -def test_timestamp_alignment_primary_first() -> None: - """Test alignment when primary messages arrive before secondary messages.""" - from reactivex import Subject - - primary_subject = Subject() - secondary_subject = Subject() - - results = [] - - # Set up alignment with a 2-second buffer - aligned = align_timestamped( - primary_subject, secondary_subject, buffer_size=2.0, match_tolerance=0.1 - ) - - # Subscribe to collect results - aligned.subscribe(lambda x: results.append(x)) - - # Send primary messages first - primary1 = SimpleTimestamped(1.0, "primary1") - primary2 = SimpleTimestamped(2.0, "primary2") - primary3 = SimpleTimestamped(3.0, "primary3") - - primary_subject.on_next(primary1) - primary_subject.on_next(primary2) - primary_subject.on_next(primary3) - - # At this point, no results should be emitted (no secondaries yet) - assert len(results) == 0 - - # Send secondary messages that match primary1 and primary2 - secondary1 = SimpleTimestamped(1.05, "secondary1") # Matches primary1 - secondary2 = SimpleTimestamped(2.02, "secondary2") # Matches primary2 - - secondary_subject.on_next(secondary1) - assert len(results) == 1 # primary1 should now be matched - assert results[0][0].data == "primary1" - assert results[0][1].data == "secondary1" - - secondary_subject.on_next(secondary2) - assert len(results) == 2 # primary2 should now be matched - assert results[1][0].data == "primary2" - assert results[1][1].data == "secondary2" - - # Send a secondary that's too far from primary3 - secondary_far = SimpleTimestamped(3.5, "secondary_far") # Too far from primary3 - secondary_subject.on_next(secondary_far) - # At this point primary3 is removed as unmatchable since secondary progressed past it - assert len(results) == 2 # primary3 should not match (outside tolerance) - - # Send a new primary that can match with the future secondary - primary4 = SimpleTimestamped(3.45, "primary4") - primary_subject.on_next(primary4) - assert len(results) == 3 # Should match with secondary_far - assert results[2][0].data == "primary4" - assert results[2][1].data == "secondary_far" - - # Complete the streams - primary_subject.on_completed() - secondary_subject.on_completed() - - -def test_timestamp_alignment_multiple_secondaries() -> None: - """Test alignment with multiple secondary observables.""" - from reactivex import Subject - - primary_subject = Subject() - secondary1_subject = Subject() - secondary2_subject = Subject() - - results = [] - - # Set up alignment with two secondary streams - aligned = align_timestamped( - primary_subject, - secondary1_subject, - secondary2_subject, - buffer_size=1.0, - match_tolerance=0.05, - ) - - # Subscribe to collect results - aligned.subscribe(lambda x: results.append(x)) - - # Send a primary message - primary1 = SimpleTimestamped(1.0, "primary1") - primary_subject.on_next(primary1) - - # No results yet (waiting for both secondaries) - assert len(results) == 0 - - # Send first secondary - sec1_msg1 = SimpleTimestamped(1.01, "sec1_msg1") - secondary1_subject.on_next(sec1_msg1) - - # Still no results (waiting for secondary2) - assert len(results) == 0 - - # Send second secondary - sec2_msg1 = SimpleTimestamped(1.02, "sec2_msg1") - secondary2_subject.on_next(sec2_msg1) - - # Now we should have a result - assert len(results) == 1 - assert results[0][0].data == "primary1" - assert results[0][1].data == "sec1_msg1" - assert results[0][2].data == "sec2_msg1" - - # Test partial match (one secondary missing) - primary2 = SimpleTimestamped(2.0, "primary2") - primary_subject.on_next(primary2) - - # Send only one secondary - sec1_msg2 = SimpleTimestamped(2.01, "sec1_msg2") - secondary1_subject.on_next(sec1_msg2) - - # No result yet - assert len(results) == 1 - - # Send a secondary2 that's too far - sec2_far = SimpleTimestamped(2.1, "sec2_far") # Outside tolerance - secondary2_subject.on_next(sec2_far) - - # Still no result (secondary2 is outside tolerance) - assert len(results) == 1 - - # Complete the streams - primary_subject.on_completed() - secondary1_subject.on_completed() - secondary2_subject.on_completed() - - -def test_timestamp_alignment_delayed_secondary() -> None: - """Test alignment when secondary messages arrive late but still within tolerance.""" - from reactivex import Subject - - primary_subject = Subject() - secondary_subject = Subject() - - results = [] - - # Set up alignment with a 2-second buffer - aligned = align_timestamped( - primary_subject, secondary_subject, buffer_size=2.0, match_tolerance=0.1 - ) - - # Subscribe to collect results - aligned.subscribe(lambda x: results.append(x)) - - # Send primary messages - primary1 = SimpleTimestamped(1.0, "primary1") - primary2 = SimpleTimestamped(2.0, "primary2") - primary3 = SimpleTimestamped(3.0, "primary3") - - primary_subject.on_next(primary1) - primary_subject.on_next(primary2) - primary_subject.on_next(primary3) - - # No results yet - assert len(results) == 0 - - # Send delayed secondaries (in timestamp order) - secondary1 = SimpleTimestamped(1.05, "secondary1") # Matches primary1 - secondary_subject.on_next(secondary1) - assert len(results) == 1 # primary1 matched - assert results[0][0].data == "primary1" - assert results[0][1].data == "secondary1" - - secondary2 = SimpleTimestamped(2.02, "secondary2") # Matches primary2 - secondary_subject.on_next(secondary2) - assert len(results) == 2 # primary2 matched - assert results[1][0].data == "primary2" - assert results[1][1].data == "secondary2" - - # Now send a secondary that's past primary3's match window - secondary_future = SimpleTimestamped(3.2, "secondary_future") # Too far from primary3 - secondary_subject.on_next(secondary_future) - # At this point, primary3 should be removed as unmatchable - assert len(results) == 2 # No new matches - - # Send a new primary that can match with secondary_future - primary4 = SimpleTimestamped(3.15, "primary4") - primary_subject.on_next(primary4) - assert len(results) == 3 # Should match immediately - assert results[2][0].data == "primary4" - assert results[2][1].data == "secondary_future" - - # Complete the streams - primary_subject.on_completed() - secondary_subject.on_completed() - - -def test_timestamp_alignment_buffer_cleanup() -> None: - """Test that old buffered primaries are cleaned up.""" - import time as time_module - - from reactivex import Subject - - primary_subject = Subject() - secondary_subject = Subject() - - results = [] - - # Set up alignment with a 0.5-second buffer - aligned = align_timestamped( - primary_subject, secondary_subject, buffer_size=0.5, match_tolerance=0.05 - ) - - # Subscribe to collect results - aligned.subscribe(lambda x: results.append(x)) - - # Use real timestamps for this test - now = time_module.time() - - # Send an old primary - old_primary = Timestamped(now - 1.0) # 1 second ago - old_primary.data = "old" - primary_subject.on_next(old_primary) - - # Send a recent secondary to trigger cleanup - recent_secondary = Timestamped(now) - recent_secondary.data = "recent" - secondary_subject.on_next(recent_secondary) - - # Old primary should not match (outside buffer window) - assert len(results) == 0 - - # Send a matching pair within buffer - new_primary = Timestamped(now + 0.1) - new_primary.data = "new_primary" - new_secondary = Timestamped(now + 0.11) - new_secondary.data = "new_secondary" - - primary_subject.on_next(new_primary) - secondary_subject.on_next(new_secondary) - - # Should have one match - assert len(results) == 1 - assert results[0][0].data == "new_primary" - assert results[0][1].data == "new_secondary" - - # Complete the streams - primary_subject.on_completed() - secondary_subject.on_completed() diff --git a/dimos/types/test_vector.py b/dimos/types/test_vector.py deleted file mode 100644 index 285d021bea..0000000000 --- a/dimos/types/test_vector.py +++ /dev/null @@ -1,384 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import numpy as np -import pytest - -from dimos.types.vector import Vector - - -def test_vector_default_init() -> None: - """Test that default initialization of Vector() has x,y,z components all zero.""" - v = Vector() - assert v.x == 0.0 - assert v.y == 0.0 - assert v.z == 0.0 - assert v.dim == 0 - assert len(v.data) == 0 - assert v.to_list() == [] - assert v.is_zero() # Empty vector should be considered zero - - -def test_vector_specific_init() -> None: - """Test initialization with specific values.""" - # 2D vector - v1 = Vector(1.0, 2.0) - assert v1.x == 1.0 - assert v1.y == 2.0 - assert v1.z == 0.0 - assert v1.dim == 2 - - # 3D vector - v2 = Vector(3.0, 4.0, 5.0) - assert v2.x == 3.0 - assert v2.y == 4.0 - assert v2.z == 5.0 - assert v2.dim == 3 - - # From list - v3 = Vector([6.0, 7.0, 8.0]) - assert v3.x == 6.0 - assert v3.y == 7.0 - assert v3.z == 8.0 - assert v3.dim == 3 - - # From numpy array - v4 = Vector(np.array([9.0, 10.0, 11.0])) - assert v4.x == 9.0 - assert v4.y == 10.0 - assert v4.z == 11.0 - assert v4.dim == 3 - - -def test_vector_addition() -> None: - """Test vector addition.""" - v1 = Vector(1.0, 2.0, 3.0) - v2 = Vector(4.0, 5.0, 6.0) - - v_add = v1 + v2 - assert v_add.x == 5.0 - assert v_add.y == 7.0 - assert v_add.z == 9.0 - - -def test_vector_subtraction() -> None: - """Test vector subtraction.""" - v1 = Vector(1.0, 2.0, 3.0) - v2 = Vector(4.0, 5.0, 6.0) - - v_sub = v2 - v1 - assert v_sub.x == 3.0 - assert v_sub.y == 3.0 - assert v_sub.z == 3.0 - - -def test_vector_scalar_multiplication() -> None: - """Test vector multiplication by a scalar.""" - v1 = Vector(1.0, 2.0, 3.0) - - v_mul = v1 * 2.0 - assert v_mul.x == 2.0 - assert v_mul.y == 4.0 - assert v_mul.z == 6.0 - - # Test right multiplication - v_rmul = 2.0 * v1 - assert v_rmul.x == 2.0 - assert v_rmul.y == 4.0 - assert v_rmul.z == 6.0 - - -def test_vector_scalar_division() -> None: - """Test vector division by a scalar.""" - v2 = Vector(4.0, 5.0, 6.0) - - v_div = v2 / 2.0 - assert v_div.x == 2.0 - assert v_div.y == 2.5 - assert v_div.z == 3.0 - - -def test_vector_dot_product() -> None: - """Test vector dot product.""" - v1 = Vector(1.0, 2.0, 3.0) - v2 = Vector(4.0, 5.0, 6.0) - - dot = v1.dot(v2) - assert dot == 32.0 - - -def test_vector_length() -> None: - """Test vector length calculation.""" - # 2D vector with length 5 - v1 = Vector(3.0, 4.0) - assert v1.length() == 5.0 - - # 3D vector - v2 = Vector(2.0, 3.0, 6.0) - assert v2.length() == pytest.approx(7.0, 0.001) - - # Test length_squared - assert v1.length_squared() == 25.0 - assert v2.length_squared() == 49.0 - - -def test_vector_normalize() -> None: - """Test vector normalization.""" - v = Vector(2.0, 3.0, 6.0) - assert not v.is_zero() - - v_norm = v.normalize() - length = v.length() - expected_x = 2.0 / length - expected_y = 3.0 / length - expected_z = 6.0 / length - - assert np.isclose(v_norm.x, expected_x) - assert np.isclose(v_norm.y, expected_y) - assert np.isclose(v_norm.z, expected_z) - assert np.isclose(v_norm.length(), 1.0) - assert not v_norm.is_zero() - - # Test normalizing a zero vector - v_zero = Vector(0.0, 0.0, 0.0) - assert v_zero.is_zero() - v_zero_norm = v_zero.normalize() - assert v_zero_norm.x == 0.0 - assert v_zero_norm.y == 0.0 - assert v_zero_norm.z == 0.0 - assert v_zero_norm.is_zero() - - -def test_vector_to_2d() -> None: - """Test conversion to 2D vector.""" - v = Vector(2.0, 3.0, 6.0) - - v_2d = v.to_2d() - assert v_2d.x == 2.0 - assert v_2d.y == 3.0 - assert v_2d.z == 0.0 - assert v_2d.dim == 2 - - # Already 2D vector - v2 = Vector(4.0, 5.0) - v2_2d = v2.to_2d() - assert v2_2d.x == 4.0 - assert v2_2d.y == 5.0 - assert v2_2d.dim == 2 - - -def test_vector_distance() -> None: - """Test distance calculations between vectors.""" - v1 = Vector(1.0, 2.0, 3.0) - v2 = Vector(4.0, 6.0, 8.0) - - # Distance - dist = v1.distance(v2) - expected_dist = np.sqrt(9.0 + 16.0 + 25.0) # sqrt((4-1)² + (6-2)² + (8-3)²) - assert dist == pytest.approx(expected_dist) - - # Distance squared - dist_sq = v1.distance_squared(v2) - assert dist_sq == 50.0 # 9 + 16 + 25 - - -def test_vector_cross_product() -> None: - """Test vector cross product.""" - v1 = Vector(1.0, 0.0, 0.0) # Unit x vector - v2 = Vector(0.0, 1.0, 0.0) # Unit y vector - - # v1 × v2 should be unit z vector - cross = v1.cross(v2) - assert cross.x == 0.0 - assert cross.y == 0.0 - assert cross.z == 1.0 - - # Test with more complex vectors - a = Vector(2.0, 3.0, 4.0) - b = Vector(5.0, 6.0, 7.0) - c = a.cross(b) - - # Cross product manually calculated: - # (3*7-4*6, 4*5-2*7, 2*6-3*5) - assert c.x == -3.0 - assert c.y == 6.0 - assert c.z == -3.0 - - # Test with 2D vectors (should raise error) - v_2d = Vector(1.0, 2.0) - with pytest.raises(ValueError): - v_2d.cross(v2) - - -def test_vector_zeros() -> None: - """Test Vector.zeros class method.""" - # 3D zero vector - v_zeros = Vector.zeros(3) - assert v_zeros.x == 0.0 - assert v_zeros.y == 0.0 - assert v_zeros.z == 0.0 - assert v_zeros.dim == 3 - assert v_zeros.is_zero() - - # 2D zero vector - v_zeros_2d = Vector.zeros(2) - assert v_zeros_2d.x == 0.0 - assert v_zeros_2d.y == 0.0 - assert v_zeros_2d.z == 0.0 - assert v_zeros_2d.dim == 2 - assert v_zeros_2d.is_zero() - - -def test_vector_ones() -> None: - """Test Vector.ones class method.""" - # 3D ones vector - v_ones = Vector.ones(3) - assert v_ones.x == 1.0 - assert v_ones.y == 1.0 - assert v_ones.z == 1.0 - assert v_ones.dim == 3 - - # 2D ones vector - v_ones_2d = Vector.ones(2) - assert v_ones_2d.x == 1.0 - assert v_ones_2d.y == 1.0 - assert v_ones_2d.z == 0.0 - assert v_ones_2d.dim == 2 - - -def test_vector_conversion_methods() -> None: - """Test vector conversion methods (to_list, to_tuple, to_numpy).""" - v = Vector(1.0, 2.0, 3.0) - - # to_list - assert v.to_list() == [1.0, 2.0, 3.0] - - # to_tuple - assert v.to_tuple() == (1.0, 2.0, 3.0) - - # to_numpy - np_array = v.to_numpy() - assert isinstance(np_array, np.ndarray) - assert np.array_equal(np_array, np.array([1.0, 2.0, 3.0])) - - -def test_vector_equality() -> None: - """Test vector equality.""" - v1 = Vector(1, 2, 3) - v2 = Vector(1, 2, 3) - v3 = Vector(4, 5, 6) - - assert v1 == v2 - assert v1 != v3 - assert v1 != Vector(1, 2) # Different dimensions - assert v1 != Vector(1.1, 2, 3) # Different values - assert v1 != [1, 2, 3] - - -def test_vector_is_zero() -> None: - """Test is_zero method for vectors.""" - # Default empty vector - v0 = Vector() - assert v0.is_zero() - - # Explicit zero vector - v1 = Vector(0.0, 0.0, 0.0) - assert v1.is_zero() - - # Zero vector with different dimensions - v2 = Vector(0.0, 0.0) - assert v2.is_zero() - - # Non-zero vectors - v3 = Vector(1.0, 0.0, 0.0) - assert not v3.is_zero() - - v4 = Vector(0.0, 2.0, 0.0) - assert not v4.is_zero() - - v5 = Vector(0.0, 0.0, 3.0) - assert not v5.is_zero() - - # Almost zero (within tolerance) - v6 = Vector(1e-10, 1e-10, 1e-10) - assert v6.is_zero() - - # Almost zero (outside tolerance) - v7 = Vector(1e-6, 1e-6, 1e-6) - assert not v7.is_zero() - - -def test_vector_bool_conversion(): - """Test boolean conversion of vectors.""" - # Zero vectors should be False - v0 = Vector() - assert not bool(v0) - - v1 = Vector(0.0, 0.0, 0.0) - assert not bool(v1) - - # Almost zero vectors should be False - v2 = Vector(1e-10, 1e-10, 1e-10) - assert not bool(v2) - - # Non-zero vectors should be True - v3 = Vector(1.0, 0.0, 0.0) - assert bool(v3) - - v4 = Vector(0.0, 2.0, 0.0) - assert bool(v4) - - v5 = Vector(0.0, 0.0, 3.0) - assert bool(v5) - - # Direct use in if statements - if v0: - raise AssertionError("Zero vector should be False in boolean context") - else: - pass # Expected path - - if v3: - pass # Expected path - else: - raise AssertionError("Non-zero vector should be True in boolean context") - - -def test_vector_add() -> None: - """Test vector addition operator.""" - v1 = Vector(1.0, 2.0, 3.0) - v2 = Vector(4.0, 5.0, 6.0) - - # Using __add__ method - v_add = v1.__add__(v2) - assert v_add.x == 5.0 - assert v_add.y == 7.0 - assert v_add.z == 9.0 - - # Using + operator - v_add_op = v1 + v2 - assert v_add_op.x == 5.0 - assert v_add_op.y == 7.0 - assert v_add_op.z == 9.0 - - # Adding zero vector should return original vector - v_zero = Vector.zeros(3) - assert (v1 + v_zero) == v1 - - -def test_vector_add_dim_mismatch() -> None: - """Test vector addition operator.""" - v1 = Vector(1.0, 2.0) - v2 = Vector(4.0, 5.0, 6.0) - - # Using + operator - v1 + v2 diff --git a/dimos/types/test_weaklist.py b/dimos/types/test_weaklist.py deleted file mode 100644 index 06f9f851ce..0000000000 --- a/dimos/types/test_weaklist.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for WeakList implementation.""" - -import gc - -import pytest - -from dimos.types.weaklist import WeakList - - -class SampleObject: - """Simple test object.""" - - def __init__(self, value) -> None: - self.value = value - - def __repr__(self) -> str: - return f"SampleObject({self.value})" - - -def test_weaklist_basic_operations() -> None: - """Test basic append, iterate, and length operations.""" - wl = WeakList() - - # Add objects - obj1 = SampleObject(1) - obj2 = SampleObject(2) - obj3 = SampleObject(3) - - wl.append(obj1) - wl.append(obj2) - wl.append(obj3) - - # Check length and iteration - assert len(wl) == 3 - assert list(wl) == [obj1, obj2, obj3] - - # Check contains - assert obj1 in wl - assert obj2 in wl - assert SampleObject(4) not in wl - - -@pytest.mark.integration -def test_weaklist_auto_removal() -> None: - """Test that objects are automatically removed when garbage collected.""" - wl = WeakList() - - obj1 = SampleObject(1) - obj2 = SampleObject(2) - obj3 = SampleObject(3) - - wl.append(obj1) - wl.append(obj2) - wl.append(obj3) - - assert len(wl) == 3 - - # Delete one object and force garbage collection - del obj2 - gc.collect() - - # Should only have 2 objects now - assert len(wl) == 2 - assert list(wl) == [obj1, obj3] - - -def test_weaklist_explicit_remove() -> None: - """Test explicit removal of objects.""" - wl = WeakList() - - obj1 = SampleObject(1) - obj2 = SampleObject(2) - - wl.append(obj1) - wl.append(obj2) - - # Remove obj1 - wl.remove(obj1) - assert len(wl) == 1 - assert obj1 not in wl - assert obj2 in wl - - # Try to remove non-existent object - with pytest.raises(ValueError): - wl.remove(SampleObject(3)) - - -def test_weaklist_indexing() -> None: - """Test index access.""" - wl = WeakList() - - obj1 = SampleObject(1) - obj2 = SampleObject(2) - obj3 = SampleObject(3) - - wl.append(obj1) - wl.append(obj2) - wl.append(obj3) - - assert wl[0] is obj1 - assert wl[1] is obj2 - assert wl[2] is obj3 - - # Test index out of range - with pytest.raises(IndexError): - _ = wl[3] - - -def test_weaklist_clear() -> None: - """Test clearing the list.""" - wl = WeakList() - - obj1 = SampleObject(1) - obj2 = SampleObject(2) - - wl.append(obj1) - wl.append(obj2) - - assert len(wl) == 2 - - wl.clear() - assert len(wl) == 0 - assert obj1 not in wl - - -@pytest.mark.integration -def test_weaklist_iteration_during_modification() -> None: - """Test that iteration works even if objects are deleted during iteration.""" - wl = WeakList() - - objects = [SampleObject(i) for i in range(5)] - for obj in objects: - wl.append(obj) - - # Verify initial state - assert len(wl) == 5 - - # Iterate and check that we can safely delete objects - seen_values = [] - for obj in wl: - seen_values.append(obj.value) - if obj.value == 2: - # Delete another object (not the current one) - del objects[3] # Delete SampleObject(3) - gc.collect() - - # The object with value 3 gets garbage collected during iteration - # so we might not see it (depends on timing) - assert len(seen_values) in [4, 5] - assert all(v in [0, 1, 2, 3, 4] for v in seen_values) - - # After iteration, the list should have 4 objects (one was deleted) - assert len(wl) == 4 diff --git a/dimos/types/timestamped.py b/dimos/types/timestamped.py deleted file mode 100644 index b229a2478e..0000000000 --- a/dimos/types/timestamped.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import defaultdict -from datetime import datetime, timezone -from typing import Generic, TypeVar, Union - -from dimos_lcm.builtin_interfaces import Time as ROSTime -from reactivex import create -from reactivex.disposable import CompositeDisposable - -# from dimos_lcm.std_msgs import Time as ROSTime -from reactivex.observable import Observable - -from dimos.memory.timeseries.inmemory import InMemoryStore -from dimos.types.weaklist import WeakList -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - -# any class that carries a timestamp should inherit from this -# this allows us to work with timeseries in consistent way, allign messages, replay etc -# aditional functionality will come to this class soon - - -# class RosStamp(TypedDict): -# sec: int -# nanosec: int - - -TimeLike = Union[int, float, datetime, ROSTime] - - -def to_timestamp(ts: TimeLike) -> float: - """Convert TimeLike to a timestamp in seconds.""" - if isinstance(ts, datetime): - return ts.timestamp() - if isinstance(ts, int | float): - return float(ts) - if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: - return ts["sec"] + ts["nanosec"] / 1e9 # type: ignore[no-any-return] - # Check for ROS Time-like objects by attributes - if hasattr(ts, "sec") and (hasattr(ts, "nanosec") or hasattr(ts, "nsec")): - # Handle both std_msgs.Time (nsec) and builtin_interfaces.Time (nanosec) - if hasattr(ts, "nanosec"): - return ts.sec + ts.nanosec / 1e9 # type: ignore[no-any-return] - else: # has nsec - return ts.sec + ts.nsec / 1e9 # type: ignore[no-any-return] - raise TypeError("unsupported timestamp type") - - -def to_ros_stamp(ts: TimeLike) -> ROSTime: - """Convert TimeLike to a ROS-style timestamp dictionary.""" - if isinstance(ts, dict) and "sec" in ts and "nanosec" in ts: - return ts - - timestamp = to_timestamp(ts) - sec = int(timestamp) - nanosec = int((timestamp - sec) * 1_000_000_000) - return ROSTime(sec=sec, nanosec=nanosec) - - -def to_human_readable(ts: float) -> str: - """Convert timestamp to human-readable format with date and time.""" - import time - - return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts)) - - -def to_datetime(ts: TimeLike, tz=None) -> datetime: # type: ignore[no-untyped-def] - if isinstance(ts, datetime): - if ts.tzinfo is None: - # Assume UTC for naive datetime - ts = ts.replace(tzinfo=timezone.utc) - if tz is not None: - return ts.astimezone(tz) - return ts.astimezone() # Convert to local tz - - # Convert to timestamp first - timestamp = to_timestamp(ts) - - # Create datetime from timestamp - if tz is not None: - return datetime.fromtimestamp(timestamp, tz=tz) - else: - # Use local timezone by default - return datetime.fromtimestamp(timestamp).astimezone() - - -class Timestamped: - ts: float - - def __init__(self, ts: float) -> None: - self.ts = ts - - def dt(self) -> datetime: - return datetime.fromtimestamp(self.ts, tz=timezone.utc).astimezone() - - def ros_timestamp(self) -> list[int]: - """Convert timestamp to ROS-style list [sec, nanosec].""" - sec = int(self.ts) - nanosec = int((self.ts - sec) * 1_000_000_000) - return [sec, nanosec] - - -T = TypeVar("T", bound=Timestamped) - - -PRIMARY = TypeVar("PRIMARY", bound=Timestamped) -SECONDARY = TypeVar("SECONDARY", bound=Timestamped) - - -class TimestampedBufferCollection(InMemoryStore[T]): - """A sliding time window buffer backed by InMemoryStore.""" - - def __init__(self, window_duration: float) -> None: - super().__init__() - self.window_duration = window_duration - - def add(self, item: T) -> None: - """Add a timestamped item and prune items outside the time window.""" - self.save(item) - self.prune_old(item.ts - self.window_duration) - - def remove(self, item: T) -> bool: - """Remove a timestamped item. Returns True if found and removed.""" - return self._delete(item.ts) is not None - - def remove_by_timestamp(self, timestamp: float) -> bool: - """Remove an item by timestamp. Returns True if found and removed.""" - return self._delete(timestamp) is not None - - -class MatchContainer(Timestamped, Generic[PRIMARY, SECONDARY]): - """ - This class stores a primary item along with its partial matches to secondary items, - tracking which secondaries are still missing to avoid redundant searches. - """ - - def __init__(self, primary: PRIMARY, matches: list[SECONDARY | None]) -> None: - super().__init__(primary.ts) - self.primary = primary - self.matches = matches # Direct list with None for missing matches - - def message_received(self, secondary_idx: int, secondary_item: SECONDARY) -> None: - """Process a secondary message and check if it matches this primary.""" - if self.matches[secondary_idx] is None: - self.matches[secondary_idx] = secondary_item - - def is_complete(self) -> bool: - """Check if all secondary matches have been found.""" - return all(match is not None for match in self.matches) - - def get_tuple(self) -> tuple[PRIMARY, ...]: - """Get the result tuple for emission.""" - return (self.primary, *self.matches) # type: ignore[arg-type] - - -def align_timestamped( - primary_observable: Observable[PRIMARY], - *secondary_observables: Observable[SECONDARY], - buffer_size: float = 1.0, # seconds - match_tolerance: float = 0.1, # seconds -) -> Observable[tuple[PRIMARY, ...]]: - """Align a primary observable with one or more secondary observables. - - Args: - primary_observable: The primary stream to align against - *secondary_observables: One or more secondary streams to align - buffer_size: Time window to keep messages in seconds - match_tolerance: Maximum time difference for matching in seconds - - Returns: - If single secondary observable: Observable that emits tuples of (primary_item, secondary_item) - If multiple secondary observables: Observable that emits tuples of (primary_item, secondary1, secondary2, ...) - Each secondary item is the closest match from the corresponding - secondary observable, or None if no match within tolerance. - """ - - def subscribe(observer, scheduler=None): # type: ignore[no-untyped-def] - # Create a timed buffer collection for each secondary observable - secondary_collections: list[TimestampedBufferCollection[SECONDARY]] = [ - TimestampedBufferCollection(buffer_size) for _ in secondary_observables - ] - - # WeakLists to track subscribers to each secondary observable - secondary_stakeholders = defaultdict(WeakList) # type: ignore[var-annotated] - - # Buffer for unmatched MatchContainers - automatically expires old items - primary_buffer: TimestampedBufferCollection[MatchContainer[PRIMARY, SECONDARY]] = ( - TimestampedBufferCollection(buffer_size) - ) - - # Subscribe to all secondary observables - secondary_subs = [] - - def has_secondary_progressed_past(secondary_ts: float, primary_ts: float) -> bool: - """Check if secondary stream has progressed past the primary + tolerance.""" - return secondary_ts > primary_ts + match_tolerance - - def remove_stakeholder(stakeholder: MatchContainer) -> None: # type: ignore[type-arg] - """Remove a stakeholder from all tracking structures.""" - primary_buffer.remove(stakeholder) - for weak_list in secondary_stakeholders.values(): - weak_list.discard(stakeholder) - - def on_secondary(i: int, secondary_item: SECONDARY) -> None: - # Add the secondary item to its collection - secondary_collections[i].add(secondary_item) - - # Check all stakeholders for this secondary stream - for stakeholder in secondary_stakeholders[i]: - # If the secondary stream has progressed past this primary, - # we won't be able to match it anymore - if has_secondary_progressed_past(secondary_item.ts, stakeholder.ts): - logger.debug(f"secondary progressed, giving up {stakeholder.ts}") - - remove_stakeholder(stakeholder) - continue - - # Check if this secondary is within tolerance of the primary - if abs(stakeholder.ts - secondary_item.ts) <= match_tolerance: - stakeholder.message_received(i, secondary_item) - - # If all secondaries matched, emit result - if stakeholder.is_complete(): - logger.debug(f"Emitting deferred match {stakeholder.ts}") - observer.on_next(stakeholder.get_tuple()) - remove_stakeholder(stakeholder) - - for i, secondary_obs in enumerate(secondary_observables): - secondary_subs.append( - secondary_obs.subscribe( - lambda x, idx=i: on_secondary(idx, x), # type: ignore[misc] - on_error=observer.on_error, - ) - ) - - def on_primary(primary_item: PRIMARY) -> None: - # Try to find matches in existing secondary collections - matches = [None] * len(secondary_observables) - - for i, collection in enumerate(secondary_collections): - closest = collection.find_closest(primary_item.ts, tolerance=match_tolerance) - if closest is not None: - matches[i] = closest # type: ignore[call-overload] - else: - # Check if this secondary stream has already progressed past this primary - if collection.end_ts is not None and has_secondary_progressed_past( - collection.end_ts, primary_item.ts - ): - # This secondary won't match, so don't buffer this primary - return - - # If all matched, emit immediately without creating MatchContainer - if all(match is not None for match in matches): - logger.debug(f"Immadiate match {primary_item.ts}") - result = (primary_item, *matches) - observer.on_next(result) - else: - logger.debug(f"Deferred match attempt {primary_item.ts}") - match_container = MatchContainer(primary_item, matches) # type: ignore[type-var] - primary_buffer.add(match_container) # type: ignore[arg-type] - - for i, match in enumerate(matches): - if match is None: - secondary_stakeholders[i].append(match_container) - - # Subscribe to primary observable - primary_sub = primary_observable.subscribe( - on_primary, on_error=observer.on_error, on_completed=observer.on_completed - ) - - # Return a CompositeDisposable for proper cleanup - return CompositeDisposable(primary_sub, *secondary_subs) - - return create(subscribe) diff --git a/dimos/types/vector.py b/dimos/types/vector.py deleted file mode 100644 index 654dc1f378..0000000000 --- a/dimos/types/vector.py +++ /dev/null @@ -1,457 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import builtins -from collections.abc import Sequence -from typing import TypeVar, Union - -import numpy as np - -from dimos.types.ros_polyfill import Vector3 - -T = TypeVar("T", bound="Vector") - -# Vector-like types that can be converted to/from Vector -VectorLike = Union[Sequence[int | float], Vector3, "Vector", np.ndarray] # type: ignore[type-arg] - - -class Vector: - """A wrapper around numpy arrays for vector operations with intuitive syntax.""" - - def __init__(self, *args: VectorLike) -> None: - """Initialize a vector from components or another iterable. - - Examples: - Vector(1, 2) # 2D vector - Vector(1, 2, 3) # 3D vector - Vector([1, 2, 3]) # From list - Vector(np.array([1, 2, 3])) # From numpy array - """ - if len(args) == 1 and hasattr(args[0], "__iter__"): - self._data = np.array(args[0], dtype=float) - - elif len(args) == 1: - self._data = np.array([args[0].x, args[0].y, args[0].z], dtype=float) # type: ignore[union-attr] - - else: - self._data = np.array(args, dtype=float) - - @property - def yaw(self) -> float: - return self.x - - @property - def tuple(self) -> tuple[float, ...]: - """Tuple representation of the vector.""" - return tuple(self._data) - - @property - def x(self) -> float: - """X component of the vector.""" - return self._data[0] if len(self._data) > 0 else 0.0 - - @property - def y(self) -> float: - """Y component of the vector.""" - return self._data[1] if len(self._data) > 1 else 0.0 - - @property - def z(self) -> float: - """Z component of the vector.""" - return self._data[2] if len(self._data) > 2 else 0.0 - - @property - def dim(self) -> int: - """Dimensionality of the vector.""" - return len(self._data) - - @property - def data(self) -> np.ndarray: # type: ignore[type-arg] - """Get the underlying numpy array.""" - return self._data - - def __getitem__(self, idx: int): # type: ignore[no-untyped-def] - return self._data[idx] - - def __repr__(self) -> str: - return f"Vector({self.data})" - - def __str__(self) -> str: - if self.dim < 2: - return self.__repr__() - - def getArrow(): # type: ignore[no-untyped-def] - repr = ["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"] - - if self.x == 0 and self.y == 0: - return "·" - - # Calculate angle in radians and convert to directional index - angle = np.arctan2(self.y, self.x) - # Map angle to 0-7 index (8 directions) with proper orientation - dir_index = int(((angle + np.pi) * 4 / np.pi) % 8) - # Get directional arrow symbol - return repr[dir_index] - - return f"{getArrow()} Vector {self.__repr__()}" # type: ignore[no-untyped-call] - - def serialize(self) -> builtins.tuple: # type: ignore[type-arg] - """Serialize the vector to a tuple.""" - return {"type": "vector", "c": self._data.tolist()} # type: ignore[return-value] - - def __eq__(self, other) -> bool: # type: ignore[no-untyped-def] - """Check if two vectors are equal using numpy's allclose for floating point comparison.""" - if not isinstance(other, Vector): - return False - if len(self._data) != len(other._data): - return False - return np.allclose(self._data, other._data) - - def __add__(self: T, other: VectorLike) -> T: - other = to_vector(other) - if self.dim != other.dim: - max_dim = max(self.dim, other.dim) - return self.pad(max_dim) + other.pad(max_dim) - return self.__class__(self._data + other._data) - - def __sub__(self: T, other: VectorLike) -> T: - other = to_vector(other) - if self.dim != other.dim: - max_dim = max(self.dim, other.dim) - return self.pad(max_dim) - other.pad(max_dim) - return self.__class__(self._data - other._data) - - def __mul__(self: T, scalar: float) -> T: - return self.__class__(self._data * scalar) - - def __rmul__(self: T, scalar: float) -> T: - return self.__mul__(scalar) - - def __truediv__(self: T, scalar: float) -> T: - return self.__class__(self._data / scalar) - - def __neg__(self: T) -> T: - return self.__class__(-self._data) - - def dot(self, other: VectorLike) -> float: - """Compute dot product.""" - other = to_vector(other) - return float(np.dot(self._data, other._data)) - - def cross(self: T, other: VectorLike) -> T: - """Compute cross product (3D vectors only).""" - if self.dim != 3: - raise ValueError("Cross product is only defined for 3D vectors") - - other = to_vector(other) - if other.dim != 3: - raise ValueError("Cross product requires two 3D vectors") - - return self.__class__(np.cross(self._data, other._data)) - - def length(self) -> float: - """Compute the Euclidean length (magnitude) of the vector.""" - return float(np.linalg.norm(self._data)) - - def length_squared(self) -> float: - """Compute the squared length of the vector (faster than length()).""" - return float(np.sum(self._data * self._data)) - - def normalize(self: T) -> T: - """Return a normalized unit vector in the same direction.""" - length = self.length() - if length < 1e-10: # Avoid division by near-zero - return self.__class__(np.zeros_like(self._data)) - return self.__class__(self._data / length) - - def to_2d(self: T) -> T: - """Convert a vector to a 2D vector by taking only the x and y components.""" - return self.__class__(self._data[:2]) - - def pad(self: T, dim: int) -> T: - """Pad a vector with zeros to reach the specified dimension. - - If vector already has dimension >= dim, it is returned unchanged. - """ - if self.dim >= dim: - return self - - padded = np.zeros(dim, dtype=float) - padded[: len(self._data)] = self._data - return self.__class__(padded) - - def distance(self, other: VectorLike) -> float: - """Compute Euclidean distance to another vector.""" - other = to_vector(other) - return float(np.linalg.norm(self._data - other._data)) - - def distance_squared(self, other: VectorLike) -> float: - """Compute squared Euclidean distance to another vector (faster than distance()).""" - other = to_vector(other) - diff = self._data - other._data - return float(np.sum(diff * diff)) - - def angle(self, other: VectorLike) -> float: - """Compute the angle (in radians) between this vector and another.""" - other = to_vector(other) - if self.length() < 1e-10 or other.length() < 1e-10: - return 0.0 - - cos_angle = np.clip( - np.dot(self._data, other._data) - / (np.linalg.norm(self._data) * np.linalg.norm(other._data)), - -1.0, - 1.0, - ) - return float(np.arccos(cos_angle)) - - def project(self: T, onto: VectorLike) -> T: - """Project this vector onto another vector.""" - onto = to_vector(onto) - onto_length_sq = np.sum(onto._data * onto._data) - if onto_length_sq < 1e-10: - return self.__class__(np.zeros_like(self._data)) - - scalar_projection = np.dot(self._data, onto._data) / onto_length_sq - return self.__class__(scalar_projection * onto._data) - - @classmethod - def zeros(cls: type[T], dim: int) -> T: - """Create a zero vector of given dimension.""" - return cls(np.zeros(dim)) - - @classmethod - def ones(cls: type[T], dim: int) -> T: - """Create a vector of ones with given dimension.""" - return cls(np.ones(dim)) - - @classmethod - def unit_x(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the x direction.""" - v = np.zeros(dim) - v[0] = 1.0 - return cls(v) - - @classmethod - def unit_y(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the y direction.""" - v = np.zeros(dim) - v[1] = 1.0 - return cls(v) - - @classmethod - def unit_z(cls: type[T], dim: int = 3) -> T: - """Create a unit vector in the z direction.""" - v = np.zeros(dim) - if dim > 2: - v[2] = 1.0 - return cls(v) - - def to_list(self) -> list[float]: - """Convert the vector to a list.""" - return self._data.tolist() # type: ignore[no-any-return] - - def to_tuple(self) -> builtins.tuple[float, ...]: - """Convert the vector to a tuple.""" - return tuple(self._data) - - def to_numpy(self) -> np.ndarray: # type: ignore[type-arg] - """Convert the vector to a numpy array.""" - return self._data - - def is_zero(self) -> bool: - """Check if this is a zero vector (all components are zero). - - Returns: - True if all components are zero, False otherwise - """ - return np.allclose(self._data, 0.0) - - def __bool__(self) -> bool: - """Boolean conversion for Vector. - - A Vector is considered False if it's a zero vector (all components are zero), - and True otherwise. - - Returns: - False if vector is zero, True otherwise - """ - return not self.is_zero() - - -def to_numpy(value: VectorLike) -> np.ndarray: # type: ignore[type-arg] - """Convert a vector-compatible value to a numpy array. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Numpy array representation - """ - if isinstance(value, Vector3): - return np.array([value.x, value.y, value.z], dtype=float) - if isinstance(value, Vector): - return value.data - elif isinstance(value, np.ndarray): - return value - else: - return np.array(value, dtype=float) - - -def to_vector(value: VectorLike) -> Vector: - """Convert a vector-compatible value to a Vector object. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Vector object - """ - if isinstance(value, Vector): - return value - else: - return Vector(value) - - -def to_tuple(value: VectorLike) -> tuple[float, ...]: - """Convert a vector-compatible value to a tuple. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Tuple of floats - """ - if isinstance(value, Vector3): - return tuple([value.x, value.y, value.z]) - if isinstance(value, Vector): - return tuple(value.data) - elif isinstance(value, np.ndarray): - return tuple(value.tolist()) - elif isinstance(value, tuple): - return value - else: - return tuple(value) - - -def to_list(value: VectorLike) -> list[float]: - """Convert a vector-compatible value to a list. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - List of floats - """ - if isinstance(value, Vector): - return value.data.tolist() # type: ignore[no-any-return] - elif isinstance(value, np.ndarray): - return value.tolist() # type: ignore[no-any-return] - elif isinstance(value, list): - return value - else: - return list(value) # type: ignore[arg-type] - - -# Helper functions to check dimensionality -def is_2d(value: VectorLike) -> bool: - """Check if a vector-compatible value is 2D. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - True if the value is 2D - """ - if isinstance(value, Vector3): - return False - elif isinstance(value, Vector): - return len(value) == 2 # type: ignore[arg-type] - elif isinstance(value, np.ndarray): - return value.shape[-1] == 2 or value.size == 2 - else: - return len(value) == 2 - - -def is_3d(value: VectorLike) -> bool: - """Check if a vector-compatible value is 3D. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - True if the value is 3D - """ - if isinstance(value, Vector): - return len(value) == 3 # type: ignore[arg-type] - elif isinstance(value, Vector3): - return True - elif isinstance(value, np.ndarray): - return value.shape[-1] == 3 or value.size == 3 - else: - return len(value) == 3 - - -# Extraction functions for XYZ components -def x(value: VectorLike) -> float: - """Get the X component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - X component as a float - """ - if isinstance(value, Vector): - return value.x - elif isinstance(value, Vector3): - return value.x # type: ignore[no-any-return] - else: - return float(to_numpy(value)[0]) - - -def y(value: VectorLike) -> float: - """Get the Y component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Y component as a float - """ - if isinstance(value, Vector): - return value.y - elif isinstance(value, Vector3): - return value.y # type: ignore[no-any-return] - else: - arr = to_numpy(value) - return float(arr[1]) if len(arr) > 1 else 0.0 - - -def z(value: VectorLike) -> float: - """Get the Z component of a vector-compatible value. - - Args: - value: Any vector-like object (Vector, numpy array, tuple, list) - - Returns: - Z component as a float - """ - if isinstance(value, Vector): - return value.z - elif isinstance(value, Vector3): - return value.z # type: ignore[no-any-return] - else: - arr = to_numpy(value) - return float(arr[2]) if len(arr) > 2 else 0.0 diff --git a/dimos/types/weaklist.py b/dimos/types/weaklist.py deleted file mode 100644 index a720d54e2d..0000000000 --- a/dimos/types/weaklist.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Weak reference list implementation that automatically removes dead references.""" - -from collections.abc import Iterator -from typing import Any -import weakref - - -class WeakList: - """A list that holds weak references to objects. - - Objects are automatically removed when garbage collected. - Supports iteration, append, remove, and length operations. - """ - - def __init__(self) -> None: - self._refs = [] # type: ignore[var-annotated] - - def append(self, obj: Any) -> None: - """Add an object to the list (stored as weak reference).""" - - def _cleanup(ref) -> None: # type: ignore[no-untyped-def] - try: - self._refs.remove(ref) - except ValueError: - pass - - self._refs.append(weakref.ref(obj, _cleanup)) - - def remove(self, obj: Any) -> None: - """Remove an object from the list.""" - for i, ref in enumerate(self._refs): - if ref() is obj: - del self._refs[i] - return - raise ValueError(f"{obj} not in WeakList") - - def discard(self, obj: Any) -> None: - """Remove an object from the list if present, otherwise do nothing.""" - try: - self.remove(obj) - except ValueError: - pass - - def __iter__(self) -> Iterator[Any]: - """Iterate over live objects, skipping dead references.""" - # Create a copy to avoid modification during iteration - for ref in self._refs[:]: - obj = ref() - if obj is not None: - yield obj - - def __len__(self) -> int: - """Return count of live objects.""" - return sum(1 for _ in self) - - def __contains__(self, obj: Any) -> bool: - """Check if object is in the list.""" - return any(ref() is obj for ref in self._refs) - - def clear(self) -> None: - """Remove all references.""" - self._refs.clear() - - def __getitem__(self, index: int) -> Any: - """Get object at index (only counting live objects).""" - for i, obj in enumerate(self): - if i == index: - return obj - raise IndexError("WeakList index out of range") - - def __repr__(self) -> str: - return f"WeakList({list(self)})" diff --git a/dimos/utils/actor_registry.py b/dimos/utils/actor_registry.py deleted file mode 100644 index 6f6d219594..0000000000 --- a/dimos/utils/actor_registry.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Shared memory registry for tracking actor deployments across processes.""" - -import json -from multiprocessing import shared_memory - - -class ActorRegistry: - """Shared memory registry of actor deployments.""" - - SHM_NAME = "dimos_actor_registry" - SHM_SIZE = 65536 # 64KB should be enough for most deployments - - @staticmethod - def update(actor_name: str, worker_id: str) -> None: - """Update registry with new actor deployment.""" - try: - shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) - except FileNotFoundError: - shm = shared_memory.SharedMemory( - name=ActorRegistry.SHM_NAME, create=True, size=ActorRegistry.SHM_SIZE - ) - - # Read existing data - data = ActorRegistry._read_from_shm(shm) - - # Update with new actor - data[actor_name] = worker_id - - # Write back - ActorRegistry._write_to_shm(shm, data) - shm.close() - - @staticmethod - def get_all() -> dict[str, str]: - """Get all actor->worker mappings.""" - try: - shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) - data = ActorRegistry._read_from_shm(shm) - shm.close() - return data - except FileNotFoundError: - return {} - - @staticmethod - def clear() -> None: - """Clear the registry and free shared memory.""" - try: - shm = shared_memory.SharedMemory(name=ActorRegistry.SHM_NAME) - ActorRegistry._write_to_shm(shm, {}) - shm.close() - shm.unlink() - except FileNotFoundError: - pass - - @staticmethod - def _read_from_shm(shm) -> dict[str, str]: # type: ignore[no-untyped-def] - """Read JSON data from shared memory.""" - raw = bytes(shm.buf[:]).rstrip(b"\x00") - if not raw: - return {} - return json.loads(raw.decode("utf-8")) # type: ignore[no-any-return] - - @staticmethod - def _write_to_shm(shm, data: dict[str, str]): # type: ignore[no-untyped-def] - """Write JSON data to shared memory.""" - json_bytes = json.dumps(data).encode("utf-8") - if len(json_bytes) > ActorRegistry.SHM_SIZE: - raise ValueError("Registry data too large for shared memory") - shm.buf[: len(json_bytes)] = json_bytes - shm.buf[len(json_bytes) :] = b"\x00" * (ActorRegistry.SHM_SIZE - len(json_bytes)) diff --git a/dimos/utils/cli/__init__.py b/dimos/utils/cli/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/utils/cli/agentspy/agentspy.py b/dimos/utils/cli/agentspy/agentspy.py deleted file mode 100644 index a0ee43a62d..0000000000 --- a/dimos/utils/cli/agentspy/agentspy.py +++ /dev/null @@ -1,238 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from collections import deque -from dataclasses import dataclass -import time -from typing import Any, Union - -from langchain_core.messages import ( - AIMessage, - HumanMessage, - SystemMessage, - ToolMessage, -) -from textual.app import App, ComposeResult -from textual.binding import Binding -from textual.widgets import Footer, RichLog - -from dimos.protocol.pubsub.impl.lcmpubsub import PickleLCM -from dimos.utils.cli import theme - -# Type alias for all message types we might receive -AnyMessage = Union[SystemMessage, ToolMessage, AIMessage, HumanMessage] - - -@dataclass -class MessageEntry: - """Store a single message with metadata.""" - - timestamp: float - message: AnyMessage - - def __post_init__(self) -> None: - """Initialize timestamp if not provided.""" - if self.timestamp is None: - self.timestamp = time.time() - - -class AgentMessageMonitor: - """Monitor agent messages published via LCM.""" - - def __init__(self, topic: str = "/agent", max_messages: int = 1000) -> None: - self.topic = topic - self.max_messages = max_messages - self.messages: deque[MessageEntry] = deque(maxlen=max_messages) - self.transport = PickleLCM() - self.transport.start() - self.callbacks: list[callable] = [] # type: ignore[valid-type] - pass - - def start(self) -> None: - """Start monitoring messages.""" - self.transport.subscribe(self.topic, self._handle_message) - - def stop(self) -> None: - """Stop monitoring.""" - # PickleLCM doesn't have explicit stop method - pass - - def _handle_message(self, msg: Any, topic: str) -> None: - """Handle incoming messages.""" - # Check if it's one of the message types we care about - if isinstance(msg, SystemMessage | ToolMessage | AIMessage | HumanMessage): - entry = MessageEntry(timestamp=time.time(), message=msg) - self.messages.append(entry) - - # Notify callbacks - for callback in self.callbacks: - callback(entry) # type: ignore[misc] - else: - pass - - def subscribe(self, callback: callable) -> None: # type: ignore[valid-type] - """Subscribe to new messages.""" - self.callbacks.append(callback) - - def get_messages(self) -> list[MessageEntry]: - """Get all stored messages.""" - return list(self.messages) - - -def format_timestamp(timestamp: float) -> str: - """Format timestamp as HH:MM:SS.mmm.""" - return ( - time.strftime("%H:%M:%S", time.localtime(timestamp)) + f".{int((timestamp % 1) * 1000):03d}" - ) - - -def get_message_type_and_style(msg: AnyMessage) -> tuple[str, str]: - """Get message type name and style color.""" - if isinstance(msg, HumanMessage): - return "Human ", "green" - elif isinstance(msg, AIMessage): - if hasattr(msg, "metadata") and msg.metadata.get("state"): - return "State ", "blue" - return "Agent ", "yellow" - elif isinstance(msg, ToolMessage): - return "Tool ", "red" - elif isinstance(msg, SystemMessage): - return "System", "red" - else: - return "Unkn ", "white" - - -def format_message_content(msg: AnyMessage) -> str: - """Format message content for display.""" - if isinstance(msg, ToolMessage): - return f"{msg.name}() -> {msg.content}" - elif isinstance(msg, AIMessage) and msg.tool_calls: - # Include tool calls in content - tool_info = [] - for tc in msg.tool_calls: - args_str = str(tc.get("args", {})) - tool_info.append(f"{tc.get('name')}({args_str})") - content = msg.content or "" - if content and tool_info: - return f"{content}\n[Tool Calls: {', '.join(tool_info)}]" - elif tool_info: - return f"[Tool Calls: {', '.join(tool_info)}]" - return content # type: ignore[return-value] - else: - return str(msg.content) if hasattr(msg, "content") else str(msg) - - -class AgentSpyApp(App): # type: ignore[type-arg] - """TUI application for monitoring agent messages.""" - - CSS_PATH = theme.CSS_PATH - - CSS = f""" - Screen {{ - layout: vertical; - background: {theme.BACKGROUND}; - }} - - RichLog {{ - height: 1fr; - border: none; - background: {theme.BACKGROUND}; - padding: 0 1; - }} - - Footer {{ - dock: bottom; - height: 1; - }} - """ - - BINDINGS = [ - Binding("q", "quit", "Quit"), - Binding("c", "clear", "Clear"), - Binding("ctrl+c", "quit", show=False), - ] - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self.monitor = AgentMessageMonitor() - self.message_log: RichLog | None = None - - def compose(self) -> ComposeResult: - """Compose the UI.""" - self.message_log = RichLog(wrap=True, highlight=True, markup=True) - yield self.message_log - yield Footer() - - def on_mount(self) -> None: - """Start monitoring when app mounts.""" - self.theme = "flexoki" - - # Subscribe to new messages - self.monitor.subscribe(self.on_new_message) - self.monitor.start() - - # Write existing messages to the log - for entry in self.monitor.get_messages(): - self.on_new_message(entry) - - def on_unmount(self) -> None: - """Stop monitoring when app unmounts.""" - self.monitor.stop() - - def on_new_message(self, entry: MessageEntry) -> None: - """Handle new messages.""" - if self.message_log: - msg = entry.message - msg_type, style = get_message_type_and_style(msg) - content = format_message_content(msg) - - # Format the message for the log - timestamp = format_timestamp(entry.timestamp) - self.message_log.write( - f"[dim white]{timestamp}[/dim white] | " - f"[bold {style}]{msg_type}[/bold {style}] | " - f"[{style}]{content}[/{style}]" - ) - - def refresh_display(self) -> None: - """Refresh the message display.""" - # Not needed anymore as messages are written directly to the log - - def action_clear(self) -> None: - """Clear message history.""" - self.monitor.messages.clear() - if self.message_log: - self.message_log.clear() - - -def main() -> None: - """Main entry point for agentspy.""" - import sys - - if len(sys.argv) > 1 and sys.argv[1] == "web": - import os - - from textual_serve.server import Server # type: ignore[import-not-found] - - server = Server(f"python {os.path.abspath(__file__)}") - server.serve() - else: - app = AgentSpyApp() - app.run() - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/cli/agentspy/demo_agentspy.py b/dimos/utils/cli/agentspy/demo_agentspy.py deleted file mode 100755 index 5229295038..0000000000 --- a/dimos/utils/cli/agentspy/demo_agentspy.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Demo script to test agent message publishing and agentspy reception.""" - -import time - -from langchain_core.messages import ( - AIMessage, - HumanMessage, - SystemMessage, - ToolMessage, -) - -from dimos.protocol.pubsub import lcm # type: ignore[attr-defined] -from dimos.protocol.pubsub.impl.lcmpubsub import PickleLCM - - -def test_publish_messages() -> None: - """Publish test messages to verify agentspy is working.""" - print("Starting agent message publisher demo...") - - # Create transport - transport = PickleLCM() - topic = lcm.Topic("/agent") - - print(f"Publishing to topic: {topic}") - - # Test messages - messages = [ - SystemMessage("System initialized for testing"), - HumanMessage("Hello agent, can you help me?"), - AIMessage( - "Of course! I'm here to help.", - tool_calls=[{"name": "get_info", "args": {"query": "test"}, "id": "1"}], - ), - ToolMessage(name="get_info", content="Test result: success", tool_call_id="1"), - AIMessage("The test was successful!", metadata={"state": True}), - ] - - # Publish messages with delays - for i, msg in enumerate(messages): - print(f"\nPublishing message {i + 1}: {type(msg).__name__}") - print(f"Content: {msg.content if hasattr(msg, 'content') else msg}") - - transport.publish(topic, msg) - time.sleep(1) # Wait 1 second between messages - - print("\nAll messages published! Check agentspy to see if they were received.") - print("Keeping publisher alive for 10 more seconds...") - time.sleep(10) - - -if __name__ == "__main__": - test_publish_messages() diff --git a/dimos/utils/cli/dimos.tcss b/dimos/utils/cli/dimos.tcss deleted file mode 100644 index 3ccbde957d..0000000000 --- a/dimos/utils/cli/dimos.tcss +++ /dev/null @@ -1,91 +0,0 @@ -/* DimOS Base Theme for Textual CLI Applications - * Based on colors.json - Official DimOS color palette - */ - -/* Base Color Palette (from colors.json) */ -$black: #0b0f0f; -$red: #ff0000; -$green: #00eeee; -$yellow: #ffcc00; -$blue: #5c9ff0; -$purple: #00eeee; -$cyan: #00eeee; -$white: #b5e4f4; - -/* Bright Colors */ -$bright-black: #404040; -$bright-red: #ff0000; -$bright-green: #00eeee; -$bright-yellow: #f2ea8c; -$bright-blue: #8cbdf2; -$bright-purple: #00eeee; -$bright-cyan: #00eeee; -$bright-white: #ffffff; - -/* Core Theme Colors */ -$background: #0b0f0f; -$foreground: #b5e4f4; -$cursor: #00eeee; - -/* Semantic Aliases */ -$bg: $black; -$border: $cyan; -$accent: $white; -$dim: $bright-black; -$timestamp: $bright-white; - -/* Message Type Colors */ -$system: $red; -$agent: #88ff88; -$tool: $cyan; -$tool-result: $yellow; -$human: $bright-white; - -/* Status Colors */ -$success: $green; -$error: $red; -$warning: $yellow; -$info: $cyan; - -/* Base Screen */ -Screen { - background: $bg; -} - -/* Default Container */ -Container { - background: $bg; -} - -/* Input Widget */ -Input { - background: $bg; - border: solid $border; - color: $accent; -} - -Input:focus { - border: solid $border; -} - -/* RichLog Widget */ -RichLog { - background: $bg; - border: solid $border; -} - -/* Button Widget */ -Button { - background: $bg; - border: solid $border; - color: $accent; -} - -Button:hover { - background: $dim; - border: solid $accent; -} - -Button:focus { - border: double $accent; -} diff --git a/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py b/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py deleted file mode 100644 index 949a500f21..0000000000 --- a/dimos/utils/cli/foxglove_bridge/run_foxglove_bridge.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -use lcm_foxglove_bridge as a module from dimos_lcm -""" - -import asyncio -import os -import threading - -import dimos_lcm -from dimos_lcm.foxglove_bridge import FoxgloveBridge - -dimos_lcm_path = os.path.dirname(os.path.abspath(dimos_lcm.__file__)) -print(f"Using dimos_lcm from: {dimos_lcm_path}") - - -def run_bridge_example() -> None: - """Example of running the bridge in a separate thread""" - - def bridge_thread() -> None: - """Thread function to run the bridge""" - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - try: - bridge_instance = FoxgloveBridge(host="0.0.0.0", port=8765, debug=False, num_threads=4) - - loop.run_until_complete(bridge_instance.run()) - except Exception as e: - print(f"Bridge error: {e}") - finally: - loop.close() - - thread = threading.Thread(target=bridge_thread, daemon=True) - thread.start() - - print("Bridge started in background thread") - print("Open Foxglove Studio and connect to ws://localhost:8765") - print("Press Ctrl+C to exit") - - try: - while True: - threading.Event().wait(1) - except KeyboardInterrupt: - print("Shutting down...") - - -def main() -> None: - run_bridge_example() - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/cli/human/humancli.py b/dimos/utils/cli/human/humancli.py deleted file mode 100644 index cf7fd8a258..0000000000 --- a/dimos/utils/cli/human/humancli.py +++ /dev/null @@ -1,409 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from datetime import datetime -import json -import textwrap -import threading -from typing import TYPE_CHECKING, Any - -from langchain_core.messages import AIMessage, HumanMessage, SystemMessage, ToolCall, ToolMessage -from rich.highlighter import JSONHighlighter -from rich.theme import Theme -from textual.app import App, ComposeResult -from textual.binding import Binding -from textual.containers import Container -from textual.geometry import Size -from textual.widgets import Input, RichLog - -from dimos.core import pLCMTransport -from dimos.utils.cli import theme -from dimos.utils.generic import truncate_display_string - -if TYPE_CHECKING: - from collections.abc import Callable - - from textual.events import Key - -# Custom theme for JSON highlighting -JSON_THEME = Theme( - { - "json.key": theme.CYAN, - "json.str": theme.ACCENT, - "json.number": theme.ACCENT, - "json.bool_true": theme.ACCENT, - "json.bool_false": theme.ACCENT, - "json.null": theme.DIM, - "json.brace": theme.BRIGHT_WHITE, - } -) - - -class ThinkingIndicator: - """Manages a throbbing 'thinking...' chat message in a RichLog.""" - - def __init__( - self, - app: App[Any], - chat_log: RichLog, - add_message_fn: Callable[[str, str, str, str], None], - ) -> None: - self._app: App[Any] = app - self._chat_log = chat_log - self._add_message = add_message_fn - self._timer: Any = None - self._strips: list[Any] = [] - self.visible = False - self._throb_dim = False - - def show(self) -> None: - if self.visible: - return - self.visible = True - self._throb_dim = False - self._write_line() - self._timer = self._app.set_interval(0.6, self._toggle_throb) - - def hide(self) -> None: - if not self.visible: - return - self.visible = False - if self._timer is not None: - self._timer.stop() - self._timer = None - self._remove_lines() - - def detach_if_needed(self) -> bool: - if self.visible and self._strips: - self._remove_lines() - return True - return False - - def reattach(self) -> None: - self._write_line() - - def _write_line(self) -> None: - before_count = len(self._chat_log.lines) - color = theme.DIM if self._throb_dim else theme.ACCENT - timestamp = datetime.now().strftime("%H:%M:%S") - self._add_message(timestamp, "", "[italic]thinking...[/italic]", color) - self._strips = list(self._chat_log.lines[before_count:]) - - def _remove_lines(self) -> None: - if not self._strips: - return - strip_ids = {id(s) for s in self._strips} - self._chat_log.lines = [line for line in self._chat_log.lines if id(line) not in strip_ids] - self._strips = [] - self._chat_log._line_cache.clear() - self._chat_log.virtual_size = Size( - self._chat_log.virtual_size.width, len(self._chat_log.lines) - ) - self._chat_log.refresh() - - def _toggle_throb(self) -> None: - if not self.visible: - return - self._remove_lines() - self._throb_dim = not self._throb_dim - self._write_line() - - -class HumanCLIApp(App): # type: ignore[type-arg] - """IRC-like interface for interacting with DimOS agents.""" - - CSS_PATH = theme.CSS_PATH - - CSS = f""" - Screen {{ - background: {theme.BACKGROUND}; - }} - - #chat-container {{ - height: 1fr; - }} - - RichLog {{ - scrollbar-size: 0 0; - }} - - Input {{ - dock: bottom; - }} - - """ - - BINDINGS = [ - Binding("q", "quit", "Quit", show=False), - Binding("ctrl+c", "quit", "Quit"), - Binding("ctrl+l", "clear", "Clear chat"), - ] - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self._human_transport = pLCMTransport("/human_input") # type: ignore[var-annotated] - self._agent_transport = pLCMTransport("/agent") # type: ignore[var-annotated] - self._agent_idle = pLCMTransport("/agent_idle") # type: ignore[var-annotated] - self.chat_log: RichLog | None = None - self.input_widget: Input | None = None - self._subscription_thread: threading.Thread | None = None - self._idle_subscription_thread: threading.Thread | None = None - self._thinking: ThinkingIndicator | None = None - self._running = False - - def compose(self) -> ComposeResult: - """Compose the IRC-like interface.""" - with Container(id="chat-container"): - self.chat_log = RichLog(highlight=True, markup=True, wrap=False) - yield self.chat_log - - self.input_widget = Input(placeholder="Type a message...") - yield self.input_widget - - def on_mount(self) -> None: - """Initialize the app when mounted.""" - self._running = True - - # Apply custom JSON theme to app console - self.console.push_theme(JSON_THEME) - - # Set custom highlighter for RichLog - self.chat_log.highlighter = JSONHighlighter() # type: ignore[union-attr] - - assert self.chat_log is not None - self._thinking = ThinkingIndicator(self, self.chat_log, self._add_message) - - # Start subscription threads - self._subscription_thread = threading.Thread(target=self._subscribe_to_agent, daemon=True) - self._subscription_thread.start() - self._idle_subscription_thread = threading.Thread( - target=self._subscribe_to_idle, daemon=True - ) - self._idle_subscription_thread.start() - - # Focus on input - self.input_widget.focus() # type: ignore[union-attr] - - self.chat_log.write(f"[{theme.ACCENT}]{theme.ascii_logo}[/{theme.ACCENT}]") # type: ignore[union-attr] - - self._add_system_message("Connected to DimOS Agent Interface") - - def on_unmount(self) -> None: - """Clean up when unmounting.""" - self._running = False - - def _subscribe_to_agent(self) -> None: - """Subscribe to agent messages in a separate thread.""" - - def receive_msg(msg) -> None: # type: ignore[no-untyped-def] - if not self._running: - return - - timestamp = datetime.now().strftime("%H:%M:%S") - - if isinstance(msg, SystemMessage): - self.call_from_thread( - self._add_message, - timestamp, - "system", - truncate_display_string(msg.content, 1000), - theme.YELLOW, - ) - elif isinstance(msg, AIMessage): - content = msg.content or "" - tool_calls = getattr(msg, "tool_calls", None) or msg.additional_kwargs.get( - "tool_calls", [] - ) - - # Display the main content first - if content: - self.call_from_thread( - self._add_message, timestamp, "agent", content, theme.AGENT - ) - - # Display tool calls separately with different formatting - if tool_calls: - for tc in tool_calls: - tool_info = self._format_tool_call(tc) - self.call_from_thread( - self._add_message, timestamp, "tool", tool_info, theme.TOOL - ) - - # If neither content nor tool calls, show a placeholder - if not content and not tool_calls: - self.call_from_thread( - self._add_message, timestamp, "agent", "", theme.DIM - ) - elif isinstance(msg, ToolMessage): - self.call_from_thread( - self._add_message, timestamp, "tool", msg.content, theme.TOOL_RESULT - ) - elif isinstance(msg, HumanMessage): - self.call_from_thread( - self._add_message, timestamp, "human", msg.content, theme.HUMAN - ) - - self._agent_transport.subscribe(receive_msg) - - def _subscribe_to_idle(self) -> None: - def receive_idle(is_idle: bool) -> None: - assert self._thinking is not None - - if not self._running: - return - - self.call_from_thread(self._thinking.hide if is_idle else self._thinking.show) - - self._agent_idle.subscribe(receive_idle) - - def _format_tool_call(self, tool_call: ToolCall) -> str: - """Format a tool call for display.""" - name = tool_call.get("name", "unknown") - args = tool_call.get("args", {}) - args_str = json.dumps(args, separators=(",", ":")) - return f"▶ {name}({args_str})" - - def _add_message(self, timestamp: str, sender: str, content: str, color: str) -> None: - assert self._thinking is not None - reattach = self._thinking.detach_if_needed() - - # Strip leading/trailing whitespace from content - content = content.strip() if content else "" - - # Format timestamp with nicer colors - split into hours, minutes, seconds - time_parts = timestamp.split(":") - if len(time_parts) == 3: - # Format as HH:MM:SS with colored colons - timestamp_formatted = f" [{theme.TIMESTAMP}]{time_parts[0]}:{time_parts[1]}:{time_parts[2]}[/{theme.TIMESTAMP}]" - else: - timestamp_formatted = f" [{theme.TIMESTAMP}]{timestamp}[/{theme.TIMESTAMP}]" - - # Format sender with consistent width - sender_formatted = f"[{color}]{sender:>8}[/{color}]" - - # Calculate the prefix length for proper indentation - # space (1) + timestamp (8) + space (1) + sender (8) + space (1) + separator (1) + space (1) = 21 - prefix = f"{timestamp_formatted} {sender_formatted} │ " - indent = " " * 19 # Spaces to align with the content after the separator - - # Get the width of the chat area (accounting for borders and padding) - width = self.chat_log.size.width - 4 if self.chat_log.size else 76 # type: ignore[union-attr] - - # Calculate the available width for text (subtract prefix length) - text_width = max(width - 20, 40) # Minimum 40 chars for text - - # Split content into lines first (respecting explicit newlines) - lines = content.split("\n") - - for line_idx, line in enumerate(lines): - # Wrap each line to fit the available width - if line_idx == 0: - # First line includes the full prefix - wrapped = textwrap.wrap( - line, width=text_width, initial_indent="", subsequent_indent="" - ) - if wrapped: - self.chat_log.write(prefix + f"[{color}]{wrapped[0]}[/{color}]") # type: ignore[union-attr] - for wrapped_line in wrapped[1:]: - self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") # type: ignore[union-attr] - else: - # Empty line - self.chat_log.write(prefix) # type: ignore[union-attr] - else: - # Subsequent lines from explicit newlines - wrapped = textwrap.wrap( - line, width=text_width, initial_indent="", subsequent_indent="" - ) - if wrapped: - for wrapped_line in wrapped: - self.chat_log.write(indent + f"│ [{color}]{wrapped_line}[/{color}]") # type: ignore[union-attr] - else: - # Empty line - self.chat_log.write(indent + "│") # type: ignore[union-attr] - - if reattach: - self._thinking.reattach() - - def _add_system_message(self, content: str) -> None: - """Add a system message to the chat.""" - timestamp = datetime.now().strftime("%H:%M:%S") - self._add_message(timestamp, "system", content, theme.YELLOW) - - def on_key(self, event: Key) -> None: - """Handle key events.""" - if event.key == "ctrl+c": - self.exit() - event.prevent_default() - - def on_input_submitted(self, event: Input.Submitted) -> None: - """Handle input submission.""" - message = event.value.strip() - if not message: - return - - # Clear input - self.input_widget.value = "" # type: ignore[union-attr] - - # Check for commands - if message.lower() in ["/exit", "/quit"]: - self.exit() - return - elif message.lower() == "/clear": - self.action_clear() - return - elif message.lower() == "/help": - help_text = """Commands: - /clear - Clear the chat log - /help - Show this help message - /exit - Exit the application - /quit - Exit the application - -Tool calls are displayed in cyan with ▶ prefix""" - self._add_system_message(help_text) - return - - # Send to agent (message will be displayed when received back) - self._human_transport.publish(message) - - def action_clear(self) -> None: - """Clear the chat log.""" - self.chat_log.clear() # type: ignore[union-attr] - - def action_quit(self) -> None: # type: ignore[override] - """Quit the application.""" - self._running = False - self.exit() - - -def main() -> None: - """Main entry point for the human CLI.""" - import sys - - if len(sys.argv) > 1 and sys.argv[1] == "web": - # Support for textual-serve web mode - import os - - from textual_serve.server import Server # type: ignore[import-not-found] - - server = Server(f"python {os.path.abspath(__file__)}") - server.serve() - else: - app = HumanCLIApp() - app.run() - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/cli/human/humanclianim.py b/dimos/utils/cli/human/humanclianim.py deleted file mode 100644 index cdd3bf3b00..0000000000 --- a/dimos/utils/cli/human/humanclianim.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import random -import sys -import threading -import time - -from terminaltexteffects import Color # type: ignore[attr-defined, import-not-found] - -from dimos.utils.cli import theme - -# Global to store the imported main function -_humancli_main = None -_import_complete = threading.Event() - -print(theme.ACCENT) - - -def import_cli_in_background() -> None: - """Import the heavy CLI modules in the background""" - global _humancli_main - try: - from dimos.utils.cli.human.humancli import main as humancli_main - - _humancli_main = humancli_main - except Exception as e: - print(f"Failed to import CLI: {e}") - finally: - _import_complete.set() - - -def get_effect_config(effect_name: str): # type: ignore[no-untyped-def] - """Get hardcoded configuration for a specific effect""" - # Hardcoded configs for each effect - global_config = { - "final_gradient_stops": [Color(theme.ACCENT)], - } - - configs = { - "randomsequence": { - "speed": 0.075, - }, - "slide": {"direction": "left", "movement_speed": 1.5}, - "sweep": {"direction": "left"}, - "print": { - "print_speed": 10, - "print_head_return_speed": 10, - "final_gradient_stops": [Color(theme.ACCENT)], - }, - "pour": {"pour_speed": 9}, - "matrix": {"rain_symbols": "01", "rain_fall_speed_range": (4, 7)}, - "decrypt": {"typing_speed": 5, "decryption_speed": 3}, - "burn": {"fire_chars": "█", "flame_color": "ffffff"}, - "expand": {"expand_direction": "center"}, - "scattered": {"movement_speed": 0.5}, - "beams": {"movement_speed": 0.5, "beam_delay": 0}, - "middleout": {"center_movement_speed": 3, "full_movement_speed": 0.5}, - "rain": { - "rain_symbols": "░▒▓█", - "rain_fall_speed_range": (5, 10), - }, - "highlight": {"highlight_brightness": 3}, - } - - return {**configs.get(effect_name, {}), **global_config} # type: ignore[dict-item] - - -def run_banner_animation() -> None: - """Run the ASCII banner animation before launching Textual""" - - # Check if we should animate - random_anim = ["scattered", "print", "expand", "slide", "rain"] - animation_style = os.environ.get("DIMOS_BANNER_ANIMATION", random.choice(random_anim)).lower() - - if animation_style == "none": - return # Skip animation - from terminaltexteffects.effects.effect_beams import Beams # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_burn import Burn # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_decrypt import Decrypt # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_expand import Expand # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_highlight import ( # type: ignore[import-not-found] - Highlight, - ) - from terminaltexteffects.effects.effect_matrix import Matrix # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_middleout import ( # type: ignore[import-not-found] - MiddleOut, - ) - from terminaltexteffects.effects.effect_overflow import ( # type: ignore[import-not-found] - Overflow, - ) - from terminaltexteffects.effects.effect_pour import Pour # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_print import Print # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_rain import Rain # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_random_sequence import ( # type: ignore[import-not-found] - RandomSequence, - ) - from terminaltexteffects.effects.effect_scattered import ( # type: ignore[import-not-found] - Scattered, - ) - from terminaltexteffects.effects.effect_slide import Slide # type: ignore[import-not-found] - from terminaltexteffects.effects.effect_sweep import Sweep # type: ignore[import-not-found] - - # The DIMENSIONAL ASCII art - ascii_art = "\n" + theme.ascii_logo.replace("\n", "\n ") - # Choose effect based on style - effect_map = { - "slide": Slide, - "sweep": Sweep, - "print": Print, - "pour": Pour, - "burn": Burn, - "matrix": Matrix, - "rain": Rain, - "scattered": Scattered, - "expand": Expand, - "decrypt": Decrypt, - "overflow": Overflow, - "randomsequence": RandomSequence, - "beams": Beams, - "middleout": MiddleOut, - "highlight": Highlight, - } - - EffectClass = effect_map.get(animation_style, Slide) - - # Clear screen before starting animation - print("\033[2J\033[H", end="", flush=True) - - # Get effect configuration - effect_config = get_effect_config(animation_style) - - # Create and run the effect with config - effect = EffectClass(ascii_art) - for key, value in effect_config.items(): - setattr(effect.effect_config, key, value) # type: ignore[attr-defined] - - # Run the animation - terminal.print() handles all screen management - with effect.terminal_output() as terminal: # type: ignore[attr-defined] - for frame in effect: # type: ignore[attr-defined] - terminal.print(frame) - - # Brief pause to see the final frame - time.sleep(0.5) - - # Clear screen for Textual to take over - print("\033[2J\033[H", end="") - - -def main() -> None: - """Main entry point - run animation then launch the real CLI""" - - # Start importing CLI in background (this is slow) - import_thread = threading.Thread(target=import_cli_in_background, daemon=True) - import_thread.start() - - # Run the animation while imports happen (if not in web mode) - if not (len(sys.argv) > 1 and sys.argv[1] == "web"): - run_banner_animation() - - # Wait for import to complete - _import_complete.wait(timeout=10) # Max 10 seconds wait - - # Launch the real CLI - if _humancli_main: - _humancli_main() - else: - # Fallback if threaded import failed - from dimos.utils.cli.human.humancli import main as humancli_main - - humancli_main() - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/cli/lcmspy/lcmspy.py b/dimos/utils/cli/lcmspy/lcmspy.py deleted file mode 100755 index 5493e53024..0000000000 --- a/dimos/utils/cli/lcmspy/lcmspy.py +++ /dev/null @@ -1,213 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections import deque -from dataclasses import dataclass -from enum import Enum -import threading -import time - -from dimos.protocol.service.lcmservice import LCMConfig, LCMService - - -class BandwidthUnit(Enum): - BP = "B" - KBP = "kB" - MBP = "MB" - GBP = "GB" - - -def human_readable_bytes(bytes_value: float, round_to: int = 2) -> tuple[float, BandwidthUnit]: - """Convert bytes to human-readable format with appropriate units""" - if bytes_value >= 1024**3: # GB - return round(bytes_value / (1024**3), round_to), BandwidthUnit.GBP - elif bytes_value >= 1024**2: # MB - return round(bytes_value / (1024**2), round_to), BandwidthUnit.MBP - elif bytes_value >= 1024: # KB - return round(bytes_value / 1024, round_to), BandwidthUnit.KBP - else: - return round(bytes_value, round_to), BandwidthUnit.BP - - -class Topic: - history_window: float = 60.0 - - def __init__(self, name: str, history_window: float = 60.0) -> None: - self.name = name - # Store (timestamp, data_size) tuples for statistics - self.message_history = deque() # type: ignore[var-annotated] - self.history_window = history_window - # Total traffic accumulator (doesn't get cleaned up) - self.total_traffic_bytes = 0 - - def msg(self, data: bytes) -> None: - # print(f"> msg {self.__str__()} {len(data)} bytes") - datalen = len(data) - self.message_history.append((time.time(), datalen)) - self.total_traffic_bytes += datalen - self._cleanup_old_messages() - - def _cleanup_old_messages(self, max_age: float | None = None) -> None: - """Remove messages older than max_age seconds""" - current_time = time.time() - while self.message_history and current_time - self.message_history[0][0] > ( - max_age or self.history_window - ): - self.message_history.popleft() - - def _get_messages_in_window(self, time_window: float): # type: ignore[no-untyped-def] - """Get messages within the specified time window""" - current_time = time.time() - cutoff_time = current_time - time_window - return [(ts, size) for ts, size in self.message_history if ts >= cutoff_time] - - # avg msg freq in the last n seconds - def freq(self, time_window: float) -> float: - messages = self._get_messages_in_window(time_window) - if not messages: - return 0.0 - return len(messages) / time_window - - # avg bandwidth in kB/s in the last n seconds - def kbps(self, time_window: float) -> float: - messages = self._get_messages_in_window(time_window) - if not messages: - return 0.0 - total_bytes = sum(size for _, size in messages) - total_kbytes = total_bytes / 1000 # Convert bytes to kB - return total_kbytes / time_window # type: ignore[no-any-return] - - def kbps_hr(self, time_window: float, round_to: int = 2) -> tuple[float, BandwidthUnit]: - """Return human-readable bandwidth with appropriate units""" - kbps_val = self.kbps(time_window) - # Convert kB/s to B/s for human_readable_bytes - bps = kbps_val * 1000 - return human_readable_bytes(bps, round_to) - - # avg msg size in the last n seconds - def size(self, time_window: float) -> float: - messages = self._get_messages_in_window(time_window) - if not messages: - return 0.0 - total_size = sum(size for _, size in messages) - return total_size / len(messages) # type: ignore[no-any-return] - - def total_traffic(self) -> int: - """Return total traffic passed in bytes since the beginning""" - return self.total_traffic_bytes - - def total_traffic_hr(self) -> tuple[float, BandwidthUnit]: - """Return human-readable total traffic with appropriate units""" - total_bytes = self.total_traffic() - return human_readable_bytes(total_bytes) - - def __str__(self) -> str: - return f"topic({self.name})" - - -@dataclass -class LCMSpyConfig(LCMConfig): - topic_history_window: float = 60.0 - - -class LCMSpy(LCMService, Topic): - default_config = LCMSpyConfig - topic = dict[str, Topic] - graph_log_window: float = 1.0 - topic_class: type[Topic] = Topic - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - Topic.__init__(self, name="total", history_window=self.config.topic_history_window) # type: ignore[attr-defined] - self.topic = {} # type: ignore[assignment] - - def start(self) -> None: - super().start() - self.l.subscribe(".*", self.msg) # type: ignore[union-attr] - - def stop(self) -> None: - """Stop the LCM spy and clean up resources""" - super().stop() - - def msg(self, topic, data) -> None: # type: ignore[no-untyped-def, override] - Topic.msg(self, data) - - if topic not in self.topic: # type: ignore[operator] - print(self.config) - self.topic[topic] = self.topic_class( # type: ignore[assignment, call-arg] - topic, - history_window=self.config.topic_history_window, # type: ignore[attr-defined] - ) - self.topic[topic].msg(data) # type: ignore[attr-defined, type-arg] - - -class GraphTopic(Topic): - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self.freq_history = deque(maxlen=20) # type: ignore[var-annotated] - self.bandwidth_history = deque(maxlen=20) # type: ignore[var-annotated] - - def update_graphs(self, step_window: float = 1.0) -> None: - """Update historical data for graphing""" - freq = self.freq(step_window) - kbps = self.kbps(step_window) - self.freq_history.append(freq) - self.bandwidth_history.append(kbps) - - -@dataclass -class GraphLCMSpyConfig(LCMSpyConfig): - graph_log_window: float = 1.0 - - -class GraphLCMSpy(LCMSpy, GraphTopic): - default_config = GraphLCMSpyConfig - - graph_log_thread: threading.Thread | None = None - graph_log_stop_event: threading.Event = threading.Event() - topic_class: type[Topic] = GraphTopic - - def __init__(self, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(**kwargs) - GraphTopic.__init__(self, name="total", history_window=self.config.topic_history_window) # type: ignore[attr-defined] - - def start(self) -> None: - super().start() - self.graph_log_thread = threading.Thread(target=self.graph_log, daemon=True) - self.graph_log_thread.start() - - def graph_log(self) -> None: - while not self.graph_log_stop_event.is_set(): - self.update_graphs(self.config.graph_log_window) # type: ignore[attr-defined] # Update global history - # Copy to list to avoid RuntimeError: dictionary changed size during iteration - for topic in list(self.topic.values()): # type: ignore[call-arg] - topic.update_graphs(self.config.graph_log_window) # type: ignore[attr-defined] - time.sleep(self.config.graph_log_window) # type: ignore[attr-defined] - - def stop(self) -> None: - """Stop the graph logging and LCM spy""" - self.graph_log_stop_event.set() - if self.graph_log_thread and self.graph_log_thread.is_alive(): - self.graph_log_thread.join(timeout=1.0) - super().stop() - - -if __name__ == "__main__": - lcm_spy = LCMSpy() - lcm_spy.start() - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("LCM Spy stopped.") diff --git a/dimos/utils/cli/lcmspy/run_lcmspy.py b/dimos/utils/cli/lcmspy/run_lcmspy.py deleted file mode 100644 index f3d31b48ba..0000000000 --- a/dimos/utils/cli/lcmspy/run_lcmspy.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from rich.text import Text -from textual.app import App, ComposeResult -from textual.color import Color -from textual.widgets import DataTable - -from dimos.utils.cli import theme -from dimos.utils.cli.lcmspy.lcmspy import GraphLCMSpy, GraphTopic as SpyTopic - - -def gradient(max_value: float, value: float) -> str: - """Gradient from cyan (low) to yellow (high) using DimOS theme colors""" - ratio = min(value / max_value, 1.0) - # Parse hex colors from theme - cyan = Color.parse(theme.CYAN) - yellow = Color.parse(theme.YELLOW) - color = cyan.blend(yellow, ratio) - - return color.hex - - -def topic_text(topic_name: str) -> Text: - """Format topic name with DimOS theme colors""" - if "#" in topic_name: - parts = topic_name.split("#", 1) - return Text(parts[0], style=theme.BRIGHT_WHITE) + Text("#" + parts[1], style=theme.BLUE) - - if topic_name[:4] == "/rpc": - return Text(topic_name[:4], style=theme.BLUE) + Text( - topic_name[4:], style=theme.BRIGHT_WHITE - ) - - return Text(topic_name, style=theme.BRIGHT_WHITE) - - -class LCMSpyApp(App): # type: ignore[type-arg] - """A real-time CLI dashboard for LCM traffic statistics using Textual.""" - - CSS_PATH = "../dimos.tcss" - - CSS = f""" - Screen {{ - layout: vertical; - background: {theme.BACKGROUND}; - }} - DataTable {{ - height: 2fr; - width: 1fr; - border: solid {theme.BORDER}; - background: {theme.BG}; - scrollbar-size: 0 0; - }} - DataTable > .datatable--header {{ - color: {theme.ACCENT}; - background: transparent; - }} - """ - - refresh_interval: float = 0.5 # seconds - - BINDINGS = [ - ("q", "quit"), - ("ctrl+c", "quit"), - ] - - def __init__(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - super().__init__(*args, **kwargs) - self.spy = GraphLCMSpy(autoconf=True, graph_log_window=0.5) - self.table: DataTable | None = None # type: ignore[type-arg] - - def compose(self) -> ComposeResult: - self.table = DataTable(zebra_stripes=False, cursor_type=None) # type: ignore[arg-type] - self.table.add_column("Topic") - self.table.add_column("Freq (Hz)") - self.table.add_column("Bandwidth") - self.table.add_column("Total Traffic") - yield self.table - - def on_mount(self) -> None: - self.spy.start() - self.set_interval(self.refresh_interval, self.refresh_table) - - async def on_unmount(self) -> None: - self.spy.stop() - - def refresh_table(self) -> None: - topics: list[SpyTopic] = list(self.spy.topic.values()) # type: ignore[arg-type, call-arg] - topics.sort(key=lambda t: t.total_traffic(), reverse=True) - self.table.clear(columns=False) # type: ignore[union-attr] - - for t in topics: - freq = t.freq(5.0) - kbps = t.kbps(5.0) - bw_val, bw_unit = t.kbps_hr(5.0) - total_val, total_unit = t.total_traffic_hr() - - self.table.add_row( # type: ignore[union-attr] - topic_text(t.name), - Text(f"{freq:.1f}", style=gradient(10, freq)), - Text(f"{bw_val} {bw_unit.value}/s", style=gradient(1024 * 3, kbps)), - Text(f"{total_val} {total_unit.value}"), - ) - - -def main() -> None: - import sys - - if len(sys.argv) > 1 and sys.argv[1] == "web": - import os - - from textual_serve.server import Server # type: ignore[import-not-found] - - server = Server(f"python {os.path.abspath(__file__)}") - server.serve() - else: - LCMSpyApp().run() - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/cli/lcmspy/test_lcmspy.py b/dimos/utils/cli/lcmspy/test_lcmspy.py deleted file mode 100644 index 530f081f29..0000000000 --- a/dimos/utils/cli/lcmspy/test_lcmspy.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos.protocol.pubsub.impl.lcmpubsub import PickleLCM, Topic -from dimos.utils.cli.lcmspy.lcmspy import GraphLCMSpy, GraphTopic, LCMSpy, Topic as TopicSpy - - -@pytest.mark.lcm -def test_spy_basic() -> None: - lcm = PickleLCM(autoconf=True) - lcm.start() - - lcmspy = LCMSpy(autoconf=True) - lcmspy.start() - - video_topic = Topic(topic="/video") - odom_topic = Topic(topic="/odom") - - for i in range(5): - lcm.publish(video_topic, f"video frame {i}") - time.sleep(0.1) - if i % 2 == 0: - lcm.publish(odom_topic, f"odometry data {i / 2}") - - # Wait a bit for messages to be processed - time.sleep(0.5) - - # Test statistics for video topic - video_topic_spy = lcmspy.topic["/video"] - assert video_topic_spy is not None - - # Test frequency (should be around 10 Hz for 5 messages in ~0.5 seconds) - freq = video_topic_spy.freq(1.0) - assert freq > 0 - print(f"Video topic frequency: {freq:.2f} Hz") - - # Test bandwidth - kbps = video_topic_spy.kbps(1.0) - assert kbps > 0 - print(f"Video topic bandwidth: {kbps:.2f} kbps") - - # Test average message size - avg_size = video_topic_spy.size(1.0) - assert avg_size > 0 - print(f"Video topic average message size: {avg_size:.2f} bytes") - - # Test statistics for odom topic - odom_topic_spy = lcmspy.topic["/odom"] - assert odom_topic_spy is not None - - freq = odom_topic_spy.freq(1.0) - assert freq > 0 - print(f"Odom topic frequency: {freq:.2f} Hz") - - kbps = odom_topic_spy.kbps(1.0) - assert kbps > 0 - print(f"Odom topic bandwidth: {kbps:.2f} kbps") - - avg_size = odom_topic_spy.size(1.0) - assert avg_size > 0 - print(f"Odom topic average message size: {avg_size:.2f} bytes") - - print(f"Video topic: {video_topic_spy}") - print(f"Odom topic: {odom_topic_spy}") - - -@pytest.mark.lcm -def test_topic_statistics_direct() -> None: - """Test Topic statistics directly without LCM""" - - topic = TopicSpy("/test") - - # Add some test messages - test_data = [b"small", b"medium sized message", b"very long message for testing purposes"] - - for _i, data in enumerate(test_data): - topic.msg(data) - time.sleep(0.1) # Simulate time passing - - # Test statistics over 1 second window - freq = topic.freq(1.0) - kbps = topic.kbps(1.0) - avg_size = topic.size(1.0) - - assert freq > 0 - assert kbps > 0 - assert avg_size > 0 - - print(f"Direct test - Frequency: {freq:.2f} Hz") - print(f"Direct test - Bandwidth: {kbps:.2f} kbps") - print(f"Direct test - Avg size: {avg_size:.2f} bytes") - - -def test_topic_cleanup() -> None: - """Test that old messages are properly cleaned up""" - - topic = TopicSpy("/test") - - # Add a message - topic.msg(b"test message") - initial_count = len(topic.message_history) - assert initial_count == 1 - - # Simulate time passing by manually adding old timestamps - old_time = time.time() - 70 # 70 seconds ago - topic.message_history.appendleft((old_time, 10)) - - # Trigger cleanup - topic._cleanup_old_messages(max_age=60.0) - - # Should only have the recent message - assert len(topic.message_history) == 1 - assert topic.message_history[0][0] > time.time() - 10 # Recent message - - -@pytest.mark.lcm -def test_graph_topic_basic() -> None: - """Test GraphTopic basic functionality""" - topic = GraphTopic("/test_graph") - - # Add some messages and update graphs - topic.msg(b"test message") - topic.update_graphs(1.0) - - # Should have history data - assert len(topic.freq_history) == 1 - assert len(topic.bandwidth_history) == 1 - assert topic.freq_history[0] > 0 - assert topic.bandwidth_history[0] > 0 - - -@pytest.mark.lcm -def test_graph_lcmspy_basic() -> None: - """Test GraphLCMSpy basic functionality""" - spy = GraphLCMSpy(autoconf=True, graph_log_window=0.1) - spy.start() - time.sleep(0.2) # Wait for thread to start - - # Simulate a message - spy.msg("/test", b"test data") - time.sleep(0.2) # Wait for graph update - - # Should create GraphTopic with history - topic = spy.topic["/test"] - assert isinstance(topic, GraphTopic) - assert len(topic.freq_history) > 0 - assert len(topic.bandwidth_history) > 0 - - spy.stop() - - -@pytest.mark.lcm -def test_lcmspy_global_totals() -> None: - """Test that LCMSpy tracks global totals as a Topic itself""" - spy = LCMSpy(autoconf=True) - spy.start() - - # Send messages to different topics - spy.msg("/video", b"video frame data") - spy.msg("/odom", b"odometry data") - spy.msg("/imu", b"imu data") - - # Verify each test topic received exactly one message (ignore LCM discovery packets) - for t in ("/video", "/odom", "/imu"): - assert len(spy.topic[t].message_history) == 1 - - # Check global statistics - global_freq = spy.freq(1.0) - global_kbps = spy.kbps(1.0) - global_size = spy.size(1.0) - - assert global_freq > 0 - assert global_kbps > 0 - assert global_size > 0 - - print(f"Global frequency: {global_freq:.2f} Hz") - print(f"Global bandwidth: {spy.kbps_hr(1.0)}") - print(f"Global avg message size: {global_size:.0f} bytes") - - spy.stop() - - -@pytest.mark.lcm -def test_graph_lcmspy_global_totals() -> None: - """Test that GraphLCMSpy tracks global totals with history""" - spy = GraphLCMSpy(autoconf=True, graph_log_window=0.1) - spy.start() - time.sleep(0.2) - - # Send messages - spy.msg("/video", b"video frame data") - spy.msg("/odom", b"odometry data") - time.sleep(0.2) # Wait for graph update - - # Update global graphs - spy.update_graphs(1.0) - - # Should have global history - assert len(spy.freq_history) == 1 - assert len(spy.bandwidth_history) == 1 - assert spy.freq_history[0] > 0 - assert spy.bandwidth_history[0] > 0 - - print(f"Global frequency history: {spy.freq_history[0]:.2f} Hz") - print(f"Global bandwidth history: {spy.bandwidth_history[0]:.2f} kB/s") - - spy.stop() diff --git a/dimos/utils/cli/plot.py b/dimos/utils/cli/plot.py deleted file mode 100644 index 336aeca6d8..0000000000 --- a/dimos/utils/cli/plot.py +++ /dev/null @@ -1,281 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Terminal plotting utilities using plotext.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from typing import TYPE_CHECKING - -import plotext as plt - -if TYPE_CHECKING: - from collections.abc import Sequence - - -def _default_size() -> tuple[int, int]: - """Return default plot size (terminal width, half terminal height).""" - tw, th = plt.terminal_size() - return tw, th // 2 - - -@dataclass -class Series: - """A data series for plotting.""" - - y: Sequence[float] - x: Sequence[float] | None = None - label: str | None = None - color: tuple[int, int, int] | None = None - marker: str = "braille" # braille, dot, hd, fhd, sd - - -@dataclass -class Plot: - """Terminal plot.""" - - title: str | None = None - xlabel: str | None = None - ylabel: str | None = None - width: int | None = None - height: int | None = None - series: list[Series] = field(default_factory=list) - - def add( - self, - y: Sequence[float], - x: Sequence[float] | None = None, - label: str | None = None, - color: tuple[int, int, int] | None = None, - marker: str = "braille", - ) -> Plot: - """Add a data series to the plot. - - Args: - y: Y values - x: X values (optional, defaults to 0, 1, 2, ...) - label: Series label for legend - color: RGB tuple (optional, auto-assigned from theme) - marker: Marker style (braille, dot, hd, fhd, sd) - - Returns: - Self for chaining - """ - self.series.append(Series(y=y, x=x, label=label, color=color, marker=marker)) - return self - - def build(self) -> str: - """Build the plot and return as string.""" - plt.clf() - plt.theme("dark") - - # Set size (default to terminal width, half terminal height) - dw, dh = _default_size() - plt.plotsize(self.width or dw, self.height or dh) - - # Plot each series - for _i, s in enumerate(self.series): - x = list(s.x) if s.x is not None else list(range(len(s.y))) - y = list(s.y) - if s.color: - plt.plot(x, y, label=s.label, marker=s.marker, color=s.color) - else: - plt.plot(x, y, label=s.label, marker=s.marker) - - # Set labels and title - if self.title: - plt.title(self.title) - if self.xlabel: - plt.xlabel(self.xlabel) - if self.ylabel: - plt.ylabel(self.ylabel) - - result: str = plt.build() - return result - - def show(self) -> None: - """Print the plot to stdout.""" - print(self.build()) - - -def plot( - y: Sequence[float], - x: Sequence[float] | None = None, - title: str | None = None, - xlabel: str | None = None, - ylabel: str | None = None, - label: str | None = None, - width: int | None = None, - height: int | None = None, -) -> None: - """Quick single-series plot. - - Args: - y: Y values - x: X values (optional) - title: Plot title - xlabel: X-axis label - ylabel: Y-axis label - label: Series label - width: Plot width in characters - height: Plot height in characters - """ - p = Plot(title=title, xlabel=xlabel, ylabel=ylabel, width=width, height=height) - p.add(y, x, label=label) - p.show() - - -def bar( - labels: Sequence[str], - values: Sequence[float], - title: str | None = None, - xlabel: str | None = None, - ylabel: str | None = None, - width: int | None = None, - height: int | None = None, - horizontal: bool = False, -) -> None: - """Quick bar chart. - - Args: - labels: Category labels - values: Values for each category - title: Plot title - xlabel: X-axis label - ylabel: Y-axis label - width: Plot width in characters - height: Plot height in characters - horizontal: If True, draw horizontal bars - """ - plt.clf() - plt.theme("dark") - dw, dh = _default_size() - plt.plotsize(width or dw, height or dh) - - if horizontal: - plt.bar(list(labels), list(values), orientation="h") - else: - plt.bar(list(labels), list(values)) - - if title: - plt.title(title) - if xlabel: - plt.xlabel(xlabel) - if ylabel: - plt.ylabel(ylabel) - - print(plt.build()) - - -def scatter( - x: Sequence[float], - y: Sequence[float], - title: str | None = None, - xlabel: str | None = None, - ylabel: str | None = None, - width: int | None = None, - height: int | None = None, -) -> None: - """Quick scatter plot. - - Args: - x: X values - y: Y values - title: Plot title - xlabel: X-axis label - ylabel: Y-axis label - width: Plot width in characters - height: Plot height in characters - """ - plt.clf() - plt.theme("dark") - dw, dh = _default_size() - plt.plotsize(width or dw, height or dh) - - plt.scatter(list(x), list(y), marker="dot") - - if title: - plt.title(title) - if xlabel: - plt.xlabel(xlabel) - if ylabel: - plt.ylabel(ylabel) - - print(plt.build()) - - -def compare_bars( - labels: Sequence[str], - data: dict[str, Sequence[float]], - title: str | None = None, - xlabel: str | None = None, - ylabel: str | None = None, - width: int | None = None, - height: int | None = None, -) -> None: - """Compare multiple series as grouped bars. - - Args: - labels: Category labels (x-axis) - data: Dict mapping series name to values - title: Plot title - xlabel: X-axis label - ylabel: Y-axis label - width: Plot width in characters - height: Plot height in characters - - Example: - compare_bars( - ["moondream-full", "moondream-512", "moondream-256"], - {"query_time": [2.1, 1.5, 0.8], "accuracy": [95, 92, 85]}, - title="Model Performance" - ) - """ - plt.clf() - plt.theme("dark") - dw, dh = _default_size() - plt.plotsize(width or dw, height or dh) - - for name, values in data.items(): - plt.bar(list(labels), list(values), label=name) - - if title: - plt.title(title) - if xlabel: - plt.xlabel(xlabel) - if ylabel: - plt.ylabel(ylabel) - - print(plt.build()) - - -if __name__ == "__main__": - # Demo - print("Line plot:") - plot([1, 4, 9, 16, 25], title="Squares", xlabel="n", ylabel="n²") - - print("\nBar chart:") - bar( - ["moondream-full", "moondream-512", "moondream-256"], - [2.1, 1.5, 0.8], - title="Query Time (s)", - ylabel="seconds", - ) - - print("\nMulti-series plot:") - p = Plot(title="Model Performance", xlabel="resize", ylabel="time (s)") - p.add([2.1, 1.5, 0.8], label="moondream") - p.add([1.8, 1.2, 0.6], label="qwen") - p.show() diff --git a/dimos/utils/cli/theme.py b/dimos/utils/cli/theme.py deleted file mode 100644 index b6b6b9ccae..0000000000 --- a/dimos/utils/cli/theme.py +++ /dev/null @@ -1,108 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Parse DimOS theme from tcss file.""" - -from __future__ import annotations - -from pathlib import Path -import re - - -def parse_tcss_colors(tcss_path: str | Path) -> dict[str, str]: - """Parse color variables from a tcss file. - - Args: - tcss_path: Path to the tcss file - - Returns: - Dictionary mapping variable names to color values - """ - tcss_path = Path(tcss_path) - content = tcss_path.read_text() - - # Match $variable: value; patterns - pattern = r"\$([a-zA-Z0-9_-]+)\s*:\s*(#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3});" - matches = re.findall(pattern, content) - - return {name: value for name, value in matches} - - -# Load DimOS theme colors -_THEME_PATH = Path(__file__).parent / "dimos.tcss" -COLORS = parse_tcss_colors(_THEME_PATH) - -# Export CSS path for Textual apps -CSS_PATH = str(_THEME_PATH) - - -# Convenience accessors for common colors -def get(name: str, default: str = "#ffffff") -> str: - """Get a color by variable name.""" - return COLORS.get(name, default) - - -# Base color palette -BLACK = COLORS.get("black", "#0b0f0f") -RED = COLORS.get("red", "#ff0000") -GREEN = COLORS.get("green", "#00eeee") -YELLOW = COLORS.get("yellow", "#ffcc00") -BLUE = COLORS.get("blue", "#5c9ff0") -PURPLE = COLORS.get("purple", "#00eeee") -CYAN = COLORS.get("cyan", "#00eeee") -WHITE = COLORS.get("white", "#b5e4f4") - -# Bright colors -BRIGHT_BLACK = COLORS.get("bright-black", "#404040") -BRIGHT_RED = COLORS.get("bright-red", "#ff0000") -BRIGHT_GREEN = COLORS.get("bright-green", "#00eeee") -BRIGHT_YELLOW = COLORS.get("bright-yellow", "#f2ea8c") -BRIGHT_BLUE = COLORS.get("bright-blue", "#8cbdf2") -BRIGHT_PURPLE = COLORS.get("bright-purple", "#00eeee") -BRIGHT_CYAN = COLORS.get("bright-cyan", "#00eeee") -BRIGHT_WHITE = COLORS.get("bright-white", "#ffffff") - -# Core theme colors -BACKGROUND = COLORS.get("background", "#0b0f0f") -FOREGROUND = COLORS.get("foreground", "#b5e4f4") -CURSOR = COLORS.get("cursor", "#00eeee") - -# Semantic aliases -BG = COLORS.get("bg", "#0b0f0f") -BORDER = COLORS.get("border", "#00eeee") -ACCENT = COLORS.get("accent", "#b5e4f4") -DIM = COLORS.get("dim", "#404040") -TIMESTAMP = COLORS.get("timestamp", "#ffffff") - -# Message type colors -SYSTEM = COLORS.get("system", "#ff0000") -AGENT = COLORS.get("agent", "#88ff88") -TOOL = COLORS.get("tool", "#00eeee") -TOOL_RESULT = COLORS.get("tool-result", "#ffff00") -HUMAN = COLORS.get("human", "#ffffff") - -# Status colors -SUCCESS = COLORS.get("success", "#00eeee") -ERROR = COLORS.get("error", "#ff0000") -WARNING = COLORS.get("warning", "#ffcc00") -INFO = COLORS.get("info", "#00eeee") - -ascii_logo = """ - ▇▇▇▇▇▇╗ ▇▇╗▇▇▇╗ ▇▇▇╗▇▇▇▇▇▇▇╗▇▇▇╗ ▇▇╗▇▇▇▇▇▇▇╗▇▇╗ ▇▇▇▇▇▇╗ ▇▇▇╗ ▇▇╗ ▇▇▇▇▇╗ ▇▇╗ - ▇▇╔══▇▇╗▇▇║▇▇▇▇╗ ▇▇▇▇║▇▇╔════╝▇▇▇▇╗ ▇▇║▇▇╔════╝▇▇║▇▇╔═══▇▇╗▇▇▇▇╗ ▇▇║▇▇╔══▇▇╗▇▇║ - ▇▇║ ▇▇║▇▇║▇▇╔▇▇▇▇╔▇▇║▇▇▇▇▇╗ ▇▇╔▇▇╗ ▇▇║▇▇▇▇▇▇▇╗▇▇║▇▇║ ▇▇║▇▇╔▇▇╗ ▇▇║▇▇▇▇▇▇▇║▇▇║ - ▇▇║ ▇▇║▇▇║▇▇║╚▇▇╔╝▇▇║▇▇╔══╝ ▇▇║╚▇▇╗▇▇║╚════▇▇║▇▇║▇▇║ ▇▇║▇▇║╚▇▇╗▇▇║▇▇╔══▇▇║▇▇║ - ▇▇▇▇▇▇╔╝▇▇║▇▇║ ╚═╝ ▇▇║▇▇▇▇▇▇▇╗▇▇║ ╚▇▇▇▇║▇▇▇▇▇▇▇║▇▇║╚▇▇▇▇▇▇╔╝▇▇║ ╚▇▇▇▇║▇▇║ ▇▇║▇▇▇▇▇▇▇╗ - ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ -""" diff --git a/dimos/utils/data.py b/dimos/utils/data.py deleted file mode 100644 index d14ac04730..0000000000 --- a/dimos/utils/data.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from functools import cache -import os -from pathlib import Path -import platform -import subprocess -import sys -import tarfile -import tempfile - -from dimos.constants import DIMOS_PROJECT_ROOT -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def _get_user_data_dir() -> Path: - """Get platform-specific user data directory.""" - system = platform.system() - # if virtual env is available, use it to keep venv's from fighting over data - # a better fix for large files will be added later to minimize storage duplication - if os.environ.get("VIRTUAL_ENV"): - venv_data_dir = Path( - f"{os.environ.get('VIRTUAL_ENV')}/lib/python{sys.version_info.major}.{sys.version_info.minor}/site-packages/dimos/data" - ) - return venv_data_dir - - if system == "Linux": - # Use XDG_DATA_HOME if set, otherwise default to ~/.local/share - xdg_data_home = os.environ.get("XDG_DATA_HOME") - if xdg_data_home: - return Path(xdg_data_home) / "dimos" - return Path.home() / ".local" / "share" / "dimos" - elif system == "Darwin": # macOS - return Path.home() / "Library" / "Application Support" / "dimos" - else: - # Fallback for other systems - return Path.home() / ".dimos" - - -@cache -def _get_repo_root() -> Path: - # Check if running from git repo - if (DIMOS_PROJECT_ROOT / ".git").exists(): - return DIMOS_PROJECT_ROOT - - # Running as installed package - clone repo to data dir - try: - data_dir = _get_user_data_dir() - data_dir.mkdir(parents=True, exist_ok=True) - # Test if writable - test_file = data_dir / ".write_test" - test_file.touch() - test_file.unlink() - logger.info(f"Using local user data directory at '{data_dir}'") - except (OSError, PermissionError): - # Fall back to temp dir if data dir not writable - data_dir = Path(tempfile.gettempdir()) / "dimos" - data_dir.mkdir(parents=True, exist_ok=True) - logger.info(f"Using tmp data directory at '{data_dir}'") - - repo_dir = data_dir / "repo" - - # Clone if not already cloned - if not (repo_dir / ".git").exists(): - try: - env = os.environ.copy() - env["GIT_LFS_SKIP_SMUDGE"] = "1" - subprocess.run( - [ - "git", - "clone", - "--depth", - "1", - "--branch", - "main", - "https://github.com/dimensionalOS/dimos.git", - str(repo_dir), - ], - check=True, - capture_output=True, - text=True, - env=env, - ) - except subprocess.CalledProcessError as e: - raise RuntimeError( - f"Failed to clone dimos repository: {e.stderr}\n" - f"Make sure you can access https://github.com/dimensionalOS/dimos.git" - ) - - return repo_dir - - -@cache -def get_data_dir(extra_path: str | None = None) -> Path: - if extra_path: - return _get_repo_root() / "data" / extra_path - return _get_repo_root() / "data" - - -@cache -def _get_lfs_dir() -> Path: - return get_data_dir() / ".lfs" - - -def _check_git_lfs_available() -> bool: - missing = [] - - # Check if git is available - try: - subprocess.run(["git", "--version"], capture_output=True, check=True, text=True) - except (subprocess.CalledProcessError, FileNotFoundError): - missing.append("git") - - # Check if git-lfs is available - try: - subprocess.run(["git-lfs", "version"], capture_output=True, check=True, text=True) - except (subprocess.CalledProcessError, FileNotFoundError): - missing.append("git-lfs") - - if missing: - raise RuntimeError( - f"Missing required tools: {', '.join(missing)}.\n\n" - "Git LFS installation instructions: https://git-lfs.github.io/" - ) - - return True - - -def _is_lfs_pointer_file(file_path: Path) -> bool: - try: - # LFS pointer files are small (typically < 200 bytes) and start with specific text - if file_path.stat().st_size > 1024: # LFS pointers are much smaller - return False - - with open(file_path, encoding="utf-8") as f: - first_line = f.readline().strip() - return first_line.startswith("version https://git-lfs.github.com/spec/") - - except (UnicodeDecodeError, OSError): - return False - - -def _lfs_pull(file_path: Path, repo_root: Path) -> None: - try: - relative_path = file_path.relative_to(repo_root) - - env = os.environ.copy() - env["GIT_LFS_FORCE_PROGRESS"] = "1" - - subprocess.run( - ["git", "lfs", "pull", "--include", str(relative_path)], - cwd=repo_root, - check=True, - env=env, - ) - except subprocess.CalledProcessError as e: - raise RuntimeError(f"Failed to pull LFS file {file_path}: {e}") - - return None - - -def _decompress_archive(filename: str | Path) -> Path: - target_dir = get_data_dir() - filename_path = Path(filename) - with tarfile.open(filename_path, "r:gz") as tar: - tar.extractall(target_dir) - return target_dir / filename_path.name.replace(".tar.gz", "") - - -def _pull_lfs_archive(filename: str | Path) -> Path: - # Check Git LFS availability first - _check_git_lfs_available() - - # Find repository root - repo_root = _get_repo_root() - - # Construct path to test data file - file_path = _get_lfs_dir() / (str(filename) + ".tar.gz") - - # Check if file exists - if not file_path.exists(): - raise FileNotFoundError( - f"Test file '{filename}' not found at {file_path}. " - f"Make sure the file is committed to Git LFS in the tests/data directory." - ) - - # If it's an LFS pointer file, ensure LFS is set up and pull the file - if _is_lfs_pointer_file(file_path): - _lfs_pull(file_path, repo_root) - - # Verify the file was actually downloaded - if _is_lfs_pointer_file(file_path): - raise RuntimeError( - f"Failed to download LFS file '{filename}'. The file is still a pointer after attempting to pull." - ) - - return file_path - - -def get_data(name: str | Path) -> Path: - """ - Get the path to a test data, downloading from LFS if needed. - - This function will: - 1. Check that Git LFS is available - 2. Locate the file in the tests/data directory - 3. Initialize Git LFS if needed - 4. Download the file from LFS if it's a pointer file - 5. Return the Path object to the actual file or dir - - Supports nested paths like "dataset/subdir/file.jpg" - will download and - decompress "dataset" archive but return the full nested path. - - Args: - name: Name of the test file or dir, optionally with nested path - (e.g., "lidar_sample.bin" or "dataset/frames/001.png") - - Returns: - Path: Path object to the test file or dir - - Raises: - RuntimeError: If Git LFS is not available or LFS operations fail - FileNotFoundError: If the test file doesn't exist - - Usage: - # Simple file/dir - file_path = get_data("sample.bin") - - # Nested path - downloads "dataset" archive, returns path to nested file - frame = get_data("dataset/frames/001.png") - """ - data_dir = get_data_dir() - file_path = data_dir / name - - # already pulled and decompressed, return it directly - if file_path.exists(): - return file_path - - # extract archive root (first path component) and nested path - path_parts = Path(name).parts - archive_name = path_parts[0] - nested_path = Path(*path_parts[1:]) if len(path_parts) > 1 else None - - # download and decompress the archive root - archive_path = _decompress_archive(_pull_lfs_archive(archive_name)) - - # return full path including nested components - if nested_path: - return archive_path / nested_path - return archive_path - - -class LfsPath(type(Path())): # type: ignore[misc] - """ - A Path subclass that lazily downloads LFS data when accessed. - - This is useful for both lazy loading and differentiating between LFS paths and regular paths. - - This class wraps pathlib.Path and ensures that get_data() is called - before any meaningful filesystem operation, making LFS data lazy-loaded. - - Usage: - path = LfsPath("sample_data") - # No download yet - - with path.open('rb') as f: # Downloads now if needed - data = f.read() - - # Or use any Path operation: - if path.exists(): # Downloads now if needed - files = list(path.iterdir()) - """ - - def __new__(cls, filename: str | Path) -> "LfsPath": - # Create instance with a placeholder path to satisfy Path.__new__ - # We use "." as a dummy path that always exists - instance: LfsPath = super().__new__(cls, ".") # type: ignore[call-arg] - # Store the actual filename as an instance attribute - object.__setattr__(instance, "_lfs_filename", filename) - object.__setattr__(instance, "_lfs_resolved_cache", None) - return instance - - def _ensure_downloaded(self) -> Path: - """Ensure the LFS data is downloaded and return the resolved path.""" - cache: Path | None = object.__getattribute__(self, "_lfs_resolved_cache") - if cache is None: - filename = object.__getattribute__(self, "_lfs_filename") - cache = get_data(filename) - object.__setattr__(self, "_lfs_resolved_cache", cache) - return cache - - def __getattribute__(self, name: str) -> object: - # During Path.__new__(), _lfs_filename hasn't been set yet. - # Fall through to normal Path behavior until construction is complete. - try: - object.__getattribute__(self, "_lfs_filename") - except AttributeError: - return object.__getattribute__(self, name) - - # After construction, allow access to our internal attributes directly - if name in ("_lfs_filename", "_lfs_resolved_cache", "_ensure_downloaded"): - return object.__getattribute__(self, name) - - # For all other attributes, ensure download first then delegate to resolved path - resolved = object.__getattribute__(self, "_ensure_downloaded")() - return getattr(resolved, name) - - def __str__(self) -> str: - """String representation returns resolved path.""" - return str(self._ensure_downloaded()) - - def __fspath__(self) -> str: - """Return filesystem path, downloading from LFS if needed.""" - return str(self._ensure_downloaded()) - - def __truediv__(self, other: object) -> Path: - """Path division operator - returns resolved path.""" - return self._ensure_downloaded() / other # type: ignore[operator, return-value] - - def __rtruediv__(self, other: object) -> Path: - """Reverse path division operator.""" - return other / self._ensure_downloaded() # type: ignore[operator, return-value] diff --git a/dimos/utils/decorators/__init__.py b/dimos/utils/decorators/__init__.py deleted file mode 100644 index 79623922a0..0000000000 --- a/dimos/utils/decorators/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Decorators and accumulators for rate limiting and other utilities.""" - -from .accumulators import Accumulator, LatestAccumulator, RollingAverageAccumulator -from .decorators import CachedMethod, limit, retry, simple_mcache - -__all__ = [ - "Accumulator", - "CachedMethod", - "LatestAccumulator", - "RollingAverageAccumulator", - "limit", - "retry", - "simple_mcache", -] diff --git a/dimos/utils/decorators/accumulators.py b/dimos/utils/decorators/accumulators.py deleted file mode 100644 index 75cb25661d..0000000000 --- a/dimos/utils/decorators/accumulators.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from abc import ABC, abstractmethod -import threading -from typing import Generic, TypeVar - -T = TypeVar("T") - - -class Accumulator(ABC, Generic[T]): - """Base class for accumulating messages between rate-limited calls.""" - - @abstractmethod - def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - """Add args and kwargs to the accumulator.""" - pass - - @abstractmethod - def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] - """Get the accumulated args and kwargs and reset the accumulator.""" - pass - - @abstractmethod - def __len__(self) -> int: - """Return the number of accumulated items.""" - pass - - -class LatestAccumulator(Accumulator[T]): - """Simple accumulator that remembers only the latest args and kwargs.""" - - def __init__(self) -> None: - self._latest: tuple[tuple, dict] | None = None # type: ignore[type-arg] - self._lock = threading.Lock() - - def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - with self._lock: - self._latest = (args, kwargs) - - def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] - with self._lock: - result = self._latest - self._latest = None - return result - - def __len__(self) -> int: - with self._lock: - return 1 if self._latest is not None else 0 - - -class RollingAverageAccumulator(Accumulator[T]): - """Accumulator that maintains a rolling average of the first argument. - - This accumulator expects the first argument to be numeric and maintains - a rolling average without storing individual values. - """ - - def __init__(self) -> None: - self._sum: float = 0.0 - self._count: int = 0 - self._latest_kwargs: dict = {} # type: ignore[type-arg] - self._lock = threading.Lock() - - def add(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def] - if not args: - raise ValueError("RollingAverageAccumulator requires at least one argument") - - with self._lock: - try: - value = float(args[0]) - self._sum += value - self._count += 1 - self._latest_kwargs = kwargs - except (TypeError, ValueError): - raise TypeError(f"First argument must be numeric, got {type(args[0])}") - - def get(self) -> tuple[tuple, dict] | None: # type: ignore[type-arg] - with self._lock: - if self._count == 0: - return None - - average = self._sum / self._count - result = ((average,), self._latest_kwargs) - - # Reset accumulator - self._sum = 0.0 - self._count = 0 - self._latest_kwargs = {} - - return result - - def __len__(self) -> int: - with self._lock: - return self._count diff --git a/dimos/utils/decorators/decorators.py b/dimos/utils/decorators/decorators.py deleted file mode 100644 index 01e9f8b553..0000000000 --- a/dimos/utils/decorators/decorators.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -from functools import wraps -import threading -import time -from typing import Any, Protocol, TypeVar - -from .accumulators import Accumulator, LatestAccumulator - -_CacheResult_co = TypeVar("_CacheResult_co", covariant=True) -_CacheReturn = TypeVar("_CacheReturn") - - -class CachedMethod(Protocol[_CacheResult_co]): - """Protocol for methods decorated with simple_mcache.""" - - def __call__(self) -> _CacheResult_co: ... - def invalidate_cache(self, instance: Any) -> None: ... - - -def limit(max_freq: float, accumulator: Accumulator | None = None): # type: ignore[no-untyped-def, type-arg] - """ - Decorator that limits function call frequency. - - If calls come faster than max_freq, they are skipped. - If calls come slower than max_freq, they pass through immediately. - - Args: - max_freq: Maximum frequency in Hz (calls per second) - accumulator: Optional accumulator to collect skipped calls (defaults to LatestAccumulator) - - Returns: - Decorated function that respects the frequency limit - """ - if max_freq <= 0: - raise ValueError("Frequency must be positive") - - min_interval = 1.0 / max_freq - - # Create default accumulator if none provided - if accumulator is None: - accumulator = LatestAccumulator() - - def decorator(func: Callable) -> Callable: # type: ignore[type-arg] - last_call_time = 0.0 - lock = threading.Lock() - timer: threading.Timer | None = None - - def execute_accumulated() -> None: - nonlocal last_call_time, timer - with lock: - if len(accumulator): - acc_args, acc_kwargs = accumulator.get() # type: ignore[misc] - last_call_time = time.time() - timer = None - func(*acc_args, **acc_kwargs) - - @wraps(func) - def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] - nonlocal last_call_time, timer - current_time = time.time() - - with lock: - time_since_last = current_time - last_call_time - - if time_since_last >= min_interval: - # Cancel any pending timer - if timer is not None: - timer.cancel() - timer = None - - # Enough time has passed, execute the function - last_call_time = current_time - - # if we have accumulated data, we get a compound value - if len(accumulator): - accumulator.add(*args, **kwargs) - acc_args, acc_kwargs = accumulator.get() # type: ignore[misc] # accumulator resets here - return func(*acc_args, **acc_kwargs) - - # No accumulated data, normal call - return func(*args, **kwargs) - - else: - # Too soon, skip this call - accumulator.add(*args, **kwargs) - - # Schedule execution for when the interval expires - if timer is not None: - timer.cancel() - - time_to_wait = min_interval - time_since_last - timer = threading.Timer(time_to_wait, execute_accumulated) - timer.start() - - return None - - return wrapper - - return decorator - - -def simple_mcache(method: Callable) -> Callable: # type: ignore[type-arg] - """ - Decorator to cache the result of a method call on the instance. - - The cached value is stored as an attribute on the instance with the name - `_cached_`. Subsequent calls to the method will return the - cached value instead of recomputing it. - - Thread-safe: Uses a lock per instance to ensure the cached value is - computed only once even in multi-threaded environments. - - Args: - method: The method to be decorated. - - Returns: - The decorated method with caching behavior. - """ - - attr_name = f"_cached_{method.__name__}" - lock_name = f"_lock_{method.__name__}" - - @wraps(method) - def getter(self): # type: ignore[no-untyped-def] - # Get or create the lock for this instance - if not hasattr(self, lock_name): - setattr(self, lock_name, threading.Lock()) - - lock = getattr(self, lock_name) - - if hasattr(self, attr_name): - return getattr(self, attr_name) - - with lock: - # Check again inside the lock - if not hasattr(self, attr_name): - setattr(self, attr_name, method(self)) - return getattr(self, attr_name) - - def invalidate_cache(instance: Any) -> None: - """Clear the cached value for the given instance.""" - if not hasattr(instance, lock_name): - return - - lock = getattr(instance, lock_name) - with lock: - if hasattr(instance, attr_name): - delattr(instance, attr_name) - - getter.invalidate_cache = invalidate_cache # type: ignore[attr-defined] - - return getter - - -def retry(max_retries: int = 3, on_exception: type[Exception] = Exception, delay: float = 0.0): # type: ignore[no-untyped-def] - """ - Decorator that retries a function call if it raises an exception. - - Args: - max_retries: Maximum number of retry attempts (default: 3) - on_exception: Exception type to catch and retry on (default: Exception) - delay: Fixed delay in seconds between retries (default: 0.0) - - Returns: - Decorated function that will retry on failure - - Example: - @retry(max_retries=5, on_exception=ConnectionError, delay=0.5) - def connect_to_server(): - # connection logic that might fail - pass - - @retry() # Use defaults: 3 retries on any Exception, no delay - def risky_operation(): - # might fail occasionally - pass - """ - if max_retries < 0: - raise ValueError("max_retries must be non-negative") - if delay < 0: - raise ValueError("delay must be non-negative") - - def decorator(func: Callable) -> Callable: # type: ignore[type-arg] - @wraps(func) - def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] - last_exception = None - - for attempt in range(max_retries + 1): - try: - return func(*args, **kwargs) - except on_exception as e: - last_exception = e - if attempt < max_retries: - # Still have retries left - if delay > 0: - time.sleep(delay) - continue - else: - # Out of retries, re-raise the last exception - raise - - # This should never be reached, but just in case - if last_exception: - raise last_exception - - return wrapper - - return decorator diff --git a/dimos/utils/decorators/test_decorators.py b/dimos/utils/decorators/test_decorators.py deleted file mode 100644 index a40a806a80..0000000000 --- a/dimos/utils/decorators/test_decorators.py +++ /dev/null @@ -1,318 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import time - -import pytest - -from dimos.utils.decorators import RollingAverageAccumulator, limit, retry, simple_mcache - - -def test_limit() -> None: - """Test limit decorator with keyword arguments.""" - calls = [] - - @limit(20) # 20 Hz - def process(msg: str, keyword: int = 0) -> str: - calls.append((msg, keyword)) - return f"{msg}:{keyword}" - - # First call goes through - result1 = process("first", keyword=1) - assert result1 == "first:1" - assert calls == [("first", 1)] - - # Quick calls get accumulated - result2 = process("second", keyword=2) - assert result2 is None - - result3 = process("third", keyword=3) - assert result3 is None - - # Wait for interval, expect to be called after it passes - time.sleep(0.6) - - result4 = process("fourth") - assert result4 == "fourth:0" - - assert calls == [("first", 1), ("third", 3), ("fourth", 0)] - - -def test_latest_rolling_average() -> None: - """Test RollingAverageAccumulator with limit decorator.""" - calls = [] - - accumulator = RollingAverageAccumulator() - - @limit(20, accumulator=accumulator) # 20 Hz - def process(value: float, label: str = "") -> str: - calls.append((value, label)) - return f"{value}:{label}" - - # First call goes through - result1 = process(10.0, label="first") - assert result1 == "10.0:first" - assert calls == [(10.0, "first")] - - # Quick calls get accumulated - result2 = process(20.0, label="second") - assert result2 is None - - result3 = process(30.0, label="third") - assert result3 is None - - # Wait for interval - time.sleep(0.6) - - # Should see the average of accumulated values - assert calls == [(10.0, "first"), (25.0, "third")] # (20+30)/2 = 25 - - -def test_retry_success_after_failures() -> None: - """Test that retry decorator retries on failure and eventually succeeds.""" - attempts = [] - - @retry(max_retries=3) - def flaky_function(fail_times: int = 2) -> str: - attempts.append(len(attempts)) - if len(attempts) <= fail_times: - raise ValueError(f"Attempt {len(attempts)} failed") - return "success" - - result = flaky_function() - assert result == "success" - assert len(attempts) == 3 # Failed twice, succeeded on third attempt - - -def test_retry_exhausted() -> None: - """Test that retry decorator raises exception when retries are exhausted.""" - attempts = [] - - @retry(max_retries=2) - def always_fails(): - attempts.append(len(attempts)) - raise RuntimeError(f"Attempt {len(attempts)} failed") - - with pytest.raises(RuntimeError) as exc_info: - always_fails() - - assert "Attempt 3 failed" in str(exc_info.value) - assert len(attempts) == 3 # Initial attempt + 2 retries - - -def test_retry_specific_exception() -> None: - """Test that retry only catches specified exception types.""" - attempts = [] - - @retry(max_retries=3, on_exception=ValueError) - def raises_different_exceptions() -> str: - attempts.append(len(attempts)) - if len(attempts) == 1: - raise ValueError("First attempt") - elif len(attempts) == 2: - raise TypeError("Second attempt - should not be retried") - return "success" - - # Should fail on TypeError (not retried) - with pytest.raises(TypeError) as exc_info: - raises_different_exceptions() - - assert "Second attempt" in str(exc_info.value) - assert len(attempts) == 2 # First attempt with ValueError, second with TypeError - - -def test_retry_no_failures() -> None: - """Test that retry decorator works when function succeeds immediately.""" - attempts = [] - - @retry(max_retries=5) - def always_succeeds() -> str: - attempts.append(len(attempts)) - return "immediate success" - - result = always_succeeds() - assert result == "immediate success" - assert len(attempts) == 1 # Only one attempt needed - - -def test_retry_with_delay() -> None: - """Test that retry decorator applies delay between attempts.""" - attempts = [] - times = [] - - @retry(max_retries=2, delay=0.1) - def delayed_failures() -> str: - times.append(time.time()) - attempts.append(len(attempts)) - if len(attempts) < 2: - raise ValueError(f"Attempt {len(attempts)}") - return "success" - - start = time.time() - result = delayed_failures() - duration = time.time() - start - - assert result == "success" - assert len(attempts) == 2 - assert duration >= 0.1 # At least one delay occurred - - # Check that delays were applied - if len(times) >= 2: - assert times[1] - times[0] >= 0.1 - - -def test_retry_zero_retries() -> None: - """Test retry with max_retries=0 (no retries, just one attempt).""" - attempts = [] - - @retry(max_retries=0) - def single_attempt(): - attempts.append(len(attempts)) - raise ValueError("Failed") - - with pytest.raises(ValueError): - single_attempt() - - assert len(attempts) == 1 # Only the initial attempt - - -def test_retry_invalid_parameters() -> None: - """Test that retry decorator validates parameters.""" - with pytest.raises(ValueError): - - @retry(max_retries=-1) - def invalid_retries() -> None: - pass - - with pytest.raises(ValueError): - - @retry(delay=-0.5) - def invalid_delay() -> None: - pass - - -def test_retry_with_methods() -> None: - """Test that retry decorator works with class methods, instance methods, and static methods.""" - - class TestClass: - def __init__(self) -> None: - self.instance_attempts = [] - self.instance_value = 42 - - @retry(max_retries=3) - def instance_method(self, fail_times: int = 2) -> str: - """Test retry on instance method.""" - self.instance_attempts.append(len(self.instance_attempts)) - if len(self.instance_attempts) <= fail_times: - raise ValueError(f"Instance attempt {len(self.instance_attempts)} failed") - return f"instance success with value {self.instance_value}" - - @classmethod - @retry(max_retries=2) - def class_method(cls, attempts_list, fail_times: int = 1) -> str: - """Test retry on class method.""" - attempts_list.append(len(attempts_list)) - if len(attempts_list) <= fail_times: - raise ValueError(f"Class attempt {len(attempts_list)} failed") - return f"class success from {cls.__name__}" - - @staticmethod - @retry(max_retries=2) - def static_method(attempts_list, fail_times: int = 1) -> str: - """Test retry on static method.""" - attempts_list.append(len(attempts_list)) - if len(attempts_list) <= fail_times: - raise ValueError(f"Static attempt {len(attempts_list)} failed") - return "static success" - - # Test instance method - obj = TestClass() - result = obj.instance_method() - assert result == "instance success with value 42" - assert len(obj.instance_attempts) == 3 # Failed twice, succeeded on third - - # Test class method - class_attempts = [] - result = TestClass.class_method(class_attempts) - assert result == "class success from TestClass" - assert len(class_attempts) == 2 # Failed once, succeeded on second - - # Test static method - static_attempts = [] - result = TestClass.static_method(static_attempts) - assert result == "static success" - assert len(static_attempts) == 2 # Failed once, succeeded on second - - # Test that self is properly maintained across retries - obj2 = TestClass() - obj2.instance_value = 100 - result = obj2.instance_method() - assert result == "instance success with value 100" - assert len(obj2.instance_attempts) == 3 - - -def test_simple_mcache() -> None: - """Test simple_mcache decorator caches and can be invalidated.""" - call_count = 0 - - class Counter: - @simple_mcache - def expensive(self) -> int: - nonlocal call_count - call_count += 1 - return call_count - - obj = Counter() - - # First call computes - assert obj.expensive() == 1 - assert call_count == 1 - - # Second call returns cached - assert obj.expensive() == 1 - assert call_count == 1 - - # Invalidate and call again - obj.expensive.invalidate_cache(obj) - assert obj.expensive() == 2 - assert call_count == 2 - - # Cached again - assert obj.expensive() == 2 - assert call_count == 2 - - -def test_simple_mcache_separate_instances() -> None: - """Test that simple_mcache caches per instance.""" - call_count = 0 - - class Counter: - @simple_mcache - def expensive(self) -> int: - nonlocal call_count - call_count += 1 - return call_count - - obj1 = Counter() - obj2 = Counter() - - assert obj1.expensive() == 1 - assert obj2.expensive() == 2 # separate cache - assert obj1.expensive() == 1 # still cached - assert call_count == 2 - - # Invalidating one doesn't affect the other - obj1.expensive.invalidate_cache(obj1) - assert obj1.expensive() == 3 - assert obj2.expensive() == 2 # still cached diff --git a/dimos/utils/demo_image_encoding.py b/dimos/utils/demo_image_encoding.py deleted file mode 100644 index 42374029f2..0000000000 --- a/dimos/utils/demo_image_encoding.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -# Usage - -Run it with uncompressed LCM: - - python dimos/utils/demo_image_encoding.py - -Run it with JPEG LCM: - - python dimos/utils/demo_image_encoding.py --use-jpeg -""" - -import argparse -import threading -import time - -from reactivex.disposable import Disposable - -from dimos.core.module import Module -from dimos.core.module_coordinator import ModuleCoordinator -from dimos.core.stream import In, Out -from dimos.core.transport import JpegLcmTransport, LCMTransport -from dimos.msgs.sensor_msgs import Image -from dimos.robot.foxglove_bridge import FoxgloveBridge -from dimos.utils.fast_image_generator import random_image - - -class EmitterModule(Module): - image: Out[Image] - - _thread: threading.Thread | None = None - _stop_event: threading.Event | None = None - - def start(self) -> None: - super().start() - self._stop_event = threading.Event() - self._thread = threading.Thread(target=self._publish_image, daemon=True) - self._thread.start() - - def stop(self) -> None: - if self._thread: - self._stop_event.set() # type: ignore[union-attr] - self._thread.join(timeout=2) - super().stop() - - def _publish_image(self) -> None: - open_file = open("/tmp/emitter-times", "w") - while not self._stop_event.is_set(): # type: ignore[union-attr] - start = time.time() - data = random_image(1280, 720) - total = time.time() - start - print("took", total) - open_file.write(str(time.time()) + "\n") - self.image.publish(Image(data=data)) - open_file.close() - - -class ReceiverModule(Module): - image: In[Image] - - _open_file = None - - def start(self) -> None: - super().start() - self._disposables.add(Disposable(self.image.subscribe(self._on_image))) - self._open_file = open("/tmp/receiver-times", "w") - - def stop(self) -> None: - self._open_file.close() # type: ignore[union-attr] - super().stop() - - def _on_image(self, image: Image) -> None: - self._open_file.write(str(time.time()) + "\n") # type: ignore[union-attr] - print("image") - - -def main() -> None: - parser = argparse.ArgumentParser(description="Demo image encoding with transport options") - parser.add_argument( - "--use-jpeg", - action="store_true", - help="Use JPEG LCM transport instead of regular LCM transport", - ) - args = parser.parse_args() - - dimos = ModuleCoordinator(n=2) - dimos.start() - emitter = dimos.deploy(EmitterModule) - receiver = dimos.deploy(ReceiverModule) - - if args.use_jpeg: - emitter.image.transport = JpegLcmTransport("/go2/color_image", Image) - else: - emitter.image.transport = LCMTransport("/go2/color_image", Image) - receiver.image.connect(emitter.image) - - foxglove_bridge = FoxgloveBridge() - foxglove_bridge.start() - - dimos.start_all_modules() - - try: - while True: - time.sleep(0.1) - except KeyboardInterrupt: - pass - finally: - foxglove_bridge.stop() - dimos.close() # type: ignore[attr-defined] - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/docs/doclinks.md b/dimos/utils/docs/doclinks.md deleted file mode 100644 index dce2e67fec..0000000000 --- a/dimos/utils/docs/doclinks.md +++ /dev/null @@ -1,96 +0,0 @@ -# doclinks - -A Markdown link resolver that automatically fills in correct file paths for code references in documentation. - -## What it does - -When writing docs, you can use placeholder links like: - - -```markdown -See [`service/spec.py`]() for the implementation. -``` - - -Running `doclinks` resolves these to actual paths: - - -```markdown -See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. -``` - - -## Features - - -- **Code file links**: `[`filename.py`]()` resolves to the file's path -- **Symbol line linking**: If another backticked term appears on the same line, it finds that symbol in the file and adds `#L`: - ```markdown - See `Configurable` in [`config.py`]() - → [`config.py`](/path/config.py#L42) - ``` -- **Doc-to-doc links**: `[Modules](.md)` resolves to `modules.md` or `modules/index.md` - -- **Multiple link modes**: absolute, relative, or GitHub URLs -- **Watch mode**: Automatically re-process on file changes -- **Ignore regions**: Skip sections with `` comments - -## Usage - -```bash -# Process a single file -doclinks docs/guide.md - -# Process a directory recursively -doclinks docs/ - -# Relative links (from doc location) -doclinks --link-mode relative docs/ - -# GitHub links -doclinks --link-mode github \ - --github-url https://github.com/org/repo docs/ - -# Dry run (preview changes) -doclinks --dry-run docs/ - -# CI check (exit 1 if changes needed) -doclinks --check docs/ - -# Watch mode (auto-update on changes) -doclinks --watch docs/ -``` - -## Options - -| Option | Description | -|--------------------|-------------------------------------------------| -| `--root PATH` | Repository root (default: auto-detect git root) | -| `--link-mode MODE` | `absolute` (default), `relative`, or `github` | -| `--github-url URL` | Base GitHub URL (required for github mode) | -| `--github-ref REF` | Branch/ref for GitHub links (default: `main`) | -| `--dry-run` | Show changes without modifying files | -| `--check` | Exit with error if changes needed (for CI) | -| `--watch` | Watch for changes and re-process | - -## Link patterns - - -| Pattern | Description | -|----------------------|------------------------------------------------| -| `[`file.py`]()` | Code file reference (empty or any link) | -| `[`path/file.py`]()` | Code file with partial path for disambiguation | -| `[`file.py`](#L42)` | Preserves existing line fragments | -| `[Doc Name](.md)` | Doc-to-doc link (resolves by name) | - - -## How resolution works - -The tool builds an index of all files in the repo. For `/dimos/protocol/service/spec.py`, it creates lookup entries for: - -- `spec.py` -- `service/spec.py` -- `protocol/service/spec.py` -- `dimos/protocol/service/spec.py` - -Use longer paths when multiple files share the same name. diff --git a/dimos/utils/docs/doclinks.py b/dimos/utils/docs/doclinks.py deleted file mode 100644 index 67d5897b28..0000000000 --- a/dimos/utils/docs/doclinks.py +++ /dev/null @@ -1,589 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Markdown reference lookup tool. - -Finds markdown links like [`service/spec.py`](...) and fills in the correct -file path from the codebase. - -Usage: - python reference_lookup.py --root /repo/root [options] markdownfile.md -""" - -import argparse -from collections import defaultdict -import os -from pathlib import Path -import re -import subprocess -import sys -from typing import Any - - -def find_git_root() -> Path | None: - """Find the git repository root from current directory.""" - try: - result = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=True, - ) - return Path(result.stdout.strip()) - except (subprocess.CalledProcessError, FileNotFoundError): - return None - - -def get_git_tracked_files(root: Path) -> list[Path]: - """ - Get list of tracked files from git ls-files. - - Returns list of Path objects relative to root. - Only includes files tracked by git, respecting .gitignore. - - Args: - root: Repository root directory - - Returns: - List of Path objects relative to root, sorted. - Returns empty list if not in git repo or on error. - """ - try: - result = subprocess.run( - ["git", "ls-files", "--full-name", "--cached", "--others", "--exclude-standard"], - capture_output=True, - text=True, - check=True, - cwd=root, - ) - if not result.stdout.strip(): - return [] - - paths = [Path(line) for line in result.stdout.strip().split("\n") if line] - return sorted(paths) - except (subprocess.CalledProcessError, FileNotFoundError): - return [] - - -def build_file_index(root: Path) -> dict[str, list[Path]]: - """ - Build an index mapping filename suffixes to full paths. - - For /dimos/protocol/service/spec.py, creates entries for: - - spec.py - - service/spec.py - - protocol/service/spec.py - - dimos/protocol/service/spec.py - """ - index: dict[str, list[Path]] = defaultdict(list) - tracked_files = get_git_tracked_files(root) - - for rel_path in tracked_files: - parts = rel_path.parts - - # Add all suffix combinations - for i in range(len(parts)): - suffix = "/".join(parts[i:]) - index[suffix].append(rel_path) - - return index - - -def build_doc_index(root: Path) -> dict[str, list[Path]]: - """ - Build an index mapping lowercase doc names to .md file paths. - - For docs/usage/modules.md, creates entry: - - "modules" -> [Path("docs/usage/modules.md")] - - Also indexes directory index files: - - "modules" -> [Path("docs/modules/index.md")] (if modules/index.md exists) - """ - index: dict[str, list[Path]] = defaultdict(list) - tracked_files = get_git_tracked_files(root) - - for rel_path in tracked_files: - if rel_path.suffix != ".md": - continue - - stem = rel_path.stem.lower() - - # For index.md files, also index by parent directory name - if stem == "index": - parent_name = rel_path.parent.name.lower() - if parent_name: - index[parent_name].append(rel_path) - else: - index[stem].append(rel_path) - - return index - - -def find_symbol_line(file_path: Path, symbol: str) -> int | None: - """Find the first line number where symbol appears.""" - try: - with open(file_path, encoding="utf-8", errors="replace") as f: - for line_num, line in enumerate(f, start=1): - if symbol in line: - return line_num - except OSError: - pass - return None - - -def extract_other_backticks(line: str, file_ref: str) -> list[str]: - """Extract other backticked terms from a line, excluding the file reference.""" - pattern = r"`([^`]+)`" - matches = re.findall(pattern, line) - return [m for m in matches if m != file_ref and not m.endswith(".py") and "/" not in m] - - -def generate_link( - rel_path: Path, - root: Path, - doc_path: Path, - link_mode: str, - github_url: str | None, - github_ref: str, - line_fragment: str = "", -) -> str: - """Generate the appropriate link format.""" - if link_mode == "absolute": - return f"/{rel_path}{line_fragment}" - elif link_mode == "relative": - doc_dir = ( - doc_path.parent.relative_to(root) if doc_path.is_relative_to(root) else doc_path.parent - ) - target = root / rel_path - try: - rel_link = os.path.relpath(target, root / doc_dir) - except ValueError: - rel_link = str(rel_path) - return f"{rel_link}{line_fragment}" - elif link_mode == "github": - if not github_url: - raise ValueError("--github-url required when using --link-mode=github") - return f"{github_url.rstrip('/')}/blob/{github_ref}/{rel_path}{line_fragment}" - else: - raise ValueError(f"Unknown link mode: {link_mode}") - - -def split_by_ignore_regions(content: str) -> list[tuple[str, bool]]: - """ - Split content into regions, marking which should be processed. - - Returns list of (text, should_process) tuples. - Regions between and are skipped. - """ - ignore_start = re.compile(r"", re.IGNORECASE) - ignore_end = re.compile(r"", re.IGNORECASE) - - regions = [] - pos = 0 - in_ignore = False - - while pos < len(content): - if not in_ignore: - # Look for start of ignore region - match = ignore_start.search(content, pos) - if match: - # Add content before ignore marker (to be processed) - if match.start() > pos: - regions.append((content[pos : match.start()], True)) - # Add the marker itself (not processed) - regions.append((content[match.start() : match.end()], False)) - pos = match.end() - in_ignore = True - else: - # No more ignore regions, add rest of content - regions.append((content[pos:], True)) - break - else: - # Look for end of ignore region - match = ignore_end.search(content, pos) - if match: - # Add ignored content including end marker - regions.append((content[pos : match.end()], False)) - pos = match.end() - in_ignore = False - else: - # Unclosed ignore region, add rest as ignored - regions.append((content[pos:], False)) - break - - return regions - - -def process_markdown( - content: str, - root: Path, - doc_path: Path, - file_index: dict[str, list[Path]], - link_mode: str, - github_url: str | None, - github_ref: str, - doc_index: dict[str, list[Path]] | None = None, -) -> tuple[str, list[str], list[str]]: - """ - Process markdown content, replacing file and doc links. - - Regions between and - are skipped. - - Returns (new_content, changes, errors). - """ - changes = [] - errors = [] - - # Pattern 1: [`filename`](link) - code file links - code_pattern = r"\[`([^`]+)`\]\(([^)]*)\)" - - # Pattern 2: [Text](.md) - doc file links - doc_pattern = r"\[([^\]]+)\]\(\.md\)" - - def replace_code_match(match: re.Match[str]) -> str: - file_ref = match.group(1) - current_link = match.group(2) - full_match = match.group(0) - - # Skip anchor-only links (e.g., [`Symbol`](#section)) - if current_link.startswith("#"): - return full_match - - # Skip if the reference doesn't look like a file path (no extension or path separator) - if "." not in file_ref and "/" not in file_ref: - return full_match - - # Look up in index - candidates = file_index.get(file_ref, []) - - if len(candidates) == 0: - errors.append(f"No file matching '{file_ref}' found in codebase") - return full_match - elif len(candidates) > 1: - errors.append(f"'{file_ref}' matches multiple files: {[str(c) for c in candidates]}") - return full_match - - resolved_path = candidates[0] - - # Determine line fragment - line_fragment = "" - - # Check if current link has a line fragment to preserve - if "#" in current_link: - line_fragment = "#" + current_link.split("#", 1)[1] - else: - # Look for other backticked symbols on the same line - line_start = content.rfind("\n", 0, match.start()) + 1 - line_end = content.find("\n", match.end()) - if line_end == -1: - line_end = len(content) - line = content[line_start:line_end] - - symbols = extract_other_backticks(line, file_ref) - if symbols: - # Try to find the first symbol in the target file - full_file_path = root / resolved_path - for symbol in symbols: - line_num = find_symbol_line(full_file_path, symbol) - if line_num is not None: - line_fragment = f"#L{line_num}" - break - - new_link = generate_link( - resolved_path, root, doc_path, link_mode, github_url, github_ref, line_fragment - ) - new_match = f"[`{file_ref}`]({new_link})" - - if new_match != full_match: - changes.append(f" {file_ref}: {current_link} -> {new_link}") - - return new_match - - def replace_doc_match(match: re.Match[str]) -> str: - """Replace [Text](.md) with resolved doc path.""" - if doc_index is None: - return match.group(0) - - link_text = match.group(1) - full_match = match.group(0) - lookup_key = link_text.lower() - - # Look up in doc index - candidates = doc_index.get(lookup_key, []) - - if len(candidates) == 0: - errors.append(f"No doc matching '{link_text}' found") - return full_match - elif len(candidates) > 1: - errors.append(f"'{link_text}' matches multiple docs: {[str(c) for c in candidates]}") - return full_match - - resolved_path = candidates[0] - new_link = generate_link(resolved_path, root, doc_path, link_mode, github_url, github_ref) - new_match = f"[{link_text}]({new_link})" - - if new_match != full_match: - changes.append(f" {link_text}: .md -> {new_link}") - - return new_match - - # Split by ignore regions and only process non-ignored parts - regions = split_by_ignore_regions(content) - result_parts = [] - - for region_content, should_process in regions: - if should_process: - # Process code links first, then doc links - processed = re.sub(code_pattern, replace_code_match, region_content) - processed = re.sub(doc_pattern, replace_doc_match, processed) - result_parts.append(processed) - else: - result_parts.append(region_content) - - new_content = "".join(result_parts) - return new_content, changes, errors - - -def collect_markdown_files(paths: list[str]) -> list[Path]: - """Collect markdown files from paths, expanding directories recursively.""" - result: list[Path] = [] - for p in paths: - path = Path(p) - if path.is_dir(): - result.extend(path.rglob("*.md")) - elif path.exists(): - result.append(path) - return sorted(set(result)) - - -USAGE = """\ -doclinks - Update markdown file links to correct codebase paths - -Finds [`filename.py`](...) patterns and resolves them to actual file paths. -Also auto-links symbols: `Configurable` on same line adds #L fragment. - -Supports doc-to-doc linking: [Modules](.md) resolves to modules.md or modules/index.md. - -Usage: - doclinks [options] - -Examples: - # Single file (auto-detects git root) - doclinks docs/guide.md - - # Recursive directory - doclinks docs/ - - # GitHub links - doclinks --root . --link-mode github \\ - --github-url https://github.com/org/repo docs/ - - # Relative links (from doc location) - doclinks --root . --link-mode relative docs/ - - # CI check (exit 1 if changes needed) - doclinks --root . --check docs/ - - # Dry run (show changes without writing) - doclinks --root . --dry-run docs/ - -Options: - --root PATH Repository root (default: git root) - --link-mode MODE absolute (default), relative, or github - --github-url URL Base GitHub URL (for github mode) - --github-ref REF Branch/ref for GitHub links (default: main) - --dry-run Show changes without modifying files - --check Exit with error if changes needed - --watch Watch for changes and re-process (requires watchdog) - -h, --help Show this help -""" - - -def main() -> None: - if len(sys.argv) == 1: - print(USAGE) - sys.exit(0) - - parser = argparse.ArgumentParser( - description="Update markdown file links to correct codebase paths", - formatter_class=argparse.RawDescriptionHelpFormatter, - add_help=False, - ) - parser.add_argument("paths", nargs="*", help="Markdown files or directories to process") - parser.add_argument("--root", type=Path, help="Repository root path") - parser.add_argument("-h", "--help", action="store_true", help="Show help") - parser.add_argument( - "--link-mode", - choices=["absolute", "relative", "github"], - default="absolute", - help="Link format (default: absolute)", - ) - parser.add_argument("--github-url", help="Base GitHub URL (required for github mode)") - parser.add_argument("--github-ref", default="main", help="GitHub branch/ref (default: main)") - parser.add_argument( - "--dry-run", action="store_true", help="Show changes without modifying files" - ) - parser.add_argument( - "--check", action="store_true", help="Exit with error if changes needed (CI mode)" - ) - parser.add_argument("--watch", action="store_true", help="Watch for changes and re-process") - - args = parser.parse_args() - - if args.help: - print(USAGE) - sys.exit(0) - - # Auto-detect git root if --root not provided - if args.root: - root = args.root.resolve() - else: - root = find_git_root() - if root is None: - print("Error: --root not provided and not in a git repository\n", file=sys.stderr) - sys.exit(1) - - if not args.paths: - print("Error: at least one path is required\n", file=sys.stderr) - print(USAGE) - sys.exit(1) - - if args.link_mode == "github" and not args.github_url: - print("Error: --github-url is required when using --link-mode=github\n", file=sys.stderr) - sys.exit(1) - - if not root.is_dir(): - print(f"Error: {root} is not a directory", file=sys.stderr) - sys.exit(1) - - print(f"Building file index from {root}...") - file_index = build_file_index(root) - doc_index = build_doc_index(root) - print( - f"Indexed {sum(len(v) for v in file_index.values())} file paths, {len(doc_index)} doc names" - ) - - def process_file(md_path: Path, quiet: bool = False) -> tuple[bool, list[str]]: - """Process a single markdown file. Returns (changed, errors).""" - md_path = md_path.resolve() - if not quiet: - rel = md_path.relative_to(root) if md_path.is_relative_to(root) else md_path - print(f"\nProcessing {rel}...") - - content = md_path.read_text() - new_content, changes, errors = process_markdown( - content, - root, - md_path, - file_index, - args.link_mode, - args.github_url, - args.github_ref, - doc_index=doc_index, - ) - - if errors: - for err in errors: - print(f" Error: {err}", file=sys.stderr) - - if changes: - if not quiet: - print(" Changes:") - for change in changes: - print(change) - if not args.dry_run and not args.check: - md_path.write_text(new_content) - if not quiet: - print(" Updated") - return True, errors - else: - if not quiet: - print(" No changes needed") - return False, errors - - # Watch mode - if args.watch: - try: - from watchdog.events import FileSystemEventHandler - from watchdog.observers import Observer - except ImportError: - print( - "Error: --watch requires watchdog. Install with: pip install watchdog", - file=sys.stderr, - ) - sys.exit(1) - - watch_paths = args.paths if args.paths else [str(root / "docs")] - - class MarkdownHandler(FileSystemEventHandler): - def on_modified(self, event: Any) -> None: - if not event.is_directory and event.src_path.endswith(".md"): - process_file(Path(event.src_path)) - - def on_created(self, event: Any) -> None: - if not event.is_directory and event.src_path.endswith(".md"): - process_file(Path(event.src_path)) - - observer = Observer() - handler = MarkdownHandler() - - for watch_path in watch_paths: - p = Path(watch_path) - if p.is_file(): - p = p.parent - print(f"Watching {p} for changes...") - observer.schedule(handler, str(p), recursive=True) - - observer.start() - try: - while True: - import time - - time.sleep(1) - except KeyboardInterrupt: - observer.stop() - observer.join() - return - - # Normal mode - markdown_files = collect_markdown_files(args.paths) - if not markdown_files: - print("No markdown files found", file=sys.stderr) - sys.exit(1) - - print(f"Found {len(markdown_files)} markdown file(s)") - - all_errors = [] - any_changes = False - - for md_path in markdown_files: - changed, errors = process_file(md_path) - if changed: - any_changes = True - all_errors.extend(errors) - - if all_errors: - print(f"\n{len(all_errors)} error(s) encountered", file=sys.stderr) - sys.exit(1) - - if args.check and any_changes: - print("\nChanges needed (--check mode)", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/dimos/utils/docs/test_doclinks.py b/dimos/utils/docs/test_doclinks.py deleted file mode 100644 index f1303a2245..0000000000 --- a/dimos/utils/docs/test_doclinks.py +++ /dev/null @@ -1,524 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for doclinks - using virtual markdown content against actual repo.""" - -from pathlib import Path - -from doclinks import ( - build_doc_index, - build_file_index, - extract_other_backticks, - find_symbol_line, - process_markdown, - split_by_ignore_regions, -) -import pytest - -# Use the actual repo root -REPO_ROOT = Path(__file__).parent.parent.parent.parent - - -@pytest.fixture(scope="module") -def file_index(): - """Build file index once for all tests.""" - return build_file_index(REPO_ROOT) - - -@pytest.fixture(scope="module") -def doc_index(): - """Build doc index once for all tests.""" - return build_doc_index(REPO_ROOT) - - -class TestFileIndex: - def test_finds_spec_files(self, file_index): - """Should find spec.py files with various path suffixes.""" - # Exact match with path - assert "protocol/service/spec.py" in file_index - candidates = file_index["protocol/service/spec.py"] - assert len(candidates) == 1 - assert candidates[0] == Path("dimos/protocol/service/spec.py") - - def test_service_spec_unique(self, file_index): - """service/spec.py should uniquely match one file.""" - candidates = file_index.get("service/spec.py", []) - assert len(candidates) == 1 - assert "protocol/service/spec.py" in str(candidates[0]) - - def test_spec_ambiguous(self, file_index): - """spec.py alone should match multiple files.""" - candidates = file_index.get("spec.py", []) - assert len(candidates) > 1 # Multiple spec.py files exist - - def test_excludes_venv(self, file_index): - """Should not include files from .venv directory.""" - for paths in file_index.values(): - for p in paths: - # Check for .venv as a path component, not just substring - assert ".venv" not in p.parts - - -class TestSymbolLookup: - def test_find_configurable_in_spec(self): - """Should find Configurable class in service/spec.py.""" - spec_path = REPO_ROOT / "dimos/protocol/service/spec.py" - line = find_symbol_line(spec_path, "Configurable") - assert line is not None - assert line > 0 - - # Verify it's the class definition line - with open(spec_path) as f: - lines = f.readlines() - assert "class Configurable" in lines[line - 1] - - def test_find_nonexistent_symbol(self): - """Should return None for symbols that don't exist.""" - spec_path = REPO_ROOT / "dimos/protocol/service/spec.py" - line = find_symbol_line(spec_path, "NonExistentSymbol12345") - assert line is None - - -class TestExtractBackticks: - def test_extracts_symbols(self): - """Should extract backticked terms excluding file refs.""" - line = "See [`service/spec.py`]() for `Configurable` and `Service`" - symbols = extract_other_backticks(line, "service/spec.py") - assert "Configurable" in symbols - assert "Service" in symbols - assert "service/spec.py" not in symbols - - def test_excludes_file_paths(self): - """Should exclude things that look like file paths.""" - line = "See [`foo.py`]() and `bar.py` and `Symbol`" - symbols = extract_other_backticks(line, "foo.py") - assert "Symbol" in symbols - assert "bar.py" not in symbols # Has .py extension - assert "foo.py" not in symbols - - -class TestProcessMarkdown: - def test_resolves_service_spec(self, file_index): - """Should resolve service/spec.py to full path.""" - content = "See [`service/spec.py`]() for details" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 0 - assert len(changes) == 1 - assert "/dimos/protocol/service/spec.py" in new_content - - def test_auto_links_symbol(self, file_index): - """Should auto-add line number for symbol on same line.""" - content = "The `Configurable` class is in [`service/spec.py`]()" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 0 - assert "#L" in new_content # Should have line number - - def test_preserves_existing_line_fragment(self, file_index): - """Should preserve existing #L fragments.""" - content = "See [`service/spec.py`](#L99)" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, _errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert "#L99" in new_content - - def test_skips_anchor_links(self, file_index): - """Should skip anchor-only links like [`Symbol`](#section).""" - content = "See [`SomeClass`](#some-section) for details" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 0 - assert len(changes) == 0 - assert new_content == content # Unchanged - - def test_skips_non_file_refs(self, file_index): - """Should skip refs that don't look like files.""" - content = "The `MyClass` is documented at [`MyClass`]()" - doc_path = REPO_ROOT / "docs/test.md" - - _new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 0 - assert len(changes) == 0 - - def test_errors_on_ambiguous(self, file_index): - """Should error when file reference is ambiguous.""" - content = "See [`spec.py`]() for details" # Multiple spec.py files - doc_path = REPO_ROOT / "docs/test.md" - - _new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 1 - assert "matches multiple files" in errors[0] - - def test_errors_on_not_found(self, file_index): - """Should error when file doesn't exist.""" - content = "See [`nonexistent/file.py`]() for details" - doc_path = REPO_ROOT / "docs/test.md" - - _new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 1 - assert "No file matching" in errors[0] - - def test_github_mode(self, file_index): - """Should generate GitHub URLs in github mode.""" - content = "See [`service/spec.py`]()" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, _errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="github", - github_url="https://github.com/org/repo", - github_ref="main", - ) - - assert "https://github.com/org/repo/blob/main/dimos/protocol/service/spec.py" in new_content - - def test_relative_mode(self, file_index): - """Should generate relative paths in relative mode.""" - content = "See [`service/spec.py`]()" - doc_path = REPO_ROOT / "docs/usage/test.md" - - new_content, _changes, _errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="relative", - github_url=None, - github_ref="main", - ) - - assert new_content.startswith("See [`service/spec.py`](../../") - assert "dimos/protocol/service/spec.py" in new_content - - -class TestDocIndex: - def test_indexes_by_stem(self, doc_index): - """Should index docs by lowercase stem.""" - assert "configuration" in doc_index - assert "modules" in doc_index - assert "blueprints" in doc_index - - def test_case_insensitive(self, doc_index): - """Should use lowercase keys.""" - # All keys should be lowercase - for key in doc_index: - assert key == key.lower() - - -class TestDocLinking: - def test_resolves_doc_link(self, file_index, doc_index): - """Should resolve [Text](.md) to doc path.""" - content = "See [Configuration](.md) for details" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert len(errors) == 0 - assert len(changes) == 1 - assert "[Configuration](/docs/" in new_content - assert ".md)" in new_content - - def test_case_insensitive_lookup(self, file_index, doc_index): - """Should match case-insensitively.""" - content = "See [CONFIGURATION](.md) for details" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert len(errors) == 0 - assert "[CONFIGURATION](" in new_content # Preserves original text - assert ".md)" in new_content - - def test_doc_link_github_mode(self, file_index, doc_index): - """Should generate GitHub URLs for doc links.""" - content = "See [Configuration](.md)" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, _errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="github", - github_url="https://github.com/org/repo", - github_ref="main", - doc_index=doc_index, - ) - - assert "https://github.com/org/repo/blob/main/docs/" in new_content - assert ".md)" in new_content - - def test_doc_link_relative_mode(self, file_index, doc_index): - """Should generate relative paths for doc links.""" - content = "See [Blueprints](.md)" - doc_path = REPO_ROOT / "docs/usage/test.md" - - new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="relative", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert len(errors) == 0 - # Should be relative path from docs/usage/ to target doc - assert "[Blueprints](blueprints.md)" in new_content - - def test_doc_not_found_error(self, file_index, doc_index): - """Should error when doc doesn't exist.""" - content = "See [NonexistentDoc](.md)" - doc_path = REPO_ROOT / "docs/test.md" - - _new_content, _changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert len(errors) == 1 - assert "No doc matching" in errors[0] - - def test_skips_regular_links(self, file_index, doc_index): - """Should not affect regular markdown links.""" - content = "See [regular link](https://example.com) here" - doc_path = REPO_ROOT / "docs/test.md" - - new_content, _changes, _errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert new_content == content # Unchanged - - -class TestIgnoreRegions: - def test_split_no_ignore(self): - """Content without ignore markers should be fully processed.""" - content = "Hello world" - regions = split_by_ignore_regions(content) - assert len(regions) == 1 - assert regions[0] == ("Hello world", True) - - def test_split_single_ignore(self): - """Should correctly split around a single ignore region.""" - content = "beforeignoredafter" - regions = split_by_ignore_regions(content) - - # Should have: before (process), marker (no), ignored+end (no), after (process) - assert len(regions) == 4 - assert regions[0] == ("before", True) - assert regions[1][1] is False # Start marker - assert regions[2][1] is False # Ignored content + end marker - assert regions[3] == ("after", True) - - def test_split_multiple_ignores(self): - """Should handle multiple ignore regions.""" - content = ( - "ax" - "byc" - ) - regions = split_by_ignore_regions(content) - - # Check that processable regions are correctly identified - processable = [r[0] for r in regions if r[1]] - assert "a" in processable - assert "b" in processable - assert "c" in processable - - def test_split_case_insensitive(self): - """Should handle different case in markers.""" - content = "beforeignoredafter" - regions = split_by_ignore_regions(content) - - processable = [r[0] for r in regions if r[1]] - assert "before" in processable - assert "after" in processable - assert "ignored" not in processable - - def test_split_unclosed_ignore(self): - """Unclosed ignore region should ignore rest of content.""" - content = "beforerest of file" - regions = split_by_ignore_regions(content) - - processable = [r[0] for r in regions if r[1]] - assert "before" in processable - assert "rest of file" not in processable - - def test_ignores_links_in_region(self, file_index): - """Links inside ignore region should not be processed.""" - content = ( - "Process [`service/spec.py`]() here\n" - "\n" - "Skip [`service/spec.py`]() here\n" - "\n" - "Process [`service/spec.py`]() again" - ) - doc_path = REPO_ROOT / "docs/test.md" - - new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - ) - - assert len(errors) == 0 - # Should have 2 changes (before and after ignore region) - assert len(changes) == 2 - - # Verify the ignored region is untouched - assert "Skip [`service/spec.py`]() here" in new_content - - # Verify the processed regions have resolved links - lines = new_content.split("\n") - assert "/dimos/protocol/service/spec.py" in lines[0] - assert "/dimos/protocol/service/spec.py" in lines[-1] - - def test_ignores_doc_links_in_region(self, file_index, doc_index): - """Doc links inside ignore region should not be processed.""" - content = ( - "[Configuration](.md)\n" - "\n" - "[Configuration](.md) example\n" - "\n" - "[Configuration](.md)" - ) - doc_path = REPO_ROOT / "docs/test.md" - - new_content, changes, errors = process_markdown( - content, - REPO_ROOT, - doc_path, - file_index, - link_mode="absolute", - github_url=None, - github_ref="main", - doc_index=doc_index, - ) - - assert len(errors) == 0 - assert len(changes) == 2 # Only 2 links processed - - # Verify the ignored region still has .md placeholder - assert "[Configuration](.md) example" in new_content - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/dimos/utils/extract_frames.py b/dimos/utils/extract_frames.py deleted file mode 100644 index 1719c77620..0000000000 --- a/dimos/utils/extract_frames.py +++ /dev/null @@ -1,83 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import argparse -from pathlib import Path - -import cv2 - - -def extract_frames(video_path, output_dir, frame_rate) -> None: # type: ignore[no-untyped-def] - """ - Extract frames from a video file at a specified frame rate. - - Parameters: - - video_path: Path to the input video file (.mov or .mp4). - - output_dir: Directory where extracted frames will be saved. - - frame_rate: Frame rate at which to extract frames (frames per second). - """ - video_path = Path(video_path) - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - - # Open the video file - cap = cv2.VideoCapture(str(video_path)) - - # Get the original frame rate of the video - original_frame_rate = cap.get(cv2.CAP_PROP_FPS) - if original_frame_rate == 0: - print(f"Could not retrieve frame rate for {video_path}") - return - - # Calculate the interval between frames to capture - frame_interval = round(original_frame_rate / frame_rate) - if frame_interval == 0: - frame_interval = 1 - - frame_count = 0 - saved_frame_count = 0 - - while cap.isOpened(): - ret, frame = cap.read() - if not ret: - break - - # Save the frame at the specified intervals - if frame_count % frame_interval == 0: - frame_filename = output_dir / f"frame_{saved_frame_count:04d}.jpg" - cv2.imwrite(str(frame_filename), frame) - saved_frame_count += 1 - - frame_count += 1 - - cap.release() - print(f"Extracted {saved_frame_count} frames to {output_dir}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Extract frames from a video file.") - parser.add_argument("video_path", type=str, help="Path to the input .mov or .mp4 video file.") - parser.add_argument( - "--output_dir", type=str, default="frames", help="Directory to save extracted frames." - ) - parser.add_argument( - "--frame_rate", - type=float, - default=1.0, - help="Frame rate at which to extract frames (frames per second).", - ) - - args = parser.parse_args() - - extract_frames(args.video_path, args.output_dir, args.frame_rate) diff --git a/dimos/utils/fast_image_generator.py b/dimos/utils/fast_image_generator.py deleted file mode 100644 index 66c4fcf951..0000000000 --- a/dimos/utils/fast_image_generator.py +++ /dev/null @@ -1,305 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Fast stateful image generator with visual features for encoding tests.""" - -from typing import Literal, TypedDict, Union - -import numpy as np -from numpy.typing import NDArray - - -class CircleObject(TypedDict): - """Type definition for circle objects.""" - - type: Literal["circle"] - x: float - y: float - vx: float - vy: float - radius: int - color: NDArray[np.float32] - - -class RectObject(TypedDict): - """Type definition for rectangle objects.""" - - type: Literal["rect"] - x: float - y: float - vx: float - vy: float - width: int - height: int - color: NDArray[np.float32] - - -Object = Union[CircleObject, RectObject] - - -class FastImageGenerator: - """ - Stateful image generator that creates images with visual features - suitable for testing image/video encoding at 30+ FPS. - - Features generated: - - Moving geometric shapes (tests motion vectors) - - Color gradients (tests gradient compression) - - Sharp edges and corners (tests edge preservation) - - Textured regions (tests detail retention) - - Smooth regions (tests flat area compression) - - High contrast boundaries (tests blocking artifacts) - """ - - def __init__(self, width: int = 1280, height: int = 720) -> None: - """Initialize the generator with pre-computed elements.""" - self.width = width - self.height = height - self.frame_count = 0 - self.objects: list[Object] = [] - - # Pre-allocate the main canvas - self.canvas = np.zeros((height, width, 3), dtype=np.float32) - - # Pre-compute coordinate grids for fast gradient generation - self.x_grid, self.y_grid = np.meshgrid( - np.linspace(0, 1, width, dtype=np.float32), np.linspace(0, 1, height, dtype=np.float32) - ) - - # Pre-compute base gradient patterns - self._init_gradients() - - # Initialize moving objects with their properties - self._init_moving_objects() - - # Pre-compute static texture pattern - self._init_texture() - - # Pre-allocate shape masks for reuse - self._init_shape_masks() - - def _init_gradients(self) -> None: - """Pre-compute gradient patterns.""" - # Diagonal gradient - self.diag_gradient = (self.x_grid + self.y_grid) * 0.5 - - # Radial gradient from center - cx, cy = 0.5, 0.5 - self.radial_gradient = np.sqrt((self.x_grid - cx) ** 2 + (self.y_grid - cy) ** 2) - self.radial_gradient = np.clip(1.0 - self.radial_gradient * 1.5, 0, 1) - - # Horizontal and vertical gradients - self.h_gradient = self.x_grid - self.v_gradient = self.y_grid - - def _init_moving_objects(self) -> None: - """Initialize properties of moving objects.""" - self.objects = [ - { - "type": "circle", - "x": 0.2, - "y": 0.3, - "vx": 0.002, - "vy": 0.003, - "radius": 60, - "color": np.array([255, 100, 100], dtype=np.float32), - }, - { - "type": "rect", - "x": 0.7, - "y": 0.6, - "vx": -0.003, - "vy": 0.002, - "width": 100, - "height": 80, - "color": np.array([100, 255, 100], dtype=np.float32), - }, - { - "type": "circle", - "x": 0.5, - "y": 0.5, - "vx": 0.004, - "vy": -0.002, - "radius": 40, - "color": np.array([100, 100, 255], dtype=np.float32), - }, - ] - - def _init_texture(self) -> None: - """Pre-compute a texture pattern.""" - # Create a simple checkerboard pattern at lower resolution - checker_size = 20 - checker_h = self.height // checker_size - checker_w = self.width // checker_size - - # Create small checkerboard - checker = np.indices((checker_h, checker_w)).sum(axis=0) % 2 - - # Upscale using repeat (fast) - self.texture = np.repeat(np.repeat(checker, checker_size, axis=0), checker_size, axis=1) - self.texture = self.texture[: self.height, : self.width].astype(np.float32) * 30 - - def _init_shape_masks(self) -> None: - """Pre-allocate reusable masks for shapes.""" - # Pre-allocate a mask array - self.temp_mask = np.zeros((self.height, self.width), dtype=np.float32) - - # Pre-compute indices for the entire image - self.y_indices, self.x_indices = np.indices((self.height, self.width)) - - def _draw_circle_fast(self, cx: int, cy: int, radius: int, color: NDArray[np.float32]) -> None: - """Draw a circle using vectorized operations - optimized version without anti-aliasing.""" - # Compute bounding box to minimize calculations - y1 = max(0, cy - radius - 1) - y2 = min(self.height, cy + radius + 2) - x1 = max(0, cx - radius - 1) - x2 = min(self.width, cx + radius + 2) - - # Work only on the bounding box region - if y1 < y2 and x1 < x2: - y_local, x_local = np.ogrid[y1:y2, x1:x2] - dist_sq = (x_local - cx) ** 2 + (y_local - cy) ** 2 - mask = dist_sq <= radius**2 - self.canvas[y1:y2, x1:x2][mask] = color - - def _draw_rect_fast(self, x: int, y: int, w: int, h: int, color: NDArray[np.float32]) -> None: - """Draw a rectangle using slicing.""" - # Clip to canvas boundaries - x1 = max(0, x) - y1 = max(0, y) - x2 = min(self.width, x + w) - y2 = min(self.height, y + h) - - if x1 < x2 and y1 < y2: - self.canvas[y1:y2, x1:x2] = color - - def _update_objects(self) -> None: - """Update positions of moving objects.""" - for obj in self.objects: - # Update position - obj["x"] += obj["vx"] - obj["y"] += obj["vy"] - - # Bounce off edges - if obj["type"] == "circle": - r = obj["radius"] / self.width - if obj["x"] - r <= 0 or obj["x"] + r >= 1: - obj["vx"] *= -1 - obj["x"] = np.clip(obj["x"], r, 1 - r) - - r = obj["radius"] / self.height - if obj["y"] - r <= 0 or obj["y"] + r >= 1: - obj["vy"] *= -1 - obj["y"] = np.clip(obj["y"], r, 1 - r) - - elif obj["type"] == "rect": - w = obj["width"] / self.width - h = obj["height"] / self.height - if obj["x"] <= 0 or obj["x"] + w >= 1: - obj["vx"] *= -1 - obj["x"] = np.clip(obj["x"], 0, 1 - w) - - if obj["y"] <= 0 or obj["y"] + h >= 1: - obj["vy"] *= -1 - obj["y"] = np.clip(obj["y"], 0, 1 - h) - - def generate_frame(self) -> NDArray[np.uint8]: - """ - Generate a single frame with visual features - optimized for 30+ FPS. - - Returns: - numpy array of shape (height, width, 3) with uint8 values - """ - # Fast gradient background - use only one gradient per frame - if self.frame_count % 2 == 0: - base_gradient = self.h_gradient - else: - base_gradient = self.v_gradient - - # Simple color mapping - self.canvas[:, :, 0] = base_gradient * 150 + 50 - self.canvas[:, :, 1] = base_gradient * 120 + 70 - self.canvas[:, :, 2] = (1 - base_gradient) * 140 + 60 - - # Add texture in corner - simplified without per-channel scaling - tex_size = self.height // 3 - self.canvas[:tex_size, :tex_size] += self.texture[:tex_size, :tex_size, np.newaxis] - - # Add test pattern bars - vectorized - bar_width = 50 - bar_start = self.width // 3 - for i in range(3): # Reduced from 5 to 3 bars - x1 = bar_start + i * bar_width * 2 - x2 = min(x1 + bar_width, self.width) - if x1 < self.width: - color_val = 180 + i * 30 - self.canvas[self.height // 2 :, x1:x2] = color_val - - # Update and draw only 2 moving objects (reduced from 3) - self._update_objects() - - # Draw only first 2 objects for speed - for obj in self.objects[:2]: - if obj["type"] == "circle": - cx = int(obj["x"] * self.width) - cy = int(obj["y"] * self.height) - self._draw_circle_fast(cx, cy, obj["radius"], obj["color"]) - elif obj["type"] == "rect": - x = int(obj["x"] * self.width) - y = int(obj["y"] * self.height) - self._draw_rect_fast(x, y, obj["width"], obj["height"], obj["color"]) - - # Simple horizontal lines pattern (faster than sine wave) - line_y = int(self.height * 0.8) - line_spacing = 10 - for i in range(0, 5): - y = line_y + i * line_spacing - if y < self.height: - self.canvas[y : y + 2, :] = [255, 200, 100] - - # Increment frame counter - self.frame_count += 1 - - # Direct conversion to uint8 (already in valid range) - return self.canvas.astype(np.uint8) - - def reset(self) -> None: - """Reset the generator to initial state.""" - self.frame_count = 0 - self._init_moving_objects() - - -# Convenience function for backward compatibility -_generator: FastImageGenerator | None = None - - -def random_image(width: int, height: int) -> NDArray[np.uint8]: - """ - Generate an image with visual features suitable for encoding tests. - Maintains state for efficient stream generation. - - Args: - width: Image width in pixels - height: Image height in pixels - - Returns: - numpy array of shape (height, width, 3) with uint8 values - """ - global _generator - - # Initialize or reinitialize if dimensions changed - if _generator is None or _generator.width != width or _generator.height != height: - _generator = FastImageGenerator(width, height) - - return _generator.generate_frame() diff --git a/dimos/utils/generic.py b/dimos/utils/generic.py deleted file mode 100644 index 84168ce057..0000000000 --- a/dimos/utils/generic.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -import hashlib -import json -import os -import string -from typing import Any, Generic, TypeVar, overload -import uuid - -_T = TypeVar("_T") - - -def truncate_display_string(arg: Any, max: int | None = None) -> str: - """ - If we print strings that are too long that potentially obscures more important logs. - - Use this function to truncate it to a reasonable length (configurable from the env). - """ - string = str(arg) - - if max is not None: - max_chars = max - else: - max_chars = int(os.getenv("TRUNCATE_MAX", "2000")) - - if max_chars == 0 or len(string) <= max_chars: - return string - - return string[:max_chars] + "...(truncated)..." - - -def extract_json_from_llm_response(response: str) -> Any: - start_idx = response.find("{") - end_idx = response.rfind("}") + 1 - - if start_idx >= 0 and end_idx > start_idx: - json_str = response[start_idx:end_idx] - try: - return json.loads(json_str) - except Exception: - pass - - return None - - -def short_id(from_string: str | None = None) -> str: - alphabet = string.digits + string.ascii_letters - base = len(alphabet) - - if from_string is None: - num = uuid.uuid4().int - else: - hash_bytes = hashlib.sha1(from_string.encode()).digest()[:16] - num = int.from_bytes(hash_bytes, "big") - - min_chars = 18 - - chars: list[str] = [] - while num > 0 or len(chars) < min_chars: - num, rem = divmod(num, base) - chars.append(alphabet[rem]) - - return "".join(reversed(chars))[:min_chars] - - -class classproperty(Generic[_T]): - def __init__(self, fget: Callable[..., _T]) -> None: - self.fget = fget - - @overload - def __get__(self, obj: None, cls: type) -> _T: ... - @overload - def __get__(self, obj: object, cls: type) -> _T: ... - def __get__(self, obj: object | None, cls: type) -> _T: - return self.fget(cls) diff --git a/dimos/utils/gpu_utils.py b/dimos/utils/gpu_utils.py deleted file mode 100644 index c1ec67b417..0000000000 --- a/dimos/utils/gpu_utils.py +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -def is_cuda_available(): # type: ignore[no-untyped-def] - try: - import pycuda.driver as cuda - - cuda.init() - return cuda.Device.count() > 0 - except Exception: - return False diff --git a/dimos/utils/llm_utils.py b/dimos/utils/llm_utils.py deleted file mode 100644 index 47d848807c..0000000000 --- a/dimos/utils/llm_utils.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import json -import re - - -def extract_json(response: str) -> dict | list: # type: ignore[type-arg] - """Extract JSON from potentially messy LLM response. - - Tries multiple strategies: - 1. Parse the entire response as JSON - 2. Find and parse JSON arrays in the response - 3. Find and parse JSON objects in the response - - Args: - response: Raw text response that may contain JSON - - Returns: - Parsed JSON object (dict or list) - - Raises: - json.JSONDecodeError: If no valid JSON can be extracted - """ - # First try to parse the whole response as JSON - try: - return json.loads(response) # type: ignore[no-any-return] - except json.JSONDecodeError: - pass - - # If that fails, try to extract JSON from the messy response - # Look for JSON arrays or objects in the text - - # Pattern to match JSON arrays (including nested arrays/objects) - # This finds the outermost [...] structure - array_pattern = r"\[(?:[^\[\]]*|\[(?:[^\[\]]*|\[[^\[\]]*\])*\])*\]" - - # Pattern to match JSON objects - object_pattern = r"\{(?:[^{}]*|\{(?:[^{}]*|\{[^{}]*\})*\})*\}" - - # Try to find JSON arrays first (most common for detections) - matches = re.findall(array_pattern, response, re.DOTALL) - for match in matches: - try: - parsed = json.loads(match) - # For detection arrays, we expect a list - if isinstance(parsed, list): - return parsed - except json.JSONDecodeError: - continue - - # Try JSON objects if no arrays found - matches = re.findall(object_pattern, response, re.DOTALL) - for match in matches: - try: - return json.loads(match) # type: ignore[no-any-return] - except json.JSONDecodeError: - continue - - # If nothing worked, raise an error with the original response - raise json.JSONDecodeError( - f"Could not extract valid JSON from response: {response[:200]}...", response, 0 - ) diff --git a/dimos/utils/logging_config.py b/dimos/utils/logging_config.py deleted file mode 100644 index a9bfc5031d..0000000000 --- a/dimos/utils/logging_config.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Mapping -from datetime import datetime -import inspect -import logging -import logging.handlers -import os -from pathlib import Path -import sys -import tempfile -import traceback -from types import TracebackType -from typing import Any - -import structlog -from structlog.processors import CallsiteParameter, CallsiteParameterAdder - -from dimos.constants import DIMOS_LOG_DIR, DIMOS_PROJECT_ROOT - -# Suppress noisy loggers -logging.getLogger("aiortc.codecs.h264").setLevel(logging.ERROR) -logging.getLogger("lcm_foxglove_bridge").setLevel(logging.ERROR) -logging.getLogger("websockets.server").setLevel(logging.ERROR) -logging.getLogger("FoxgloveServer").setLevel(logging.ERROR) -logging.getLogger("asyncio").setLevel(logging.ERROR) - -_LOG_FILE_PATH = None - - -def _get_log_directory() -> Path: - # Check if running from a git repository - if (DIMOS_PROJECT_ROOT / ".git").exists(): - log_dir = DIMOS_LOG_DIR - else: - # Running from an installed package - use XDG_STATE_HOME - xdg_state_home = os.getenv("XDG_STATE_HOME") - if xdg_state_home: - log_dir = Path(xdg_state_home) / "dimos" / "logs" - else: - log_dir = Path.home() / ".local" / "state" / "dimos" / "logs" - - try: - log_dir.mkdir(parents=True, exist_ok=True) - except (PermissionError, OSError): - log_dir = Path(tempfile.gettempdir()) / "dimos" / "logs" - log_dir.mkdir(parents=True, exist_ok=True) - - return log_dir - - -def _get_log_file_path() -> Path: - log_dir = _get_log_directory() - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - pid = os.getpid() - return log_dir / f"dimos_{timestamp}_{pid}.jsonl" - - -def _configure_structlog() -> Path: - global _LOG_FILE_PATH - - if _LOG_FILE_PATH: - return _LOG_FILE_PATH - - _LOG_FILE_PATH = _get_log_file_path() - - shared_processors: list[Any] = [ - structlog.stdlib.add_log_level, - structlog.stdlib.add_logger_name, - structlog.stdlib.PositionalArgumentsFormatter(), - structlog.processors.TimeStamper(fmt="iso"), - structlog.processors.StackInfoRenderer(), - structlog.processors.UnicodeDecoder(), - CallsiteParameterAdder( - parameters=[ - CallsiteParameter.FUNC_NAME, - CallsiteParameter.LINENO, - ] - ), - structlog.processors.format_exc_info, # Add this to format exception info - ] - - structlog.configure( - processors=[ - structlog.stdlib.filter_by_level, - *shared_processors, - structlog.stdlib.ProcessorFormatter.wrap_for_formatter, - ], - context_class=dict, - logger_factory=structlog.stdlib.LoggerFactory(), - cache_logger_on_first_use=True, - ) - - return _LOG_FILE_PATH - - -_CONSOLE_PATH_WIDTH = 30 -_CONSOLE_USE_COLORS = hasattr(sys.stdout, "isatty") and sys.stdout.isatty() - -_CONSOLE_LEVEL_COLORS = { - "dbg": "\033[1;36m", # bold cyan - "inf": "\033[1;32m", # bold green - "war": "\033[1;33m", # bold yellow - "err": "\033[1;31m", # bold red - "cri": "\033[1;31m", # bold red -} -_CONSOLE_RESET = "\033[0m" -_CONSOLE_FIXED = "\033[2m" # dim -_CONSOLE_TEXT = "\033[0;34m" # blue -_CONSOLE_KEY = "\033[0;36m" # cyan -_CONSOLE_VAL = "\033[0;35m" # magenta -_CONSOLE_EQ = "\033[0;37m" # white - - -def _compact_console_processor(logger: Any, method_name: str, event_dict: Mapping[str, Any]) -> str: - """Format log lines as: HH:MM:SS.mmm[lvl][file.py ] Event key=value ...""" - event_dict = dict(event_dict) - - # Time — HH:MM:SS.mmm - timestamp = event_dict.pop("timestamp", "") - if timestamp: - try: - dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00")) - time_str = dt.strftime("%H:%M:%S") + f".{dt.microsecond // 1000:03d}" - except (ValueError, AttributeError): - time_str = str(timestamp)[:12] - else: - now = datetime.now() - time_str = now.strftime("%H:%M:%S") + f".{now.microsecond // 1000:03d}" - - # Level — 3-letter lowercase abbreviation - level = event_dict.pop("level", "???") - level_short = level[:3].lower() - - # File path — fixed width, truncated from the left, padded on the right - file_path = event_dict.pop("logger", "") - if len(file_path) > _CONSOLE_PATH_WIDTH: - file_path = file_path[-_CONSOLE_PATH_WIDTH:] - file_path = f"{file_path:<{_CONSOLE_PATH_WIDTH}s}" - - # Event message - event = event_dict.pop("event", "") - - # Remove internal / callsite / exception fields - for key in ( - "func_name", - "lineno", - "exception", - "exc_info", - "exception_type", - "exception_message", - "traceback_lines", - "_record", - "_from_structlog", - ): - event_dict.pop(key, None) - - # Assemble the line - if _CONSOLE_USE_COLORS: - R = _CONSOLE_RESET - color = _CONSOLE_LEVEL_COLORS.get(level_short, "") - line = ( - f"{_CONSOLE_FIXED}{time_str}{R}" - f"{color}[{level_short}]{R}" - f"{_CONSOLE_FIXED}[{file_path}]{R} " - f"{_CONSOLE_TEXT}{event}{R}" - ) - if event_dict: - kv_parts = " ".join( - f"{_CONSOLE_KEY}{k}{_CONSOLE_EQ}={_CONSOLE_VAL}{v}{R}" - for k, v in sorted(event_dict.items()) - ) - line += " " + kv_parts - else: - kv_str = " ".join(f"{k}={v}" for k, v in sorted(event_dict.items())) - line = f"{time_str} [{level_short}][{file_path}] {event}" - if kv_str: - line += " " + kv_str - - return line - - -def setup_logger(*, level: int | None = None) -> Any: - """Set up a structured logger using structlog. - - Args: - level: The logging level. - - Returns: - A configured structlog logger instance. - """ - - caller_frame = inspect.stack()[1] - name = caller_frame.filename - - # Convert absolute path to relative path - try: - name = str(Path(name).relative_to(DIMOS_PROJECT_ROOT)) - except (ValueError, TypeError): - pass - - log_file_path = _configure_structlog() - - if level is None: - level_name = os.getenv("DIMOS_LOG_LEVEL", "INFO") - level = getattr(logging, level_name) - - stdlib_logger = logging.getLogger(name) - - # Remove any existing handlers. - if stdlib_logger.hasHandlers(): - stdlib_logger.handlers.clear() - - stdlib_logger.setLevel(level) - stdlib_logger.propagate = False - - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setLevel(level) - console_formatter = structlog.stdlib.ProcessorFormatter( - processor=_compact_console_processor, - ) - console_handler.setFormatter(console_formatter) - stdlib_logger.addHandler(console_handler) - - # Create rotating file handler with JSON formatting. - file_handler = logging.handlers.RotatingFileHandler( - log_file_path, - mode="a", - maxBytes=10 * 1024 * 1024, # 10MiB - backupCount=20, - encoding="utf-8", - ) - file_handler.setLevel(level) - file_formatter = structlog.stdlib.ProcessorFormatter( - processor=structlog.processors.JSONRenderer(), - ) - file_handler.setFormatter(file_formatter) - stdlib_logger.addHandler(file_handler) - - return structlog.get_logger(name) - - -def setup_exception_handler() -> None: - def handle_exception( - exc_type: type[BaseException], - exc_value: BaseException, - exc_traceback: TracebackType | None, - ) -> None: - # Don't log KeyboardInterrupt - if issubclass(exc_type, KeyboardInterrupt): - sys.__excepthook__(exc_type, exc_value, exc_traceback) - return - - # Get a logger for uncaught exceptions - logger = setup_logger() - - # Log the exception with full traceback to JSON - logger.error( - "Uncaught exception occurred", - exc_info=(exc_type, exc_value, exc_traceback), - exception_type=exc_type.__name__, - exception_message=str(exc_value), - traceback_lines=traceback.format_exception(exc_type, exc_value, exc_traceback), - ) - - # Still display the exception nicely on console using Rich if available - try: - from rich.console import Console - from rich.traceback import Traceback - - console = Console() - tb = Traceback.from_exception(exc_type, exc_value, exc_traceback) - console.print(tb) - except ImportError: - # Fall back to standard exception display if Rich is not available - sys.__excepthook__(exc_type, exc_value, exc_traceback) - - # Set our custom exception handler - sys.excepthook = handle_exception diff --git a/dimos/utils/metrics.py b/dimos/utils/metrics.py deleted file mode 100644 index bf7bf45cdc..0000000000 --- a/dimos/utils/metrics.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -import functools -import time -from typing import Any, TypeVar, cast - -from dimos_lcm.std_msgs import Float32 -import rerun as rr - -from dimos.core import LCMTransport, Transport - -F = TypeVar("F", bound=Callable[..., Any]) - - -def timed( - transport: Callable[[F], Transport[Float32]] | Transport[Float32] | None = None, -) -> Callable[[F], F]: - def timed_decorator(func: F) -> F: - t: Transport[Float32] - if transport is None: - t = LCMTransport(f"/metrics/{func.__name__}", Float32) - elif callable(transport): - t = transport(func) - else: - t = transport - - @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - start = time.perf_counter() - result = func(*args, **kwargs) - elapsed = time.perf_counter() - start - - msg = Float32() - msg.data = elapsed * 1000 # ms - t.publish(msg) - return result - - return cast("F", wrapper) - - return timed_decorator - - -def log_timing_to_rerun(entity_path: str) -> Callable[[F], F]: - """Decorator to log function execution time to Rerun. - - Automatically measures the execution time of the decorated function - and logs it as a scalar value to the specified Rerun entity path. - - Args: - entity_path: Rerun entity path for timing metrics - (e.g., "metrics/costmap/calc_ms") - - Returns: - Decorator function - - Example: - @log_timing_to_rerun("metrics/costmap/calc_ms") - def _calculate_costmap(self, msg): - # ... expensive computation - return result - - # Timing automatically logged to Rerun as a time series! - """ - - def decorator(func: F) -> F: - @functools.wraps(func) - def wrapper(*args: Any, **kwargs: Any) -> Any: - start = time.perf_counter() - result = func(*args, **kwargs) - elapsed_ms = (time.perf_counter() - start) * 1000 - - rr.log(entity_path, rr.Scalars(elapsed_ms)) - return result - - return cast("F", wrapper) - - return decorator diff --git a/dimos/utils/monitoring.py b/dimos/utils/monitoring.py deleted file mode 100644 index ca3e03c55e..0000000000 --- a/dimos/utils/monitoring.py +++ /dev/null @@ -1,307 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Note, to enable ps-spy to run without sudo you need: - - echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope -""" - -from functools import cache -import os -import re -import shutil -import subprocess -import threading - -from distributed import get_client -from distributed.client import Client - -from dimos.core import Module, rpc -from dimos.utils.actor_registry import ActorRegistry -from dimos.utils.logging_config import setup_logger - -logger = setup_logger() - - -def print_data_table(data) -> None: # type: ignore[no-untyped-def] - headers = [ - "cpu_percent", - "active_percent", - "gil_percent", - "n_threads", - "pid", - "worker_id", - "modules", - ] - numeric_headers = {"cpu_percent", "active_percent", "gil_percent", "n_threads", "pid"} - - # Add registered modules. - modules = ActorRegistry.get_all() - for worker in data: - worker["modules"] = ", ".join( - module_name.split("-", 1)[0] - for module_name, worker_id_str in modules.items() - if worker_id_str == str(worker["worker_id"]) - ) - - # Determine column widths - col_widths = [] - for h in headers: - max_len = max(len(str(d[h])) for d in data) - col_widths.append(max(len(h), max_len)) - - # Print header with DOS box characters - header_row = " │ ".join(h.ljust(col_widths[i]) for i, h in enumerate(headers)) - border_parts = ["─" * w for w in col_widths] - border_line = "─┼─".join(border_parts) - print(border_line) - print(header_row) - print(border_line) - - # Print rows - for row in data: - formatted_cells = [] - for i, h in enumerate(headers): - value = str(row[h]) - if h in numeric_headers: - formatted_cells.append(value.rjust(col_widths[i])) - else: - formatted_cells.append(value.ljust(col_widths[i])) - print(" │ ".join(formatted_cells)) - - -class UtilizationThread(threading.Thread): - _module: "UtilizationModule" - _stop_event: threading.Event - _monitors: dict # type: ignore[type-arg] - - def __init__(self, module) -> None: # type: ignore[no-untyped-def] - super().__init__(daemon=True) - self._module = module - self._stop_event = threading.Event() - self._monitors = {} - - def run(self) -> None: - while not self._stop_event.is_set(): - workers = self._module.client.scheduler_info()["workers"] # type: ignore[union-attr] - pids = {pid: None for pid in get_worker_pids()} # type: ignore[no-untyped-call] - for worker, info in workers.items(): - pid = get_pid_by_port(worker.rsplit(":", 1)[-1]) - if pid is None: - continue - pids[pid] = info["id"] - data = [] - for pid, worker_id in pids.items(): - if pid not in self._monitors: - self._monitors[pid] = GilMonitorThread(pid) - self._monitors[pid].start() - cpu, gil, active, n_threads = self._monitors[pid].get_values() - data.append( - { - "cpu_percent": cpu, - "worker_id": worker_id, - "pid": pid, - "gil_percent": gil, - "active_percent": active, - "n_threads": n_threads, - } - ) - data.sort(key=lambda x: x["pid"]) - self._fix_missing_ids(data) - print_data_table(data) - self._stop_event.wait(1) - - def stop(self) -> None: - self._stop_event.set() - for monitor in self._monitors.values(): - monitor.stop() - monitor.join(timeout=2) - - def _fix_missing_ids(self, data) -> None: # type: ignore[no-untyped-def] - """ - Some worker IDs are None. But if we order the workers by PID and all - non-None ids are in order, then we can deduce that the None ones are the - missing indices. - """ - if all(x["worker_id"] in (i, None) for i, x in enumerate(data)): - for i, worker in enumerate(data): - worker["worker_id"] = i - - -class UtilizationModule(Module): - client: Client | None - _utilization_thread: UtilizationThread | None - - def __init__(self) -> None: - super().__init__() - self.client = None - self._utilization_thread = None - - if not os.getenv("MEASURE_GIL_UTILIZATION"): - logger.info("Set `MEASURE_GIL_UTILIZATION=true` to print GIL utilization.") - return - - if not _can_use_py_spy(): # type: ignore[no-untyped-call] - logger.warning( - "Cannot start UtilizationModule because in order to run py-spy without " - "being root you need to enable this:\n" - "\n" - " echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope" - ) - return - - if not shutil.which("py-spy"): - logger.warning("Cannot start UtilizationModule because `py-spy` is not installed.") - return - - self.client = get_client() - self._utilization_thread = UtilizationThread(self) - - @rpc - def start(self) -> None: - super().start() - - if self._utilization_thread: - self._utilization_thread.start() - - @rpc - def stop(self) -> None: - if self._utilization_thread: - self._utilization_thread.stop() - self._utilization_thread.join(timeout=2) - super().stop() - - -utilization = UtilizationModule.blueprint - - -__all__ = ["UtilizationModule", "utilization"] - - -def _can_use_py_spy(): # type: ignore[no-untyped-def] - try: - with open("/proc/sys/kernel/yama/ptrace_scope") as f: - value = f.read().strip() - return value == "0" - except Exception: - pass - return False - - -@cache -def get_pid_by_port(port: int) -> int | None: - try: - result = subprocess.run( - ["lsof", "-ti", f":{port}"], capture_output=True, text=True, check=True - ) - pid_str = result.stdout.strip() - return int(pid_str) if pid_str else None - except subprocess.CalledProcessError: - return None - - -def get_worker_pids(): # type: ignore[no-untyped-def] - pids = [] - for pid in os.listdir("/proc"): - if not pid.isdigit(): - continue - try: - with open(f"/proc/{pid}/cmdline") as f: - cmdline = f.read().replace("\x00", " ") - if "spawn_main" in cmdline: - pids.append(int(pid)) - except (FileNotFoundError, PermissionError): - continue - return pids - - -class GilMonitorThread(threading.Thread): - pid: int - _latest_values: tuple[float, float, float, int] - _stop_event: threading.Event - _lock: threading.Lock - - def __init__(self, pid: int) -> None: - super().__init__(daemon=True) - self.pid = pid - self._latest_values = (-1.0, -1.0, -1.0, -1) - self._stop_event = threading.Event() - self._lock = threading.Lock() - - def run(self): # type: ignore[no-untyped-def] - command = ["py-spy", "top", "--pid", str(self.pid), "--rate", "100"] - process = None - try: - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, # Line-buffered output - ) - - for line in iter(process.stdout.readline, ""): # type: ignore[union-attr] - if self._stop_event.is_set(): - break - - if "GIL:" not in line: - continue - - match = re.search( - r"GIL:\s*([\d.]+?)%,\s*Active:\s*([\d.]+?)%,\s*Threads:\s*(\d+)", line - ) - if not match: - continue - - try: - cpu_percent = _get_cpu_percent(self.pid) - gil_percent = float(match.group(1)) - active_percent = float(match.group(2)) - num_threads = int(match.group(3)) - - with self._lock: - self._latest_values = ( - cpu_percent, - gil_percent, - active_percent, - num_threads, - ) - except (ValueError, IndexError): - pass - except Exception as e: - logger.error(f"An error occurred in GilMonitorThread for PID {self.pid}: {e}") - raise - finally: - if process: - process.terminate() - process.wait(timeout=1) - self._stop_event.set() - - def get_values(self): # type: ignore[no-untyped-def] - with self._lock: - return self._latest_values - - def stop(self) -> None: - self._stop_event.set() - - -def _get_cpu_percent(pid: int) -> float: - try: - result = subprocess.run( - ["ps", "-p", str(pid), "-o", "%cpu="], capture_output=True, text=True, check=True - ) - return float(result.stdout.strip()) - except Exception: - return -1.0 diff --git a/dimos/utils/path_utils.py b/dimos/utils/path_utils.py deleted file mode 100644 index 794d36e34d..0000000000 --- a/dimos/utils/path_utils.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from pathlib import Path - - -def get_project_root() -> Path: - """ - Returns the absolute path to the project root directory. - """ - return Path(__file__).resolve().parent.parent.parent diff --git a/dimos/utils/reactive.py b/dimos/utils/reactive.py deleted file mode 100644 index 4397e0171e..0000000000 --- a/dimos/utils/reactive.py +++ /dev/null @@ -1,324 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable, Generator -from queue import Queue -import threading -from typing import Any, Generic, TypeVar - -import reactivex as rx -from reactivex import operators as ops -from reactivex.abc import DisposableBase -from reactivex.disposable import Disposable -from reactivex.observable import Observable -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.rxpy_backpressure import BackPressure -from dimos.utils.threadpool import get_scheduler - -T = TypeVar("T") - - -# Observable ─► ReplaySubject─► observe_on(pool) ─► backpressure.latest ─► sub1 (fast) -# ├──► observe_on(pool) ─► backpressure.latest ─► sub2 (slow) -# └──► observe_on(pool) ─► backpressure.latest ─► sub3 (slower) -def backpressure( - observable: Observable[T], - scheduler: ThreadPoolScheduler | None = None, - drop_unprocessed: bool = True, -) -> Observable[T]: - if scheduler is None: - scheduler = get_scheduler() - - # hot, latest-cached core (similar to replay subject) - core = observable.pipe( - ops.replay(buffer_size=1), - ops.ref_count(), # Shared but still synchronous! - ) - - # per-subscriber factory - def per_sub(): # type: ignore[no-untyped-def] - # Move processing to thread pool - base = core.pipe(ops.observe_on(scheduler)) - - # optional back-pressure handling - if not drop_unprocessed: - return base - - def _subscribe(observer, sch=None): # type: ignore[no-untyped-def] - return base.subscribe(BackPressure.LATEST(observer), scheduler=sch) - - return rx.create(_subscribe) - - # each `.subscribe()` call gets its own async backpressure chain - return rx.defer(lambda *_: per_sub()) # type: ignore[no-untyped-call] - - -class LatestReader(DisposableBase, Generic[T]): - """A callable object that returns the latest value from an observable.""" - - def __init__(self, initial_value: T, subscription, connection=None) -> None: # type: ignore[no-untyped-def] - self._value = initial_value - self._subscription = subscription - self._connection = connection - - def __call__(self) -> T: - """Return the latest value from the observable.""" - return self._value - - def dispose(self) -> None: - """Dispose of the subscription to the observable.""" - self._subscription.dispose() - if self._connection: - self._connection.dispose() - - -def getter_ondemand(observable: Observable[T], timeout: float | None = 30.0) -> T: - def getter(): # type: ignore[no-untyped-def] - result = [] - error = [] - event = threading.Event() - - def on_next(value) -> None: # type: ignore[no-untyped-def] - result.append(value) - event.set() - - def on_error(e) -> None: # type: ignore[no-untyped-def] - error.append(e) - event.set() - - def on_completed() -> None: - event.set() - - # Subscribe and wait for first value - subscription = observable.pipe(ops.first()).subscribe( - on_next=on_next, on_error=on_error, on_completed=on_completed - ) - - try: - if timeout is not None: - if not event.wait(timeout): - raise TimeoutError(f"No value received after {timeout} seconds") - else: - event.wait() - - if error: - raise error[0] - - if not result: - raise Exception("Observable completed without emitting a value") - - return result[0] - finally: - subscription.dispose() - - return getter # type: ignore[return-value] - - -def getter_cold(source: Observable[T], timeout: float | None = 30.0) -> T: - return getter_ondemand(source, timeout) - - -T = TypeVar("T") # type: ignore[misc] - - -def getter_streaming( - source: Observable[T], - timeout: float | None = 30.0, - *, - nonblocking: bool = False, -) -> LatestReader[T]: - shared = source.pipe( - ops.replay(buffer_size=1), - ops.ref_count(), # auto-connect & auto-disconnect - ) - - _val_lock = threading.Lock() - _val: T | None = None - _ready = threading.Event() - - def _update(v: T) -> None: - nonlocal _val - with _val_lock: - _val = v - _ready.set() - - sub = shared.subscribe(_update) - - # If we’re in blocking mode, wait right now - if not nonblocking: - if timeout is not None and not _ready.wait(timeout): - sub.dispose() - raise TimeoutError(f"No value received after {timeout} s") - else: - _ready.wait() # wait indefinitely if timeout is None - - def reader() -> T: - if not _ready.is_set(): # first call in non-blocking mode - if timeout is not None and not _ready.wait(timeout): - raise TimeoutError(f"No value received after {timeout} s") - else: - _ready.wait() - with _val_lock: - return _val # type: ignore[return-value] - - def _dispose() -> None: - sub.dispose() - - reader.dispose = _dispose # type: ignore[attr-defined] - return reader # type: ignore[return-value] - - -def getter_hot( - source: Observable[T], timeout: float | None = 30.0, *, nonblocking: bool = False -) -> LatestReader[T]: - return getter_streaming(source, timeout, nonblocking=nonblocking) - - -T = TypeVar("T") # type: ignore[misc] -CB = Callable[[T], Any] - - -def callback_to_observable( - start: Callable[[CB[T]], Any], - stop: Callable[[CB[T]], Any], -) -> Observable[T]: - """Convert a register/unregister callback API to an Observable. - - Use this for APIs where you register a callback with one function and - unregister with another, passing the same callback reference: - - sensor.register(callback) # start - sensor.unregister(callback) # stop - needs the same callback - - Example: - obs = callback_to_observable( - start=sensor.register, - stop=sensor.unregister, - ) - sub = obs.subscribe(lambda x: print(x)) - # ... - sub.dispose() # calls sensor.unregister(callback) - - For APIs where subscribe() returns an unsubscribe callable, use - unsub_to_observable() instead. - """ - - def _subscribe(observer, _scheduler=None): # type: ignore[no-untyped-def] - def _on_msg(value: T) -> None: - observer.on_next(value) - - start(_on_msg) - return Disposable(lambda: stop(_on_msg)) - - return rx.create(_subscribe) - - -def to_observable( - subscribe: Callable[[Callable[[T], Any]], Callable[[], None]], -) -> Observable[T]: - """Convert a subscribe-returns-unsub API to an Observable. - - Use this for APIs where subscribe() returns an unsubscribe callable: - - unsub = pubsub.subscribe(callback) # returns unsubscribe function - unsub() # to unsubscribe - - Example: - obs = to_observable(pubsub.subscribe) - sub = obs.subscribe(lambda x: print(x)) - # ... - sub.dispose() # calls the unsub function returned by pubsub.subscribe - - For APIs with separate register/unregister functions, use - callback_to_observable() instead. - """ - - def _subscribe(observer, _scheduler=None): # type: ignore[no-untyped-def] - unsub = subscribe(observer.on_next) - return Disposable(unsub) - - return rx.create(_subscribe) - - -def spy(name: str): # type: ignore[no-untyped-def] - def spyfun(x): # type: ignore[no-untyped-def] - print(f"SPY {name}:", x) - return x - - return ops.map(spyfun) - - -def quality_barrier( - quality_func: Callable[[T], float], target_frequency: float -) -> Callable[[Observable[T]], Observable[T]]: - """ - RxPY pipe operator that selects the highest quality item within each time window. - - Args: - quality_func: Function to compute quality score for each item - target_frequency: Output frequency in Hz (e.g., 1.0 for 1 item per second) - - Returns: - A pipe operator that can be used with .pipe() - """ - window_duration = 1.0 / target_frequency # Duration of each window in seconds - - def _quality_barrier(source: Observable[T]) -> Observable[T]: - return source.pipe( - # Create non-overlapping time-based windows - ops.window_with_time(window_duration, window_duration), - # For each window, find the highest quality item - ops.flat_map( - lambda window: window.pipe( # type: ignore[attr-defined] - ops.to_list(), - ops.map(lambda items: max(items, key=quality_func) if items else None), # type: ignore[call-overload] - ops.filter(lambda x: x is not None), # type: ignore[arg-type] - ) - ), - ) - - return _quality_barrier - - -def iter_observable(observable: Observable[T]) -> Generator[T, None, None]: - """Convert an Observable to a blocking iterator. - - Yields items as they arrive from the observable. Properly disposes - the subscription when the generator is closed. - """ - q: Queue[T | None] = Queue() - done = threading.Event() - - def on_next(value: T) -> None: - q.put(value) - - def on_complete() -> None: - done.set() - q.put(None) - - def on_error(e: Exception) -> None: - done.set() - q.put(None) - - sub = observable.subscribe(on_next=on_next, on_completed=on_complete, on_error=on_error) - - try: - while not done.is_set() or not q.empty(): - item = q.get() - if item is None and done.is_set(): - break - yield item # type: ignore[misc] - finally: - sub.dispose() diff --git a/dimos/utils/sequential_ids.py b/dimos/utils/sequential_ids.py deleted file mode 100644 index d467e8a22d..0000000000 --- a/dimos/utils/sequential_ids.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from threading import RLock - - -class SequentialIds: - def __init__(self) -> None: - self._value = 0 - self._lock: RLock = RLock() - - def next(self) -> int: - with self._lock: - v = self._value - self._value += 1 - return v diff --git a/dimos/utils/simple_controller.py b/dimos/utils/simple_controller.py deleted file mode 100644 index f95350552c..0000000000 --- a/dimos/utils/simple_controller.py +++ /dev/null @@ -1,172 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - - -def normalize_angle(angle: float): # type: ignore[no-untyped-def] - """Normalize angle to the range [-pi, pi].""" - return math.atan2(math.sin(angle), math.cos(angle)) - - -# ---------------------------- -# PID Controller Class -# ---------------------------- -class PIDController: - def __init__( # type: ignore[no-untyped-def] - self, - kp, - ki: float = 0.0, - kd: float = 0.0, - output_limits=(None, None), - integral_limit=None, - deadband: float = 0.0, - output_deadband: float = 0.0, - inverse_output: bool = False, - ) -> None: - """ - Initialize the PID controller. - - Args: - kp (float): Proportional gain. - ki (float): Integral gain. - kd (float): Derivative gain. - output_limits (tuple): (min_output, max_output). Use None for no limit. - integral_limit (float): Maximum absolute value for the integral term (anti-windup). - deadband (float): Size of the deadband region. Error smaller than this will be compensated. - output_deadband (float): Deadband applied to the output to overcome physical system deadband. - inverse_output (bool): When True, the output will be multiplied by -1. - """ - self.kp = kp - self.ki = ki - self.kd = kd - self.min_output, self.max_output = output_limits - self.integral_limit = integral_limit - self.output_deadband = output_deadband - self.deadband = deadband - self.integral = 0.0 - self.prev_error = 0.0 - self.inverse_output = inverse_output - - def update(self, error, dt): # type: ignore[no-untyped-def] - """Compute the PID output with anti-windup, output deadband compensation and output saturation.""" - # Update integral term with windup protection. - self.integral += error * dt - if self.integral_limit is not None: - self.integral = max(-self.integral_limit, min(self.integral, self.integral_limit)) - - # Compute derivative term. - derivative = (error - self.prev_error) / dt if dt > 0 else 0.0 - - if abs(error) < self.deadband: - # Prevent integral windup by not increasing integral term when error is small. - self.integral = 0.0 - derivative = 0.0 - - # Compute raw output. - output = self.kp * error + self.ki * self.integral + self.kd * derivative - - # Apply deadband compensation to the output - output = self._apply_output_deadband_compensation(output) # type: ignore[no-untyped-call] - - # Apply output limits if specified. - if self.max_output is not None: - output = min(self.max_output, output) - if self.min_output is not None: - output = max(self.min_output, output) - - self.prev_error = error - if self.inverse_output: - return -output - return output - - def _apply_output_deadband_compensation(self, output): # type: ignore[no-untyped-def] - """ - Apply deadband compensation to the output. - - This simply adds the deadband value to the magnitude of the output - while preserving the sign, ensuring we overcome the physical deadband. - """ - if self.output_deadband == 0.0 or output == 0.0: - return output - - if output > self.max_output * 0.05: - # For positive output, add the deadband - return output + self.output_deadband - elif output < self.min_output * 0.05: - # For negative output, subtract the deadband - return output - self.output_deadband - else: - return output - - def _apply_deadband_compensation(self, error): # type: ignore[no-untyped-def] - """ - Apply deadband compensation to the error. - - This maintains the original error value, as the deadband compensation - will be applied to the output, not the error. - """ - return error - - -# ---------------------------- -# Visual Servoing Controller Class -# ---------------------------- -class VisualServoingController: - def __init__(self, distance_pid_params, angle_pid_params) -> None: # type: ignore[no-untyped-def] - """ - Initialize the visual servoing controller using enhanced PID controllers. - - Args: - distance_pid_params (tuple): (kp, ki, kd, output_limits, integral_limit, deadband) for distance. - angle_pid_params (tuple): (kp, ki, kd, output_limits, integral_limit, deadband) for angle. - """ - self.distance_pid = PIDController(*distance_pid_params) - self.angle_pid = PIDController(*angle_pid_params) - self.prev_measured_angle = 0.0 # Used for angular feed-forward damping - - def compute_control( # type: ignore[no-untyped-def] - self, measured_distance, measured_angle, desired_distance, desired_angle, dt - ): - """ - Compute the forward (x) and angular (z) commands. - - Args: - measured_distance (float): Current distance to target (from camera). - measured_angle (float): Current angular offset to target (radians). - desired_distance (float): Desired distance to target. - desired_angle (float): Desired angular offset (e.g., 0 for centered). - dt (float): Timestep. - - Returns: - tuple: (forward_command, angular_command) - """ - # Compute the errors. - error_distance = measured_distance - desired_distance - error_angle = normalize_angle(measured_angle - desired_angle) - - # Get raw PID outputs. - forward_command_raw = self.distance_pid.update(error_distance, dt) # type: ignore[no-untyped-call] - angular_command_raw = self.angle_pid.update(error_angle, dt) # type: ignore[no-untyped-call] - - # print("forward: {} angular: {}".format(forward_command_raw, angular_command_raw)) - - angular_command = angular_command_raw - - # Couple forward command to angular error: - # scale the forward command smoothly. - scaling_factor = max(0.0, min(1.0, math.exp(-2.0 * abs(error_angle)))) - forward_command = forward_command_raw * scaling_factor - - return forward_command, angular_command diff --git a/dimos/utils/test_data.py b/dimos/utils/test_data.py deleted file mode 100644 index e5be4307c7..0000000000 --- a/dimos/utils/test_data.py +++ /dev/null @@ -1,349 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import hashlib -import os -from pathlib import Path -import subprocess - -import pytest - -from dimos.utils import data -from dimos.utils.data import LfsPath - - -@pytest.mark.heavy -def test_pull_file() -> None: - repo_root = data._get_repo_root() - test_file_name = "cafe.jpg" - test_file_compressed = data._get_lfs_dir() / (test_file_name + ".tar.gz") - test_file_decompressed = data.get_data_dir() / test_file_name - - # delete decompressed test file if it exists - if test_file_decompressed.exists(): - test_file_decompressed.unlink() - - # delete lfs archive file if it exists - if test_file_compressed.exists(): - test_file_compressed.unlink() - - assert not test_file_compressed.exists() - assert not test_file_decompressed.exists() - - # pull the lfs file reference from git - env = os.environ.copy() - env["GIT_LFS_SKIP_SMUDGE"] = "1" - subprocess.run( - ["git", "checkout", "HEAD", "--", test_file_compressed], - cwd=repo_root, - env=env, - check=True, - capture_output=True, - ) - - # ensure we have a pointer file from git (small ASCII text file) - assert test_file_compressed.exists() - assert test_file_compressed.stat().st_size < 200 - - # trigger a data file pull - assert data.get_data(test_file_name) == test_file_decompressed - - # validate data is received - assert test_file_compressed.exists() - assert test_file_decompressed.exists() - - # validate hashes - with test_file_compressed.open("rb") as f: - assert test_file_compressed.stat().st_size > 200 - compressed_sha256 = hashlib.sha256(f.read()).hexdigest() - assert ( - compressed_sha256 == "b8cf30439b41033ccb04b09b9fc8388d18fb544d55b85c155dbf85700b9e7603" - ) - - with test_file_decompressed.open("rb") as f: - decompressed_sha256 = hashlib.sha256(f.read()).hexdigest() - assert ( - decompressed_sha256 - == "55d451dde49b05e3ad386fdd4ae9e9378884b8905bff1ca8aaea7d039ff42ddd" - ) - - -@pytest.mark.heavy -def test_pull_dir() -> None: - repo_root = data._get_repo_root() - test_dir_name = "ab_lidar_frames" - test_dir_compressed = data._get_lfs_dir() / (test_dir_name + ".tar.gz") - test_dir_decompressed = data.get_data_dir() / test_dir_name - - # delete decompressed test directory if it exists - if test_dir_decompressed.exists(): - for item in test_dir_decompressed.iterdir(): - item.unlink() - test_dir_decompressed.rmdir() - - # delete lfs archive file if it exists - if test_dir_compressed.exists(): - test_dir_compressed.unlink() - - # pull the lfs file reference from git - env = os.environ.copy() - env["GIT_LFS_SKIP_SMUDGE"] = "1" - subprocess.run( - ["git", "checkout", "HEAD", "--", test_dir_compressed], - cwd=repo_root, - env=env, - check=True, - capture_output=True, - ) - - # ensure we have a pointer file from git (small ASCII text file) - assert test_dir_compressed.exists() - assert test_dir_compressed.stat().st_size < 200 - - # trigger a data file pull - assert data.get_data(test_dir_name) == test_dir_decompressed - assert test_dir_compressed.stat().st_size > 200 - - # validate data is received - assert test_dir_compressed.exists() - assert test_dir_decompressed.exists() - - for [file, expected_hash] in zip( - sorted(test_dir_decompressed.iterdir()), - [ - "6c3aaa9a79853ea4a7453c7db22820980ceb55035777f7460d05a0fa77b3b1b3", - "456cc2c23f4ffa713b4e0c0d97143c27e48bbe6ef44341197b31ce84b3650e74", - ], - strict=False, - ): - with file.open("rb") as f: - sha256 = hashlib.sha256(f.read()).hexdigest() - assert sha256 == expected_hash - - -# ============================================================================ -# LfsPath Tests -# ============================================================================ - - -def test_lfs_path_lazy_creation() -> None: - """Test that creating LfsPath doesn't trigger download.""" - lfs_path = LfsPath("test_data_file") - - # Check that the object is created - assert isinstance(lfs_path, LfsPath) - - # Check that cache is None (not downloaded yet) - cache = object.__getattribute__(lfs_path, "_lfs_resolved_cache") - assert cache is None - - # Check that filename is stored - filename = object.__getattribute__(lfs_path, "_lfs_filename") - assert filename == "test_data_file" - - -def test_lfs_path_safe_attributes() -> None: - """Test that safe attributes don't trigger download.""" - lfs_path = LfsPath("test_data_file") - - # Access safe attributes directly - filename = object.__getattribute__(lfs_path, "_lfs_filename") - cache = object.__getattribute__(lfs_path, "_lfs_resolved_cache") - ensure_fn = object.__getattribute__(lfs_path, "_ensure_downloaded") - - # Verify they exist and cache is still None - assert filename == "test_data_file" - assert cache is None - assert callable(ensure_fn) - - -def test_lfs_path_no_download_on_creation() -> None: - """Test that LfsPath construction doesn't trigger download. - - Path(lfs_path) extracts internal _raw_paths (\".\") and does NOT - call __fspath__, so it won't trigger download. The correct way to - convert is Path(str(lfs_path)), which triggers __str__ -> download. - """ - lfs_path = LfsPath("nonexistent_file") - - # Construction should not trigger download - cache = object.__getattribute__(lfs_path, "_lfs_resolved_cache") - assert cache is None - - # Accessing internal LfsPath attributes should not trigger download - filename = object.__getattribute__(lfs_path, "_lfs_filename") - assert filename == "nonexistent_file" - assert cache is None - - -@pytest.mark.heavy -def test_lfs_path_with_real_file() -> None: - """Test LfsPath with a real small LFS file.""" - # Use a small existing LFS file - filename = "three_paths.png" - lfs_path = LfsPath(filename) - - # Initially, cache should be None - cache = object.__getattribute__(lfs_path, "_lfs_resolved_cache") - assert cache is None - - # Access a Path method - this should trigger download - exists = lfs_path.exists() - - # Now cache should be populated - cache = object.__getattribute__(lfs_path, "_lfs_resolved_cache") - assert cache is not None - assert isinstance(cache, Path) - - # File should exist after download - assert exists is True - - # Should be able to get file stats - stat_result = lfs_path.stat() - assert stat_result.st_size > 0 - - # Should be able to read the file - content = lfs_path.read_bytes() - assert len(content) > 0 - - # Verify it's a PNG file - assert content.startswith(b"\x89PNG") - - -@pytest.mark.heavy -def test_lfs_path_unload_and_reload() -> None: - """Test unloading and reloading an LFS file.""" - filename = "three_paths.png" - data_dir = data.get_data_dir() - file_path = data_dir / filename - - # Clean up if file already exists - if file_path.exists(): - file_path.unlink() - - # Create LfsPath - lfs_path = LfsPath(filename) - - # Verify file doesn't exist yet - assert not file_path.exists() - - # Access the file - this triggers download - content_first = lfs_path.read_bytes() - assert file_path.exists() - - # Get hash of first download - hash_first = hashlib.sha256(content_first).hexdigest() - - # Now unload (delete the file) - file_path.unlink() - assert not file_path.exists() - - # Create a new LfsPath instance for the same file - lfs_path_2 = LfsPath(filename) - - # Access the file again - should re-download - content_second = lfs_path_2.read_bytes() - assert file_path.exists() - - # Get hash of second download - hash_second = hashlib.sha256(content_second).hexdigest() - - # Hashes should match (same file downloaded) - assert hash_first == hash_second - - # Content should be identical - assert content_first == content_second - - -@pytest.mark.heavy -def test_lfs_path_operations() -> None: - """Test various Path operations with LfsPath.""" - filename = "three_paths.png" - lfs_path = LfsPath(filename) - - # Test is_file - assert lfs_path.is_file() is True - assert lfs_path.is_dir() is False - - # Test absolute path - abs_path = lfs_path.absolute() - assert abs_path.is_absolute() - - # Test resolve - resolved = lfs_path.resolve() - assert resolved.is_absolute() - - # Test string conversion - path_str = str(lfs_path) - assert isinstance(path_str, str) - assert filename in path_str - - # Test __fspath__ - fspath_result = os.fspath(lfs_path) - assert isinstance(fspath_result, str) - assert filename in fspath_result - - -@pytest.mark.heavy -def test_lfs_path_division_operator() -> None: - """Test path division operator with LfsPath.""" - # Use a directory for testing - lfs_path = LfsPath("three_paths.png") - - # Test truediv - this should trigger download and return resolved path - result = lfs_path / "subpath" - assert isinstance(result, Path) - - # The result should be the resolved path with subpath appended - assert "three_paths.png" in str(result) - - -@pytest.mark.heavy -def test_lfs_path_multiple_instances() -> None: - """Test that multiple LfsPath instances for same file work correctly.""" - filename = "three_paths.png" - - # Create two separate instances - lfs_path_1 = LfsPath(filename) - lfs_path_2 = LfsPath(filename) - - # Both should start with None cache - cache_1 = object.__getattribute__(lfs_path_1, "_lfs_resolved_cache") - cache_2 = object.__getattribute__(lfs_path_2, "_lfs_resolved_cache") - assert cache_1 is None - assert cache_2 is None - - # Access file through first instance - content_1 = lfs_path_1.read_bytes() - - # First instance should have cache - cache_1 = object.__getattribute__(lfs_path_1, "_lfs_resolved_cache") - assert cache_1 is not None - - # Second instance cache should still be None (separate instance) - cache_2 = object.__getattribute__(lfs_path_2, "_lfs_resolved_cache") - assert cache_2 is None - - # Access through second instance - content_2 = lfs_path_2.read_bytes() - - # Now second instance should also have cache - cache_2 = object.__getattribute__(lfs_path_2, "_lfs_resolved_cache") - assert cache_2 is not None - - # Content should be the same - assert content_1 == content_2 - - # Both caches should point to the same file - assert cache_1 == cache_2 diff --git a/dimos/utils/test_foxglove_bridge.py b/dimos/utils/test_foxglove_bridge.py deleted file mode 100644 index cbac324c26..0000000000 --- a/dimos/utils/test_foxglove_bridge.py +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Test for foxglove bridge import and basic functionality -""" - -import warnings - -import pytest - -warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.server") -warnings.filterwarnings("ignore", category=DeprecationWarning, module="websockets.legacy") - - -def test_foxglove_bridge_import() -> None: - """Test that the foxglove bridge can be imported successfully.""" - try: - from dimos_lcm.foxglove_bridge import FoxgloveBridge - - assert FoxgloveBridge is not None - except ImportError as e: - pytest.fail(f"Failed to import foxglove bridge: {e}") - - -def test_foxglove_bridge_runner_init() -> None: - """Test that LcmFoxgloveBridge can be initialized with default parameters.""" - try: - from dimos_lcm.foxglove_bridge import FoxgloveBridge - - runner = FoxgloveBridge(host="localhost", port=8765, debug=False, num_threads=2) - - # Check that the runner was created successfully - assert runner is not None - - except Exception as e: - pytest.fail(f"Failed to initialize LcmFoxgloveBridge: {e}") - - -def test_foxglove_bridge_runner_params() -> None: - """Test that LcmFoxgloveBridge accepts various parameter configurations.""" - try: - from dimos_lcm.foxglove_bridge import FoxgloveBridge - - configs = [ - {"host": "0.0.0.0", "port": 8765, "debug": True, "num_threads": 1}, - {"host": "127.0.0.1", "port": 9090, "debug": False, "num_threads": 4}, - {"host": "localhost", "port": 8080, "debug": True, "num_threads": 2}, - ] - - for config in configs: - runner = FoxgloveBridge(**config) - assert runner is not None - - except Exception as e: - pytest.fail(f"Failed to create runner with different configs: {e}") - - -def test_bridge_runner_has_run_method() -> None: - """Test that the bridge runner has a run method that can be called.""" - try: - from dimos_lcm.foxglove_bridge import FoxgloveBridge - - runner = FoxgloveBridge(host="localhost", port=8765, debug=False, num_threads=1) - - # Check that the run method exists - assert hasattr(runner, "run") - assert callable(runner.run) - - except Exception as e: - pytest.fail(f"Failed to verify run method: {e}") diff --git a/dimos/utils/test_generic.py b/dimos/utils/test_generic.py deleted file mode 100644 index 0f691bc23c..0000000000 --- a/dimos/utils/test_generic.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from uuid import UUID - -from dimos.utils.generic import short_id - - -def test_short_id_hello_world() -> None: - assert short_id("HelloWorld") == "6GgJmzi1KYf4iaHVxk" - - -def test_short_id_uuid_one(mocker) -> None: - mocker.patch("uuid.uuid4", return_value=UUID("11111111-1111-1111-1111-111111111111")) - assert short_id() == "wcFtOGNXQnQFZ8QRh1" - - -def test_short_id_uuid_zero(mocker) -> None: - mocker.patch("uuid.uuid4", return_value=UUID("00000000-0000-0000-0000-000000000000")) - assert short_id() == "000000000000000000" diff --git a/dimos/utils/test_llm_utils.py b/dimos/utils/test_llm_utils.py deleted file mode 100644 index 0a3812aeaf..0000000000 --- a/dimos/utils/test_llm_utils.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Tests for LLM utility functions.""" - -import json - -import pytest - -from dimos.utils.llm_utils import extract_json - - -def test_extract_json_clean_response() -> None: - """Test extract_json with clean JSON response.""" - clean_json = '[["object", 1, 2, 3, 4]]' - result = extract_json(clean_json) - assert result == [["object", 1, 2, 3, 4]] - - -def test_extract_json_with_text_before_after() -> None: - """Test extract_json with text before and after JSON.""" - messy = """Here's what I found: - [ - ["person", 10, 20, 30, 40], - ["car", 50, 60, 70, 80] - ] - Hope this helps!""" - result = extract_json(messy) - assert result == [["person", 10, 20, 30, 40], ["car", 50, 60, 70, 80]] - - -def test_extract_json_with_emojis() -> None: - """Test extract_json with emojis and markdown code blocks.""" - messy = """Sure! 😊 Here are the detections: - - ```json - [["human", 100, 200, 300, 400]] - ``` - - Let me know if you need anything else! 👍""" - result = extract_json(messy) - assert result == [["human", 100, 200, 300, 400]] - - -def test_extract_json_multiple_json_blocks() -> None: - """Test extract_json when there are multiple JSON blocks.""" - messy = """First attempt (wrong format): - {"error": "not what we want"} - - Correct format: - [ - ["cat", 10, 10, 50, 50], - ["dog", 60, 60, 100, 100] - ] - - Another block: {"also": "not needed"}""" - result = extract_json(messy) - # Should return the first valid array - assert result == [["cat", 10, 10, 50, 50], ["dog", 60, 60, 100, 100]] - - -def test_extract_json_object() -> None: - """Test extract_json with JSON object instead of array.""" - response = 'The result is: {"status": "success", "count": 5}' - result = extract_json(response) - assert result == {"status": "success", "count": 5} - - -def test_extract_json_nested_structures() -> None: - """Test extract_json with nested arrays and objects.""" - response = """Processing complete: - [ - ["label1", 1, 2, 3, 4], - {"nested": {"value": 10}}, - ["label2", 5, 6, 7, 8] - ]""" - result = extract_json(response) - assert result[0] == ["label1", 1, 2, 3, 4] - assert result[1] == {"nested": {"value": 10}} - assert result[2] == ["label2", 5, 6, 7, 8] - - -def test_extract_json_invalid() -> None: - """Test extract_json raises error when no valid JSON found.""" - response = "This response has no valid JSON at all!" - with pytest.raises(json.JSONDecodeError) as exc_info: - extract_json(response) - assert "Could not extract valid JSON" in str(exc_info.value) - - -# Test with actual LLM response format -MOCK_LLM_RESPONSE = """ - Yes :) - - [ - ["humans", 76, 368, 219, 580], - ["humans", 354, 372, 512, 525], - ["humans", 409, 370, 615, 748], - ["humans", 628, 350, 762, 528], - ["humans", 785, 323, 960, 650] - ] - - Hope this helps!😀😊 :)""" - - -def test_extract_json_with_real_llm_response() -> None: - """Test extract_json with actual messy LLM response.""" - result = extract_json(MOCK_LLM_RESPONSE) - assert isinstance(result, list) - assert len(result) == 5 - assert result[0] == ["humans", 76, 368, 219, 580] - assert result[-1] == ["humans", 785, 323, 960, 650] diff --git a/dimos/utils/test_reactive.py b/dimos/utils/test_reactive.py deleted file mode 100644 index 5bfc0a590f..0000000000 --- a/dimos/utils/test_reactive.py +++ /dev/null @@ -1,298 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from collections.abc import Callable -import time -from typing import Any, TypeVar - -import numpy as np -import pytest -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import Disposable -from reactivex.scheduler import ThreadPoolScheduler - -from dimos.utils.reactive import ( - backpressure, - callback_to_observable, - getter_ondemand, - getter_streaming, - iter_observable, -) - - -def measure_time(func: Callable[[], Any], iterations: int = 1) -> float: - start_time = time.time() - result = func() - end_time = time.time() - total_time = end_time - start_time - return result, total_time - - -def assert_time( - func: Callable[[], Any], assertion: Callable[[int], bool], assert_fail_msg=None -) -> None: - [result, total_time] = measure_time(func) - assert assertion(total_time), assert_fail_msg + f", took {round(total_time, 2)}s" - return result - - -def min_time( - func: Callable[[], Any], min_t: int, assert_fail_msg: str = "Function returned too fast" -): - return assert_time( - func, (lambda t: t >= min_t * 0.98), assert_fail_msg + f", min: {min_t} seconds" - ) - - -def max_time(func: Callable[[], Any], max_t: int, assert_fail_msg: str = "Function took too long"): - return assert_time(func, (lambda t: t < max_t), assert_fail_msg + f", max: {max_t} seconds") - - -T = TypeVar("T") - - -def dispose_spy(source: rx.Observable[T]) -> rx.Observable[T]: - state = {"active": 0} - - def factory(observer, scheduler=None): - state["active"] += 1 - upstream = source.subscribe(observer, scheduler=scheduler) - - def _dispose() -> None: - upstream.dispose() - state["active"] -= 1 - - return Disposable(_dispose) - - proxy = rx.create(factory) - proxy.subs_number = lambda: state["active"] - proxy.is_disposed = lambda: state["active"] == 0 - return proxy - - -@pytest.mark.integration -def test_backpressure_handling() -> None: - # Create a dedicated scheduler for this test to avoid thread leaks - test_scheduler = ThreadPoolScheduler(max_workers=8) - try: - received_fast = [] - received_slow = [] - # Create an observable that emits numpy arrays instead of integers - source = dispose_spy( - rx.interval(0.1).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50)) - ) - - # Wrap with backpressure handling - safe_source = backpressure(source, scheduler=test_scheduler) - - # Fast sub - subscription1 = safe_source.subscribe(lambda x: received_fast.append(x)) - - # Slow sub (shouldn't block above) - subscription2 = safe_source.subscribe(lambda x: (time.sleep(0.25), received_slow.append(x))) - - time.sleep(2.5) - - subscription1.dispose() - assert not source.is_disposed(), "Observable should not be disposed yet" - subscription2.dispose() - # Wait longer to ensure background threads finish processing - # (the slow subscriber sleeps for 0.25s, so we need to wait at least that long) - time.sleep(0.5) - assert source.is_disposed(), "Observable should be disposed" - - # Check results - print("Fast observer received:", len(received_fast), [arr[0] for arr in received_fast]) - print("Slow observer received:", len(received_slow), [arr[0] for arr in received_slow]) - - # Fast observer should get all or nearly all items - assert len(received_fast) > 15, ( - f"Expected fast observer to receive most items, got {len(received_fast)}" - ) - - # Slow observer should get fewer items due to backpressure handling - assert len(received_slow) < len(received_fast), ( - "Slow observer should receive fewer items than fast observer" - ) - # Specifically, processing at 0.25s means ~4 items per second, so expect 8-10 items - assert 7 <= len(received_slow) <= 11, f"Expected 7-11 items, got {len(received_slow)}" - - # The slow observer should skip items (not process them in sequence) - # We test this by checking that the difference between consecutive arrays is sometimes > 1 - has_skips = False - for i in range(1, len(received_slow)): - if received_slow[i][0] - received_slow[i - 1][0] > 1: - has_skips = True - break - assert has_skips, "Slow observer should skip items due to backpressure" - finally: - # Always shutdown the scheduler to clean up threads - test_scheduler.executor.shutdown(wait=True) - - -@pytest.mark.integration -def test_getter_streaming_blocking() -> None: - source = dispose_spy( - rx.interval(0.2).pipe(ops.map(lambda i: np.array([i, i + 1, i + 2])), ops.take(50)) - ) - assert source.is_disposed() - - getter = min_time( - lambda: getter_streaming(source), - 0.2, - "Latest getter needs to block until first msg is ready", - ) - assert np.array_equal(getter(), np.array([0, 1, 2])), ( - f"Expected to get the first array [0,1,2], got {getter()}" - ) - - time.sleep(0.5) - assert getter()[0] >= 2, f"Expected array with first value >= 2, got {getter()}" - time.sleep(0.5) - assert getter()[0] >= 4, f"Expected array with first value >= 4, got {getter()}" - - getter.dispose() - time.sleep(0.3) # Wait for background interval timer threads to finish - assert source.is_disposed(), "Observable should be disposed" - - -def test_getter_streaming_blocking_timeout() -> None: - source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) - with pytest.raises(Exception): - getter = getter_streaming(source, timeout=0.1) - getter.dispose() - time.sleep(0.3) # Wait for background interval timer threads to finish - assert source.is_disposed() - - -@pytest.mark.integration -def test_getter_streaming_nonblocking() -> None: - source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) - - getter = max_time( - lambda: getter_streaming(source, nonblocking=True), - 0.1, - "nonblocking getter init shouldn't block", - ) - min_time(getter, 0.1, "Expected for first value call to block if cache is empty") - assert getter() == 0 - - time.sleep(0.5) - assert getter() >= 2, f"Expected value >= 2, got {getter()}" - - # sub is active - assert not source.is_disposed() - - time.sleep(0.5) - assert getter() >= 4, f"Expected value >= 4, got {getter()}" - - getter.dispose() - time.sleep(0.3) # Wait for background interval timer threads to finish - assert source.is_disposed(), "Observable should be disposed" - - -def test_getter_streaming_nonblocking_timeout() -> None: - source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) - getter = getter_streaming(source, timeout=0.1, nonblocking=True) - with pytest.raises(Exception): - getter() - - assert not source.is_disposed(), "is not disposed, this is a job of the caller" - - # Clean up the subscription to avoid thread leak - getter.dispose() - time.sleep(0.3) # Wait for background threads to finish - assert source.is_disposed(), "Observable should be disposed after cleanup" - - -def test_getter_ondemand() -> None: - # Create a controlled scheduler to avoid thread leaks from rx.interval - test_scheduler = ThreadPoolScheduler(max_workers=4) - try: - source = dispose_spy(rx.interval(0.1, scheduler=test_scheduler).pipe(ops.take(50))) - getter = getter_ondemand(source) - assert source.is_disposed(), "Observable should be disposed" - result = min_time(getter, 0.05) - assert result == 0, f"Expected to get the first value of 0, got {result}" - # Wait for background threads to clean up - time.sleep(0.3) - assert source.is_disposed(), "Observable should be disposed" - result2 = getter() - assert result2 == 0, f"Expected to get the first value of 0, got {result2}" - assert source.is_disposed(), "Observable should be disposed" - # Wait for threads to finish - time.sleep(0.3) - finally: - # Explicitly shutdown the scheduler to clean up threads - test_scheduler.executor.shutdown(wait=True) - - -def test_getter_ondemand_timeout() -> None: - source = dispose_spy(rx.interval(0.2).pipe(ops.take(50))) - getter = getter_ondemand(source, timeout=0.1) - with pytest.raises(Exception): - getter() - assert source.is_disposed(), "Observable should be disposed" - # Wait for background interval timer threads to finish - time.sleep(0.3) - - -def test_callback_to_observable() -> None: - # Test converting a callback-based API to an Observable - received = [] - callback = None - - # Mock start function that captures the callback - def start_fn(cb) -> str: - nonlocal callback - callback = cb - return "start_result" - - # Mock stop function - stop_called = False - - def stop_fn(cb) -> None: - nonlocal stop_called - stop_called = True - - # Create observable from callback - observable = callback_to_observable(start_fn, stop_fn) - - # Subscribe to the observable - subscription = observable.subscribe(lambda x: received.append(x)) - - # Verify start was called and we have access to the callback - assert callback is not None - - # Simulate callback being triggered with different messages - callback("message1") - callback(42) - callback({"key": "value"}) - - # Check that all messages were received - assert received == ["message1", 42, {"key": "value"}] - - # Dispose subscription and check that stop was called - subscription.dispose() - assert stop_called, "Stop function should be called on dispose" - - -def test_iter_observable() -> None: - source = dispose_spy(rx.of(1, 2, 3, 4, 5)) - - result = list(iter_observable(source)) - - assert result == [1, 2, 3, 4, 5] - assert source.is_disposed(), "Observable should be disposed after iteration" diff --git a/dimos/utils/test_transform_utils.py b/dimos/utils/test_transform_utils.py deleted file mode 100644 index b404579598..0000000000 --- a/dimos/utils/test_transform_utils.py +++ /dev/null @@ -1,678 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import numpy as np -import pytest -from scipy.spatial.transform import Rotation as R - -from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 -from dimos.utils import transform_utils - - -class TestNormalizeAngle: - def test_normalize_angle_zero(self) -> None: - assert transform_utils.normalize_angle(0) == 0 - - def test_normalize_angle_pi(self) -> None: - assert np.isclose(transform_utils.normalize_angle(np.pi), np.pi) - - def test_normalize_angle_negative_pi(self) -> None: - assert np.isclose(transform_utils.normalize_angle(-np.pi), -np.pi) - - def test_normalize_angle_two_pi(self) -> None: - # 2*pi should normalize to 0 - assert np.isclose(transform_utils.normalize_angle(2 * np.pi), 0, atol=1e-10) - - def test_normalize_angle_large_positive(self) -> None: - # Large positive angle should wrap to [-pi, pi] - angle = 5 * np.pi - normalized = transform_utils.normalize_angle(angle) - assert -np.pi <= normalized <= np.pi - assert np.isclose(normalized, np.pi) - - def test_normalize_angle_large_negative(self) -> None: - # Large negative angle should wrap to [-pi, pi] - angle = -5 * np.pi - normalized = transform_utils.normalize_angle(angle) - assert -np.pi <= normalized <= np.pi - # -5*pi = -pi (odd multiple of pi wraps to -pi) - assert np.isclose(normalized, -np.pi) or np.isclose(normalized, np.pi) - - -# Tests for distance_angle_to_goal_xy removed as function doesn't exist in the module - - -class TestPoseToMatrix: - def test_identity_pose(self) -> None: - pose = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - T = transform_utils.pose_to_matrix(pose) - assert np.allclose(T, np.eye(4)) - - def test_translation_only(self) -> None: - pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) - T = transform_utils.pose_to_matrix(pose) - expected = np.eye(4) - expected[:3, 3] = [1, 2, 3] - assert np.allclose(T, expected) - - def test_rotation_only_90_degrees_z(self) -> None: - # 90 degree rotation around z-axis - quat = R.from_euler("z", np.pi / 2).as_quat() - pose = Pose(Vector3(0, 0, 0), Quaternion(quat[0], quat[1], quat[2], quat[3])) - T = transform_utils.pose_to_matrix(pose) - - # Check rotation part - expected_rot = R.from_euler("z", np.pi / 2).as_matrix() - assert np.allclose(T[:3, :3], expected_rot) - - # Check translation is zero - assert np.allclose(T[:3, 3], [0, 0, 0]) - - def test_translation_and_rotation(self) -> None: - quat = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_quat() - pose = Pose(Vector3(5, -3, 2), Quaternion(quat[0], quat[1], quat[2], quat[3])) - T = transform_utils.pose_to_matrix(pose) - - # Check translation - assert np.allclose(T[:3, 3], [5, -3, 2]) - - # Check rotation - expected_rot = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_matrix() - assert np.allclose(T[:3, :3], expected_rot) - - # Check bottom row - assert np.allclose(T[3, :], [0, 0, 0, 1]) - - def test_zero_norm_quaternion(self) -> None: - # Test handling of zero norm quaternion - pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 0)) - T = transform_utils.pose_to_matrix(pose) - - # Should use identity rotation - expected = np.eye(4) - expected[:3, 3] = [1, 2, 3] - assert np.allclose(T, expected) - - -class TestMatrixToPose: - def test_identity_matrix(self) -> None: - T = np.eye(4) - pose = transform_utils.matrix_to_pose(T) - assert pose.position.x == 0 - assert pose.position.y == 0 - assert pose.position.z == 0 - assert np.isclose(pose.orientation.w, 1) - assert np.isclose(pose.orientation.x, 0) - assert np.isclose(pose.orientation.y, 0) - assert np.isclose(pose.orientation.z, 0) - - def test_translation_only(self) -> None: - T = np.eye(4) - T[:3, 3] = [1, 2, 3] - pose = transform_utils.matrix_to_pose(T) - assert pose.position.x == 1 - assert pose.position.y == 2 - assert pose.position.z == 3 - assert np.isclose(pose.orientation.w, 1) - - def test_rotation_only(self) -> None: - T = np.eye(4) - T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() - pose = transform_utils.matrix_to_pose(T) - - # Check position is zero - assert pose.position.x == 0 - assert pose.position.y == 0 - assert pose.position.z == 0 - - # Check rotation - quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] - recovered_rot = R.from_quat(quat).as_matrix() - assert np.allclose(recovered_rot, T[:3, :3]) - - def test_round_trip_conversion(self) -> None: - # Test that pose -> matrix -> pose gives same result - # Use a properly normalized quaternion - quat = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_quat() - original_pose = Pose( - Vector3(1.5, -2.3, 0.7), Quaternion(quat[0], quat[1], quat[2], quat[3]) - ) - T = transform_utils.pose_to_matrix(original_pose) - recovered_pose = transform_utils.matrix_to_pose(T) - - assert np.isclose(recovered_pose.position.x, original_pose.position.x) - assert np.isclose(recovered_pose.position.y, original_pose.position.y) - assert np.isclose(recovered_pose.position.z, original_pose.position.z) - assert np.isclose(recovered_pose.orientation.x, original_pose.orientation.x, atol=1e-6) - assert np.isclose(recovered_pose.orientation.y, original_pose.orientation.y, atol=1e-6) - assert np.isclose(recovered_pose.orientation.z, original_pose.orientation.z, atol=1e-6) - assert np.isclose(recovered_pose.orientation.w, original_pose.orientation.w, atol=1e-6) - - -class TestApplyTransform: - def test_identity_transform(self) -> None: - pose = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) - T_identity = np.eye(4) - result = transform_utils.apply_transform(pose, T_identity) - - assert np.isclose(result.position.x, pose.position.x) - assert np.isclose(result.position.y, pose.position.y) - assert np.isclose(result.position.z, pose.position.z) - - def test_translation_transform(self) -> None: - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - T = np.eye(4) - T[:3, 3] = [2, 3, 4] - result = transform_utils.apply_transform(pose, T) - - assert np.isclose(result.position.x, 3) # 2 + 1 - assert np.isclose(result.position.y, 3) # 3 + 0 - assert np.isclose(result.position.z, 4) # 4 + 0 - - def test_rotation_transform(self) -> None: - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - T = np.eye(4) - T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() # 90 degree rotation - result = transform_utils.apply_transform(pose, T) - - # After 90 degree rotation around z, point (1,0,0) becomes (0,1,0) - assert np.isclose(result.position.x, 0, atol=1e-10) - assert np.isclose(result.position.y, 1) - assert np.isclose(result.position.z, 0) - - def test_transform_with_transform_object(self) -> None: - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - pose.frame_id = "base" - - transform = Transform() - transform.frame_id = "world" - transform.child_frame_id = "base" - transform.translation = Vector3(2, 3, 4) - transform.rotation = Quaternion(0, 0, 0, 1) - - result = transform_utils.apply_transform(pose, transform) - assert np.isclose(result.position.x, 3) - assert np.isclose(result.position.y, 3) - assert np.isclose(result.position.z, 4) - - def test_transform_frame_mismatch_raises(self) -> None: - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - pose.frame_id = "base" - - transform = Transform() - transform.frame_id = "world" - transform.child_frame_id = "different_frame" - transform.translation = Vector3(2, 3, 4) - transform.rotation = Quaternion(0, 0, 0, 1) - - with pytest.raises(ValueError, match="does not match"): - transform_utils.apply_transform(pose, transform) - - -class TestOpticalToRobotFrame: - def test_identity_at_origin(self) -> None: - pose = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - result = transform_utils.optical_to_robot_frame(pose) - assert result.position.x == 0 - assert result.position.y == 0 - assert result.position.z == 0 - - def test_position_transformation(self) -> None: - # Optical: X=right(1), Y=down(0), Z=forward(0) - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - result = transform_utils.optical_to_robot_frame(pose) - - # Robot: X=forward(0), Y=left(-1), Z=up(0) - assert np.isclose(result.position.x, 0) # Forward = Camera Z - assert np.isclose(result.position.y, -1) # Left = -Camera X - assert np.isclose(result.position.z, 0) # Up = -Camera Y - - def test_forward_position(self) -> None: - # Optical: X=right(0), Y=down(0), Z=forward(2) - pose = Pose(Vector3(0, 0, 2), Quaternion(0, 0, 0, 1)) - result = transform_utils.optical_to_robot_frame(pose) - - # Robot: X=forward(2), Y=left(0), Z=up(0) - assert np.isclose(result.position.x, 2) - assert np.isclose(result.position.y, 0) - assert np.isclose(result.position.z, 0) - - def test_down_position(self) -> None: - # Optical: X=right(0), Y=down(3), Z=forward(0) - pose = Pose(Vector3(0, 3, 0), Quaternion(0, 0, 0, 1)) - result = transform_utils.optical_to_robot_frame(pose) - - # Robot: X=forward(0), Y=left(0), Z=up(-3) - assert np.isclose(result.position.x, 0) - assert np.isclose(result.position.y, 0) - assert np.isclose(result.position.z, -3) - - def test_round_trip_optical_robot(self) -> None: - original_pose = Pose(Vector3(1, 2, 3), Quaternion(0.1, 0.2, 0.3, 0.9165151389911680)) - robot_pose = transform_utils.optical_to_robot_frame(original_pose) - recovered_pose = transform_utils.robot_to_optical_frame(robot_pose) - - assert np.isclose(recovered_pose.position.x, original_pose.position.x, atol=1e-10) - assert np.isclose(recovered_pose.position.y, original_pose.position.y, atol=1e-10) - assert np.isclose(recovered_pose.position.z, original_pose.position.z, atol=1e-10) - - -class TestRobotToOpticalFrame: - def test_position_transformation(self) -> None: - # Robot: X=forward(1), Y=left(0), Z=up(0) - pose = Pose(Vector3(1, 0, 0), Quaternion(0, 0, 0, 1)) - result = transform_utils.robot_to_optical_frame(pose) - - # Optical: X=right(0), Y=down(0), Z=forward(1) - assert np.isclose(result.position.x, 0) - assert np.isclose(result.position.y, 0) - assert np.isclose(result.position.z, 1) - - def test_left_position(self) -> None: - # Robot: X=forward(0), Y=left(2), Z=up(0) - pose = Pose(Vector3(0, 2, 0), Quaternion(0, 0, 0, 1)) - result = transform_utils.robot_to_optical_frame(pose) - - # Optical: X=right(-2), Y=down(0), Z=forward(0) - assert np.isclose(result.position.x, -2) - assert np.isclose(result.position.y, 0) - assert np.isclose(result.position.z, 0) - - def test_up_position(self) -> None: - # Robot: X=forward(0), Y=left(0), Z=up(3) - pose = Pose(Vector3(0, 0, 3), Quaternion(0, 0, 0, 1)) - result = transform_utils.robot_to_optical_frame(pose) - - # Optical: X=right(0), Y=down(-3), Z=forward(0) - assert np.isclose(result.position.x, 0) - assert np.isclose(result.position.y, -3) - assert np.isclose(result.position.z, 0) - - -class TestYawTowardsPoint: - def test_yaw_from_origin(self) -> None: - # Point at (1, 0) from origin should have yaw = 0 - position = Vector3(1, 0, 0) - yaw = transform_utils.yaw_towards_point(position) - assert np.isclose(yaw, 0) - - def test_yaw_ninety_degrees(self) -> None: - # Point at (0, 1) from origin should have yaw = pi/2 - position = Vector3(0, 1, 0) - yaw = transform_utils.yaw_towards_point(position) - assert np.isclose(yaw, np.pi / 2) - - def test_yaw_negative_ninety_degrees(self) -> None: - # Point at (0, -1) from origin should have yaw = -pi/2 - position = Vector3(0, -1, 0) - yaw = transform_utils.yaw_towards_point(position) - assert np.isclose(yaw, -np.pi / 2) - - def test_yaw_forty_five_degrees(self) -> None: - # Point at (1, 1) from origin should have yaw = pi/4 - position = Vector3(1, 1, 0) - yaw = transform_utils.yaw_towards_point(position) - assert np.isclose(yaw, np.pi / 4) - - def test_yaw_with_custom_target(self) -> None: - # Point at (3, 2) from target (1, 1) - position = Vector3(3, 2, 0) - target = Vector3(1, 1, 0) - yaw = transform_utils.yaw_towards_point(position, target) - # Direction is (2, 1), so yaw = atan2(1, 2) - expected = np.arctan2(1, 2) - assert np.isclose(yaw, expected) - - -# Tests for transform_robot_to_map removed as function doesn't exist in the module - - -class TestCreateTransformFrom6DOF: - def test_identity_transform(self) -> None: - trans = Vector3(0, 0, 0) - euler = Vector3(0, 0, 0) - T = transform_utils.create_transform_from_6dof(trans, euler) - assert np.allclose(T, np.eye(4)) - - def test_translation_only(self) -> None: - trans = Vector3(1, 2, 3) - euler = Vector3(0, 0, 0) - T = transform_utils.create_transform_from_6dof(trans, euler) - - expected = np.eye(4) - expected[:3, 3] = [1, 2, 3] - assert np.allclose(T, expected) - - def test_rotation_only(self) -> None: - trans = Vector3(0, 0, 0) - euler = Vector3(np.pi / 4, np.pi / 6, np.pi / 3) - T = transform_utils.create_transform_from_6dof(trans, euler) - - expected_rot = R.from_euler("xyz", [np.pi / 4, np.pi / 6, np.pi / 3]).as_matrix() - assert np.allclose(T[:3, :3], expected_rot) - assert np.allclose(T[:3, 3], [0, 0, 0]) - assert np.allclose(T[3, :], [0, 0, 0, 1]) - - def test_translation_and_rotation(self) -> None: - trans = Vector3(5, -3, 2) - euler = Vector3(0.1, 0.2, 0.3) - T = transform_utils.create_transform_from_6dof(trans, euler) - - expected_rot = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_matrix() - assert np.allclose(T[:3, :3], expected_rot) - assert np.allclose(T[:3, 3], [5, -3, 2]) - - def test_small_angles_threshold(self) -> None: - trans = Vector3(1, 2, 3) - euler = Vector3(1e-7, 1e-8, 1e-9) # Very small angles - T = transform_utils.create_transform_from_6dof(trans, euler) - - # Should be effectively identity rotation - expected = np.eye(4) - expected[:3, 3] = [1, 2, 3] - assert np.allclose(T, expected, atol=1e-6) - - -class TestInvertTransform: - def test_identity_inverse(self) -> None: - T = np.eye(4) - T_inv = transform_utils.invert_transform(T) - assert np.allclose(T_inv, np.eye(4)) - - def test_translation_inverse(self) -> None: - T = np.eye(4) - T[:3, 3] = [1, 2, 3] - T_inv = transform_utils.invert_transform(T) - - # Inverse should negate translation - expected = np.eye(4) - expected[:3, 3] = [-1, -2, -3] - assert np.allclose(T_inv, expected) - - def test_rotation_inverse(self) -> None: - T = np.eye(4) - T[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() - T_inv = transform_utils.invert_transform(T) - - # Inverse rotation is transpose - expected = np.eye(4) - expected[:3, :3] = R.from_euler("z", -np.pi / 2).as_matrix() - assert np.allclose(T_inv, expected) - - def test_general_transform_inverse(self) -> None: - T = np.eye(4) - T[:3, :3] = R.from_euler("xyz", [0.1, 0.2, 0.3]).as_matrix() - T[:3, 3] = [1, 2, 3] - - T_inv = transform_utils.invert_transform(T) - - # T @ T_inv should be identity - result = T @ T_inv - assert np.allclose(result, np.eye(4)) - - # T_inv @ T should also be identity - result2 = T_inv @ T - assert np.allclose(result2, np.eye(4)) - - -class TestComposeTransforms: - def test_no_transforms(self) -> None: - result = transform_utils.compose_transforms() - assert np.allclose(result, np.eye(4)) - - def test_single_transform(self) -> None: - T = np.eye(4) - T[:3, 3] = [1, 2, 3] - result = transform_utils.compose_transforms(T) - assert np.allclose(result, T) - - def test_two_translations(self) -> None: - T1 = np.eye(4) - T1[:3, 3] = [1, 0, 0] - - T2 = np.eye(4) - T2[:3, 3] = [0, 2, 0] - - result = transform_utils.compose_transforms(T1, T2) - - expected = np.eye(4) - expected[:3, 3] = [1, 2, 0] - assert np.allclose(result, expected) - - def test_three_transforms(self) -> None: - T1 = np.eye(4) - T1[:3, 3] = [1, 0, 0] - - T2 = np.eye(4) - T2[:3, :3] = R.from_euler("z", np.pi / 2).as_matrix() - - T3 = np.eye(4) - T3[:3, 3] = [1, 0, 0] - - result = transform_utils.compose_transforms(T1, T2, T3) - expected = T1 @ T2 @ T3 - assert np.allclose(result, expected) - - -class TestEulerToQuaternion: - def test_zero_euler(self) -> None: - euler = Vector3(0, 0, 0) - quat = transform_utils.euler_to_quaternion(euler) - assert np.isclose(quat.w, 1) - assert np.isclose(quat.x, 0) - assert np.isclose(quat.y, 0) - assert np.isclose(quat.z, 0) - - def test_roll_only(self) -> None: - euler = Vector3(np.pi / 2, 0, 0) - quat = transform_utils.euler_to_quaternion(euler) - - # Verify by converting back - recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") - assert np.isclose(recovered[0], np.pi / 2) - assert np.isclose(recovered[1], 0) - assert np.isclose(recovered[2], 0) - - def test_pitch_only(self) -> None: - euler = Vector3(0, np.pi / 3, 0) - quat = transform_utils.euler_to_quaternion(euler) - - recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") - assert np.isclose(recovered[0], 0) - assert np.isclose(recovered[1], np.pi / 3) - assert np.isclose(recovered[2], 0) - - def test_yaw_only(self) -> None: - euler = Vector3(0, 0, np.pi / 4) - quat = transform_utils.euler_to_quaternion(euler) - - recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz") - assert np.isclose(recovered[0], 0) - assert np.isclose(recovered[1], 0) - assert np.isclose(recovered[2], np.pi / 4) - - def test_degrees_mode(self) -> None: - euler = Vector3(45, 30, 60) # degrees - quat = transform_utils.euler_to_quaternion(euler, degrees=True) - - recovered = R.from_quat([quat.x, quat.y, quat.z, quat.w]).as_euler("xyz", degrees=True) - assert np.isclose(recovered[0], 45) - assert np.isclose(recovered[1], 30) - assert np.isclose(recovered[2], 60) - - -class TestQuaternionToEuler: - def test_identity_quaternion(self) -> None: - quat = Quaternion(0, 0, 0, 1) - euler = transform_utils.quaternion_to_euler(quat) - assert np.isclose(euler.x, 0) - assert np.isclose(euler.y, 0) - assert np.isclose(euler.z, 0) - - def test_90_degree_yaw(self) -> None: - # Create quaternion for 90 degree yaw rotation - r = R.from_euler("z", np.pi / 2) - q = r.as_quat() - quat = Quaternion(q[0], q[1], q[2], q[3]) - - euler = transform_utils.quaternion_to_euler(quat) - assert np.isclose(euler.x, 0) - assert np.isclose(euler.y, 0) - assert np.isclose(euler.z, np.pi / 2) - - def test_round_trip_euler_quaternion(self) -> None: - original_euler = Vector3(0.3, 0.5, 0.7) - quat = transform_utils.euler_to_quaternion(original_euler) - recovered_euler = transform_utils.quaternion_to_euler(quat) - - assert np.isclose(recovered_euler.x, original_euler.x, atol=1e-10) - assert np.isclose(recovered_euler.y, original_euler.y, atol=1e-10) - assert np.isclose(recovered_euler.z, original_euler.z, atol=1e-10) - - def test_degrees_mode(self) -> None: - # Create quaternion for 45 degree yaw rotation - r = R.from_euler("z", 45, degrees=True) - q = r.as_quat() - quat = Quaternion(q[0], q[1], q[2], q[3]) - - euler = transform_utils.quaternion_to_euler(quat, degrees=True) - assert np.isclose(euler.x, 0) - assert np.isclose(euler.y, 0) - assert np.isclose(euler.z, 45) - - def test_angle_normalization(self) -> None: - # Test that angles are normalized to [-pi, pi] - r = R.from_euler("xyz", [3 * np.pi, -3 * np.pi, 2 * np.pi]) - q = r.as_quat() - quat = Quaternion(q[0], q[1], q[2], q[3]) - - euler = transform_utils.quaternion_to_euler(quat) - assert -np.pi <= euler.x <= np.pi - assert -np.pi <= euler.y <= np.pi - assert -np.pi <= euler.z <= np.pi - - -class TestGetDistance: - def test_same_pose(self) -> None: - pose1 = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(1, 2, 3), Quaternion(0.1, 0.2, 0.3, 0.9)) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, 0) - - def test_vector_distance(self) -> None: - pose1 = Vector3(1, 2, 3) - pose2 = Vector3(4, 5, 6) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, np.sqrt(3**2 + 3**2 + 3**2)) - - def test_distance_x_axis(self) -> None: - pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(5, 0, 0), Quaternion(0, 0, 0, 1)) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, 5) - - def test_distance_y_axis(self) -> None: - pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(0, 3, 0), Quaternion(0, 0, 0, 1)) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, 3) - - def test_distance_z_axis(self) -> None: - pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(0, 0, 4), Quaternion(0, 0, 0, 1)) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, 4) - - def test_3d_distance(self) -> None: - pose1 = Pose(Vector3(0, 0, 0), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(3, 4, 0), Quaternion(0, 0, 0, 1)) - distance = transform_utils.get_distance(pose1, pose2) - assert np.isclose(distance, 5) # 3-4-5 triangle - - def test_negative_coordinates(self) -> None: - pose1 = Pose(Vector3(-1, -2, -3), Quaternion(0, 0, 0, 1)) - pose2 = Pose(Vector3(1, 2, 3), Quaternion(0, 0, 0, 1)) - distance = transform_utils.get_distance(pose1, pose2) - expected = np.sqrt(4 + 16 + 36) # sqrt(56) - assert np.isclose(distance, expected) - - -class TestRetractDistance: - def test_retract_along_negative_z(self) -> None: - # Default case: gripper approaches along -z axis - # Positive distance moves away from the surface (opposite to approach direction) - target_pose = Pose(Vector3(0, 0, 1), Quaternion(0, 0, 0, 1)) - retracted = transform_utils.offset_distance(target_pose, 0.5) - - # Moving along -z approach vector with positive distance = retracting upward - # Since approach is -z and we retract (positive distance), we move in +z - assert np.isclose(retracted.position.x, 0) - assert np.isclose(retracted.position.y, 0) - assert np.isclose(retracted.position.z, 0.5) # 1 + 0.5 * (-1) = 0.5 - - # Orientation should remain unchanged - assert retracted.orientation.x == target_pose.orientation.x - assert retracted.orientation.y == target_pose.orientation.y - assert retracted.orientation.z == target_pose.orientation.z - assert retracted.orientation.w == target_pose.orientation.w - - def test_retract_with_rotation(self) -> None: - # Test with a rotated pose (90 degrees around x-axis) - r = R.from_euler("x", np.pi / 2) - q = r.as_quat() - target_pose = Pose(Vector3(0, 0, 1), Quaternion(q[0], q[1], q[2], q[3])) - - retracted = transform_utils.offset_distance(target_pose, 0.5) - - # After 90 degree rotation around x, -z becomes +y - assert np.isclose(retracted.position.x, 0) - assert np.isclose(retracted.position.y, 0.5) # Move along +y - assert np.isclose(retracted.position.z, 1) - - def test_retract_negative_distance(self) -> None: - # Negative distance should move forward (toward the approach direction) - target_pose = Pose(Vector3(0, 0, 1), Quaternion(0, 0, 0, 1)) - retracted = transform_utils.offset_distance(target_pose, -0.3) - - # Moving along -z approach vector with negative distance = moving downward - assert np.isclose(retracted.position.x, 0) - assert np.isclose(retracted.position.y, 0) - assert np.isclose(retracted.position.z, 1.3) # 1 + (-0.3) * (-1) = 1.3 - - def test_retract_arbitrary_pose(self) -> None: - # Test with arbitrary position and rotation - r = R.from_euler("xyz", [0.1, 0.2, 0.3]) - q = r.as_quat() - target_pose = Pose(Vector3(5, 3, 2), Quaternion(q[0], q[1], q[2], q[3])) - - distance = 1.0 - retracted = transform_utils.offset_distance(target_pose, distance) - - # Verify the distance between original and retracted is as expected - # (approximately, due to the approach vector direction) - T_target = transform_utils.pose_to_matrix(target_pose) - rotation_matrix = T_target[:3, :3] - approach_vector = rotation_matrix @ np.array([0, 0, -1]) - - expected_x = target_pose.position.x + distance * approach_vector[0] - expected_y = target_pose.position.y + distance * approach_vector[1] - expected_z = target_pose.position.z + distance * approach_vector[2] - - assert np.isclose(retracted.position.x, expected_x) - assert np.isclose(retracted.position.y, expected_y) - assert np.isclose(retracted.position.z, expected_z) - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/dimos/utils/test_trigonometry.py b/dimos/utils/test_trigonometry.py deleted file mode 100644 index 199061a629..0000000000 --- a/dimos/utils/test_trigonometry.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - -import pytest - -from dimos.utils.trigonometry import angle_diff - - -def from_rad(x): - return x / (math.pi / 180) - - -def to_rad(x): - return x * (math.pi / 180) - - -def test_angle_diff(): - a = to_rad(1) - b = to_rad(359) - - assert from_rad(angle_diff(a, b)) == pytest.approx(2, abs=0.00000000001) - - assert from_rad(angle_diff(b, a)) == pytest.approx(-2, abs=0.00000000001) diff --git a/dimos/utils/testing/__init__.py b/dimos/utils/testing/__init__.py deleted file mode 100644 index 568cd3604f..0000000000 --- a/dimos/utils/testing/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "moment": ["Moment", "OutputMoment", "SensorMoment"], - "replay": ["SensorReplay", "TimedSensorReplay", "TimedSensorStorage"], - }, -) diff --git a/dimos/utils/testing/moment.py b/dimos/utils/testing/moment.py deleted file mode 100644 index e92d771687..0000000000 --- a/dimos/utils/testing/moment.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Generic, TypeVar - -from dimos.core.resource import Resource -from dimos.types.timestamped import Timestamped -from dimos.utils.testing.replay import TimedSensorReplay - -if TYPE_CHECKING: - from dimos.core import Transport - -T = TypeVar("T", bound=Timestamped) - - -class SensorMoment(Generic[T], Resource): - value: T | None = None - - def __init__(self, name: str, transport: Transport[T]) -> None: - self.replay: TimedSensorReplay[T] = TimedSensorReplay(name) - self.transport = transport - - def seek(self, timestamp: float) -> None: - self.value = self.replay.find_closest_seek(timestamp) - - def publish(self) -> None: - if self.value is not None: - self.transport.publish(self.value) - - def start(self) -> None: - pass - - def stop(self) -> None: - self.transport.stop() - - -class OutputMoment(Generic[T], Resource): - value: T | None = None - transport: Transport[T] - - def __init__(self, transport: Transport[T]): - self.transport = transport - - def set(self, value: T) -> None: - self.value = value - - def publish(self) -> None: - if self.value is not None: - self.transport.publish(self.value) - - def start(self) -> None: - pass - - def stop(self) -> None: - self.transport.stop() - - -class Moment(Resource): - def moments( - self, *classes: type[SensorMoment[Any]] | type[OutputMoment[Any]] - ) -> list[SensorMoment[Any] | OutputMoment[Any]]: - moments: list[SensorMoment[Any] | OutputMoment[Any]] = [] - for attr_name in dir(self): - attr_value = getattr(self, attr_name) - if isinstance(attr_value, classes): - moments.append(attr_value) - return moments - - def seekable_moments(self) -> list[SensorMoment[Any]]: - return [m for m in self.moments(SensorMoment) if isinstance(m, SensorMoment)] - - def publishable_moments(self) -> list[SensorMoment[Any] | OutputMoment[Any]]: - return self.moments(OutputMoment, SensorMoment) - - def seek(self, timestamp: float) -> None: - for moment in self.seekable_moments(): - moment.seek(timestamp) - - def publish(self) -> None: - for moment in self.publishable_moments(): - moment.publish() - - def start(self) -> None: ... - - def stop(self) -> None: - for moment in self.publishable_moments(): - moment.stop() diff --git a/dimos/utils/testing/replay.py b/dimos/utils/testing/replay.py deleted file mode 100644 index 588b63e099..0000000000 --- a/dimos/utils/testing/replay.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Shim for TimedSensorReplay/TimedSensorStorage.""" - -from dimos.memory.timeseries.legacy import LegacyPickleStore - -SensorReplay = LegacyPickleStore -SensorStorage = LegacyPickleStore -TimedSensorReplay = LegacyPickleStore -TimedSensorStorage = LegacyPickleStore diff --git a/dimos/utils/testing/test_moment.py b/dimos/utils/testing/test_moment.py deleted file mode 100644 index 6764610d0e..0000000000 --- a/dimos/utils/testing/test_moment.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import time - -from dimos.core import LCMTransport -from dimos.msgs.geometry_msgs import PoseStamped, Transform -from dimos.msgs.sensor_msgs import CameraInfo, Image, PointCloud2 -from dimos.protocol.tf import TF -from dimos.robot.unitree.go2 import connection -from dimos.utils.data import get_data -from dimos.utils.testing.moment import Moment, SensorMoment - -data_dir = get_data("unitree_go2_office_walk2") - - -class Go2Moment(Moment): - lidar: SensorMoment[PointCloud2] - video: SensorMoment[Image] - odom: SensorMoment[PoseStamped] - - def __init__(self) -> None: - self.lidar = SensorMoment(f"{data_dir}/lidar", LCMTransport("/lidar", PointCloud2)) - self.video = SensorMoment(f"{data_dir}/video", LCMTransport("/color_image", Image)) - self.odom = SensorMoment(f"{data_dir}/odom", LCMTransport("/odom", PoseStamped)) - - @property - def transforms(self) -> list[Transform]: - if self.odom.value is None: - return [] - - # we just make sure to change timestamps so that we can jump - # back and forth through time and foxglove doesn't get confused - odom = self.odom.value - odom.ts = time.time() - return connection.GO2Connection._odom_to_tf(odom) - - def publish(self) -> None: - t = TF() - t.publish(*self.transforms) - t.stop() - - camera_info = connection._camera_info_static() - camera_info.ts = time.time() - camera_info_transport: LCMTransport[CameraInfo] = LCMTransport("/camera_info", CameraInfo) - camera_info_transport.publish(camera_info) - camera_info_transport.stop() - - super().publish() - - -def test_moment_seek_and_publish() -> None: - moment = Go2Moment() - - # Seek to 5 seconds - moment.seek(5.0) - - # Check that frames were loaded - assert moment.lidar.value is not None - assert moment.video.value is not None - assert moment.odom.value is not None - - # Publish all frames - moment.publish() - moment.stop() diff --git a/dimos/utils/testing/test_replay.py b/dimos/utils/testing/test_replay.py deleted file mode 100644 index e3020777b4..0000000000 --- a/dimos/utils/testing/test_replay.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import re - -from reactivex import operators as ops - -from dimos.msgs.sensor_msgs import PointCloud2 -from dimos.robot.unitree.type.lidar import pointcloud2_from_webrtc_lidar -from dimos.robot.unitree.type.odometry import Odometry -from dimos.utils.data import get_data -from dimos.utils.testing import replay - - -def test_timed_sensor_replay() -> None: - get_data("unitree_office_walk") - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom") - - itermsgs = [] - for msg in odom_store.iterate(): - itermsgs.append(msg) - if len(itermsgs) > 9: - break - - assert len(itermsgs) == 10 - - print("\n") - - timed_msgs = [] - - for msg in odom_store.stream().pipe(ops.take(10), ops.to_list()).run(): - timed_msgs.append(msg) - - assert len(timed_msgs) == 10 - - for i in range(10): - print(itermsgs[i], timed_msgs[i]) - assert itermsgs[i] == timed_msgs[i] - - -def test_iterate_ts_no_seek() -> None: - """Test iterate_ts without seek (start_timestamp=None)""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Test without seek - ts_msgs = [] - for ts, msg in odom_store.iterate_ts(): - ts_msgs.append((ts, msg)) - if len(ts_msgs) >= 5: - break - - assert len(ts_msgs) == 5 - # Check that we get tuples of (timestamp, data) - for ts, msg in ts_msgs: - assert isinstance(ts, float) - assert isinstance(msg, Odometry) - - -def test_iterate_ts_with_from_timestamp() -> None: - """Test iterate_ts with from_timestamp (absolute timestamp)""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom") - - # First get all messages to find a good seek point - all_msgs = [] - for ts, msg in odom_store.iterate_ts(): - all_msgs.append((ts, msg)) - if len(all_msgs) >= 10: - break - - # Seek to timestamp of 5th message - seek_timestamp = all_msgs[4][0] - - # Test with from_timestamp - seeked_msgs = [] - for ts, msg in odom_store.iterate_ts(from_timestamp=seek_timestamp): - seeked_msgs.append((ts, msg)) - if len(seeked_msgs) >= 5: - break - - assert len(seeked_msgs) == 5 - # First message should be at or after seek timestamp - assert seeked_msgs[0][0] >= seek_timestamp - # Should match the data from position 5 onward - assert seeked_msgs[0][1] == all_msgs[4][1] - - -def test_iterate_ts_with_relative_seek() -> None: - """Test iterate_ts with seek (relative seconds after first timestamp)""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom") - - # Get first few messages to understand timing - all_msgs = [] - for ts, msg in odom_store.iterate_ts(): - all_msgs.append((ts, msg)) - if len(all_msgs) >= 10: - break - - # Calculate relative seek time (e.g., 0.5 seconds after start) - first_ts = all_msgs[0][0] - seek_seconds = 0.5 - expected_start_ts = first_ts + seek_seconds - - # Test with relative seek - seeked_msgs = [] - for ts, msg in odom_store.iterate_ts(seek=seek_seconds): - seeked_msgs.append((ts, msg)) - if len(seeked_msgs) >= 5: - break - - # First message should be at or after expected timestamp - assert seeked_msgs[0][0] >= expected_start_ts - # Make sure we're actually skipping some messages - assert seeked_msgs[0][0] > first_ts - - -def test_stream_with_seek() -> None: - """Test stream method with seek parameters""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom") - - # Test stream with relative seek - msgs_with_seek = [] - for msg in odom_store.stream(seek=0.2).pipe(ops.take(5), ops.to_list()).run(): - msgs_with_seek.append(msg) - - assert len(msgs_with_seek) == 5 - - # Test stream with from_timestamp - # First get a reference timestamp - first_msgs = [] - for msg in odom_store.stream().pipe(ops.take(3), ops.to_list()).run(): - first_msgs.append(msg) - - # Now test from_timestamp (would need actual timestamps from iterate_ts to properly test) - # This is a basic test to ensure the parameter is accepted - msgs_with_timestamp = [] - for msg in ( - odom_store.stream(from_timestamp=1000000000.0).pipe(ops.take(3), ops.to_list()).run() - ): - msgs_with_timestamp.append(msg) - - -def test_duration_with_loop() -> None: - """Test duration parameter with looping in TimedSensorReplay""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom") - - # Collect timestamps from a small duration window - collected_ts = [] - duration = 0.3 # 300ms window - - # First pass: collect timestamps in the duration window - for ts, _msg in odom_store.iterate_ts(duration=duration): - collected_ts.append(ts) - if len(collected_ts) >= 100: # Safety limit - break - - # Should have some messages but not too many - assert len(collected_ts) > 0 - assert len(collected_ts) < 20 # Assuming ~30Hz data - - # Test looping with duration - should repeat the same window - loop_count = 0 - prev_ts = None - - for ts, _msg in odom_store.iterate_ts(duration=duration, loop=True): - if prev_ts is not None and ts < prev_ts: - # We've looped back to the beginning - loop_count += 1 - if loop_count >= 2: # Stop after 2 full loops - break - prev_ts = ts - - assert loop_count >= 2 # Verify we actually looped - - -def test_first_methods() -> None: - """Test first() and first_timestamp() methods""" - - # Test SensorReplay.first() - lidar_replay = replay.SensorReplay("office_lidar", autocast=pointcloud2_from_webrtc_lidar) - - print("first file", lidar_replay.files[0]) - # Verify the first file ends with 000.pickle using regex - assert re.search(r"000\.pickle$", str(lidar_replay.files[0])), ( - f"Expected first file to end with 000.pickle, got {lidar_replay.files[0]}" - ) - - first_msg = lidar_replay.first() - assert first_msg is not None - assert isinstance(first_msg, PointCloud2) - - # Verify it's the same type as first item from iterate() - first_from_iterate = next(lidar_replay.iterate()) - print("DONE") - assert type(first_msg) is type(first_from_iterate) - # Since pointcloud2_from_webrtc_lidar uses time.time(), timestamps will be slightly different - assert abs(first_msg.ts - first_from_iterate.ts) < 1.0 # Within 1 second tolerance - - # Test TimedSensorReplay.first_timestamp() - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - first_ts = odom_store.first_timestamp() - assert first_ts is not None - assert isinstance(first_ts, float) - - # Verify it matches the timestamp from iterate_ts - ts_from_iterate, _ = next(odom_store.iterate_ts()) - assert first_ts == ts_from_iterate - - # Test that first() returns just the data - first_data = odom_store.first() - assert first_data is not None - assert isinstance(first_data, Odometry) - - -def test_find_closest() -> None: - """Test find_closest method in TimedSensorReplay""" - odom_store = replay.TimedSensorReplay("unitree_office_walk/odom", autocast=Odometry.from_msg) - - # Get some reference timestamps - timestamps = [] - for ts, _msg in odom_store.iterate_ts(): - timestamps.append(ts) - if len(timestamps) >= 10: - break - - # Test exact match - target_ts = timestamps[5] - result = odom_store.find_closest(target_ts) - assert result is not None - assert isinstance(result, Odometry) - - # Test between timestamps - mid_ts = (timestamps[3] + timestamps[4]) / 2 - result = odom_store.find_closest(mid_ts) - assert result is not None - - # Test with tolerance - far_future = timestamps[-1] + 100.0 - result = odom_store.find_closest(far_future, tolerance=1.0) - assert result is None # Too far away - - result = odom_store.find_closest(timestamps[0] - 0.001, tolerance=0.01) - assert result is not None # Within tolerance - - # Test find_closest_seek - result = odom_store.find_closest_seek(0.5) # 0.5 seconds from start - assert result is not None - assert isinstance(result, Odometry) - - # Test with negative seek (before start) - result = odom_store.find_closest_seek(-1.0) - assert result is not None # Should still return closest (first frame) diff --git a/dimos/utils/threadpool.py b/dimos/utils/threadpool.py deleted file mode 100644 index a2adc90725..0000000000 --- a/dimos/utils/threadpool.py +++ /dev/null @@ -1,79 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Thread pool functionality for parallel execution in the Dimos framework. - -This module provides a shared ThreadPoolExecutor exposed through a -ReactiveX scheduler, ensuring consistent thread management across the application. -""" - -import multiprocessing -import os - -from reactivex.scheduler import ThreadPoolScheduler - -from .logging_config import setup_logger - -logger = setup_logger() - - -def get_max_workers() -> int: - """Determine the number of workers for the thread pool. - - Returns: - int: The number of workers, configurable via the DIMOS_MAX_WORKERS - environment variable, defaulting to 4 times the CPU count. - """ - env_value = os.getenv("DIMOS_MAX_WORKERS", "") - return int(env_value) if env_value.strip() else multiprocessing.cpu_count() - - -# Create a ThreadPoolScheduler with a configurable number of workers. -try: - max_workers = get_max_workers() - scheduler = ThreadPoolScheduler(max_workers=max_workers) - # logger.info(f"Using {max_workers} workers") -except Exception as e: - logger.error(f"Failed to initialize ThreadPoolScheduler: {e}") - raise - - -def get_scheduler() -> ThreadPoolScheduler: - """Return the global ThreadPoolScheduler instance. - - The thread pool is configured with a fixed number of workers and is shared - across the application to manage system resources efficiently. - - Returns: - ThreadPoolScheduler: The global scheduler instance for scheduling - operations on the thread pool. - """ - return scheduler - - -def make_single_thread_scheduler() -> ThreadPoolScheduler: - """Create a new ThreadPoolScheduler with a single worker. - - This provides a dedicated scheduler for tasks that should run serially - on their own thread rather than using the shared thread pool. - - Returns: - ThreadPoolScheduler: A scheduler instance with a single worker thread. - """ - return ThreadPoolScheduler(max_workers=1) - - -# Example usage: -# scheduler = get_scheduler() -# # Use the scheduler for parallel tasks diff --git a/dimos/utils/transform_utils.py b/dimos/utils/transform_utils.py deleted file mode 100644 index ed82f6116f..0000000000 --- a/dimos/utils/transform_utils.py +++ /dev/null @@ -1,386 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -import numpy as np -from scipy.spatial.transform import Rotation as R # type: ignore[import-untyped] - -from dimos.msgs.geometry_msgs import Pose, Quaternion, Transform, Vector3 - - -def normalize_angle(angle: float) -> float: - """Normalize angle to [-pi, pi] range""" - return np.arctan2(np.sin(angle), np.cos(angle)) # type: ignore[no-any-return] - - -def pose_to_matrix(pose: Pose) -> np.ndarray: # type: ignore[type-arg] - """ - Convert pose to 4x4 homogeneous transform matrix. - - Args: - pose: Pose object with position and orientation (quaternion) - - Returns: - 4x4 transformation matrix - """ - # Extract position - tx, ty, tz = pose.position.x, pose.position.y, pose.position.z - - # Create rotation matrix from quaternion using scipy - quat = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] - - # Check for zero norm quaternion and use identity if invalid - quat_norm = np.linalg.norm(quat) - if quat_norm == 0.0: - # Use identity quaternion [0, 0, 0, 1] if zero norm detected - quat = [0.0, 0.0, 0.0, 1.0] - - rotation = R.from_quat(quat) - Rot = rotation.as_matrix() - - # Create 4x4 transform - T = np.eye(4) - T[:3, :3] = Rot - T[:3, 3] = [tx, ty, tz] - - return T - - -def matrix_to_pose(T: np.ndarray) -> Pose: # type: ignore[type-arg] - """ - Convert 4x4 transformation matrix to Pose object. - - Args: - T: 4x4 transformation matrix - - Returns: - Pose object with position and orientation (quaternion) - """ - # Extract position - pos = Vector3(T[0, 3], T[1, 3], T[2, 3]) - - # Extract rotation matrix and convert to quaternion - Rot = T[:3, :3] - rotation = R.from_matrix(Rot) - quat = rotation.as_quat() # Returns [x, y, z, w] - - orientation = Quaternion(quat[0], quat[1], quat[2], quat[3]) - - return Pose(pos, orientation) - - -def apply_transform(pose: Pose, transform: np.ndarray | Transform) -> Pose: # type: ignore[type-arg] - """ - Apply a transformation matrix to a pose. - - Args: - pose: Input pose - transform_matrix: 4x4 transformation matrix to apply - - Returns: - Transformed pose - """ - if isinstance(transform, Transform): - if transform.child_frame_id != pose.frame_id: - raise ValueError( - f"Transform frame_id {transform.frame_id} does not match pose frame_id {pose.frame_id}" - ) - transform = pose_to_matrix(transform.to_pose()) - - # Convert pose to matrix - T_pose = pose_to_matrix(pose) - - # Apply transform - T_result = transform @ T_pose - - # Convert back to pose - return matrix_to_pose(T_result) - - -def optical_to_robot_frame(pose: Pose) -> Pose: - """ - Convert pose from optical camera frame to robot frame convention. - - Optical Camera Frame (e.g., ZED): - - X: Right - - Y: Down - - Z: Forward (away from camera) - - Robot Frame (ROS/REP-103): - - X: Forward - - Y: Left - - Z: Up - - Args: - pose: Pose in optical camera frame - - Returns: - Pose in robot frame - """ - # Position transformation - robot_x = pose.position.z # Forward = Camera Z - robot_y = -pose.position.x # Left = -Camera X - robot_z = -pose.position.y # Up = -Camera Y - - # Rotation transformation using quaternions - # First convert quaternion to rotation matrix - quat_optical = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] - R_optical = R.from_quat(quat_optical).as_matrix() - - # Coordinate frame transformation matrix from optical to robot - # X_robot = Z_optical, Y_robot = -X_optical, Z_robot = -Y_optical - T_frame = np.array( - [ - [0, 0, 1], # X_robot = Z_optical - [-1, 0, 0], # Y_robot = -X_optical - [0, -1, 0], # Z_robot = -Y_optical - ] - ) - - # Transform the rotation matrix - R_robot = T_frame @ R_optical @ T_frame.T - - # Convert back to quaternion - quat_robot = R.from_matrix(R_robot).as_quat() # [x, y, z, w] - - return Pose( - Vector3(robot_x, robot_y, robot_z), - Quaternion(quat_robot[0], quat_robot[1], quat_robot[2], quat_robot[3]), - ) - - -def robot_to_optical_frame(pose: Pose) -> Pose: - """ - Convert pose from robot frame to optical camera frame convention. - This is the inverse of optical_to_robot_frame. - - Args: - pose: Pose in robot frame - - Returns: - Pose in optical camera frame - """ - # Position transformation (inverse) - optical_x = -pose.position.y # Right = -Left - optical_y = -pose.position.z # Down = -Up - optical_z = pose.position.x # Forward = Forward - - # Rotation transformation using quaternions - quat_robot = [pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w] - R_robot = R.from_quat(quat_robot).as_matrix() - - # Coordinate frame transformation matrix from Robot to optical (inverse of optical to Robot) - # This is the transpose of the forward transformation - T_frame_inv = np.array( - [ - [0, -1, 0], # X_optical = -Y_robot - [0, 0, -1], # Y_optical = -Z_robot - [1, 0, 0], # Z_optical = X_robot - ] - ) - - # Transform the rotation matrix - R_optical = T_frame_inv @ R_robot @ T_frame_inv.T - - # Convert back to quaternion - quat_optical = R.from_matrix(R_optical).as_quat() # [x, y, z, w] - - return Pose( - Vector3(optical_x, optical_y, optical_z), - Quaternion(quat_optical[0], quat_optical[1], quat_optical[2], quat_optical[3]), - ) - - -def yaw_towards_point(position: Vector3, target_point: Vector3 = None) -> float: # type: ignore[assignment] - """ - Calculate yaw angle from target point to position (away from target). - This is commonly used for object orientation in grasping applications. - Assumes robot frame where X is forward and Y is left. - - Args: - position: Current position in robot frame - target_point: Reference point (default: origin) - - Returns: - Yaw angle in radians pointing from target_point to position - """ - if target_point is None: - target_point = Vector3(0.0, 0.0, 0.0) - direction_x = position.x - target_point.x - direction_y = position.y - target_point.y - return np.arctan2(direction_y, direction_x) # type: ignore[no-any-return] - - -def create_transform_from_6dof(translation: Vector3, euler_angles: Vector3) -> np.ndarray: # type: ignore[type-arg] - """ - Create a 4x4 transformation matrix from 6DOF parameters. - - Args: - translation: Translation vector [x, y, z] in meters - euler_angles: Euler angles [rx, ry, rz] in radians (XYZ convention) - - Returns: - 4x4 transformation matrix - """ - # Create transformation matrix - T = np.eye(4) - - # Set translation - T[0:3, 3] = [translation.x, translation.y, translation.z] - - # Set rotation using scipy - if np.linalg.norm([euler_angles.x, euler_angles.y, euler_angles.z]) > 1e-6: - rotation = R.from_euler("xyz", [euler_angles.x, euler_angles.y, euler_angles.z]) - T[0:3, 0:3] = rotation.as_matrix() - - return T - - -def invert_transform(T: np.ndarray) -> np.ndarray: # type: ignore[type-arg] - """ - Invert a 4x4 transformation matrix efficiently. - - Args: - T: 4x4 transformation matrix - - Returns: - Inverted 4x4 transformation matrix - """ - # For homogeneous transform matrices, we can use the special structure: - # [R t]^-1 = [R^T -R^T*t] - # [0 1] [0 1 ] - - Rot = T[:3, :3] - t = T[:3, 3] - - T_inv = np.eye(4) - T_inv[:3, :3] = Rot.T - T_inv[:3, 3] = -Rot.T @ t - - return T_inv - - -def compose_transforms(*transforms: np.ndarray) -> np.ndarray: # type: ignore[type-arg] - """ - Compose multiple transformation matrices. - - Args: - *transforms: Variable number of 4x4 transformation matrices - - Returns: - Composed 4x4 transformation matrix (T1 @ T2 @ ... @ Tn) - """ - result = np.eye(4) - for T in transforms: - result = result @ T - return result - - -def euler_to_quaternion(euler_angles: Vector3, degrees: bool = False) -> Quaternion: - """ - Convert euler angles to quaternion. - - Args: - euler_angles: Euler angles as Vector3 [roll, pitch, yaw] in radians (XYZ convention) - - Returns: - Quaternion object [x, y, z, w] - """ - rotation = R.from_euler( - "xyz", [euler_angles.x, euler_angles.y, euler_angles.z], degrees=degrees - ) - quat = rotation.as_quat() # Returns [x, y, z, w] - return Quaternion(quat[0], quat[1], quat[2], quat[3]) - - -def quaternion_to_euler(quaternion: Quaternion, degrees: bool = False) -> Vector3: - """ - Convert quaternion to euler angles. - - Args: - quaternion: Quaternion object [x, y, z, w] - - Returns: - Euler angles as Vector3 [roll, pitch, yaw] in radians (XYZ convention) - """ - quat = [quaternion.x, quaternion.y, quaternion.z, quaternion.w] - rotation = R.from_quat(quat) - euler = rotation.as_euler("xyz", degrees=degrees) # Returns [roll, pitch, yaw] - if not degrees: - return Vector3( - normalize_angle(euler[0]), normalize_angle(euler[1]), normalize_angle(euler[2]) - ) - else: - return Vector3(euler[0], euler[1], euler[2]) - - -def get_distance(pose1: Pose | Vector3, pose2: Pose | Vector3) -> float: - """ - Calculate Euclidean distance between two poses. - - Args: - pose1: First pose - pose2: Second pose - - Returns: - Euclidean distance between the two poses in meters - """ - if hasattr(pose1, "position"): - pose1 = pose1.position - if hasattr(pose2, "position"): - pose2 = pose2.position - - dx = pose1.x - pose2.x - dy = pose1.y - pose2.y - dz = pose1.z - pose2.z - - return np.linalg.norm(np.array([dx, dy, dz])) # type: ignore[return-value] - - -def offset_distance( - target_pose: Pose, distance: float, approach_vector: Vector3 = Vector3(0, 0, -1) -) -> Pose: - """ - Apply distance offset to target pose along its approach direction. - - This is commonly used in grasping to offset the gripper by a certain distance - along the approach vector before or after grasping. - - Args: - target_pose: Target pose (e.g., grasp pose) - distance: Distance to offset along the approach direction (meters) - - Returns: - Target pose offset by the specified distance along its approach direction - """ - # Convert pose to transformation matrix to extract rotation - T_target = pose_to_matrix(target_pose) - rotation_matrix = T_target[:3, :3] - - # Define the approach vector based on the target pose orientation - # Assuming the gripper approaches along its local -z axis (common for downward grasps) - # You can change this to [1, 0, 0] for x-axis or [0, 1, 0] for y-axis based on your gripper - approach_vector_local = np.array([approach_vector.x, approach_vector.y, approach_vector.z]) - - # Transform approach vector to world coordinates - approach_vector_world = rotation_matrix @ approach_vector_local - - # Apply offset along the approach direction - offset_position = Vector3( - target_pose.position.x + distance * approach_vector_world[0], - target_pose.position.y + distance * approach_vector_world[1], - target_pose.position.z + distance * approach_vector_world[2], - ) - - return Pose(position=offset_position, orientation=target_pose.orientation) diff --git a/dimos/utils/trigonometry.py b/dimos/utils/trigonometry.py deleted file mode 100644 index 528192050c..0000000000 --- a/dimos/utils/trigonometry.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import math - - -def angle_diff(a: float, b: float) -> float: - return (a - b + math.pi) % (2 * math.pi) - math.pi diff --git a/dimos/utils/urdf.py b/dimos/utils/urdf.py deleted file mode 100644 index 474658df1a..0000000000 --- a/dimos/utils/urdf.py +++ /dev/null @@ -1,69 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""URDF generation utilities.""" - -from __future__ import annotations - - -def box_urdf( - width: float, - height: float, - depth: float, - name: str = "box_robot", - mass: float = 1.0, - rgba: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.5), -) -> str: - """Generate a simple URDF with a box as the base_link. - - Args: - width: Box size in X direction (meters) - height: Box size in Y direction (meters) - depth: Box size in Z direction (meters) - name: Robot name - mass: Mass of the box (kg) - rgba: Color as (red, green, blue, alpha), default red with 0.5 transparency - - Returns: - URDF XML string - """ - # Simple box inertia (solid cuboid) - ixx = (mass / 12.0) * (height**2 + depth**2) - iyy = (mass / 12.0) * (width**2 + depth**2) - izz = (mass / 12.0) * (width**2 + height**2) - - r, g, b, a = rgba - return f""" - - - - - - - - - - - - - - - - - - - - - -""" diff --git a/dimos/visualization/rerun/bridge.py b/dimos/visualization/rerun/bridge.py deleted file mode 100644 index 1dc104f1b4..0000000000 --- a/dimos/visualization/rerun/bridge.py +++ /dev/null @@ -1,346 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Rerun bridge for logging pubsub messages with to_rerun() methods.""" - -from __future__ import annotations - -from dataclasses import dataclass, field -from functools import lru_cache -from typing import ( - TYPE_CHECKING, - Any, - Literal, - Protocol, - TypeAlias, - TypeGuard, - cast, - runtime_checkable, -) - -from reactivex.disposable import Disposable -from toolz import pipe # type: ignore[import-untyped] -import typer - -from dimos.core import Module, rpc -from dimos.core.module import ModuleConfig -from dimos.protocol.pubsub.impl.lcmpubsub import LCM -from dimos.protocol.pubsub.patterns import Glob, pattern_matches -from dimos.utils.logging_config import setup_logger - -RERUN_GRPC_PORT = 9876 -RERUN_WEB_PORT = 9090 - -# TODO OUT visual annotations -# -# In the future it would be nice if modules can annotate their individual OUTs with (general or rerun specific) -# hints related to their visualization -# -# so stuff like color, update frequency etc (some Image needs to be rendered on the 3d floor like occupancy grid) -# some other image is an image to be streamed into a specific 2D view etc. -# -# To achieve this we'd feed a full blueprint into the rerun bridge. -# -# rerun bridge can then inspect all transports used, all modules with their outs, -# automatically spy an all the transports and read visualization hints -# -# Temporarily we are using these "sideloading" visual_override={} dict on the bridge -# to define custom visualizations for specific topics -# -# as well as pubsubs={} to specify which protocols to listen to. - - -# TODO better TF processing -# -# this is rerun bridge specific, rerun has a specific (better) way of handling TFs -# using entity path conventions, each of these nodes in a path are TF frames: -# -# /world/robot1/base_link/camera/optical -# -# While here since we are just listening on TFMessage messages which optionally contain -# just a subset of full TF tree we don't know the full tree structure to build full entity -# path for a transform being published -# -# This is easy to reconstruct but a service/tf.py already does this so should be integrated here -# -# we have decoupled entity paths and actual transforms (like ROS TF frames) -# https://rerun.io/docs/concepts/logging-and-ingestion/transforms -# -# tf#/world -# tf#/base_link -# tf#/camera -# -# In order to solve this, bridge needs to own it's own tf service -# and render it's tf tree into correct rerun entity paths - - -logger = setup_logger() - -if TYPE_CHECKING: - from collections.abc import Callable - - from rerun._baseclasses import Archetype - from rerun.blueprint import Blueprint - - from dimos.protocol.pubsub.spec import SubscribeAllCapable - -BlueprintFactory: TypeAlias = "Callable[[], Blueprint]" - -# to_rerun() can return a single archetype or a list of (entity_path, archetype) tuples -RerunMulti: TypeAlias = "list[tuple[str, Archetype]]" -RerunData: TypeAlias = "Archetype | RerunMulti" - - -def is_rerun_multi(data: Any) -> TypeGuard[RerunMulti]: - """Check if data is a list of (entity_path, archetype) tuples.""" - from rerun._baseclasses import Archetype - - return ( - isinstance(data, list) - and bool(data) - and isinstance(data[0], tuple) - and len(data[0]) == 2 - and isinstance(data[0][0], str) - and isinstance(data[0][1], Archetype) - ) - - -@runtime_checkable -class RerunConvertible(Protocol): - """Protocol for messages that can be converted to Rerun data.""" - - def to_rerun(self) -> RerunData: ... - - -ViewerMode = Literal["native", "web", "none"] - - -def _default_blueprint() -> Blueprint: - """Default blueprint with black background and raised grid.""" - import rerun as rr - import rerun.blueprint as rrb - - return rrb.Blueprint( # type: ignore[no-any-return] - rrb.Spatial3DView( - origin="world", - background=rrb.Background(kind="SolidColor", color=[0, 0, 0]), - line_grid=rrb.LineGrid3D( - plane=rr.components.Plane3D.XY.with_distance(0.2), - ), - ), - ) - - -@dataclass -class Config(ModuleConfig): - """Configuration for RerunBridgeModule.""" - - pubsubs: list[SubscribeAllCapable[Any, Any]] = field( - default_factory=lambda: [LCM(autoconf=True)] - ) - - visual_override: dict[Glob | str, Callable[[Any], Archetype]] = field(default_factory=dict) - - # Static items logged once after start. Maps entity_path -> callable(rr) returning Archetype - static: dict[str, Callable[[Any], Archetype]] = field(default_factory=dict) - - entity_prefix: str = "world" - topic_to_entity: Callable[[Any], str] | None = None - viewer_mode: ViewerMode = "native" - memory_limit: str = "25%" - - # Blueprint factory: callable(rrb) -> Blueprint for viewer layout configuration - # Set to None to disable default blueprint - blueprint: BlueprintFactory | None = _default_blueprint - - -class RerunBridgeModule(Module): - """Bridge that logs messages from pubsubs to Rerun. - - Spawns its own Rerun viewer and subscribes to all topics on each provided - pubsub. Any message that has a to_rerun() method is automatically logged. - - Example: - from dimos.protocol.pubsub.impl.lcmpubsub import LCM - - lcm = LCM(autoconf=True) - bridge = RerunBridgeModule(pubsubs=[lcm]) - bridge.start() - # All messages with to_rerun() are now logged to Rerun - bridge.stop() - """ - - default_config = Config - config: Config - - @lru_cache(maxsize=256) - def _visual_override_for_entity_path( - self, entity_path: str - ) -> Callable[[Any], RerunData | None]: - """Return a composed visual override for the entity path. - - Chains matching overrides from config, ending with final_convert - which handles .to_rerun() or passes through Archetypes. - """ - from rerun._baseclasses import Archetype - - # find all matching converters for this entity path - matches = [ - fn - for pattern, fn in self.config.visual_override.items() - if pattern_matches(pattern, entity_path) - ] - - # None means "suppress this topic entirely" - if any(fn is None for fn in matches): - return lambda msg: None - - # final step (ensures we return Archetype or None) - def final_convert(msg: Any) -> RerunData | None: - if isinstance(msg, Archetype): - return msg - if is_rerun_multi(msg): - return msg - if isinstance(msg, RerunConvertible): - return msg.to_rerun() - return None - - # compose all converters - return lambda msg: pipe(msg, *matches, final_convert) - - def _get_entity_path(self, topic: Any) -> str: - """Convert a topic to a Rerun entity path.""" - if self.config.topic_to_entity: - return self.config.topic_to_entity(topic) - - # Default: use topic.name if available (LCM Topic), else str - topic_str = getattr(topic, "name", None) or str(topic) - # Strip everything after # (LCM topic suffix) - topic_str = topic_str.split("#")[0] - return f"{self.config.entity_prefix}{topic_str}" - - def _on_message(self, msg: Any, topic: Any) -> None: - """Handle incoming message - log to rerun.""" - import rerun as rr - - # convert a potentially complex topic object into an str rerun entity path - entity_path: str = self._get_entity_path(topic) - - # apply visual overrides (including final_convert which handles .to_rerun()) - rerun_data: RerunData | None = self._visual_override_for_entity_path(entity_path)(msg) - - # converters can also suppress logging by returning None - if not rerun_data: - return - - # TFMessage for example returns list of (entity_path, archetype) tuples - if is_rerun_multi(rerun_data): - for path, archetype in rerun_data: - rr.log(path, archetype) - else: - rr.log(entity_path, cast("Archetype", rerun_data)) - - @rpc - def start(self) -> None: - import rerun as rr - - super().start() - - # Initialize and spawn Rerun viewer - rr.init("dimos") - - if self.config.viewer_mode == "native": - rr.spawn(connect=True, memory_limit=self.config.memory_limit) - elif self.config.viewer_mode == "web": - server_uri = rr.serve_grpc() - rr.serve_web_viewer(connect_to=server_uri, open_browser=False) - # "none" - just init, no viewer (connect externally) - - if self.config.blueprint: - rr.send_blueprint(self.config.blueprint()) - - # Start pubsubs and subscribe to all messages - for pubsub in self.config.pubsubs: - logger.info(f"bridge listening on {pubsub.__class__.__name__}") - if hasattr(pubsub, "start"): - pubsub.start() # type: ignore[union-attr] - unsub = pubsub.subscribe_all(self._on_message) - self._disposables.add(Disposable(unsub)) - - # Add pubsub stop as disposable - for pubsub in self.config.pubsubs: - if hasattr(pubsub, "stop"): - self._disposables.add(Disposable(pubsub.stop)) # type: ignore[union-attr] - - self._log_static() - - def _log_static(self) -> None: - import rerun as rr - - for entity_path, factory in self.config.static.items(): - data = factory(rr) - if isinstance(data, list): - for archetype in data: - rr.log(entity_path, archetype, static=True) - else: - rr.log(entity_path, data, static=True) - - @rpc - def stop(self) -> None: - super().stop() - - -def run_bridge( - viewer_mode: str = "native", - memory_limit: str = "25%", -) -> None: - """Start a RerunBridgeModule with default LCM config and block until interrupted.""" - import signal - - bridge = RerunBridgeModule( - viewer_mode=viewer_mode, - memory_limit=memory_limit, - # any pubsub that supports subscribe_all and topic that supports str(topic) - # is acceptable here - pubsubs=[LCM(autoconf=True)], - ) - - bridge.start() - - signal.signal(signal.SIGINT, lambda *_: bridge.stop()) - signal.pause() - - -app = typer.Typer() - - -@app.command() -def cli( - viewer_mode: str = typer.Option( - "native", help="Viewer mode: native (desktop), web (browser), none (headless)" - ), - memory_limit: str = typer.Option( - "25%", help="Memory limit for Rerun viewer (e.g., '4GB', '16GB', '25%')" - ), -) -> None: - """Rerun bridge for LCM messages.""" - run_bridge(viewer_mode=viewer_mode, memory_limit=memory_limit) - - -if __name__ == "__main__": - app() - -# you don't need to include this in your blueprint if you are not creating a -# custom rerun configuration for your deployment, you can also run rerun-bridge standalone -rerun_bridge = RerunBridgeModule.blueprint diff --git a/dimos/web/__init__.py b/dimos/web/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/web/command-center-extension/.gitignore b/dimos/web/command-center-extension/.gitignore deleted file mode 100644 index 1cb79e0e3c..0000000000 --- a/dimos/web/command-center-extension/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -*.foxe -/dist -/dist-standalone -/node_modules -!/package.json -!/package-lock.json diff --git a/dimos/web/command-center-extension/.prettierrc.yaml b/dimos/web/command-center-extension/.prettierrc.yaml deleted file mode 100644 index e57cc20758..0000000000 --- a/dimos/web/command-center-extension/.prettierrc.yaml +++ /dev/null @@ -1,5 +0,0 @@ -arrowParens: always -printWidth: 100 -trailingComma: "all" -tabWidth: 2 -semi: true diff --git a/dimos/web/command-center-extension/CHANGELOG.md b/dimos/web/command-center-extension/CHANGELOG.md deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/web/command-center-extension/eslint.config.js b/dimos/web/command-center-extension/eslint.config.js deleted file mode 100644 index 63cc3a243a..0000000000 --- a/dimos/web/command-center-extension/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -// @ts-check - -const foxglove = require("@foxglove/eslint-plugin"); -const globals = require("globals"); -const tseslint = require("typescript-eslint"); - -module.exports = tseslint.config({ - files: ["src/**/*.ts", "src/**/*.tsx"], - extends: [foxglove.configs.base, foxglove.configs.react, foxglove.configs.typescript], - languageOptions: { - globals: { - ...globals.es2020, - ...globals.browser, - }, - parserOptions: { - project: "tsconfig.json", - tsconfigRootDir: __dirname, - }, - }, - rules: { - "react-hooks/exhaustive-deps": "error", - }, -}); diff --git a/dimos/web/command-center-extension/index.html b/dimos/web/command-center-extension/index.html deleted file mode 100644 index e1e9ce85ad..0000000000 --- a/dimos/web/command-center-extension/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - Command Center - - - -
- - - diff --git a/dimos/web/command-center-extension/package-lock.json b/dimos/web/command-center-extension/package-lock.json deleted file mode 100644 index 09f9be88b4..0000000000 --- a/dimos/web/command-center-extension/package-lock.json +++ /dev/null @@ -1,8602 +0,0 @@ -{ - "name": "command-center-extension", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "command-center-extension", - "version": "0.0.1", - "license": "UNLICENSED", - "dependencies": { - "@types/pako": "^2.0.4", - "d3": "^7.9.0", - "leaflet": "^1.9.4", - "pako": "^2.1.0", - "react-leaflet": "^4.2.1", - "socket.io-client": "^4.8.1" - }, - "devDependencies": { - "@foxglove/eslint-plugin": "2.1.0", - "@foxglove/extension": "2.34.0", - "@types/d3": "^7.4.3", - "@types/leaflet": "^1.9.21", - "@types/react": "18.3.24", - "@types/react-dom": "18.3.7", - "@vitejs/plugin-react": "^4.3.4", - "create-foxglove-extension": "1.0.6", - "eslint": "9.34.0", - "prettier": "3.6.2", - "react": "18.3.1", - "react-dom": "^18.3.1", - "typescript": "5.9.2", - "vite": "^6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.5" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/compat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.3.2.tgz", - "integrity": "sha512-jRNwzTbd6p2Rw4sZ1CgWRS8YMtqG15YyZf7zvb6gY2rB2u6n+2Z+ELW0GtL0fQgyl0pr4Y/BzBfng/BdsereRA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.40 || 9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.15.2", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@foxglove/eslint-plugin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@foxglove/eslint-plugin/-/eslint-plugin-2.1.0.tgz", - "integrity": "sha512-EQrEns2BneSY7ODsOnJ6YIvn6iOVhwypHT4OwrzuPX2jqncghF7BXypkdDP3KlFtyDGC1+ff3+VXZMmyc8vpfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint/compat": "^1", - "@eslint/js": "^9", - "@typescript-eslint/utils": "^8", - "eslint-config-prettier": "^9", - "eslint-plugin-es": "^4", - "eslint-plugin-filenames": "^1", - "eslint-plugin-import": "^2", - "eslint-plugin-jest": "^28", - "eslint-plugin-prettier": "^5", - "eslint-plugin-react": "^7", - "eslint-plugin-react-hooks": "^5", - "tsutils": "^3", - "typescript-eslint": "^8" - }, - "peerDependencies": { - "eslint": "^9.27.0" - } - }, - "node_modules/@foxglove/extension": { - "version": "2.34.0", - "resolved": "https://registry.npmjs.org/@foxglove/extension/-/extension-2.34.0.tgz", - "integrity": "sha512-muZGa//A4gsNVRjwZevwvnSqQdabCJePdh75VFm5LhEb0fkP7VXjU3Rzh84EHRJvkUctiV7IbiI9OAPJmENGeQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pkgr/core": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", - "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@react-leaflet/core": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", - "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", - "license": "Hippocratic-2.1", - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", - "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", - "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", - "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", - "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", - "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", - "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", - "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", - "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", - "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", - "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", - "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", - "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", - "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", - "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", - "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", - "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", - "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", - "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", - "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", - "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", - "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", - "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rtsao/scc": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", - "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "license": "MIT" - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/leaflet": { - "version": "1.9.21", - "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz", - "integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==", - "dev": true, - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.10.0" - } - }, - "node_modules/@types/pako": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", - "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", - "license": "MIT" - }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "18.3.24", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz", - "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz", - "integrity": "sha512-w/EboPlBwnmOBtRbiOvzjD+wdiZdgFeo17lkltrtn7X37vagKKWJABvyfsJXTlHe6XBzugmYgd4A4nW+k8Mixw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/type-utils": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "graphemer": "^1.4.0", - "ignore": "^7.0.0", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.40.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.40.0.tgz", - "integrity": "sha512-jCNyAuXx8dr5KJMkecGmZ8KI61KBUhkCob+SD+C+I5+Y1FWI2Y3QmY4/cxMCC5WAsZqoEtEETVhUiUMIGCf6Bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.40.0.tgz", - "integrity": "sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.40.0", - "@typescript-eslint/types": "^8.40.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.40.0.tgz", - "integrity": "sha512-y9ObStCcdCiZKzwqsE8CcpyuVMwRouJbbSrNuThDpv16dFAj429IkM6LNb1dZ2m7hK5fHyzNcErZf7CEeKXR4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.40.0.tgz", - "integrity": "sha512-jtMytmUaG9d/9kqSl/W3E3xaWESo4hFDxAIHGVW/WKKtQhesnRIJSAJO6XckluuJ6KDB5woD1EiqknriCtAmcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.40.0.tgz", - "integrity": "sha512-eE60cK4KzAc6ZrzlJnflXdrMqOBaugeukWICO2rB0KNvwdIMaEaYiywwHMzA1qFpTxrLhN9Lp4E/00EgWcD3Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.40.0.tgz", - "integrity": "sha512-ETdbFlgbAmXHyFPwqUIYrfc12ArvpBhEVgGAxVYSwli26dn8Ko+lIo4Su9vI9ykTZdJn+vJprs/0eZU0YMAEQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.40.0.tgz", - "integrity": "sha512-k1z9+GJReVVOkc1WfVKs1vBrR5MIKKbdAjDTPvIK3L8De6KbFfPFt6BKpdkdk7rZS2GtC/m6yI5MYX+UsuvVYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.40.0", - "@typescript-eslint/tsconfig-utils": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/visitor-keys": "8.40.0", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.1.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.40.0.tgz", - "integrity": "sha512-Cgzi2MXSZyAUOY+BFwGs17s7ad/7L+gKt6Y8rAVVWS+7o6wrjeFN4nVfTpbE25MNcxyJ+iYUXflbs2xR9h4UBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.40.0", - "@typescript-eslint/types": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.40.0.tgz", - "integrity": "sha512-8CZ47QwalyRjsypfwnbI3hKy5gJDPmrkLjkgMxhi0+DZZ2QNx2naS6/hWoVYUHU7LU2zleF68V9miaVZvhFfTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.40.0", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.findlastindex": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", - "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-shim-unscopables": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.25.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", - "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001735", - "electron-to-chromium": "^1.5.204", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001737", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", - "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-4.0.0.tgz", - "integrity": "sha512-WuWE1nyTNAyW5T7oNyys2EN0cfP2fdRxhxnIQWiAp0bMabPdHhoGxM8A6YL2GhqwgrPnnaemVE7nv5XJ2Fhh2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/create-foxglove-extension": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/create-foxglove-extension/-/create-foxglove-extension-1.0.6.tgz", - "integrity": "sha512-Gp0qOQ+nU6dkqgpQlEdqdYVL4PJtdG+HXnfw09npEJCGT9M+5KFLj9V6Xt07oV3rSO/vthoTKPLR6xAD/+nPZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-webpack-plugin": "4.0.0", - "commander": "12.1.0", - "jszip": "3.10.1", - "mkdirp": "3.0.1", - "ncp": "2.0.0", - "node-fetch": "2.7.0", - "path-browserify": "1.0.1", - "rimraf": "6.0.1", - "sanitize-filename": "1.6.3", - "ts-loader": "9.5.1", - "webpack": "5.96.1" - }, - "bin": { - "create-foxglove-extension": "dist/bin/create-foxglove-extension.js", - "foxglove-extension": "dist/bin/foxglove-extension.js" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/d3": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", - "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", - "license": "ISC", - "dependencies": { - "d3-array": "3", - "d3-axis": "3", - "d3-brush": "3", - "d3-chord": "3", - "d3-color": "3", - "d3-contour": "4", - "d3-delaunay": "6", - "d3-dispatch": "3", - "d3-drag": "3", - "d3-dsv": "3", - "d3-ease": "3", - "d3-fetch": "3", - "d3-force": "3", - "d3-format": "3", - "d3-geo": "3", - "d3-hierarchy": "3", - "d3-interpolate": "3", - "d3-path": "3", - "d3-polygon": "3", - "d3-quadtree": "3", - "d3-random": "3", - "d3-scale": "4", - "d3-scale-chromatic": "3", - "d3-selection": "3", - "d3-shape": "3", - "d3-time": "3", - "d3-time-format": "4", - "d3-timer": "3", - "d3-transition": "3", - "d3-zoom": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-array": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", - "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-axis": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", - "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-brush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", - "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "3", - "d3-transition": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-chord": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", - "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", - "license": "ISC", - "dependencies": { - "d3-path": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-color": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", - "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-contour": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", - "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", - "license": "ISC", - "dependencies": { - "d3-array": "^3.2.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", - "license": "ISC", - "dependencies": { - "delaunator": "5" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dispatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", - "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-drag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", - "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-selection": "3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", - "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", - "license": "ISC", - "dependencies": { - "commander": "7", - "iconv-lite": "0.6", - "rw": "1" - }, - "bin": { - "csv2json": "bin/dsv2json.js", - "csv2tsv": "bin/dsv2dsv.js", - "dsv2dsv": "bin/dsv2dsv.js", - "dsv2json": "bin/dsv2json.js", - "json2csv": "bin/json2dsv.js", - "json2dsv": "bin/json2dsv.js", - "json2tsv": "bin/json2dsv.js", - "tsv2csv": "bin/dsv2dsv.js", - "tsv2json": "bin/dsv2json.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-dsv/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/d3-ease": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", - "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-fetch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", - "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", - "license": "ISC", - "dependencies": { - "d3-dsv": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-force": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", - "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-quadtree": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-geo": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", - "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2.5.0 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-hierarchy": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", - "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", - "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-polygon": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", - "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-quadtree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", - "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-random": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", - "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", - "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", - "license": "ISC", - "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-interpolate": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", - "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", - "license": "ISC", - "dependencies": { - "d3-array": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", - "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-timer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", - "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-transition": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", - "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", - "license": "ISC", - "dependencies": { - "d3-color": "1 - 3", - "d3-dispatch": "1 - 3", - "d3-ease": "1 - 3", - "d3-interpolate": "1 - 3", - "d3-timer": "1 - 3" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "d3-selection": "2 - 3" - } - }, - "node_modules/d3-zoom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", - "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", - "license": "ISC", - "dependencies": { - "d3-dispatch": "1 - 3", - "d3-drag": "2 - 3", - "d3-interpolate": "1 - 3", - "d3-selection": "2 - 3", - "d3-transition": "2 - 3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", - "license": "ISC", - "dependencies": { - "robust-predicates": "^3.0.2" - } - }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.208", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz", - "integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==", - "dev": true, - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true, - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", - "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" - } - }, - "node_modules/eslint-import-resolver-node/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^3.2.7" - }, - "engines": { - "node": ">=4" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-es": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", - "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-filenames": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz", - "integrity": "sha512-tqxJTiEM5a0JmRCUYQmxw23vtTxrb2+a3Q2mMOPhFxvt7ZQQJmdiuMby9B/vUAuVMghyP7oET+nIf6EO6CBd/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.camelcase": "4.3.0", - "lodash.kebabcase": "4.1.1", - "lodash.snakecase": "4.1.1", - "lodash.upperfirst": "4.3.1" - }, - "peerDependencies": { - "eslint": "*" - } - }, - "node_modules/eslint-plugin-import": { - "version": "2.32.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", - "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.9", - "array.prototype.findlastindex": "^1.2.6", - "array.prototype.flat": "^1.3.3", - "array.prototype.flatmap": "^1.3.3", - "debug": "^3.2.7", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.1", - "hasown": "^2.0.2", - "is-core-module": "^2.16.1", - "is-glob": "^4.0.3", - "minimatch": "^3.1.2", - "object.fromentries": "^2.0.8", - "object.groupby": "^1.0.3", - "object.values": "^1.2.1", - "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.9", - "tsconfig-paths": "^3.15.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" - } - }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-import/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jest": { - "version": "28.14.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.14.0.tgz", - "integrity": "sha512-P9s/qXSMTpRTerE2FQ0qJet2gKbcGyFTPAJipoKxmWqR6uuFqIqk8FuEfg5yBieOezVrEfAMZrEwJ6yEp+1MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" - }, - "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0 || ^8.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", - "jest": "*" - }, - "peerDependenciesMeta": { - "@typescript-eslint/eslint-plugin": { - "optional": true - }, - "jest": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", - "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.11.7" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/internmap": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", - "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/jackspeak": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", - "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/jszip": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", - "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "dev": true, - "license": "(MIT OR GPL-3.0-or-later)", - "dependencies": { - "lie": "~3.3.0", - "pako": "~1.0.2", - "readable-stream": "~2.3.6", - "setimmediate": "^1.0.5" - } - }, - "node_modules/jszip/node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true, - "license": "(MIT AND Zlib)" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/leaflet": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", - "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lie": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", - "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" - } - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lru-cache": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", - "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, - "license": "ISC", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", - "dev": true, - "license": "MIT", - "bin": { - "ncp": "bin/ncp" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/pako": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", - "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", - "license": "(MIT AND Zlib)" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", - "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" - }, - "peerDependencies": { - "react": "^18.3.1" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-leaflet": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", - "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", - "dependencies": { - "@react-leaflet/core": "^2.1.0" - }, - "peerDependencies": { - "leaflet": "^1.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", - "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", - "license": "Unlicense" - }, - "node_modules/rollup": { - "version": "4.54.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", - "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.54.0", - "@rollup/rollup-android-arm64": "4.54.0", - "@rollup/rollup-darwin-arm64": "4.54.0", - "@rollup/rollup-darwin-x64": "4.54.0", - "@rollup/rollup-freebsd-arm64": "4.54.0", - "@rollup/rollup-freebsd-x64": "4.54.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", - "@rollup/rollup-linux-arm-musleabihf": "4.54.0", - "@rollup/rollup-linux-arm64-gnu": "4.54.0", - "@rollup/rollup-linux-arm64-musl": "4.54.0", - "@rollup/rollup-linux-loong64-gnu": "4.54.0", - "@rollup/rollup-linux-ppc64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-gnu": "4.54.0", - "@rollup/rollup-linux-riscv64-musl": "4.54.0", - "@rollup/rollup-linux-s390x-gnu": "4.54.0", - "@rollup/rollup-linux-x64-gnu": "4.54.0", - "@rollup/rollup-linux-x64-musl": "4.54.0", - "@rollup/rollup-openharmony-arm64": "4.54.0", - "@rollup/rollup-win32-arm64-msvc": "4.54.0", - "@rollup/rollup-win32-ia32-msvc": "4.54.0", - "@rollup/rollup-win32-x64-gnu": "4.54.0", - "@rollup/rollup-win32-x64-msvc": "4.54.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, - "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, - "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true, - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", - "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.2.9" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, - "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, - "license": "MIT" - }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/ts-loader": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.1.tgz", - "integrity": "sha512-rNH3sK9kGZcH9dYzC7CewQm4NtxJTjSEVRJ2DyBZR7f8/wcta+iV44UPCXc5+nzDzivKtlzV6c9P4e+oFhDLYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - } - }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", - "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0", - "reflect.getprototypeof": "^1.0.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typescript": { - "version": "5.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", - "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.40.0.tgz", - "integrity": "sha512-Xvd2l+ZmFDPEt4oj1QEXzA4A2uUK6opvKu3eGN9aGjB8au02lIVcLyi375w94hHyejTOmzIU77L8ol2sRg9n7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.40.0", - "@typescript-eslint/parser": "8.40.0", - "@typescript-eslint/typescript-estree": "8.40.0", - "@typescript-eslint/utils": "8.40.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", - "dev": true, - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/webpack": { - "version": "5.96.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", - "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.6", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.14.0", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/dimos/web/command-center-extension/package.json b/dimos/web/command-center-extension/package.json deleted file mode 100644 index f3cd836205..0000000000 --- a/dimos/web/command-center-extension/package.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "name": "command-center-extension", - "displayName": "command-center-extension", - "description": "2D costmap visualization with robot and path overlay", - "publisher": "dimensional", - "homepage": "", - "version": "0.0.1", - "license": "UNLICENSED", - "main": "./dist/extension.js", - "keywords": [], - "scripts": { - "build": "foxglove-extension build", - "build:standalone": "vite build", - "dev": "vite", - "preview": "vite preview", - "foxglove:prepublish": "foxglove-extension build --mode production", - "lint": "eslint .", - "lint:ci": "eslint .", - "lint:fix": "eslint --fix .", - "local-install": "foxglove-extension install", - "package": "foxglove-extension package", - "pretest": "foxglove-extension pretest" - }, - "devDependencies": { - "@foxglove/eslint-plugin": "2.1.0", - "@foxglove/extension": "2.34.0", - "@types/d3": "^7.4.3", - "@types/leaflet": "^1.9.21", - "@types/react": "18.3.24", - "@types/react-dom": "18.3.7", - "@vitejs/plugin-react": "^4.3.4", - "create-foxglove-extension": "1.0.6", - "eslint": "9.34.0", - "prettier": "3.6.2", - "react": "18.3.1", - "react-dom": "^18.3.1", - "typescript": "5.9.2", - "vite": "^6.0.0" - }, - "dependencies": { - "@types/pako": "^2.0.4", - "d3": "^7.9.0", - "leaflet": "^1.9.4", - "pako": "^2.1.0", - "react-leaflet": "^4.2.1", - "socket.io-client": "^4.8.1" - } -} diff --git a/dimos/web/command-center-extension/src/App.tsx b/dimos/web/command-center-extension/src/App.tsx deleted file mode 100644 index dc0c90e7ea..0000000000 --- a/dimos/web/command-center-extension/src/App.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import * as React from "react"; - -import Connection from "./Connection"; -import ExplorePanel from "./ExplorePanel"; -import GpsButton from "./GpsButton"; -import Button from "./Button"; -import KeyboardControlPanel from "./KeyboardControlPanel"; -import VisualizerWrapper from "./components/VisualizerWrapper"; -import LeafletMap from "./components/LeafletMap"; -import { AppAction, AppState, LatLon } from "./types"; - -function appReducer(state: AppState, action: AppAction): AppState { - switch (action.type) { - case "SET_COSTMAP": - return { ...state, costmap: action.payload }; - case "SET_ROBOT_POSE": - return { ...state, robotPose: action.payload }; - case "SET_GPS_LOCATION": - return { ...state, gpsLocation: action.payload }; - case "SET_GPS_TRAVEL_GOAL_POINTS": - return { ...state, gpsTravelGoalPoints: action.payload }; - case "SET_PATH": - return { ...state, path: action.payload }; - case "SET_FULL_STATE": - return { ...state, ...action.payload }; - default: - return state; - } -} - -const initialState: AppState = { - costmap: null, - robotPose: null, - gpsLocation: null, - gpsTravelGoalPoints: null, - path: null, -}; - -export default function App(): React.ReactElement { - const [state, dispatch] = React.useReducer(appReducer, initialState); - const [isGpsMode, setIsGpsMode] = React.useState(false); - const connectionRef = React.useRef(null); - - React.useEffect(() => { - connectionRef.current = new Connection(dispatch); - - return () => { - if (connectionRef.current) { - connectionRef.current.disconnect(); - } - }; - }, []); - - const handleWorldClick = React.useCallback((worldX: number, worldY: number) => { - connectionRef.current?.worldClick(worldX, worldY); - }, []); - - const handleStartExplore = React.useCallback(() => { - connectionRef.current?.startExplore(); - }, []); - - const handleStopExplore = React.useCallback(() => { - connectionRef.current?.stopExplore(); - }, []); - - const handleGpsGoal = React.useCallback((goal: LatLon) => { - connectionRef.current?.sendGpsGoal(goal); - }, []); - - const handleSendMoveCommand = React.useCallback( - (linear: [number, number, number], angular: [number, number, number]) => { - connectionRef.current?.sendMoveCommand(linear, angular); - }, - [], - ); - - const handleStopMoveCommand = React.useCallback(() => { - connectionRef.current?.stopMoveCommand(); - }, []); - - const handleReturnHome = React.useCallback(() => { - connectionRef.current?.worldClick(0, 0); - }, []); - - const handleStop = React.useCallback(() => { - if (state.robotPose) { - connectionRef.current?.worldClick(state.robotPose.coords[0]!, state.robotPose.coords[1]!); - } - }, [state.robotPose]); - - return ( -
- {isGpsMode ? ( - - ) : ( - - )} -
- setIsGpsMode(true)} - onUseCostmap={() => setIsGpsMode(false)} - > - - - - -
-
- ); -} diff --git a/dimos/web/command-center-extension/src/Button.tsx b/dimos/web/command-center-extension/src/Button.tsx deleted file mode 100644 index 8714bb8611..0000000000 --- a/dimos/web/command-center-extension/src/Button.tsx +++ /dev/null @@ -1,24 +0,0 @@ -interface ButtonProps { - onClick: () => void; - isActive: boolean; - children: React.ReactNode; -} - -export default function Button({ onClick, isActive, children }: ButtonProps): React.ReactElement { - return ( - - ); -} diff --git a/dimos/web/command-center-extension/src/Connection.ts b/dimos/web/command-center-extension/src/Connection.ts deleted file mode 100644 index 7a23c6b98c..0000000000 --- a/dimos/web/command-center-extension/src/Connection.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { io, Socket } from "socket.io-client"; - -import { - AppAction, - Costmap, - EncodedCostmap, - EncodedPath, - EncodedVector, - FullStateData, - LatLon, - Path, - TwistCommand, - Vector, -} from "./types"; - -export default class Connection { - socket: Socket; - dispatch: React.Dispatch; - - constructor(dispatch: React.Dispatch) { - this.dispatch = dispatch; - this.socket = io("ws://localhost:7779"); - - this.socket.on("costmap", (data: EncodedCostmap) => { - const costmap = Costmap.decode(data); - this.dispatch({ type: "SET_COSTMAP", payload: costmap }); - }); - - this.socket.on("robot_pose", (data: EncodedVector) => { - const robotPose = Vector.decode(data); - this.dispatch({ type: "SET_ROBOT_POSE", payload: robotPose }); - }); - - this.socket.on("gps_location", (data: LatLon) => { - this.dispatch({ type: "SET_GPS_LOCATION", payload: data }); - }); - - this.socket.on("gps_travel_goal_points", (data: LatLon[]) => { - this.dispatch({ type: "SET_GPS_TRAVEL_GOAL_POINTS", payload: data }); - }); - - this.socket.on("path", (data: EncodedPath) => { - const path = Path.decode(data); - this.dispatch({ type: "SET_PATH", payload: path }); - }); - - this.socket.on("full_state", (data: FullStateData) => { - const state: Partial<{ costmap: Costmap; robotPose: Vector; gpsLocation: LatLon; gpsTravelGoalPoints: LatLon[]; path: Path }> = {}; - - if (data.costmap != undefined) { - state.costmap = Costmap.decode(data.costmap); - } - if (data.robot_pose != undefined) { - state.robotPose = Vector.decode(data.robot_pose); - } - if (data.gps_location != undefined) { - state.gpsLocation = data.gps_location; - } - if (data.path != undefined) { - state.path = Path.decode(data.path); - } - - this.dispatch({ type: "SET_FULL_STATE", payload: state }); - }); - } - - worldClick(worldX: number, worldY: number): void { - this.socket.emit("click", [worldX, worldY]); - } - - startExplore(): void { - this.socket.emit("start_explore"); - } - - stopExplore(): void { - this.socket.emit("stop_explore"); - } - - sendMoveCommand(linear: [number, number, number], angular: [number, number, number]): void { - const twist: TwistCommand = { - linear: { - x: linear[0], - y: linear[1], - z: linear[2], - }, - angular: { - x: angular[0], - y: angular[1], - z: angular[2], - }, - }; - this.socket.emit("move_command", twist); - } - - sendGpsGoal(goal: LatLon): void { - this.socket.emit("gps_goal", goal); - } - - stopMoveCommand(): void { - const twist: TwistCommand = { - linear: { x: 0, y: 0, z: 0 }, - angular: { x: 0, y: 0, z: 0 }, - }; - this.socket.emit("move_command", twist); - } - - disconnect(): void { - this.socket.disconnect(); - } -} diff --git a/dimos/web/command-center-extension/src/ExplorePanel.tsx b/dimos/web/command-center-extension/src/ExplorePanel.tsx deleted file mode 100644 index 6210664591..0000000000 --- a/dimos/web/command-center-extension/src/ExplorePanel.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; - -import Button from "./Button"; - -interface ExplorePanelProps { - onStartExplore: () => void; - onStopExplore: () => void; -} - -export default function ExplorePanel({ - onStartExplore, - onStopExplore, -}: ExplorePanelProps): React.ReactElement { - const [exploring, setExploring] = React.useState(false); - - return ( -
- {exploring ? ( - - ) : ( - - )} -
- ); -} diff --git a/dimos/web/command-center-extension/src/GpsButton.tsx b/dimos/web/command-center-extension/src/GpsButton.tsx deleted file mode 100644 index 74f0d73dfd..0000000000 --- a/dimos/web/command-center-extension/src/GpsButton.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; - -import Button from "./Button"; - -interface GpsButtonProps { - onUseGps: () => void; - onUseCostmap: () => void; -} - -export default function GpsButton({ - onUseGps, - onUseCostmap, -}: GpsButtonProps): React.ReactElement { - const [gps, setGps] = React.useState(false); - - return ( -
- {gps ? ( - - ) : ( - - )} -
- ); -} diff --git a/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx b/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx deleted file mode 100644 index d4f5402557..0000000000 --- a/dimos/web/command-center-extension/src/KeyboardControlPanel.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from "react"; - -import Button from "./Button"; - -interface KeyboardControlPanelProps { - onSendMoveCommand: (linear: [number, number, number], angular: [number, number, number]) => void; - onStopMoveCommand: () => void; -} - -const linearSpeed = 0.5; -const angularSpeed = 0.8; -const publishRate = 10.0; // Hz - -function calculateVelocities(keys: Set) { - let linearX = 0.0; - let linearY = 0.0; - let angularY = 0.0; - let angularZ = 0.0; - - let speedMultiplier = 1.0; - if (keys.has("Shift")) { - speedMultiplier = 2.0; // Boost mode - } else if (keys.has("Control")) { - speedMultiplier = 0.5; // Slow mode - } - - // Check for stop command (space) - if (keys.has(" ")) { - return { linearX: 0, linearY: 0, angularY: 0, angularZ: 0 }; - } - - // Linear X (forward/backward) - W/S - if (keys.has("w")) { - linearX = linearSpeed * speedMultiplier; - } else if (keys.has("s")) { - linearX = -linearSpeed * speedMultiplier; - } - - // Angular Z (yaw/turn) - A/D - if (keys.has("a")) { - angularZ = angularSpeed * speedMultiplier; - } else if (keys.has("d")) { - angularZ = -angularSpeed * speedMultiplier; - } - - // Linear Y (strafe) - Left/Right arrows - if (keys.has("ArrowLeft")) { - linearY = linearSpeed * speedMultiplier; - } else if (keys.has("ArrowRight")) { - linearY = -linearSpeed * speedMultiplier; - } - - // Angular Y (pitch) - Up/Down arrows - if (keys.has("ArrowUp")) { - angularY = angularSpeed * speedMultiplier; - } else if (keys.has("ArrowDown")) { - angularY = -angularSpeed * speedMultiplier; - } - - return { linearX, linearY, angularY, angularZ }; -} - -export default function KeyboardControlPanel({ - onSendMoveCommand, - onStopMoveCommand, -}: KeyboardControlPanelProps): React.ReactElement { - const [isActive, setIsActive] = React.useState(false); - const keysPressed = React.useRef>(new Set()); - const intervalRef = React.useRef(null); - - const handleKeyDown = React.useCallback((event: KeyboardEvent) => { - // Prevent default for arrow keys and space to avoid scrolling - if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " "].includes(event.key)) { - event.preventDefault(); - } - - const normalizedKey = event.key.length === 1 ? event.key.toLowerCase() : event.key; - keysPressed.current.add(normalizedKey); - }, []); - - const handleKeyUp = React.useCallback((event: KeyboardEvent) => { - const normalizedKey = event.key.length === 1 ? event.key.toLowerCase() : event.key; - keysPressed.current.delete(normalizedKey); - }, []); - - // Start/stop keyboard control - React.useEffect(() => { - keysPressed.current.clear(); - - if (!isActive) { - return undefined; - } - - document.addEventListener("keydown", handleKeyDown); - document.addEventListener("keyup", handleKeyUp); - - // Start publishing loop - intervalRef.current = setInterval(() => { - const velocities = calculateVelocities(keysPressed.current); - - onSendMoveCommand( - [velocities.linearX, velocities.linearY, 0], - [0, velocities.angularY, velocities.angularZ], - ); - }, 1000 / publishRate); - - return () => { - document.removeEventListener("keydown", handleKeyDown); - document.removeEventListener("keyup", handleKeyUp); - - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - - keysPressed.current.clear(); - onStopMoveCommand(); - }; - }, [isActive, handleKeyDown, handleKeyUp, onSendMoveCommand, onStopMoveCommand]); - - const toggleKeyboardControl = () => { - if (isActive) { - keysPressed.current.clear(); - setIsActive(false); - } else { - setIsActive(true); - } - }; - - React.useEffect(() => { - const handleBlur = () => { - if (isActive) { - keysPressed.current.clear(); - setIsActive(false); - } - }; - - const handleFocus = () => { - // Clear keys when window regains focus to avoid stuck keys - keysPressed.current.clear(); - }; - - window.addEventListener("blur", handleBlur); - window.addEventListener("focus", handleFocus); - - return () => { - window.removeEventListener("blur", handleBlur); - window.removeEventListener("focus", handleFocus); - }; - }, [isActive]); - - return ( -
- {isActive && ( -
-
Controls:
-
W/S: Forward/Backward | A/D: Turn
-
Arrows: Strafe/Pitch | Space: Stop
-
Shift: Boost | Ctrl: Slow
-
- )} - -
- ); -} diff --git a/dimos/web/command-center-extension/src/components/CostmapLayer.tsx b/dimos/web/command-center-extension/src/components/CostmapLayer.tsx deleted file mode 100644 index 3881f6f0d5..0000000000 --- a/dimos/web/command-center-extension/src/components/CostmapLayer.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { Costmap } from "../types"; -import GridLayer from "./GridLayer"; - -interface CostmapLayerProps { - costmap: Costmap; - width: number; - height: number; -} - -const CostmapLayer = React.memo(({ costmap, width, height }) => { - const canvasRef = React.useRef(null); - const { grid, origin, resolution } = costmap; - const rows = Math.max(1, grid.shape[0] || 1); - const cols = Math.max(1, grid.shape[1] || 1); - - const axisMargin = { left: 60, bottom: 40 }; - const availableWidth = Math.max(1, width - axisMargin.left); - const availableHeight = Math.max(1, height - axisMargin.bottom); - - const cell = Math.max(0, Math.min(availableWidth / cols, availableHeight / rows)); - const gridW = Math.max(0, cols * cell); - const gridH = Math.max(0, rows * cell); - const offsetX = axisMargin.left + (availableWidth - gridW) / 2; - const offsetY = (availableHeight - gridH) / 2; - - // Pre-compute color lookup table using exact D3 colors (computed once on mount) - const colorLookup = React.useMemo(() => { - const lookup = new Uint8ClampedArray(256 * 3); // RGB values for -1 to 254 (255 total values) - - const customColorScale = (t: number) => { - if (t === 0) { - return "black"; - } - if (t < 0) { - return "#2d2136"; - } - if (t > 0.95) { - return "#000000"; - } - - const color = d3.interpolateTurbo(t * 2 - 1); - const hsl = d3.hsl(color); - hsl.s *= 0.75; - return hsl.toString(); - }; - - const colour = d3.scaleSequential(customColorScale).domain([-1, 100]); - - // Pre-compute all 256 possible color values - for (let i = 0; i < 256; i++) { - const value = i === 255 ? -1 : i; - const colorStr = colour(value); - const c = d3.color(colorStr); - - if (c) { - const rgb = c as d3.RGBColor; - lookup[i * 3] = rgb.r; - lookup[i * 3 + 1] = rgb.g; - lookup[i * 3 + 2] = rgb.b; - } else { - lookup[i * 3] = 0; - lookup[i * 3 + 1] = 0; - lookup[i * 3 + 2] = 0; - } - } - - return lookup; - }, []); - - React.useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) { - return; - } - - // Validate grid data length matches dimensions - const expectedLength = rows * cols; - if (grid.data.length !== expectedLength) { - console.warn( - `Grid data length mismatch: expected ${expectedLength}, got ${grid.data.length} (rows=${rows}, cols=${cols})` - ); - } - - canvas.width = cols; - canvas.height = rows; - const ctx = canvas.getContext("2d"); - if (!ctx) { - return; - } - - const img = ctx.createImageData(cols, rows); - const data = grid.data; - const imgData = img.data; - - for (let i = 0; i < data.length && i < rows * cols; i++) { - const row = Math.floor(i / cols); - const col = i % cols; - const invertedRow = rows - 1 - row; - const srcIdx = invertedRow * cols + col; - - if (srcIdx < 0 || srcIdx >= data.length) { - continue; - } - - const value = data[i]!; - // Map value to lookup index (handle -1 -> 255 mapping) - const lookupIdx = value === -1 ? 255 : Math.min(254, Math.max(0, value)); - - const o = srcIdx * 4; - if (o < 0 || o + 3 >= imgData.length) { - continue; - } - - // Use pre-computed colors from lookup table - const colorOffset = lookupIdx * 3; - imgData[o] = colorLookup[colorOffset]!; - imgData[o + 1] = colorLookup[colorOffset + 1]!; - imgData[o + 2] = colorLookup[colorOffset + 2]!; - imgData[o + 3] = 255; - } - - ctx.putImageData(img, 0, 0); - }, [grid.data, cols, rows, colorLookup]); - - return ( - - -
- -
-
- -
- ); -}); - -CostmapLayer.displayName = "CostmapLayer"; - -export default CostmapLayer; diff --git a/dimos/web/command-center-extension/src/components/GridLayer.tsx b/dimos/web/command-center-extension/src/components/GridLayer.tsx deleted file mode 100644 index 87018cd3af..0000000000 --- a/dimos/web/command-center-extension/src/components/GridLayer.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { Vector } from "../types"; - -interface GridLayerProps { - width: number; - height: number; - origin: Vector; - resolution: number; - rows: number; - cols: number; -} - -const GridLayer = React.memo( - ({ width, height, origin, resolution, rows, cols }) => { - const minX = origin.coords[0]!; - const minY = origin.coords[1]!; - const maxX = minX + cols * resolution; - const maxY = minY + rows * resolution; - - const xScale = d3.scaleLinear().domain([minX, maxX]).range([0, width]); - const yScale = d3.scaleLinear().domain([minY, maxY]).range([height, 0]); - - const gridSize = 1 / resolution; - const gridLines = React.useMemo(() => { - const lines = []; - for (const x of d3.range(Math.ceil(minX / gridSize) * gridSize, maxX, gridSize)) { - lines.push( - , - ); - } - for (const y of d3.range(Math.ceil(minY / gridSize) * gridSize, maxY, gridSize)) { - lines.push( - , - ); - } - return lines; - }, [minX, minY, maxX, maxY, gridSize, xScale, yScale, width, height]); - - const xAxisRef = React.useRef(null); - const yAxisRef = React.useRef(null); - - React.useEffect(() => { - if (xAxisRef.current) { - const xAxis = d3.axisBottom(xScale).ticks(7); - d3.select(xAxisRef.current).call(xAxis); - d3.select(xAxisRef.current) - .selectAll("line,path") - .attr("stroke", "#ffffff") - .attr("stroke-width", 1); - d3.select(xAxisRef.current).selectAll("text").attr("fill", "#ffffff"); - } - if (yAxisRef.current) { - const yAxis = d3.axisLeft(yScale).ticks(7); - d3.select(yAxisRef.current).call(yAxis); - d3.select(yAxisRef.current) - .selectAll("line,path") - .attr("stroke", "#ffffff") - .attr("stroke-width", 1); - d3.select(yAxisRef.current).selectAll("text").attr("fill", "#ffffff"); - } - }, [xScale, yScale]); - - const showOrigin = minX <= 0 && 0 <= maxX && minY <= 0 && 0 <= maxY; - - return ( - <> - {gridLines} - - - {showOrigin && ( - - - - World Origin (0,0) - - - )} - - ); - }, -); - -GridLayer.displayName = "GridLayer"; - -export default GridLayer; diff --git a/dimos/web/command-center-extension/src/components/LeafletMap.tsx b/dimos/web/command-center-extension/src/components/LeafletMap.tsx deleted file mode 100644 index d0ad2380c4..0000000000 --- a/dimos/web/command-center-extension/src/components/LeafletMap.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import * as React from "react"; -import { MapContainer, TileLayer, Marker, Popup, useMapEvents } from "react-leaflet"; -import L, { LatLngExpression } from "leaflet"; -import { LatLon } from "../types"; - -// Fix for default marker icons in react-leaflet -// Using CDN URLs since webpack can't handle the image imports directly -const DefaultIcon = L.icon({ - iconUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png", - shadowUrl: "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png", - iconSize: [25, 41], - iconAnchor: [12, 41], -}); - -L.Marker.prototype.options.icon = DefaultIcon; - -// Component to handle map click events -function MapClickHandler({ onMapClick }: { onMapClick: (lat: number, lng: number) => void }) { - useMapEvents({ - click: (e) => { - onMapClick(e.latlng.lat, e.latlng.lng); - }, - }); - return null; -} - -interface LeafletMapProps { - gpsLocation: LatLon | null; - gpsTravelGoalPoints: LatLon[] | null; - onGpsGoal: (goal: LatLon) => void; -} - -const LeafletMap: React.FC = ({ gpsLocation, gpsTravelGoalPoints, onGpsGoal }) => { - if (!gpsLocation) { - return ( -
- GPS location not received yet. -
- ); - } - - const center: LatLngExpression = [gpsLocation.lat, gpsLocation.lon]; - - return ( -
- - - - { - onGpsGoal({ lat, lon: lng }); - }} /> - - Current GPS Location - - {gpsTravelGoalPoints !== null && ( - gpsTravelGoalPoints.map(p => ( - - )) - )} - -
- ); -}; - -const leafletCss = ` -.leaflet-control-container { - display: none; -} -.leaflet-container { - width: 100%; - height: 100%; - position: relative; -} -.leaflet-pane, -.leaflet-tile, -.leaflet-marker-icon, -.leaflet-marker-shadow, -.leaflet-tile-container, -.leaflet-pane > svg, -.leaflet-pane > canvas, -.leaflet-zoom-box, -.leaflet-image-layer, -.leaflet-layer { - position: absolute; - left: 0; - top: 0; -} -.leaflet-container { - overflow: hidden; - -webkit-tap-highlight-color: transparent; - background: #ddd; - outline: 0; - font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif; -} -.leaflet-tile { - filter: inherit; - visibility: hidden; -} -.leaflet-tile-loaded { - visibility: inherit; -} -.leaflet-zoom-box { - width: 0; - height: 0; - -moz-box-sizing: border-box; - box-sizing: border-box; - z-index: 800; -} -.leaflet-control { - position: relative; - z-index: 800; - pointer-events: visiblePainted; - pointer-events: auto; -} -.leaflet-top, -.leaflet-bottom { - position: absolute; - z-index: 1000; - pointer-events: none; -} -.leaflet-top { - top: 0; -} -.leaflet-right { - right: 0; -} -.leaflet-bottom { - bottom: 0; -} -.leaflet-left { - left: 0; -} -`; - -export default LeafletMap; diff --git a/dimos/web/command-center-extension/src/components/PathLayer.tsx b/dimos/web/command-center-extension/src/components/PathLayer.tsx deleted file mode 100644 index 969c9cf7dc..0000000000 --- a/dimos/web/command-center-extension/src/components/PathLayer.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { Path } from "../types"; - -interface PathLayerProps { - path: Path; - worldToPx: (x: number, y: number) => [number, number]; -} - -const PathLayer = React.memo(({ path, worldToPx }) => { - const points = React.useMemo( - () => path.coords.map(([x, y]) => worldToPx(x, y)), - [path.coords, worldToPx], - ); - - const pathData = React.useMemo(() => { - const line = d3.line(); - return line(points); - }, [points]); - - const gradientId = React.useMemo(() => `path-gradient-${Date.now()}`, []); - - if (path.coords.length < 2) { - return null; - } - - return ( - <> - - - - - - - - - ); -}); - -PathLayer.displayName = "PathLayer"; - -export default PathLayer; diff --git a/dimos/web/command-center-extension/src/components/VectorLayer.tsx b/dimos/web/command-center-extension/src/components/VectorLayer.tsx deleted file mode 100644 index 87b932d0a4..0000000000 --- a/dimos/web/command-center-extension/src/components/VectorLayer.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import * as React from "react"; - -import { Vector } from "../types"; - -interface VectorLayerProps { - vector: Vector; - label: string; - worldToPx: (x: number, y: number) => [number, number]; -} - -const VectorLayer = React.memo(({ vector, label, worldToPx }) => { - const [cx, cy] = worldToPx(vector.coords[0]!, vector.coords[1]!); - const text = `${label} (${vector.coords[0]!.toFixed(2)}, ${vector.coords[1]!.toFixed(2)})`; - - return ( - <> - - - - - - - - {text} - - - - ); -}); - -VectorLayer.displayName = "VectorLayer"; - -export default VectorLayer; diff --git a/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx b/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx deleted file mode 100644 index e5bdb7f58e..0000000000 --- a/dimos/web/command-center-extension/src/components/VisualizerComponent.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { Costmap, Path, Vector } from "../types"; -import CostmapLayer from "./CostmapLayer"; -import PathLayer from "./PathLayer"; -import VectorLayer from "./VectorLayer"; - -interface VisualizerComponentProps { - costmap: Costmap | null; - robotPose: Vector | null; - path: Path | null; -} - -const VisualizerComponent: React.FC = ({ costmap, robotPose, path }) => { - const svgRef = React.useRef(null); - const [dimensions, setDimensions] = React.useState({ width: 800, height: 600 }); - const { width, height } = dimensions; - - React.useEffect(() => { - if (!svgRef.current?.parentElement) { - return; - } - - const updateDimensions = () => { - const rect = svgRef.current?.parentElement?.getBoundingClientRect(); - if (rect) { - setDimensions({ width: rect.width, height: rect.height }); - } - }; - - updateDimensions(); - const observer = new ResizeObserver(updateDimensions); - observer.observe(svgRef.current.parentElement); - - return () => { - observer.disconnect(); - }; - }, []); - - const { worldToPx } = React.useMemo(() => { - if (!costmap) { - return { worldToPx: undefined }; - } - - const { - grid: { shape }, - origin, - resolution, - } = costmap; - const rows = shape[0]!; - const cols = shape[1]!; - - const axisMargin = { left: 60, bottom: 40 }; - const availableWidth = width - axisMargin.left; - const availableHeight = height - axisMargin.bottom; - - const cell = Math.min(availableWidth / cols, availableHeight / rows); - const gridW = cols * cell; - const gridH = rows * cell; - const offsetX = axisMargin.left + (availableWidth - gridW) / 2; - const offsetY = (availableHeight - gridH) / 2; - - const xScale = d3 - .scaleLinear() - .domain([origin.coords[0]!, origin.coords[0]! + cols * resolution]) - .range([offsetX, offsetX + gridW]); - - const yScale = d3 - .scaleLinear() - .domain([origin.coords[1]!, origin.coords[1]! + rows * resolution]) - .range([offsetY + gridH, offsetY]); - - const worldToPxFn = (x: number, y: number): [number, number] => [xScale(x), yScale(y)]; - - return { worldToPx: worldToPxFn }; - }, [costmap, width, height]); - - return ( -
- - {costmap && } - {path && worldToPx && } - {robotPose && worldToPx && ( - - )} - -
- ); -}; - -export default React.memo(VisualizerComponent); diff --git a/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx b/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx deleted file mode 100644 index e137019ae1..0000000000 --- a/dimos/web/command-center-extension/src/components/VisualizerWrapper.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as d3 from "d3"; -import * as React from "react"; - -import { AppState } from "../types"; -import VisualizerComponent from "./VisualizerComponent"; - -interface VisualizerWrapperProps { - data: AppState; - onWorldClick: (worldX: number, worldY: number) => void; -} - -const VisualizerWrapper: React.FC = ({ data, onWorldClick }) => { - const containerRef = React.useRef(null); - const lastClickTime = React.useRef(0); - const clickThrottleMs = 150; - - const handleClick = React.useCallback( - (event: React.MouseEvent) => { - if (!data.costmap || !containerRef.current) { - return; - } - - event.stopPropagation(); - - const now = Date.now(); - if (now - lastClickTime.current < clickThrottleMs) { - console.log("Click throttled"); - return; - } - lastClickTime.current = now; - - const svgElement = containerRef.current.querySelector("svg"); - if (!svgElement) { - return; - } - - const svgRect = svgElement.getBoundingClientRect(); - const clickX = event.clientX - svgRect.left; - const clickY = event.clientY - svgRect.top; - - const costmap = data.costmap; - const { - grid: { shape }, - origin, - resolution, - } = costmap; - const rows = shape[0]!; - const cols = shape[1]!; - const width = svgRect.width; - const height = svgRect.height; - - const axisMargin = { left: 60, bottom: 40 }; - const availableWidth = width - axisMargin.left; - const availableHeight = height - axisMargin.bottom; - - const cell = Math.min(availableWidth / cols, availableHeight / rows); - const gridW = cols * cell; - const gridH = rows * cell; - const offsetX = axisMargin.left + (availableWidth - gridW) / 2; - const offsetY = (availableHeight - gridH) / 2; - - const xScale = d3 - .scaleLinear() - .domain([origin.coords[0]!, origin.coords[0]! + cols * resolution]) - .range([offsetX, offsetX + gridW]); - const yScale = d3 - .scaleLinear() - .domain([origin.coords[1]!, origin.coords[1]! + rows * resolution]) - .range([offsetY + gridH, offsetY]); - - const worldX = xScale.invert(clickX); - const worldY = yScale.invert(clickY); - - onWorldClick(worldX, worldY); - }, - [data.costmap, onWorldClick], - ); - - return ( -
- -
- ); -}; - -export default VisualizerWrapper; diff --git a/dimos/web/command-center-extension/src/index.ts b/dimos/web/command-center-extension/src/index.ts deleted file mode 100644 index 052f967e37..0000000000 --- a/dimos/web/command-center-extension/src/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PanelExtensionContext, ExtensionContext } from "@foxglove/extension"; - -import { initializeApp } from "./init"; - -export function activate(extensionContext: ExtensionContext): void { - extensionContext.registerPanel({ name: "command-center", initPanel }); -} - -export function initPanel(context: PanelExtensionContext): () => void { - initializeApp(context.panelElement); - return () => { - // Cleanup function - }; -} diff --git a/dimos/web/command-center-extension/src/init.ts b/dimos/web/command-center-extension/src/init.ts deleted file mode 100644 index f57f3aa582..0000000000 --- a/dimos/web/command-center-extension/src/init.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from "react"; -import * as ReactDOMClient from "react-dom/client"; - -import App from "./App"; - -export function initializeApp(element: HTMLElement): void { - const root = ReactDOMClient.createRoot(element); - root.render(React.createElement(App)); -} diff --git a/dimos/web/command-center-extension/src/optimizedCostmap.ts b/dimos/web/command-center-extension/src/optimizedCostmap.ts deleted file mode 100644 index 2244437eab..0000000000 --- a/dimos/web/command-center-extension/src/optimizedCostmap.ts +++ /dev/null @@ -1,120 +0,0 @@ -import * as pako from 'pako'; - -export interface EncodedOptimizedGrid { - update_type: "full" | "delta"; - shape: [number, number]; - dtype: string; - compressed: boolean; - compression?: "zlib" | "none"; - data?: string; - chunks?: Array<{ - pos: [number, number]; - size: [number, number]; - data: string; - }>; -} - -export class OptimizedGrid { - private fullGrid: Uint8Array | null = null; - private shape: [number, number] = [0, 0]; - - decode(msg: EncodedOptimizedGrid): Float32Array { - if (msg.update_type === "full") { - return this.decodeFull(msg); - } else { - return this.decodeDelta(msg); - } - } - - private decodeFull(msg: EncodedOptimizedGrid): Float32Array { - if (!msg.data) { - throw new Error("Missing data for full update"); - } - - const binaryString = atob(msg.data); - const compressed = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - compressed[i] = binaryString.charCodeAt(i); - } - - // Decompress if needed - let decompressed: Uint8Array; - if (msg.compressed && msg.compression === "zlib") { - decompressed = pako.inflate(compressed); - } else { - decompressed = compressed; - } - - // Store for delta updates - this.fullGrid = decompressed; - this.shape = msg.shape; - - // Convert uint8 back to float32 costmap values - const float32Data = new Float32Array(decompressed.length); - for (let i = 0; i < decompressed.length; i++) { - // Map 255 back to -1 for unknown cells - const val = decompressed[i]!; - float32Data[i] = val === 255 ? -1 : val; - } - - return float32Data; - } - - private decodeDelta(msg: EncodedOptimizedGrid): Float32Array { - if (!this.fullGrid) { - console.warn("No full grid available for delta update - skipping until full update arrives"); - const size = msg.shape[0] * msg.shape[1]; - return new Float32Array(size).fill(-1); - } - - if (!msg.chunks) { - throw new Error("Missing chunks for delta update"); - } - - // Apply delta updates to the full grid - for (const chunk of msg.chunks) { - const [y, x] = chunk.pos; - const [h, w] = chunk.size; - - // Decode chunk data - const binaryString = atob(chunk.data); - const compressed = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - compressed[i] = binaryString.charCodeAt(i); - } - - let decompressed: Uint8Array; - if (msg.compressed && msg.compression === "zlib") { - decompressed = pako.inflate(compressed); - } else { - decompressed = compressed; - } - - // Update the full grid with chunk data - const width = this.shape[1]; - let chunkIdx = 0; - for (let cy = 0; cy < h; cy++) { - for (let cx = 0; cx < w; cx++) { - const gridIdx = (y + cy) * width + (x + cx); - const val = decompressed[chunkIdx++]; - if (val !== undefined) { - this.fullGrid[gridIdx] = val; - } - } - } - } - - // Convert to float32 - const float32Data = new Float32Array(this.fullGrid.length); - for (let i = 0; i < this.fullGrid.length; i++) { - const val = this.fullGrid[i]!; - float32Data[i] = val === 255 ? -1 : val; - } - - return float32Data; - } - - getShape(): [number, number] { - return this.shape; - } -} diff --git a/dimos/web/command-center-extension/src/standalone.tsx b/dimos/web/command-center-extension/src/standalone.tsx deleted file mode 100644 index 7fefcab0fd..0000000000 --- a/dimos/web/command-center-extension/src/standalone.tsx +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Standalone entry point for the Command Center React app. - * This allows the command center to run outside of Foxglove as a regular web page. - */ -import * as React from "react"; -import { createRoot } from "react-dom/client"; - -import App from "./App"; - -const container = document.getElementById("root"); -if (container) { - const root = createRoot(container); - root.render( - - - - ); -} else { - console.error("Root element not found"); -} diff --git a/dimos/web/command-center-extension/src/types.ts b/dimos/web/command-center-extension/src/types.ts deleted file mode 100644 index 5f3a804a9c..0000000000 --- a/dimos/web/command-center-extension/src/types.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { EncodedOptimizedGrid, OptimizedGrid } from './optimizedCostmap'; - -export type EncodedVector = Encoded<"vector"> & { - c: number[]; -}; - -export class Vector { - coords: number[]; - constructor(...coords: number[]) { - this.coords = coords; - } - - static decode(data: EncodedVector): Vector { - return new Vector(...data.c); - } -} - -export interface LatLon { - lat: number; - lon: number; - alt?: number; -} - -export type EncodedPath = Encoded<"path"> & { - points: Array<[number, number]>; -}; - -export class Path { - constructor(public coords: Array<[number, number]>) {} - - static decode(data: EncodedPath): Path { - return new Path(data.points); - } -} - -export type EncodedCostmap = Encoded<"costmap"> & { - grid: EncodedOptimizedGrid; - origin: EncodedVector; - resolution: number; - origin_theta: number; -}; - -export class Costmap { - constructor( - public grid: Grid, - public origin: Vector, - public resolution: number, - public origin_theta: number, - ) { - this.grid = grid; - this.origin = origin; - this.resolution = resolution; - this.origin_theta = origin_theta; - } - - private static decoder: OptimizedGrid | null = null; - - static decode(data: EncodedCostmap): Costmap { - // Use a singleton decoder to maintain state for delta updates - if (!Costmap.decoder) { - Costmap.decoder = new OptimizedGrid(); - } - - const float32Data = Costmap.decoder.decode(data.grid); - const shape = data.grid.shape; - - // Create a Grid object from the decoded data - const grid = new Grid(float32Data, shape); - - return new Costmap( - grid, - Vector.decode(data.origin), - data.resolution, - data.origin_theta, - ); - } -} - -export class Grid { - constructor( - public data: Float32Array | Float64Array | Int32Array | Int8Array, - public shape: number[], - ) {} -} - -export type Drawable = Costmap | Vector | Path; - -export type Encoded = { - type: T; -}; - -export interface FullStateData { - costmap?: EncodedCostmap; - robot_pose?: EncodedVector; - gps_location?: LatLon; - gps_travel_goal_points?: LatLon[]; - path?: EncodedPath; -} - -export interface TwistCommand { - linear: { - x: number; - y: number; - z: number; - }; - angular: { - x: number; - y: number; - z: number; - }; -} - -export interface AppState { - costmap: Costmap | null; - robotPose: Vector | null; - gpsLocation: LatLon | null; - gpsTravelGoalPoints: LatLon[] | null; - path: Path | null; -} - -export type AppAction = - | { type: "SET_COSTMAP"; payload: Costmap } - | { type: "SET_ROBOT_POSE"; payload: Vector } - | { type: "SET_GPS_LOCATION"; payload: LatLon } - | { type: "SET_GPS_TRAVEL_GOAL_POINTS"; payload: LatLon[] } - | { type: "SET_PATH"; payload: Path } - | { type: "SET_FULL_STATE"; payload: Partial }; diff --git a/dimos/web/command-center-extension/tsconfig.json b/dimos/web/command-center-extension/tsconfig.json deleted file mode 100644 index b4ead7c4a8..0000000000 --- a/dimos/web/command-center-extension/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "create-foxglove-extension/tsconfig/tsconfig.json", - "include": [ - "./src/**/*" - ], - "compilerOptions": { - "rootDir": "./src", - "outDir": "./dist", - "lib": [ - "dom" - ], - "composite": false, - "declaration": false, - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noUncheckedIndexedAccess": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "forceConsistentCasingInFileNames": true - } -} diff --git a/dimos/web/command-center-extension/vite.config.ts b/dimos/web/command-center-extension/vite.config.ts deleted file mode 100644 index 064f2bc7c5..0000000000 --- a/dimos/web/command-center-extension/vite.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import { resolve } from "path"; - -export default defineConfig({ - plugins: [react()], - root: ".", - build: { - outDir: "dist-standalone", - emptyDirBeforeWrite: true, - rollupOptions: { - input: { - main: resolve(__dirname, "index.html"), - }, - }, - }, - server: { - port: 3000, - open: false, - }, -}); diff --git a/dimos/web/dimos_interface/.gitignore b/dimos/web/dimos_interface/.gitignore deleted file mode 100644 index 8f2a0d7c82..0000000000 --- a/dimos/web/dimos_interface/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -# Dependencies and builds -node_modules -dist -dist-ssr -.vite/ -*.local -dist.zip -yarn.lock -package-lock.json - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Environment variables -.env -.env.* -!.env.example - -# GitHub directory from original repo -.github/ - -docs/ -vite.config.ts.timestamp-* -httpd.conf diff --git a/dimos/web/dimos_interface/__init__.py b/dimos/web/dimos_interface/__init__.py deleted file mode 100644 index 3bdc622cee..0000000000 --- a/dimos/web/dimos_interface/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -Dimensional Interface package -""" - -import lazy_loader as lazy - -__getattr__, __dir__, __all__ = lazy.attach( - __name__, - submod_attrs={ - "api.server": ["FastAPIServer"], - }, -) diff --git a/dimos/web/dimos_interface/api/README.md b/dimos/web/dimos_interface/api/README.md deleted file mode 100644 index a2c15015e8..0000000000 --- a/dimos/web/dimos_interface/api/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Unitree API Server - -This is a minimal FastAPI server implementation that provides API endpoints for the terminal frontend. - -## Quick Start - -```bash -# Navigate to the api directory -cd api - -# Install minimal requirements -pip install -r requirements.txt - -# Run the server -python unitree_server.py -``` - -The server will start on `http://0.0.0.0:5555`. - -## Integration with Frontend - -1. Start the API server as described above -2. In another terminal, run the frontend from the root directory: - ```bash - cd .. # Navigate to root directory (if you're in api/) - yarn dev - ``` -3. Use the `unitree` command in the terminal interface: - - `unitree status` - Check the API status - - `unitree command ` - Send a command to the API - -## Integration with DIMOS Agents - -See DimOS Documentation for more info. - -```python -from dimos.agents_deprecated.agent import OpenAIAgent -from dimos.robot.unitree.unitree_go2 import UnitreeGo2 -from dimos.robot.unitree.unitree_skills import MyUnitreeSkills -from dimos.web.robot_web_interface import RobotWebInterface - -robot_ip = os.getenv("ROBOT_IP") - -# Initialize robot -logger.info("Initializing Unitree Robot") -robot = UnitreeGo2(ip=robot_ip, - connection_method=connection_method, - output_dir=output_dir) - -# Set up video stream -logger.info("Starting video stream") -video_stream = robot.get_ros_video_stream() - -# Create FastAPI server with video stream -logger.info("Initializing FastAPI server") -streams = {"unitree_video": video_stream} -web_interface = RobotWebInterface(port=5555, **streams) - -# Initialize agent with robot skills -skills_instance = MyUnitreeSkills(robot=robot) - -agent = OpenAIAgent( - dev_name="UnitreeQueryPerceptionAgent", - input_query_stream=web_interface.query_stream, - output_dir=output_dir, - skills=skills_instance, -) - -web_interface.run() -``` - -## API Endpoints - -- **GET /unitree/status**: Check the status of the Unitree API -- **POST /unitree/command**: Send a command to the Unitree API - -## How It Works - -The frontend and backend are separate applications: - -1. The Svelte frontend runs on port 3000 via Vite -2. The FastAPI backend runs on port 5555 -3. Vite's development server proxies requests from `/unitree/*` to the FastAPI server -4. The `unitree` command in the terminal interface sends requests to these endpoints - -This architecture allows the frontend and backend to be developed and run independently. diff --git a/dimos/web/dimos_interface/api/__init__.py b/dimos/web/dimos_interface/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/dimos/web/dimos_interface/api/requirements.txt b/dimos/web/dimos_interface/api/requirements.txt deleted file mode 100644 index a1ab33e428..0000000000 --- a/dimos/web/dimos_interface/api/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi==0.104.1 -uvicorn==0.24.0 -reactivex==4.0.4 -numpy<2.0.0 # Specify older NumPy version for cv2 compatibility -opencv-python==4.8.1.78 -python-multipart==0.0.6 -jinja2==3.1.2 diff --git a/dimos/web/dimos_interface/api/server.py b/dimos/web/dimos_interface/api/server.py deleted file mode 100644 index 6692e90f46..0000000000 --- a/dimos/web/dimos_interface/api/server.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -# Working FastAPI/Uvicorn Impl. - -# Notes: Do not use simultaneously with Flask, this includes imports. -# Workers are not yet setup, as this requires a much more intricate -# reorganization. There appears to be possible signalling issues when -# opening up streams on multiple windows/reloading which will need to -# be fixed. Also note, Chrome only supports 6 simultaneous web streams, -# and its advised to test threading/worker performance with another -# browser like Safari. - -# Fast Api & Uvicorn -import asyncio - -# For audio processing -import io -from pathlib import Path -from queue import Empty, Queue -from threading import Lock -import time - -import cv2 -from fastapi import FastAPI, File, Form, HTTPException, Request, UploadFile -from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse -from fastapi.templating import Jinja2Templates -import ffmpeg # type: ignore[import-untyped] -import numpy as np -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import SingleAssignmentDisposable -import soundfile as sf # type: ignore[import-untyped] -from sse_starlette.sse import EventSourceResponse -import uvicorn - -from dimos.stream.audio.base import AudioEvent -from dimos.web.edge_io import EdgeIO - -# TODO: Resolve threading, start/stop stream functionality. - - -class FastAPIServer(EdgeIO): - def __init__( # type: ignore[no-untyped-def] - self, - dev_name: str = "FastAPI Server", - edge_type: str = "Bidirectional", - host: str = "0.0.0.0", - port: int = 5555, - text_streams=None, - audio_subject=None, - **streams, - ) -> None: - print("Starting FastAPIServer initialization...") # Debug print - super().__init__(dev_name, edge_type) - self.app = FastAPI() - self._server: uvicorn.Server | None = None - - # Add CORS middleware with more permissive settings for development - self.app.add_middleware( - CORSMiddleware, - allow_origins=["*"], # More permissive for development - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - expose_headers=["*"], - ) - - self.port = port - self.host = host - BASE_DIR = Path(__file__).resolve().parent - self.templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) - self.streams = streams - self.active_streams = {} - self.stream_locks = {key: Lock() for key in self.streams} - self.stream_queues = {} # type: ignore[var-annotated] - self.stream_disposables = {} # type: ignore[var-annotated] - - # Initialize text streams - self.text_streams = text_streams or {} - self.text_queues = {} # type: ignore[var-annotated] - self.text_disposables = {} - self.text_clients = set() # type: ignore[var-annotated] - - # Create a Subject for text queries - self.query_subject = rx.subject.Subject() # type: ignore[var-annotated] - self.query_stream = self.query_subject.pipe(ops.share()) - self.audio_subject = audio_subject - - for key in self.streams: - if self.streams[key] is not None: - self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_fastapi), ops.share() - ) - - # Set up text stream subscriptions - for key, stream in self.text_streams.items(): - if stream is not None: - self.text_queues[key] = Queue(maxsize=100) - disposable = stream.subscribe( - lambda text, k=key: self.text_queues[k].put(text) if text is not None else None, - lambda e, k=key: self.text_queues[k].put(None), - lambda k=key: self.text_queues[k].put(None), - ) - self.text_disposables[key] = disposable - self.disposables.add(disposable) - - print("Setting up routes...") # Debug print - self.setup_routes() - print("FastAPIServer initialization complete") # Debug print - - def process_frame_fastapi(self, frame): # type: ignore[no-untyped-def] - """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode(".jpg", frame) - return buffer.tobytes() - - def stream_generator(self, key): # type: ignore[no-untyped-def] - """Generate frames for a given video stream.""" - - def generate(): # type: ignore[no-untyped-def] - if key not in self.stream_queues: - self.stream_queues[key] = Queue(maxsize=10) - - frame_queue = self.stream_queues[key] - - # Clear any existing disposable for this stream - if key in self.stream_disposables: - self.stream_disposables[key].dispose() - - disposable = SingleAssignmentDisposable() - self.stream_disposables[key] = disposable - self.disposables.add(disposable) - - if key in self.active_streams: - with self.stream_locks[key]: - # Clear the queue before starting new subscription - while not frame_queue.empty(): - try: - frame_queue.get_nowait() - except Empty: - break - - disposable.disposable = self.active_streams[key].subscribe( - lambda frame: frame_queue.put(frame) if frame is not None else None, - lambda e: frame_queue.put(None), - lambda: frame_queue.put(None), - ) - - try: - while True: - try: - frame = frame_queue.get(timeout=1) - if frame is None: - break - yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") - except Empty: - # Instead of breaking, continue waiting for new frames - continue - finally: - if key in self.stream_disposables: - self.stream_disposables[key].dispose() - - return generate - - def create_video_feed_route(self, key): # type: ignore[no-untyped-def] - """Create a video feed route for a specific stream.""" - - async def video_feed(): # type: ignore[no-untyped-def] - return StreamingResponse( - self.stream_generator(key)(), # type: ignore[no-untyped-call] - media_type="multipart/x-mixed-replace; boundary=frame", - ) - - return video_feed - - async def text_stream_generator(self, key): # type: ignore[no-untyped-def] - """Generate SSE events for text stream.""" - client_id = id(object()) - self.text_clients.add(client_id) - - try: - while True: - if key not in self.text_queues: - yield {"event": "ping", "data": ""} - await asyncio.sleep(0.1) - continue - - try: - text = self.text_queues[key].get_nowait() - if text is not None: - yield {"event": "message", "id": key, "data": text} - else: - break - except Empty: - yield {"event": "ping", "data": ""} - await asyncio.sleep(0.1) - finally: - self.text_clients.remove(client_id) - - @staticmethod - def _decode_audio(raw: bytes) -> tuple[np.ndarray, int]: # type: ignore[type-arg] - """Convert the webm/opus blob sent by the browser into mono 16-kHz PCM.""" - try: - # Use ffmpeg to convert to 16-kHz mono 16-bit PCM WAV in memory - out, _ = ( - ffmpeg.input("pipe:0") - .output( - "pipe:1", - format="wav", - acodec="pcm_s16le", - ac=1, - ar="16000", - loglevel="quiet", - ) - .run(input=raw, capture_stdout=True, capture_stderr=True) - ) - # Load with soundfile (returns float32 by default) - audio, sr = sf.read(io.BytesIO(out), dtype="float32") - # Ensure 1-D array (mono) - if audio.ndim > 1: - audio = audio[:, 0] - return np.array(audio), sr - except Exception as exc: - print(f"ffmpeg decoding failed: {exc}") - return None, None # type: ignore[return-value] - - def setup_routes(self) -> None: - """Set up FastAPI routes.""" - - @self.app.get("/streams") - async def get_streams(): # type: ignore[no-untyped-def] - """Get list of available video streams""" - return {"streams": list(self.streams.keys())} - - @self.app.get("/text_streams") - async def get_text_streams(): # type: ignore[no-untyped-def] - """Get list of available text streams""" - return {"streams": list(self.text_streams.keys())} - - @self.app.get("/", response_class=HTMLResponse) - async def index(request: Request): # type: ignore[no-untyped-def] - stream_keys = list(self.streams.keys()) - text_stream_keys = list(self.text_streams.keys()) - return self.templates.TemplateResponse( - "index_fastapi.html", - { - "request": request, - "stream_keys": stream_keys, - "text_stream_keys": text_stream_keys, - "has_voice": self.audio_subject is not None, - }, - ) - - @self.app.post("/submit_query") - async def submit_query(query: str = Form(...)): # type: ignore[no-untyped-def] - # Using Form directly as a dependency ensures proper form handling - try: - if query: - # Emit the query through our Subject - self.query_subject.on_next(query) - return JSONResponse({"success": True, "message": "Query received"}) - return JSONResponse({"success": False, "message": "No query provided"}) - except Exception as e: - # Ensure we always return valid JSON even on error - return JSONResponse( - status_code=500, - content={"success": False, "message": f"Server error: {e!s}"}, - ) - - @self.app.post("/upload_audio") - async def upload_audio(file: UploadFile = File(...)): # type: ignore[no-untyped-def] - """Handle audio upload from the browser.""" - if self.audio_subject is None: - return JSONResponse( - status_code=400, - content={"success": False, "message": "Voice input not configured"}, - ) - - try: - data = await file.read() - audio_np, sr = self._decode_audio(data) - if audio_np is None: - return JSONResponse( - status_code=400, - content={"success": False, "message": "Unable to decode audio"}, - ) - - event = AudioEvent( - data=audio_np, - sample_rate=sr, - timestamp=time.time(), - channels=1 if audio_np.ndim == 1 else audio_np.shape[1], - ) - - # Push to reactive stream - self.audio_subject.on_next(event) - print(f"Received audio - {event.data.shape[0] / sr:.2f} s, {sr} Hz") - return {"success": True} - except Exception as e: - print(f"Failed to process uploaded audio: {e}") - return JSONResponse(status_code=500, content={"success": False, "message": str(e)}) - - # Unitree API endpoints - @self.app.get("/unitree/status") - async def unitree_status(): # type: ignore[no-untyped-def] - """Check the status of the Unitree API server""" - return JSONResponse({"status": "online", "service": "unitree"}) - - @self.app.post("/unitree/command") - async def unitree_command(request: Request): # type: ignore[no-untyped-def] - """Process commands sent from the terminal frontend""" - try: - data = await request.json() - command_text = data.get("command", "") - - # Emit the command through the query_subject - self.query_subject.on_next(command_text) - - response = { - "success": True, - "command": command_text, - "result": f"Processed command: {command_text}", - } - - return JSONResponse(response) - except Exception as e: - print(f"Error processing command: {e!s}") - return JSONResponse( - status_code=500, - content={"success": False, "message": f"Error processing command: {e!s}"}, - ) - - @self.app.get("/text_stream/{key}") - async def text_stream(key: str): # type: ignore[no-untyped-def] - if key not in self.text_streams: - raise HTTPException(status_code=404, detail=f"Text stream '{key}' not found") - return EventSourceResponse(self.text_stream_generator(key)) # type: ignore[no-untyped-call] - - for key in self.streams: - self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) # type: ignore[no-untyped-call] - - def run(self) -> None: - config = uvicorn.Config( - self.app, - host=self.host, - port=self.port, - log_level="error", # Reduce verbosity - ) - self._server = uvicorn.Server(config) - self._server.run() - - def shutdown(self) -> None: - if self._server is not None: - self._server.should_exit = True - - -if __name__ == "__main__": - server = FastAPIServer() - server.run() diff --git a/dimos/web/dimos_interface/api/templates/index_fastapi.html b/dimos/web/dimos_interface/api/templates/index_fastapi.html deleted file mode 100644 index 4cfe943fc7..0000000000 --- a/dimos/web/dimos_interface/api/templates/index_fastapi.html +++ /dev/null @@ -1,541 +0,0 @@ - - - - - - Unitree Robot Interface - - - Video Stream Example - - - -

Live Video Streams

- -
-

Ask a Question

-
- - - {% if has_voice %} - - {% endif %} -
-
-
- - - {% if text_stream_keys %} -
-

Text Streams

- {% for key in text_stream_keys %} -
-

{{ key.replace('_', ' ').title() }}

-
-
- - - -
-
- {% endfor %} -
- {% endif %} - -
- {% for key in stream_keys %} -
-

{{ key.replace('_', ' ').title() }}

- {{ key }} Feed -
- - -
-
- {% endfor %} -
- - - - - - diff --git a/dimos/web/dimos_interface/index.html b/dimos/web/dimos_interface/index.html deleted file mode 100644 index e98be4de0c..0000000000 --- a/dimos/web/dimos_interface/index.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - DimOS | Terminal - - - -
- - - - - diff --git a/dimos/web/dimos_interface/package.json b/dimos/web/dimos_interface/package.json deleted file mode 100644 index 3be3376bef..0000000000 --- a/dimos/web/dimos_interface/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "terminal", - "private": true, - "version": "0.0.1", - "type": "module", - "license": "MIT", - "author": { - "name": "S Pomichter", - "url": "https://dimensionalOS.com", - "email": "stashp@mit.edu" - }, - "funding": { - "type": "SAFE", - "url": "https://docdrop.org/static/drop-pdf/YC---Form-of-SAFE-Valuation-Cap-and-Discount--tNRDy.pdf" - }, - "donate": { - "type": "venmo", - "url": "https://venmo.com/u/StashPomichter" - }, - "repository": { - "type": "git", - "url": "https://github.com/m4tt72/terminal" - }, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-check --tsconfig ./tsconfig.json" - }, - "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.1", - "@tsconfig/svelte": "^5.0.2", - "@types/node": "^22.3.0", - "autoprefixer": "^10.4.16", - "postcss": "^8.4.32", - "svelte": "^4.2.8", - "svelte-check": "^3.6.2", - "tailwindcss": "^3.4.0", - "tslib": "^2.6.2", - "typescript": "^5.2.2", - "vite": "^5.0.13" - }, - "engines": { - "node": ">=18.17.0" - } -} diff --git a/dimos/web/dimos_interface/postcss.config.js b/dimos/web/dimos_interface/postcss.config.js deleted file mode 100644 index 574690b9d5..0000000000 --- a/dimos/web/dimos_interface/postcss.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/dimos/web/dimos_interface/public/icon.png b/dimos/web/dimos_interface/public/icon.png deleted file mode 100644 index 4b0b2f153a..0000000000 --- a/dimos/web/dimos_interface/public/icon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:92d072d6c735eb45c8c8004b99d825c39bd0d21b62d8b2115ef18a55b8a16cab -size 2147 diff --git a/dimos/web/dimos_interface/src/App.svelte b/dimos/web/dimos_interface/src/App.svelte deleted file mode 100644 index 8ca51f866d..0000000000 --- a/dimos/web/dimos_interface/src/App.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - - {#if import.meta.env.VITE_TRACKING_ENABLED === 'true'} - - {/if} - - -
- - - -
- - -
-
- - diff --git a/dimos/web/dimos_interface/src/app.css b/dimos/web/dimos_interface/src/app.css deleted file mode 100644 index 0a6e38b76b..0000000000 --- a/dimos/web/dimos_interface/src/app.css +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -@tailwind base; -@tailwind components; -@tailwind utilities; - -* { - font-family: monospace; -} - -* { - scrollbar-width: thin; - scrollbar-color: #888 #f1f1f1; -} - -::-webkit-scrollbar { - width: 5px; - height: 5px; -} - -::-webkit-scrollbar-track { - background: #f1f1f1; -} - -::-webkit-scrollbar-thumb { - background: #888; -} - -::-webkit-scrollbar-thumb:hover { - background: #555; -} diff --git a/dimos/web/dimos_interface/src/components/History.svelte b/dimos/web/dimos_interface/src/components/History.svelte deleted file mode 100644 index daa6d51a40..0000000000 --- a/dimos/web/dimos_interface/src/components/History.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -{#each $history as { command, outputs }} -
-
- - -
-

- -

{command}

-
-
- - {#each outputs as output} -

- {output} -

- {/each} -
-{/each} diff --git a/dimos/web/dimos_interface/src/components/Input.svelte b/dimos/web/dimos_interface/src/components/Input.svelte deleted file mode 100644 index 3a2b515f3d..0000000000 --- a/dimos/web/dimos_interface/src/components/Input.svelte +++ /dev/null @@ -1,109 +0,0 @@ - - - { - input.focus(); - }} -/> - -
-

- - -
diff --git a/dimos/web/dimos_interface/src/components/Ps1.svelte b/dimos/web/dimos_interface/src/components/Ps1.svelte deleted file mode 100644 index ad7c4ecc8e..0000000000 --- a/dimos/web/dimos_interface/src/components/Ps1.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -

- guest - @ - {hostname} - :~$ -

diff --git a/dimos/web/dimos_interface/src/components/StreamViewer.svelte b/dimos/web/dimos_interface/src/components/StreamViewer.svelte deleted file mode 100644 index 43fe4739dd..0000000000 --- a/dimos/web/dimos_interface/src/components/StreamViewer.svelte +++ /dev/null @@ -1,196 +0,0 @@ - - -
-
-
Unitree Robot Feeds
- {#if $streamStore.isVisible} - {#each streamUrls as {key, url}} -
- {#if url} - {`Robot handleError(key)} - on:load={() => handleLoad(key)} - /> - {/if} - {#if errorMessages[key]} -
- {errorMessages[key]} -
- {/if} -
- {/each} - {/if} - -
-
- - diff --git a/dimos/web/dimos_interface/src/components/VoiceButton.svelte b/dimos/web/dimos_interface/src/components/VoiceButton.svelte deleted file mode 100644 index a316836d2e..0000000000 --- a/dimos/web/dimos_interface/src/components/VoiceButton.svelte +++ /dev/null @@ -1,262 +0,0 @@ - - - - - - - - - diff --git a/dimos/web/dimos_interface/src/interfaces/command.ts b/dimos/web/dimos_interface/src/interfaces/command.ts deleted file mode 100644 index 376518a4c9..0000000000 --- a/dimos/web/dimos_interface/src/interfaces/command.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Command { - command: string; - outputs: string[]; -} diff --git a/dimos/web/dimos_interface/src/interfaces/theme.ts b/dimos/web/dimos_interface/src/interfaces/theme.ts deleted file mode 100644 index 91ba9e28c5..0000000000 --- a/dimos/web/dimos_interface/src/interfaces/theme.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface Theme { - name: string; - black: string; - red: string; - green: string; - yellow: string; - blue: string; - purple: string; - cyan: string; - white: string; - brightBlack: string; - brightRed: string; - brightGreen: string; - brightYellow: string; - brightBlue: string; - brightPurple: string; - brightCyan: string; - brightWhite: string; - foreground: string; - background: string; - cursorColor: string; -} diff --git a/dimos/web/dimos_interface/src/main.ts b/dimos/web/dimos_interface/src/main.ts deleted file mode 100644 index 72c8b953a3..0000000000 --- a/dimos/web/dimos_interface/src/main.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import './app.css'; -import App from './App.svelte'; - -const app = new App({ - target: document.getElementById('app'), -}); - -export default app; diff --git a/dimos/web/dimos_interface/src/stores/history.ts b/dimos/web/dimos_interface/src/stores/history.ts deleted file mode 100644 index 9b98f79e02..0000000000 --- a/dimos/web/dimos_interface/src/stores/history.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { writable } from 'svelte/store'; -import type { Command } from '../interfaces/command'; - -export const history = writable>( - JSON.parse(localStorage.getItem('history') || '[]'), -); - -history.subscribe((value) => { - localStorage.setItem('history', JSON.stringify(value)); -}); diff --git a/dimos/web/dimos_interface/src/stores/stream.ts b/dimos/web/dimos_interface/src/stores/stream.ts deleted file mode 100644 index 649fd515ce..0000000000 --- a/dimos/web/dimos_interface/src/stores/stream.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { writable, derived, get } from 'svelte/store'; -import { simulationManager, simulationStore } from '../utils/simulation'; -import { history } from './history'; - -// Get the server URL dynamically based on current location -const getServerUrl = () => { - // In production, use the same host as the frontend but on port 5555 - const hostname = window.location.hostname; - return `http://${hostname}:5555`; -}; - -interface StreamState { - isVisible: boolean; - url: string | null; - isLoading: boolean; - error: string | null; - streamKeys: string[]; - availableStreams: string[]; -} - -interface TextStreamState { - isStreaming: boolean; - messages: string[]; - currentStream: EventSource | null; - streamKey: string | null; -} - -const initialState: StreamState = { - isVisible: false, - url: null, - isLoading: false, - error: null, - streamKeys: [], - availableStreams: [] -}; - -const initialTextState: TextStreamState = { - isStreaming: false, - messages: [], - currentStream: null, - streamKey: null -}; - -export const streamStore = writable(initialState); -export const textStreamStore = writable(initialTextState); -// Derive stream state from both stores -export const combinedStreamState = derived( - [streamStore, simulationStore], - ([$stream, $simulation]) => ({ - ...$stream, - isLoading: $stream.isLoading || $simulation.isConnecting, - error: $stream.error || $simulation.error - }) -); - -// Function to fetch available streams -async function fetchAvailableStreams(): Promise { - try { - const response = await fetch(`${getServerUrl()}/streams`, { - headers: { - 'Accept': 'application/json' - } - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - return data.streams; - } catch (error) { - console.error('Failed to fetch available streams:', error); - return []; - } -} - -// Initialize store with available streams -fetchAvailableStreams().then(streams => { - streamStore.update(state => ({ ...state, availableStreams: streams })); -}); - -export const showStream = async (streamKey?: string) => { - streamStore.update(state => ({ ...state, isLoading: true, error: null })); - - try { - const streams = await fetchAvailableStreams(); - if (streams.length === 0) { - throw new Error('No video streams available'); - } - - // If streamKey is provided, only show that stream, otherwise show all available streams - const selectedStreams = streamKey ? [streamKey] : streams; - - streamStore.set({ - isVisible: true, - url: getServerUrl(), - streamKeys: selectedStreams, - isLoading: false, - error: null, - availableStreams: streams, - }); - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to connect to stream'; - streamStore.update(state => ({ - ...state, - isLoading: false, - error: errorMessage - })); - throw error; - } -}; - -export const hideStream = async () => { - await simulationManager.stopSimulation(); - streamStore.set(initialState); -}; - -// Simple store to track active event sources -const textEventSources: Record = {}; - -export const connectTextStream = (key: string): void => { - // Close existing stream if any - if (textEventSources[key]) { - textEventSources[key].close(); - delete textEventSources[key]; - } - - // Create new EventSource - const eventSource = new EventSource(`${getServerUrl()}/text_stream/${key}`); - textEventSources[key] = eventSource; - // Handle incoming messages - eventSource.addEventListener('message', (event) => { - // Append message to the last history entry - history.update(h => { - const lastEntry = h[h.length - 1]; - const newEntry = { - ...lastEntry, - outputs: [...lastEntry.outputs, event.data] - }; - return [ - ...h.slice(0, -1), - newEntry - ]; - }); - }); - - // Handle errors - eventSource.onerror = (error) => { - console.error('Stream error details:', { - key, - error, - readyState: eventSource.readyState, - url: eventSource.url - }); - eventSource.close(); - delete textEventSources[key]; - }; -}; - -export const disconnectTextStream = (key: string): void => { - if (textEventSources[key]) { - textEventSources[key].close(); - delete textEventSources[key]; - } -}; diff --git a/dimos/web/dimos_interface/src/stores/theme.ts b/dimos/web/dimos_interface/src/stores/theme.ts deleted file mode 100644 index 89d1aa466f..0000000000 --- a/dimos/web/dimos_interface/src/stores/theme.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { writable } from 'svelte/store'; -import themes from '../../themes.json'; -import type { Theme } from '../interfaces/theme'; - -const defaultColorscheme: Theme = themes.find((t) => t.name === 'DimOS')!; - -export const theme = writable( - JSON.parse( - localStorage.getItem('colorscheme') || JSON.stringify(defaultColorscheme), - ), -); - -theme.subscribe((value) => { - localStorage.setItem('colorscheme', JSON.stringify(value)); -}); diff --git a/dimos/web/dimos_interface/src/utils/commands.ts b/dimos/web/dimos_interface/src/utils/commands.ts deleted file mode 100644 index 53755630ac..0000000000 --- a/dimos/web/dimos_interface/src/utils/commands.ts +++ /dev/null @@ -1,374 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import packageJson from '../../package.json'; -import themes from '../../themes.json'; -import { get } from 'svelte/store'; -import { history } from '../stores/history'; -import { theme } from '../stores/theme'; -import { showStream, hideStream } from '../stores/stream'; -import { simulationStore, type SimulationState } from '../utils/simulation'; - -let bloop: string | null = null; -const hostname = window.location.hostname; -const bleepbloop = import.meta.env.VITE_ENV_VARIABLE; -const xXx_VaRiAbLeOfDeAtH_xXx = "01011010 01000100 01000110 01110100 01001101 00110010 00110100 01101011 01100001 01010111 00111001 01110101 01011000 01101010 01000101 01100111 01011001 01111010 01000010 01110100 01010000 00110011 01010110 01010101 01001101 01010111 00110101 01101110"; -function someRandomFunctionIforget(binary: string): string { - return atob(binary.split(' ').map(bin => String.fromCharCode(parseInt(bin, 2))).join('')); -} -const var23temp_pls_dont_touch = someRandomFunctionIforget(xXx_VaRiAbLeOfDeAtH_xXx); -const magic_url = "https://agsu5pgehztgo2fuuyip6dwuna0uneua.lambda-url.us-east-2.on.aws/"; - -type CommandResult = string | { - type: 'STREAM_START'; - streamKey: string; - initialMessage: string; -}; - -// Function to fetch available text stream keys -async function fetchTextStreamKeys(): Promise { - try { - const response = await fetch('/text_streams'); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - return data.streams; - } catch (error) { - console.error('Failed to fetch text stream keys:', error); - return []; - } -} - -// Cache the text stream keys -let textStreamKeys: string[] = []; -fetchTextStreamKeys().then(keys => { - textStreamKeys = keys; -}); - -export const commands: Record Promise | CommandResult> = { - help: () => 'Available commands: ' + Object.keys(commands).join(', '), - hostname: () => hostname, - whoami: () => 'guest', - join: () => 'Actively recruiting all-star contributors. Build the future of dimensional computing with us. Reach out to build@dimensionalOS.com', - date: () => new Date().toLocaleString(), - vi: () => `why use vi? try 'vim'`, - emacs: () => `why use emacs? try 'vim'`, - echo: (args: string[]) => args.join(' '), - sudo: (args: string[]) => { - window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); - - return `Permission denied: unable to run the command '${args[0]}'. Not based.`; - }, - theme: (args: string[]) => { - const usage = `Usage: theme [args]. - [args]: - ls: list all available themes - set: set theme to [theme] - - [Examples]: - theme ls - theme set gruvboxdark - `; - if (args.length === 0) { - return usage; - } - - switch (args[0]) { - case 'ls': { - const themeNames = themes.map((t) => t.name.toLowerCase()); - const formattedThemes = themeNames - .reduce((acc: string[], theme: string, i: number) => { - const readableTheme = theme.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase(); - const paddedTheme = readableTheme.padEnd(30, ' '); // Increased padding to 30 chars - if (i % 5 === 4 || i === themeNames.length - 1) { - return [...acc, paddedTheme + '\n']; - } - return [...acc, paddedTheme]; - }, []) - .join(''); - - return formattedThemes; - } - - case 'set': { - if (args.length !== 2) { - return usage; - } - - const selectedTheme = args[1]; - const t = themes.find((t) => t.name.toLowerCase() === selectedTheme); - - if (!t) { - return `Theme '${selectedTheme}' not found. Try 'theme ls' to see all available themes.`; - } - - theme.set(t); - - return `Theme set to ${selectedTheme}`; - } - - default: { - return usage; - } - } - }, - clear: () => { - history.set([]); - - return ''; - }, - contact: () => { - window.open(`mailto:${packageJson.author.email}`); - - return `Opening mailto:${packageJson.author.email}...`; - }, - donate: () => { - window.open(packageJson.donate.url, '_blank'); - - return 'Opening donation url...'; - }, - invest: () => { - window.open(packageJson.funding.url, '_blank'); - - return 'Opening SAFE url...'; - }, - weather: async (args: string[]) => { - const city = args.join('+'); - - if (!city) { - return 'Usage: weather [city]. Example: weather Brussels'; - } - - const weather = await fetch(`https://wttr.in/${city}?ATm`); - - return weather.text(); - }, - - ls: () => { - return 'whitepaper.txt'; - }, - cd: () => { - return 'Permission denied: you are not that guy, pal'; - }, - curl: async (args: string[]) => { - if (args.length === 0) { - return 'curl: no URL provided'; - } - - const url = args[0]; - - try { - const response = await fetch(url); - const data = await response.text(); - - return data; - } catch (error) { - return `curl: could not fetch URL ${url}. Details: ${error}`; - } - }, - banner: () => ` - -██████╗ ██╗███╗ ███╗███████╗███╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗ -██╔══██╗██║████╗ ████║██╔════╝████╗ ██║██╔════╝██║██╔═══██╗████╗ ██║██╔══██╗██║ -██║ ██║██║██╔████╔██║█████╗ ██╔██╗ ██║███████╗██║██║ ██║██╔██╗ ██║███████║██║ -██║ ██║██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║ -██████╔╝██║██║ ╚═╝ ██║███████╗██║ ╚████║███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗ -╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝v${packageJson.version} - -Powering generalist robotics - -Type 'help' to see list of available commands. -`, - vim: async (args: string[])=> { - const filename = args.join(' '); - - if (!filename) { - return 'Usage: vim [filename]. Example: vim robbie.txt'; - } - - if (filename === "whitepaper.txt") { - if (bloop === null) { - return `File ${filename} is encrypted. Use 'vim -x ${filename}' to access.`; - } else { - return `Incorrect encryption key for ${filename}. Access denied.`; - } - } - - if (args[0] === '-x' && args[1] === "whitepaper.txt") { - const bloop_master = prompt("Enter encryption key:"); - - if (bloop_master === var23temp_pls_dont_touch) { - try { - const response = await fetch(magic_url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({key: bloop_master}), - }); - - if (response.status === 403) { - return "Access denied. You are not worthy."; - } - - if (response.ok) { - const manifestoText = await response.text(); - bloop = bloop_master; - return manifestoText; - } else { - return "Failed to retrieve. You are not worthy."; - } - } catch (error) { - return `Error: ${error.message}`; - } - } else { - return "Access denied. You are not worthy."; - } - } - - return `bash: ${filename}: No such file`; - }, - simulate: (args: string[]) => { - if (args.length === 0) { - return 'Usage: simulate [start|stop] - Start or stop the simulation stream'; - } - - const command = args[0].toLowerCase(); - - if (command === 'stop') { - hideStream(); - return 'Stream stopped.'; - } - - if (command === 'start') { - showStream(); - return 'Starting simulation stream... Use "simulate stop" to end the stream'; - } - - return 'Invalid command. Use "simulate start" to begin or "simulate stop" to end.'; - }, - control: async (args: string[]) => { - if (args.length === 0) { - return 'Usage: control [joint_positions] - Send comma-separated joint positions to control the robot\nExample: control 0,0,0.5,1,0.3'; - } - - const state = get(simulationStore) as SimulationState; - if (!state.connection) { - return 'Error: No active simulation. Use "simulate start" first.'; - } - - const jointPositions = args.join(' '); - - try { - const jointPositionsArray = jointPositions.split(',').map(x => parseFloat(x.trim())); - const response = await fetch(`${state.connection.url}/control?t=${Date.now()}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json' - }, - body: JSON.stringify({ joint_positions: jointPositionsArray }) - }); - - const data = await response.json(); - - if (response.ok) { - return `${data.message} ✓`; - } else { - return `Error: ${data.message}`; - } - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return `Failed to send command: ${errorMessage}. Make sure the simulator is running.`; - } - }, - unitree: async (args: string[]) => { - if (args.length === 0) { - return 'Usage: unitree [status|start_stream|stop_stream|command ] - Interact with the Unitree API'; - } - - const subcommand = args[0].toLowerCase(); - - if (subcommand === 'status') { - try { - const response = await fetch('/unitree/status'); - if (!response.ok) { - throw new Error(`Server returned ${response.status}`); - } - const data = await response.json(); - return `Unitree API Status: ${data.status}`; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Server unreachable'; - return `Failed to get Unitree status: ${message}. Make sure the API server is running.`; - } - } - - if (subcommand === 'start_stream') { - try { - showStream(); - return 'Starting Unitree video stream... Use "unitree stop_stream" to end the stream'; - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Server unreachable'; - return `Failed to start video stream: ${message}. Make sure the API server is running.`; - } - } - - if (subcommand === 'stop_stream') { - hideStream(); - return 'Stopped Unitree video stream.'; - } - - if (subcommand === 'command') { - if (args.length < 2) { - return 'Usage: unitree command - Send a command to the Unitree API'; - } - - const commandText = args.slice(1).join(' '); - - try { - // Ensure we have the text stream keys - if (textStreamKeys.length === 0) { - textStreamKeys = await fetchTextStreamKeys(); - } - - const response = await fetch('/unitree/command', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ command: commandText }) - }); - - if (!response.ok) { - throw new Error(`Server returned ${response.status}`); - } - - return { - type: 'STREAM_START' as const, - streamKey: textStreamKeys[0], // Using the first available text stream - initialMessage: `Command sent: ${commandText}\nPlanningAgent output...` - }; - - } catch (error) { - const message = error instanceof Error ? error.message : 'Server unreachable'; - return `Failed to send command: ${message}. Make sure the API server is running.`; - } - } - - return 'Invalid subcommand. Available subcommands: status, start_stream, stop_stream, command'; - }, -}; diff --git a/dimos/web/dimos_interface/src/utils/simulation.ts b/dimos/web/dimos_interface/src/utils/simulation.ts deleted file mode 100644 index 6e71dda358..0000000000 --- a/dimos/web/dimos_interface/src/utils/simulation.ts +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { writable, get } from 'svelte/store'; - -interface SimulationConnection { - url: string; - instanceId: string; - expiresAt: number; -} - -export interface SimulationState { - connection: SimulationConnection | null; - isConnecting: boolean; - error: string | null; - lastActivityTime: number; -} - -const initialState: SimulationState = { - connection: null, - isConnecting: false, - error: null, - lastActivityTime: 0 -}; - -export const simulationStore = writable(initialState); - -class SimulationError extends Error { - constructor(message: string) { - super(message); - this.name = 'SimulationError'; - } -} - -export class SimulationManager { - private static readonly PROD_API_ENDPOINT = 'https://0rqz7w5rvf.execute-api.us-east-2.amazonaws.com/default/getGenesis'; - private static readonly DEV_API_ENDPOINT = '/api'; // This will be handled by Vite's proxy - private static readonly MAX_RETRIES = 3; - private static readonly RETRY_DELAY = 1000; - private static readonly INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5 minutes in milliseconds - private inactivityTimer: NodeJS.Timeout | null = null; - - private get apiEndpoint(): string { - return import.meta.env.DEV ? SimulationManager.DEV_API_ENDPOINT : SimulationManager.PROD_API_ENDPOINT; - } - - private async fetchWithRetry(url: string, options: RequestInit = {}, retries = SimulationManager.MAX_RETRIES): Promise { - try { - const response = await fetch(url, { - ...options, - headers: { - ...options.headers, - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - }); - - if (import.meta.env.DEV && !response.ok) { - console.error('Request failed:', { - status: response.status, - statusText: response.statusText, - headers: Object.fromEntries(response.headers.entries()), - url - }); - } - - if (!response.ok) { - throw new SimulationError(`HTTP error! status: ${response.status} - ${response.statusText}`); - } - return response; - } catch (error) { - if (retries > 0) { - console.warn(`Request failed, retrying... (${retries} attempts left)`); - await new Promise(resolve => setTimeout(resolve, SimulationManager.RETRY_DELAY)); - return this.fetchWithRetry(url, options, retries - 1); - } - throw error; - } - } - - private startInactivityTimer() { - if (this.inactivityTimer) { - clearTimeout(this.inactivityTimer); - } - - this.inactivityTimer = setTimeout(async () => { - const state = get(simulationStore); - const now = Date.now(); - if (state.lastActivityTime && (now - state.lastActivityTime) >= SimulationManager.INACTIVITY_TIMEOUT) { - await this.stopSimulation(); - } - }, SimulationManager.INACTIVITY_TIMEOUT); - } - - private updateActivityTime() { - simulationStore.update(state => ({ - ...state, - lastActivityTime: Date.now() - })); - this.startInactivityTimer(); - } - - async requestSimulation(): Promise { - simulationStore.update(state => ({ ...state, isConnecting: true, error: null })); - - try { - // Request instance allocation - const response = await this.fetchWithRetry(this.apiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - user_id: 'user-' + Date.now() - }) - }); - - const instanceInfo = await response.json(); - - if (import.meta.env.DEV) { - console.log('API Response:', instanceInfo); - } - - if (!instanceInfo.instance_id || !instanceInfo.public_ip || !instanceInfo.port) { - throw new SimulationError( - `Invalid API response: Missing required fields. Got: ${JSON.stringify(instanceInfo)}` - ); - } - - // In development, use direct HTTP to EC2. In production, use HTTPS through ALB - const connection = { - instanceId: instanceInfo.instance_id, - url: import.meta.env.DEV - ? `http://${instanceInfo.public_ip}:${instanceInfo.port}` - : `https://sim.dimensionalos.com`, - expiresAt: Date.now() + SimulationManager.INACTIVITY_TIMEOUT - }; - - if (import.meta.env.DEV) { - console.log('Creating stream connection:', { - instanceId: connection.instanceId, - url: connection.url, - isDev: true, - expiresAt: new Date(connection.expiresAt).toISOString() - }); - } - - simulationStore.update(state => ({ - ...state, - connection, - isConnecting: false, - lastActivityTime: Date.now() - })); - - this.startInactivityTimer(); - return connection; - - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to request simulation'; - simulationStore.update(state => ({ - ...state, - isConnecting: false, - error: errorMessage - })); - - if (import.meta.env.DEV) { - console.error('Simulation request failed:', error); - } - - throw error; - } - } - - async stopSimulation() { - const state = get(simulationStore); - if (state.connection) { - try { - await this.fetchWithRetry(this.apiEndpoint, { - method: 'DELETE', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - instance_id: state.connection.instanceId - }) - }); - } catch (error) { - console.error('Error releasing instance:', error); - } - } - - if (this.inactivityTimer) { - clearTimeout(this.inactivityTimer); - this.inactivityTimer = null; - } - - simulationStore.set(initialState); - } -} - -export const simulationManager = new SimulationManager(); diff --git a/dimos/web/dimos_interface/src/utils/tracking.ts b/dimos/web/dimos_interface/src/utils/tracking.ts deleted file mode 100644 index 9cb71fdf4a..0000000000 --- a/dimos/web/dimos_interface/src/utils/tracking.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -declare global { - interface Window { - umami: { - track: (event: string, data?: Record) => Promise; - }; - } -} - -export const track = (cmd: string, ...args: string[]) => { - if (window.umami) { - window.umami.track(cmd, { - args: args.join(' '), - }); - } -}; diff --git a/dimos/web/dimos_interface/src/vite-env.d.ts b/dimos/web/dimos_interface/src/vite-env.d.ts deleted file mode 100644 index 562d8decf2..0000000000 --- a/dimos/web/dimos_interface/src/vite-env.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/// -/// diff --git a/dimos/web/dimos_interface/svelte.config.js b/dimos/web/dimos_interface/svelte.config.js deleted file mode 100644 index 9d9fd8b8c7..0000000000 --- a/dimos/web/dimos_interface/svelte.config.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' - -export default { - // Consult https://svelte.dev/docs#compile-time-svelte-preprocess - // for more information about preprocessors - preprocess: vitePreprocess(), -} diff --git a/dimos/web/dimos_interface/tailwind.config.js b/dimos/web/dimos_interface/tailwind.config.js deleted file mode 100644 index 9fc7e4b399..0000000000 --- a/dimos/web/dimos_interface/tailwind.config.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./index.html', './src/**/*.{svelte,js,ts,jsx,tsx}'], - theme: {}, - plugins: [], -}; diff --git a/dimos/web/dimos_interface/themes.json b/dimos/web/dimos_interface/themes.json deleted file mode 100644 index 910cc27f93..0000000000 --- a/dimos/web/dimos_interface/themes.json +++ /dev/null @@ -1,4974 +0,0 @@ -[ - { - "name": "DimOS", - "black": "#0b0f0f", - "red": "#ff0000", - "green": "#00eeee", - "yellow": "#ffcc00", - "blue": "#5c9ff0", - "purple": "#00eeee", - "cyan": "#00eeee", - "white": "#b5e4f4", - "brightBlack": "#404040", - "brightRed": "#ff0000", - "brightGreen": "#00eeee", - "brightYellow": "#f2ea8c", - "brightBlue": "#8cbdf2", - "brightPurple": "#00eeee", - "brightCyan": "#00eeee", - "brightWhite": "#ffffff", - "foreground": "#b5e4f4", - "background": "#0b0f0f", - "cursorColor": "#00eeee" - }, - { - "name": "3024Day", - "black": "#090300", - "red": "#db2d20", - "green": "#01a252", - "yellow": "#fded02", - "blue": "#01a0e4", - "purple": "#a16a94", - "cyan": "#b5e4f4", - "white": "#a5a2a2", - "brightBlack": "#5c5855", - "brightRed": "#e8bbd0", - "brightGreen": "#3a3432", - "brightYellow": "#4a4543", - "brightBlue": "#807d7c", - "brightPurple": "#d6d5d4", - "brightCyan": "#cdab53", - "brightWhite": "#f7f7f7", - "foreground": "#4a4543", - "background": "#f7f7f7", - "cursorColor": "#4a4543" - }, - { - "name": "3024Night", - "black": "#090300", - "red": "#db2d20", - "green": "#01a252", - "yellow": "#fded02", - "blue": "#01a0e4", - "purple": "#a16a94", - "cyan": "#b5e4f4", - "white": "#a5a2a2", - "brightBlack": "#5c5855", - "brightRed": "#e8bbd0", - "brightGreen": "#3a3432", - "brightYellow": "#4a4543", - "brightBlue": "#807d7c", - "brightPurple": "#d6d5d4", - "brightCyan": "#cdab53", - "brightWhite": "#f7f7f7", - "foreground": "#a5a2a2", - "background": "#090300", - "cursorColor": "#a5a2a2" - }, - { - "name": "Aci", - "black": "#363636", - "red": "#ff0883", - "green": "#83ff08", - "yellow": "#ff8308", - "blue": "#0883ff", - "purple": "#8308ff", - "cyan": "#08ff83", - "white": "#b6b6b6", - "brightBlack": "#424242", - "brightRed": "#ff1e8e", - "brightGreen": "#8eff1e", - "brightYellow": "#ff8e1e", - "brightBlue": "#1e8eff", - "brightPurple": "#8e1eff", - "brightCyan": "#1eff8e", - "brightWhite": "#c2c2c2", - "foreground": "#b4e1fd", - "background": "#0d1926", - "cursorColor": "#b4e1fd" - }, - { - "name": "Aco", - "black": "#3f3f3f", - "red": "#ff0883", - "green": "#83ff08", - "yellow": "#ff8308", - "blue": "#0883ff", - "purple": "#8308ff", - "cyan": "#08ff83", - "white": "#bebebe", - "brightBlack": "#474747", - "brightRed": "#ff1e8e", - "brightGreen": "#8eff1e", - "brightYellow": "#ff8e1e", - "brightBlue": "#1e8eff", - "brightPurple": "#8e1eff", - "brightCyan": "#1eff8e", - "brightWhite": "#c4c4c4", - "foreground": "#b4e1fd", - "background": "#1f1305", - "cursorColor": "#b4e1fd" - }, - { - "name": "AdventureTime", - "black": "#050404", - "red": "#bd0013", - "green": "#4ab118", - "yellow": "#e7741e", - "blue": "#0f4ac6", - "purple": "#665993", - "cyan": "#70a598", - "white": "#f8dcc0", - "brightBlack": "#4e7cbf", - "brightRed": "#fc5f5a", - "brightGreen": "#9eff6e", - "brightYellow": "#efc11a", - "brightBlue": "#1997c6", - "brightPurple": "#9b5953", - "brightCyan": "#c8faf4", - "brightWhite": "#f6f5fb", - "foreground": "#f8dcc0", - "background": "#1f1d45", - "cursorColor": "#f8dcc0" - }, - { - "name": "Afterglow", - "black": "#151515", - "red": "#a53c23", - "green": "#7b9246", - "yellow": "#d3a04d", - "blue": "#6c99bb", - "purple": "#9f4e85", - "cyan": "#7dd6cf", - "white": "#d0d0d0", - "brightBlack": "#505050", - "brightRed": "#a53c23", - "brightGreen": "#7b9246", - "brightYellow": "#d3a04d", - "brightBlue": "#547c99", - "brightPurple": "#9f4e85", - "brightCyan": "#7dd6cf", - "brightWhite": "#f5f5f5", - "foreground": "#d0d0d0", - "background": "#222222", - "cursorColor": "#d0d0d0" - }, - { - "name": "AlienBlood", - "black": "#112616", - "red": "#7f2b27", - "green": "#2f7e25", - "yellow": "#717f24", - "blue": "#2f6a7f", - "purple": "#47587f", - "cyan": "#327f77", - "white": "#647d75", - "brightBlack": "#3c4812", - "brightRed": "#e08009", - "brightGreen": "#18e000", - "brightYellow": "#bde000", - "brightBlue": "#00aae0", - "brightPurple": "#0058e0", - "brightCyan": "#00e0c4", - "brightWhite": "#73fa91", - "foreground": "#637d75", - "background": "#0f1610", - "cursorColor": "#637d75" - }, - { - "name": "Argonaut", - "black": "#232323", - "red": "#ff000f", - "green": "#8ce10b", - "yellow": "#ffb900", - "blue": "#008df8", - "purple": "#6d43a6", - "cyan": "#00d8eb", - "white": "#ffffff", - "brightBlack": "#444444", - "brightRed": "#ff2740", - "brightGreen": "#abe15b", - "brightYellow": "#ffd242", - "brightBlue": "#0092ff", - "brightPurple": "#9a5feb", - "brightCyan": "#67fff0", - "brightWhite": "#ffffff", - "foreground": "#fffaf4", - "background": "#0e1019", - "cursorColor": "#fffaf4" - }, - { - "name": "Arthur", - "black": "#3d352a", - "red": "#cd5c5c", - "green": "#86af80", - "yellow": "#e8ae5b", - "blue": "#6495ed", - "purple": "#deb887", - "cyan": "#b0c4de", - "white": "#bbaa99", - "brightBlack": "#554444", - "brightRed": "#cc5533", - "brightGreen": "#88aa22", - "brightYellow": "#ffa75d", - "brightBlue": "#87ceeb", - "brightPurple": "#996600", - "brightCyan": "#b0c4de", - "brightWhite": "#ddccbb", - "foreground": "#ddeedd", - "background": "#1c1c1c", - "cursorColor": "#ddeedd" - }, - { - "name": "Atom", - "black": "#000000", - "red": "#fd5ff1", - "green": "#87c38a", - "yellow": "#ffd7b1", - "blue": "#85befd", - "purple": "#b9b6fc", - "cyan": "#85befd", - "white": "#e0e0e0", - "brightBlack": "#000000", - "brightRed": "#fd5ff1", - "brightGreen": "#94fa36", - "brightYellow": "#f5ffa8", - "brightBlue": "#96cbfe", - "brightPurple": "#b9b6fc", - "brightCyan": "#85befd", - "brightWhite": "#e0e0e0", - "foreground": "#c5c8c6", - "background": "#161719", - "cursorColor": "#c5c8c6" - }, - { - "name": "Aura", - "black": "#110f18", - "red": "#ff6767", - "green": "#61ffca", - "yellow": "#ffca85", - "blue": "#a277ff", - "purple": "#a277ff", - "cyan": "#61ffca", - "white": "#edecee", - "brightBlack": "#6d6d6d", - "brightRed": "#ffca85", - "brightGreen": "#a277ff", - "brightYellow": "#ffca85", - "brightBlue": "#a277ff", - "brightPurple": "#a277ff", - "brightCyan": "#61ffca", - "brightWhite": "#edecee", - "foreground": "#edecee", - "background": "#15141B", - "cursorColor": "#edecee" - }, - { - "name": "AyuDark", - "black": "#0A0E14", - "red": "#FF3333", - "green": "#C2D94C", - "yellow": "#FF8F40", - "blue": "#59C2FF", - "purple": "#FFEE99", - "cyan": "#95E6CB", - "white": "#B3B1AD", - "brightBlack": "#4D5566", - "brightRed": "#FF3333", - "brightGreen": "#C2D94C", - "brightYellow": "#FF8F40", - "brightBlue": "#59C2FF", - "brightPurple": "#FFEE99", - "brightCyan": "#95E6CB", - "brightWhite": "#B3B1AD", - "foreground": "#B3B1AD", - "background": "#0A0E14", - "cursorColor": "#E6B450" - }, - { - "name": "AyuLight", - "black": "#575F66", - "red": "#F51818", - "green": "#86B300", - "yellow": "#F2AE49", - "blue": "#399EE6", - "purple": "#A37ACC", - "cyan": "#4CBF99", - "white": "#FAFAFA", - "brightBlack": "#8A9199", - "brightRed": "#F51818", - "brightGreen": "#86B300", - "brightYellow": "#F2AE49", - "brightBlue": "#399EE6", - "brightPurple": "#A37ACC", - "brightCyan": "#4CBF99", - "brightWhite": "#FAFAFA", - "foreground": "#575F66", - "background": "#FAFAFA", - "cursorColor": "#FF9940" - }, - { - "name": "AyuMirage", - "black": "#1F2430", - "red": "#FF3333", - "green": "#BAE67E", - "yellow": "#FFA759", - "blue": "#73D0FF", - "purple": "#D4BFFF", - "cyan": "#95E6CB", - "white": "#CBCCC6", - "brightBlack": "#707A8C", - "brightRed": "#FF3333", - "brightGreen": "#BAE67E", - "brightYellow": "#FFA759", - "brightBlue": "#73D0FF", - "brightPurple": "#D4BFFF", - "brightCyan": "#95E6CB", - "brightWhite": "#CBCCC6", - "foreground": "#CBCCC6", - "background": "#1F2430", - "cursorColor": "#FFCC66" - }, - { - "name": "Azu", - "black": "#000000", - "red": "#ac6d74", - "green": "#74ac6d", - "yellow": "#aca46d", - "blue": "#6d74ac", - "purple": "#a46dac", - "cyan": "#6daca4", - "white": "#e6e6e6", - "brightBlack": "#262626", - "brightRed": "#d6b8bc", - "brightGreen": "#bcd6b8", - "brightYellow": "#d6d3b8", - "brightBlue": "#b8bcd6", - "brightPurple": "#d3b8d6", - "brightCyan": "#b8d6d3", - "brightWhite": "#ffffff", - "foreground": "#d9e6f2", - "background": "#09111a", - "cursorColor": "#d9e6f2" - }, - { - "name": "BelafonteDay", - "black": "#20111b", - "red": "#be100e", - "green": "#858162", - "yellow": "#eaa549", - "blue": "#426a79", - "purple": "#97522c", - "cyan": "#989a9c", - "white": "#968c83", - "brightBlack": "#5e5252", - "brightRed": "#be100e", - "brightGreen": "#858162", - "brightYellow": "#eaa549", - "brightBlue": "#426a79", - "brightPurple": "#97522c", - "brightCyan": "#989a9c", - "brightWhite": "#d5ccba", - "foreground": "#45373c", - "background": "#d5ccba", - "cursorColor": "#45373c" - }, - { - "name": "BelafonteNight", - "black": "#20111b", - "red": "#be100e", - "green": "#858162", - "yellow": "#eaa549", - "blue": "#426a79", - "purple": "#97522c", - "cyan": "#989a9c", - "white": "#968c83", - "brightBlack": "#5e5252", - "brightRed": "#be100e", - "brightGreen": "#858162", - "brightYellow": "#eaa549", - "brightBlue": "#426a79", - "brightPurple": "#97522c", - "brightCyan": "#989a9c", - "brightWhite": "#d5ccba", - "foreground": "#968c83", - "background": "#20111b", - "cursorColor": "#968c83" - }, - { - "name": "Bim", - "black": "#2c2423", - "red": "#f557a0", - "green": "#a9ee55", - "yellow": "#f5a255", - "blue": "#5ea2ec", - "purple": "#a957ec", - "cyan": "#5eeea0", - "white": "#918988", - "brightBlack": "#918988", - "brightRed": "#f579b2", - "brightGreen": "#bbee78", - "brightYellow": "#f5b378", - "brightBlue": "#81b3ec", - "brightPurple": "#bb79ec", - "brightCyan": "#81eeb2", - "brightWhite": "#f5eeec", - "foreground": "#a9bed8", - "background": "#012849", - "cursorColor": "#a9bed8" - }, - { - "name": "BirdsOfParadise", - "black": "#573d26", - "red": "#be2d26", - "green": "#6ba18a", - "yellow": "#e99d2a", - "blue": "#5a86ad", - "purple": "#ac80a6", - "cyan": "#74a6ad", - "white": "#e0dbb7", - "brightBlack": "#9b6c4a", - "brightRed": "#e84627", - "brightGreen": "#95d8ba", - "brightYellow": "#d0d150", - "brightBlue": "#b8d3ed", - "brightPurple": "#d19ecb", - "brightCyan": "#93cfd7", - "brightWhite": "#fff9d5", - "foreground": "#e0dbb7", - "background": "#2a1f1d", - "cursorColor": "#e0dbb7" - }, - { - "name": "Blazer", - "black": "#000000", - "red": "#b87a7a", - "green": "#7ab87a", - "yellow": "#b8b87a", - "blue": "#7a7ab8", - "purple": "#b87ab8", - "cyan": "#7ab8b8", - "white": "#d9d9d9", - "brightBlack": "#262626", - "brightRed": "#dbbdbd", - "brightGreen": "#bddbbd", - "brightYellow": "#dbdbbd", - "brightBlue": "#bdbddb", - "brightPurple": "#dbbddb", - "brightCyan": "#bddbdb", - "brightWhite": "#ffffff", - "foreground": "#d9e6f2", - "background": "#0d1926", - "cursorColor": "#d9e6f2" - }, - { - "name": "BlulocoLight", - "black": "#d5d6dd", - "red": "#d52753", - "green": "#23974a", - "yellow": "#df631c", - "blue": "#275fe4", - "purple": "#823ff1", - "cyan": "#27618d", - "white": "#000000", - "brightBlack": "#e4e5ed", - "brightRed": "#ff6480", - "brightGreen": "#3cbc66", - "brightYellow": "#c5a332", - "brightBlue": "#0099e1", - "brightPurple": "#ce33c0", - "brightCyan": "#6d93bb", - "brightWhite": "#26272d", - "foreground": "#383a42", - "background": "#f9f9f9", - "cursorColor": "#383a42" - }, - { - "name": "BlulocoZshLight", - "black": "#e4e5f1", - "red": "#d52753", - "green": "#23974a", - "yellow": "#df631c", - "blue": "#275fe4", - "purple": "#823ff1", - "cyan": "#27618d", - "white": "#000000", - "brightBlack": "#5794de", - "brightRed": "#ff6480", - "brightGreen": "#3cbc66", - "brightYellow": "#c5a332", - "brightBlue": "#0099e1", - "brightPurple": "#ce33c0", - "brightCyan": "#6d93bb", - "brightWhite": "#26272d", - "foreground": "#383a42", - "background": "#f9f9f9", - "cursorColor": "#383a42" - }, - { - "name": "MS-DOS", - "black": "#4f4f4f", - "red": "#ff6c60", - "green": "#a8ff60", - "yellow": "#ffffb6", - "blue": "#96cbfe", - "purple": "#ff73fd", - "cyan": "#c6c5fe", - "white": "#eeeeee", - "brightBlack": "#7c7c7c", - "brightRed": "#ffb6b0", - "brightGreen": "#ceffac", - "brightYellow": "#ffffcc", - "brightBlue": "#b5dcff", - "brightPurple": "#ff9cfe", - "brightCyan": "#dfdffe", - "brightWhite": "#ffffff", - "foreground": "#ffff4e", - "background": "#0000a4", - "cursorColor": "#ffff4e" - }, - { - "name": "Broadcast", - "black": "#000000", - "red": "#da4939", - "green": "#519f50", - "yellow": "#ffd24a", - "blue": "#6d9cbe", - "purple": "#d0d0ff", - "cyan": "#6e9cbe", - "white": "#ffffff", - "brightBlack": "#323232", - "brightRed": "#ff7b6b", - "brightGreen": "#83d182", - "brightYellow": "#ffff7c", - "brightBlue": "#9fcef0", - "brightPurple": "#ffffff", - "brightCyan": "#a0cef0", - "brightWhite": "#ffffff", - "foreground": "#e6e1dc", - "background": "#2b2b2b", - "cursorColor": "#e6e1dc" - }, - { - "name": "Brogrammer", - "black": "#1f1f1f", - "red": "#f81118", - "green": "#2dc55e", - "yellow": "#ecba0f", - "blue": "#2a84d2", - "purple": "#4e5ab7", - "cyan": "#1081d6", - "white": "#d6dbe5", - "brightBlack": "#d6dbe5", - "brightRed": "#de352e", - "brightGreen": "#1dd361", - "brightYellow": "#f3bd09", - "brightBlue": "#1081d6", - "brightPurple": "#5350b9", - "brightCyan": "#0f7ddb", - "brightWhite": "#ffffff", - "foreground": "#d6dbe5", - "background": "#131313", - "cursorColor": "#d6dbe5" - }, - { - "name": "C64", - "black": "#090300", - "red": "#883932", - "green": "#55a049", - "yellow": "#bfce72", - "blue": "#40318d", - "purple": "#8b3f96", - "cyan": "#67b6bd", - "white": "#ffffff", - "brightBlack": "#000000", - "brightRed": "#883932", - "brightGreen": "#55a049", - "brightYellow": "#bfce72", - "brightBlue": "#40318d", - "brightPurple": "#8b3f96", - "brightCyan": "#67b6bd", - "brightWhite": "#f7f7f7", - "foreground": "#7869c4", - "background": "#40318d", - "cursorColor": "#7869c4" - }, - { - "name": "Cai", - "black": "#000000", - "red": "#ca274d", - "green": "#4dca27", - "yellow": "#caa427", - "blue": "#274dca", - "purple": "#a427ca", - "cyan": "#27caa4", - "white": "#808080", - "brightBlack": "#808080", - "brightRed": "#e98da3", - "brightGreen": "#a3e98d", - "brightYellow": "#e9d48d", - "brightBlue": "#8da3e9", - "brightPurple": "#d48de9", - "brightCyan": "#8de9d4", - "brightWhite": "#ffffff", - "foreground": "#d9e6f2", - "background": "#09111a", - "cursorColor": "#d9e6f2" - }, - { - "name": "Chalk", - "black": "#646464", - "red": "#F58E8E", - "green": "#A9D3AB", - "yellow": "#FED37E", - "blue": "#7AABD4", - "purple": "#D6ADD5", - "cyan": "#79D4D5", - "white": "#D4D4D4", - "brightBlack": "#646464", - "brightRed": "#F58E8E", - "brightGreen": "#A9D3AB", - "brightYellow": "#FED37E", - "brightBlue": "#7AABD4", - "brightPurple": "#D6ADD5", - "brightCyan": "#79D4D5", - "brightWhite": "#D4D4D4", - "foreground": "#D4D4D4", - "background": "#2D2D2D", - "cursorColor": "#D4D4D4" - }, - { - "name": "Chalkboard", - "black": "#000000", - "red": "#c37372", - "green": "#72c373", - "yellow": "#c2c372", - "blue": "#7372c3", - "purple": "#c372c2", - "cyan": "#72c2c3", - "white": "#d9d9d9", - "brightBlack": "#323232", - "brightRed": "#dbaaaa", - "brightGreen": "#aadbaa", - "brightYellow": "#dadbaa", - "brightBlue": "#aaaadb", - "brightPurple": "#dbaada", - "brightCyan": "#aadadb", - "brightWhite": "#ffffff", - "foreground": "#d9e6f2", - "background": "#29262f", - "cursorColor": "#d9e6f2" - }, - { - "name": "Chameleon", - "black": "#2C2C2C", - "red": "#CC231C", - "green": "#689D69", - "yellow": "#D79922", - "blue": "#366B71", - "purple": "#4E5165", - "cyan": "#458587", - "white": "#C8BB97", - "brightBlack": "#777777", - "brightRed": "#CC231C", - "brightGreen": "#689D69", - "brightYellow": "#D79922", - "brightBlue": "#366B71", - "brightPurple": "#4E5165", - "brightCyan": "#458587", - "brightWhite": "#C8BB97", - "foreground": "#DEDEDE", - "background": "#2C2C2C", - "cursorColor": "#DEDEDE" - }, - { - "name": "Ciapre", - "black": "#181818", - "red": "#810009", - "green": "#48513b", - "yellow": "#cc8b3f", - "blue": "#576d8c", - "purple": "#724d7c", - "cyan": "#5c4f4b", - "white": "#aea47f", - "brightBlack": "#555555", - "brightRed": "#ac3835", - "brightGreen": "#a6a75d", - "brightYellow": "#dcdf7c", - "brightBlue": "#3097c6", - "brightPurple": "#d33061", - "brightCyan": "#f3dbb2", - "brightWhite": "#f4f4f4", - "foreground": "#aea47a", - "background": "#191c27", - "cursorColor": "#aea47a" - }, - { - "name": "CloneofUbuntu", - "black": "#2E3436", - "red": "#CC0000", - "green": "#4E9A06", - "yellow": "#C4A000", - "blue": "#3465A4", - "purple": "#75507B", - "cyan": "#06989A", - "white": "#D3D7CF", - "brightBlack": "#555753", - "brightRed": "#EF2929", - "brightGreen": "#8AE234", - "brightYellow": "#FCE94F", - "brightBlue": "#729FCF", - "brightPurple": "#AD7FA8", - "brightCyan": "#34E2E2", - "brightWhite": "#EEEEEC", - "foreground": "#ffffff", - "background": "#300a24", - "cursorColor": "#ffffff" - }, - { - "name": "CLRS", - "black": "#000000", - "red": "#f8282a", - "green": "#328a5d", - "yellow": "#fa701d", - "blue": "#135cd0", - "purple": "#9f00bd", - "cyan": "#33c3c1", - "white": "#b3b3b3", - "brightBlack": "#555753", - "brightRed": "#fb0416", - "brightGreen": "#2cc631", - "brightYellow": "#fdd727", - "brightBlue": "#1670ff", - "brightPurple": "#e900b0", - "brightCyan": "#3ad5ce", - "brightWhite": "#eeeeec", - "foreground": "#262626", - "background": "#ffffff", - "cursorColor": "#262626" - }, - { - "name": "CobaltNeon", - "black": "#142631", - "red": "#ff2320", - "green": "#3ba5ff", - "yellow": "#e9e75c", - "blue": "#8ff586", - "purple": "#781aa0", - "cyan": "#8ff586", - "white": "#ba46b2", - "brightBlack": "#fff688", - "brightRed": "#d4312e", - "brightGreen": "#8ff586", - "brightYellow": "#e9f06d", - "brightBlue": "#3c7dd2", - "brightPurple": "#8230a7", - "brightCyan": "#6cbc67", - "brightWhite": "#8ff586", - "foreground": "#8ff586", - "background": "#142838", - "cursorColor": "#8ff586" - }, - { - "name": "Cobalt2", - "black": "#000000", - "red": "#ff0000", - "green": "#38de21", - "yellow": "#ffe50a", - "blue": "#1460d2", - "purple": "#ff005d", - "cyan": "#00bbbb", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#f40e17", - "brightGreen": "#3bd01d", - "brightYellow": "#edc809", - "brightBlue": "#5555ff", - "brightPurple": "#ff55ff", - "brightCyan": "#6ae3fa", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#132738", - "cursorColor": "#ffffff" - }, - { - "name": "Colorcli", - "black": "#000000", - "red": "#D70000", - "green": "#5FAF00", - "yellow": "#5FAF00", - "blue": "#005F87", - "purple": "#D70000", - "cyan": "#5F5F5F", - "white": "#E4E4E4", - "brightBlack": "#5F5F5F", - "brightRed": "#D70000", - "brightGreen": "#5F5F5F", - "brightYellow": "#FFFF00", - "brightBlue": "#0087AF", - "brightPurple": "#0087AF", - "brightCyan": "#0087AF", - "brightWhite": "#FFFFFF", - "foreground": "#005F87", - "background": "#FFFFFF", - "cursorColor": "#005F87" - }, - { - "name": "CrayonPonyFish", - "black": "#2b1b1d", - "red": "#91002b", - "green": "#579524", - "yellow": "#ab311b", - "blue": "#8c87b0", - "purple": "#692f50", - "cyan": "#e8a866", - "white": "#68525a", - "brightBlack": "#3d2b2e", - "brightRed": "#c5255d", - "brightGreen": "#8dff57", - "brightYellow": "#c8381d", - "brightBlue": "#cfc9ff", - "brightPurple": "#fc6cba", - "brightCyan": "#ffceaf", - "brightWhite": "#b0949d", - "foreground": "#68525a", - "background": "#150707", - "cursorColor": "#68525a" - }, - { - "name": "DarkPastel", - "black": "#000000", - "red": "#ff5555", - "green": "#55ff55", - "yellow": "#ffff55", - "blue": "#5555ff", - "purple": "#ff55ff", - "cyan": "#55ffff", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#55ff55", - "brightYellow": "#ffff55", - "brightBlue": "#5555ff", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#000000", - "cursorColor": "#ffffff" - }, - { - "name": "Darkside", - "black": "#000000", - "red": "#e8341c", - "green": "#68c256", - "yellow": "#f2d42c", - "blue": "#1c98e8", - "purple": "#8e69c9", - "cyan": "#1c98e8", - "white": "#bababa", - "brightBlack": "#000000", - "brightRed": "#e05a4f", - "brightGreen": "#77b869", - "brightYellow": "#efd64b", - "brightBlue": "#387cd3", - "brightPurple": "#957bbe", - "brightCyan": "#3d97e2", - "brightWhite": "#bababa", - "foreground": "#bababa", - "background": "#222324", - "cursorColor": "#bababa" - }, - { - "name": "DeHydration", - "black": "#333333", - "red": "#ff5555", - "green": "#5fd38d", - "yellow": "#ff9955", - "blue": "#3771c8", - "purple": "#bc5fd3", - "cyan": "#5fd3bc", - "white": "#999999", - "brightBlack": "#666666", - "brightRed": "#ff8080", - "brightGreen": "#87deaa", - "brightYellow": "#ffb380", - "brightBlue": "#5f8dd3", - "brightPurple": "#cd87de", - "brightCyan": "#87decd", - "brightWhite": "#cccccc", - "foreground": "#cccccc", - "background": "#333333", - "cursorColor": "#cccccc" - }, - { - "name": "Desert", - "black": "#4d4d4d", - "red": "#ff2b2b", - "green": "#98fb98", - "yellow": "#f0e68c", - "blue": "#cd853f", - "purple": "#ffdead", - "cyan": "#ffa0a0", - "white": "#f5deb3", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#55ff55", - "brightYellow": "#ffff55", - "brightBlue": "#87ceff", - "brightPurple": "#ff55ff", - "brightCyan": "#ffd700", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#333333", - "cursorColor": "#ffffff" - }, - { - "name": "DimmedMonokai", - "black": "#3a3d43", - "red": "#be3f48", - "green": "#879a3b", - "yellow": "#c5a635", - "blue": "#4f76a1", - "purple": "#855c8d", - "cyan": "#578fa4", - "white": "#b9bcba", - "brightBlack": "#888987", - "brightRed": "#fb001f", - "brightGreen": "#0f722f", - "brightYellow": "#c47033", - "brightBlue": "#186de3", - "brightPurple": "#fb0067", - "brightCyan": "#2e706d", - "brightWhite": "#fdffb9", - "foreground": "#b9bcba", - "background": "#1f1f1f", - "cursorColor": "#b9bcba" - }, - { - "name": "Dissonance", - "black": "#000000", - "red": "#dc322f", - "green": "#56db3a", - "yellow": "#ff8400", - "blue": "#0084d4", - "purple": "#b729d9", - "cyan": "#ccccff", - "white": "#ffffff", - "brightBlack": "#d6dbe5", - "brightRed": "#dc322f", - "brightGreen": "#56db3a", - "brightYellow": "#ff8400", - "brightBlue": "#0084d4", - "brightPurple": "#b729d9", - "brightCyan": "#ccccff", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#000000", - "cursorColor": "#dc322f" - }, - { - "name": "Dracula", - "black": "#44475a", - "red": "#ff5555", - "green": "#50fa7b", - "yellow": "#ffb86c", - "blue": "#8be9fd", - "purple": "#bd93f9", - "cyan": "#ff79c6", - "white": "#94A3A5", - "brightBlack": "#000000", - "brightRed": "#ff5555", - "brightGreen": "#50fa7b", - "brightYellow": "#ffb86c", - "brightBlue": "#8be9fd", - "brightPurple": "#bd93f9", - "brightCyan": "#ff79c6", - "brightWhite": "#ffffff", - "foreground": "#94A3A5", - "background": "#282a36", - "cursorColor": "#94A3A5" - }, - { - "name": "Earthsong", - "black": "#121418", - "red": "#c94234", - "green": "#85c54c", - "yellow": "#f5ae2e", - "blue": "#1398b9", - "purple": "#d0633d", - "cyan": "#509552", - "white": "#e5c6aa", - "brightBlack": "#675f54", - "brightRed": "#ff645a", - "brightGreen": "#98e036", - "brightYellow": "#e0d561", - "brightBlue": "#5fdaff", - "brightPurple": "#ff9269", - "brightCyan": "#84f088", - "brightWhite": "#f6f7ec", - "foreground": "#e5c7a9", - "background": "#292520", - "cursorColor": "#e5c7a9" - }, - { - "name": "Elemental", - "black": "#3c3c30", - "red": "#98290f", - "green": "#479a43", - "yellow": "#7f7111", - "blue": "#497f7d", - "purple": "#7f4e2f", - "cyan": "#387f58", - "white": "#807974", - "brightBlack": "#555445", - "brightRed": "#e0502a", - "brightGreen": "#61e070", - "brightYellow": "#d69927", - "brightBlue": "#79d9d9", - "brightPurple": "#cd7c54", - "brightCyan": "#59d599", - "brightWhite": "#fff1e9", - "foreground": "#807a74", - "background": "#22211d", - "cursorColor": "#807a74" - }, - { - "name": "Elementary", - "black": "#303030", - "red": "#e1321a", - "green": "#6ab017", - "yellow": "#ffc005", - "blue": "#004f9e", - "purple": "#ec0048", - "cyan": "#2aa7e7", - "white": "#f2f2f2", - "brightBlack": "#5d5d5d", - "brightRed": "#ff361e", - "brightGreen": "#7bc91f", - "brightYellow": "#ffd00a", - "brightBlue": "#0071ff", - "brightPurple": "#ff1d62", - "brightCyan": "#4bb8fd", - "brightWhite": "#a020f0", - "foreground": "#f2f2f2", - "background": "#101010", - "cursorColor": "#f2f2f2" - }, - { - "name": "Elic", - "black": "#303030", - "red": "#e1321a", - "green": "#6ab017", - "yellow": "#ffc005", - "blue": "#729FCF", - "purple": "#ec0048", - "cyan": "#f2f2f2", - "white": "#2aa7e7", - "brightBlack": "#5d5d5d", - "brightRed": "#ff361e", - "brightGreen": "#7bc91f", - "brightYellow": "#ffd00a", - "brightBlue": "#0071ff", - "brightPurple": "#ff1d62", - "brightCyan": "#4bb8fd", - "brightWhite": "#a020f0", - "foreground": "#f2f2f2", - "background": "#4A453E", - "cursorColor": "#f2f2f2" - }, - { - "name": "Elio", - "black": "#303030", - "red": "#e1321a", - "green": "#6ab017", - "yellow": "#ffc005", - "blue": "#729FCF", - "purple": "#ec0048", - "cyan": "#2aa7e7", - "white": "#f2f2f2", - "brightBlack": "#5d5d5d", - "brightRed": "#ff361e", - "brightGreen": "#7bc91f", - "brightYellow": "#ffd00a", - "brightBlue": "#0071ff", - "brightPurple": "#ff1d62", - "brightCyan": "#4bb8fd", - "brightWhite": "#a020f0", - "foreground": "#f2f2f2", - "background": "#041A3B", - "cursorColor": "#f2f2f2" - }, - { - "name": "EspressoLibre", - "black": "#000000", - "red": "#cc0000", - "green": "#1a921c", - "yellow": "#f0e53a", - "blue": "#0066ff", - "purple": "#c5656b", - "cyan": "#06989a", - "white": "#d3d7cf", - "brightBlack": "#555753", - "brightRed": "#ef2929", - "brightGreen": "#9aff87", - "brightYellow": "#fffb5c", - "brightBlue": "#43a8ed", - "brightPurple": "#ff818a", - "brightCyan": "#34e2e2", - "brightWhite": "#eeeeec", - "foreground": "#b8a898", - "background": "#2a211c", - "cursorColor": "#b8a898" - }, - { - "name": "Espresso", - "black": "#353535", - "red": "#d25252", - "green": "#a5c261", - "yellow": "#ffc66d", - "blue": "#6c99bb", - "purple": "#d197d9", - "cyan": "#bed6ff", - "white": "#eeeeec", - "brightBlack": "#535353", - "brightRed": "#f00c0c", - "brightGreen": "#c2e075", - "brightYellow": "#e1e48b", - "brightBlue": "#8ab7d9", - "brightPurple": "#efb5f7", - "brightCyan": "#dcf4ff", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#323232", - "cursorColor": "#ffffff" - }, - { - "name": "FairyFloss", - "black": "#42395D", - "red": "#A8757B", - "green": "#FF857F", - "yellow": "#E6C000", - "blue": "#AE81FF", - "purple": "#716799", - "cyan": "#C2FFDF", - "white": "#F8F8F2", - "brightBlack": "#75507B", - "brightRed": "#FFB8D1", - "brightGreen": "#F1568E", - "brightYellow": "#D5A425", - "brightBlue": "#C5A3FF", - "brightPurple": "#8077A8", - "brightCyan": "#C2FFFF", - "brightWhite": "#F8F8F0", - "foreground": "#C2FFDF", - "background": "#5A5475", - "cursorColor": "#FFB8D1" - }, - { - "name": "FairyFlossDark", - "black": "#42395D", - "red": "#A8757B", - "green": "#FF857F", - "yellow": "#E6C000", - "blue": "#AE81FF", - "purple": "#716799", - "cyan": "#C2FFDF", - "white": "#F8F8F2", - "brightBlack": "#75507B", - "brightRed": "#FFB8D1", - "brightGreen": "#F1568E", - "brightYellow": "#D5A425", - "brightBlue": "#C5A3FF", - "brightPurple": "#8077A8", - "brightCyan": "#C2FFFF", - "brightWhite": "#F8F8F0", - "foreground": "#C2FFDF", - "background": "#42395D", - "cursorColor": "#FFB8D1" - }, - { - "name": "Fishtank", - "black": "#03073c", - "red": "#c6004a", - "green": "#acf157", - "yellow": "#fecd5e", - "blue": "#525fb8", - "purple": "#986f82", - "cyan": "#968763", - "white": "#ecf0fc", - "brightBlack": "#6c5b30", - "brightRed": "#da4b8a", - "brightGreen": "#dbffa9", - "brightYellow": "#fee6a9", - "brightBlue": "#b2befa", - "brightPurple": "#fda5cd", - "brightCyan": "#a5bd86", - "brightWhite": "#f6ffec", - "foreground": "#ecf0fe", - "background": "#232537", - "cursorColor": "#ecf0fe" - }, - { - "name": "FlatRemix", - "black": "#1F2229", - "red": "#D41919", - "green": "#5EBDAB", - "yellow": "#FEA44C", - "blue": "#367bf0", - "purple": "#BF2E5D", - "cyan": "#49AEE6", - "white": "#E6E6E6", - "brightBlack": "#8C42AB", - "brightRed": "#EC0101", - "brightGreen": "#47D4B9", - "brightYellow": "#FF8A18", - "brightBlue": "#277FFF", - "brightPurple": "#D71655", - "brightCyan": "#05A1F7", - "brightWhite": "#FFFFFF", - "foreground": "#FFFFFF", - "background": "#272a34", - "cursorColor": "#FFFFFF" - }, - { - "name": "Flat", - "black": "#2c3e50", - "red": "#c0392b", - "green": "#27ae60", - "yellow": "#f39c12", - "blue": "#2980b9", - "purple": "#8e44ad", - "cyan": "#16a085", - "white": "#bdc3c7", - "brightBlack": "#34495e", - "brightRed": "#e74c3c", - "brightGreen": "#2ecc71", - "brightYellow": "#f1c40f", - "brightBlue": "#3498db", - "brightPurple": "#9b59b6", - "brightCyan": "#2AA198", - "brightWhite": "#ecf0f1", - "foreground": "#1abc9c", - "background": "#1F2D3A", - "cursorColor": "#1abc9c" - }, - { - "name": "Flatland", - "black": "#1d1d19", - "red": "#f18339", - "green": "#9fd364", - "yellow": "#f4ef6d", - "blue": "#5096be", - "purple": "#695abc", - "cyan": "#d63865", - "white": "#ffffff", - "brightBlack": "#1d1d19", - "brightRed": "#d22a24", - "brightGreen": "#a7d42c", - "brightYellow": "#ff8949", - "brightBlue": "#61b9d0", - "brightPurple": "#695abc", - "brightCyan": "#d63865", - "brightWhite": "#ffffff", - "foreground": "#b8dbef", - "background": "#1d1f21", - "cursorColor": "#b8dbef" - }, - { - "name": "Foxnightly", - "black": "#2A2A2E", - "red": "#B98EFF", - "green": "#FF7DE9", - "yellow": "#729FCF", - "blue": "#66A05B", - "purple": "#75507B", - "cyan": "#ACACAE", - "white": "#FFFFFF", - "brightBlack": "#A40000", - "brightRed": "#BF4040", - "brightGreen": "#66A05B", - "brightYellow": "#FFB86C", - "brightBlue": "#729FCF", - "brightPurple": "#8F5902", - "brightCyan": "#C4A000", - "brightWhite": "#5C3566", - "foreground": "#D7D7DB", - "background": "#2A2A2E", - "cursorColor": "#D7D7DB" - }, - { - "name": "Freya", - "black": "#073642", - "red": "#dc322f", - "green": "#859900", - "yellow": "#b58900", - "blue": "#268bd2", - "purple": "#ec0048", - "cyan": "#2aa198", - "white": "#94a3a5", - "brightBlack": "#586e75", - "brightRed": "#cb4b16", - "brightGreen": "#859900", - "brightYellow": "#b58900", - "brightBlue": "#268bd2", - "brightPurple": "#d33682", - "brightCyan": "#2aa198", - "brightWhite": "#6c71c4", - "foreground": "#94a3a5", - "background": "#252e32", - "cursorColor": "#839496" - }, - { - "name": "FrontendDelight", - "black": "#242526", - "red": "#f8511b", - "green": "#565747", - "yellow": "#fa771d", - "blue": "#2c70b7", - "purple": "#f02e4f", - "cyan": "#3ca1a6", - "white": "#adadad", - "brightBlack": "#5fac6d", - "brightRed": "#f74319", - "brightGreen": "#74ec4c", - "brightYellow": "#fdc325", - "brightBlue": "#3393ca", - "brightPurple": "#e75e4f", - "brightCyan": "#4fbce6", - "brightWhite": "#8c735b", - "foreground": "#adadad", - "background": "#1b1c1d", - "cursorColor": "#adadad" - }, - { - "name": "FrontendFunForrest", - "black": "#000000", - "red": "#d6262b", - "green": "#919c00", - "yellow": "#be8a13", - "blue": "#4699a3", - "purple": "#8d4331", - "cyan": "#da8213", - "white": "#ddc265", - "brightBlack": "#7f6a55", - "brightRed": "#e55a1c", - "brightGreen": "#bfc65a", - "brightYellow": "#ffcb1b", - "brightBlue": "#7cc9cf", - "brightPurple": "#d26349", - "brightCyan": "#e6a96b", - "brightWhite": "#ffeaa3", - "foreground": "#dec165", - "background": "#251200", - "cursorColor": "#dec165" - }, - { - "name": "FrontendGalaxy", - "black": "#000000", - "red": "#f9555f", - "green": "#21b089", - "yellow": "#fef02a", - "blue": "#589df6", - "purple": "#944d95", - "cyan": "#1f9ee7", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#fa8c8f", - "brightGreen": "#35bb9a", - "brightYellow": "#ffff55", - "brightBlue": "#589df6", - "brightPurple": "#e75699", - "brightCyan": "#3979bc", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#1d2837", - "cursorColor": "#ffffff" - }, - { - "name": "GeoHot", - "black": "#F9F5F5", - "red": "#CC0000", - "green": "#1F1E1F", - "yellow": "#ADA110", - "blue": "#FF004E", - "purple": "#75507B", - "cyan": "#06919A", - "white": "#FFFFFF", - "brightBlack": "#555753", - "brightRed": "#EF2929", - "brightGreen": "#FF0000", - "brightYellow": "#ADA110", - "brightBlue": "#5F4AA6", - "brightPurple": "#B74438", - "brightCyan": "#408F0C", - "brightWhite": "#FFFFFF", - "foreground": "#FFFFFF", - "background": "#1F1E1F", - "cursorColor": "#FFFFFF" - }, - { - "name": "Github", - "black": "#3e3e3e", - "red": "#970b16", - "green": "#07962a", - "yellow": "#f8eec7", - "blue": "#003e8a", - "purple": "#e94691", - "cyan": "#89d1ec", - "white": "#ffffff", - "brightBlack": "#666666", - "brightRed": "#de0000", - "brightGreen": "#87d5a2", - "brightYellow": "#f1d007", - "brightBlue": "#2e6cba", - "brightPurple": "#ffa29f", - "brightCyan": "#1cfafe", - "brightWhite": "#ffffff", - "foreground": "#3e3e3e", - "background": "#f4f4f4", - "cursorColor": "#3e3e3e" - }, - { - "name": "Gogh", - "black": "#292D3E", - "red": "#F07178", - "green": "#62DE84", - "yellow": "#FFCB6B", - "blue": "#75A1FF", - "purple": "#F580FF", - "cyan": "#60BAEC", - "white": "#ABB2BF", - "brightBlack": "#959DCB", - "brightRed": "#F07178", - "brightGreen": "#C3E88D", - "brightYellow": "#FF5572", - "brightBlue": "#82AAFF", - "brightPurple": "#FFCB6B", - "brightCyan": "#676E95", - "brightWhite": "#FFFEFE", - "foreground": "#BFC7D5", - "background": "#292D3E", - "cursorColor": "#BFC7D5" - }, - { - "name": "gooey", - "black": "#000009", - "red": "#BB4F6C", - "green": "#72CCAE", - "yellow": "#C65E3D", - "blue": "#58B6CA", - "purple": "#6488C4", - "cyan": "#8D84C6", - "white": "#858893", - "brightBlack": "#1f222d", - "brightRed": "#ee829f", - "brightGreen": "#a5ffe1", - "brightYellow": "#f99170", - "brightBlue": "#8be9fd", - "brightPurple": "#97bbf7", - "brightCyan": "#c0b7f9", - "brightWhite": "#ffffff", - "foreground": "#EBEEF9", - "background": "#0D101B", - "cursorColor": "#EBEEF9" - }, - { - "name": "GoogleDark", - "black": "#202124", - "red": "#EA4335", - "green": "#34A853", - "yellow": "#FBBC04", - "blue": "#4285F4", - "purple": "#A142F4", - "cyan": "#24C1E0", - "white": "#E8EAED", - "brightBlack": "#5F6368", - "brightRed": "#EA4335", - "brightGreen": "#34A853", - "brightYellow": "#FBBC05", - "brightBlue": "#4285F4", - "brightPurple": "#A142F4", - "brightCyan": "#24C1E0", - "brightWhite": "#FFFFFF", - "foreground": "#E8EAED", - "background": "#202124", - "cursorColor": "#E8EAED" - }, - { - "name": "GoogleLight", - "black": "#202124", - "red": "#EA4335", - "green": "#34A853", - "yellow": "#FBBC04", - "blue": "#4285F4", - "purple": "#A142F4", - "cyan": "#24C1E0", - "white": "#E8EAED", - "brightBlack": "#5F6368", - "brightRed": "#EA4335", - "brightGreen": "#34A853", - "brightYellow": "#FBBC05", - "brightBlue": "#4285F4", - "brightPurple": "#A142F4", - "brightCyan": "#24C1E0", - "brightWhite": "#FFFFFF", - "foreground": "#5F6368", - "background": "#FFFFFF", - "cursorColor": "#5F6368" - }, - { - "name": "gotham", - "black": "#0a0f14", - "red": "#c33027", - "green": "#26a98b", - "yellow": "#edb54b", - "blue": "#195465", - "purple": "#4e5165", - "cyan": "#33859d", - "white": "#98d1ce", - "brightBlack": "#10151b", - "brightRed": "#d26939", - "brightGreen": "#081f2d", - "brightYellow": "#245361", - "brightBlue": "#093748", - "brightPurple": "#888ba5", - "brightCyan": "#599caa", - "brightWhite": "#d3ebe9", - "foreground": "#98d1ce", - "background": "#0a0f14", - "cursorColor": "#98d1ce" - }, - { - "name": "Grape", - "black": "#2d283f", - "red": "#ed2261", - "green": "#1fa91b", - "yellow": "#8ddc20", - "blue": "#487df4", - "purple": "#8d35c9", - "cyan": "#3bdeed", - "white": "#9e9ea0", - "brightBlack": "#59516a", - "brightRed": "#f0729a", - "brightGreen": "#53aa5e", - "brightYellow": "#b2dc87", - "brightBlue": "#a9bcec", - "brightPurple": "#ad81c2", - "brightCyan": "#9de3eb", - "brightWhite": "#a288f7", - "foreground": "#9f9fa1", - "background": "#171423", - "cursorColor": "#9f9fa1" - }, - { - "name": "Grass", - "black": "#000000", - "red": "#bb0000", - "green": "#00bb00", - "yellow": "#e7b000", - "blue": "#0000a3", - "purple": "#950062", - "cyan": "#00bbbb", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#bb0000", - "brightGreen": "#00bb00", - "brightYellow": "#e7b000", - "brightBlue": "#0000bb", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#fff0a5", - "background": "#13773d", - "cursorColor": "#fff0a5" - }, - { - "name": "GruvboxDark", - "black": "#282828", - "red": "#cc241d", - "green": "#98971a", - "yellow": "#d79921", - "blue": "#458588", - "purple": "#b16286", - "cyan": "#689d6a", - "white": "#a89984", - "brightBlack": "#928374", - "brightRed": "#fb4934", - "brightGreen": "#b8bb26", - "brightYellow": "#fabd2f", - "brightBlue": "#83a598", - "brightPurple": "#d3869b", - "brightCyan": "#8ec07c", - "brightWhite": "#ebdbb2", - "foreground": "#ebdbb2", - "background": "#282828", - "cursorColor": "#ebdbb2" - }, - { - "name": "Gruvbox", - "black": "#fbf1c7", - "red": "#cc241d", - "green": "#98971a", - "yellow": "#d79921", - "blue": "#458588", - "purple": "#b16286", - "cyan": "#689d6a", - "white": "#7c6f64", - "brightBlack": "#928374", - "brightRed": "#9d0006", - "brightGreen": "#79740e", - "brightYellow": "#b57614", - "brightBlue": "#076678", - "brightPurple": "#8f3f71", - "brightCyan": "#427b58", - "brightWhite": "#3c3836", - "foreground": "#3c3836", - "background": "#fbf1c7", - "cursorColor": "#3c3836" - }, - { - "name": "Hardcore", - "black": "#1b1d1e", - "red": "#f92672", - "green": "#a6e22e", - "yellow": "#fd971f", - "blue": "#66d9ef", - "purple": "#9e6ffe", - "cyan": "#5e7175", - "white": "#ccccc6", - "brightBlack": "#505354", - "brightRed": "#ff669d", - "brightGreen": "#beed5f", - "brightYellow": "#e6db74", - "brightBlue": "#66d9ef", - "brightPurple": "#9e6ffe", - "brightCyan": "#a3babf", - "brightWhite": "#f8f8f2", - "foreground": "#a0a0a0", - "background": "#121212", - "cursorColor": "#a0a0a0" - }, - { - "name": "Harper", - "black": "#010101", - "red": "#f8b63f", - "green": "#7fb5e1", - "yellow": "#d6da25", - "blue": "#489e48", - "purple": "#b296c6", - "cyan": "#f5bfd7", - "white": "#a8a49d", - "brightBlack": "#726e6a", - "brightRed": "#f8b63f", - "brightGreen": "#7fb5e1", - "brightYellow": "#d6da25", - "brightBlue": "#489e48", - "brightPurple": "#b296c6", - "brightCyan": "#f5bfd7", - "brightWhite": "#fefbea", - "foreground": "#a8a49d", - "background": "#010101", - "cursorColor": "#a8a49d" - }, - { - "name": "HemisuDark", - "black": "#444444", - "red": "#FF0054", - "green": "#B1D630", - "yellow": "#9D895E", - "blue": "#67BEE3", - "purple": "#B576BC", - "cyan": "#569A9F", - "white": "#EDEDED", - "brightBlack": "#777777", - "brightRed": "#D65E75", - "brightGreen": "#BAFFAA", - "brightYellow": "#ECE1C8", - "brightBlue": "#9FD3E5", - "brightPurple": "#DEB3DF", - "brightCyan": "#B6E0E5", - "brightWhite": "#FFFFFF", - "foreground": "#FFFFFF", - "background": "#000000", - "cursorColor": "#BAFFAA" - }, - { - "name": "HemisuLight", - "black": "#777777", - "red": "#FF0055", - "green": "#739100", - "yellow": "#503D15", - "blue": "#538091", - "purple": "#5B345E", - "cyan": "#538091", - "white": "#999999", - "brightBlack": "#999999", - "brightRed": "#D65E76", - "brightGreen": "#9CC700", - "brightYellow": "#947555", - "brightBlue": "#9DB3CD", - "brightPurple": "#A184A4", - "brightCyan": "#85B2AA", - "brightWhite": "#BABABA", - "foreground": "#444444", - "background": "#EFEFEF", - "cursorColor": "#FF0054" - }, - { - "name": "Highway", - "black": "#000000", - "red": "#d00e18", - "green": "#138034", - "yellow": "#ffcb3e", - "blue": "#006bb3", - "purple": "#6b2775", - "cyan": "#384564", - "white": "#ededed", - "brightBlack": "#5d504a", - "brightRed": "#f07e18", - "brightGreen": "#b1d130", - "brightYellow": "#fff120", - "brightBlue": "#4fc2fd", - "brightPurple": "#de0071", - "brightCyan": "#5d504a", - "brightWhite": "#ffffff", - "foreground": "#ededed", - "background": "#222225", - "cursorColor": "#ededed" - }, - { - "name": "HipsterGreen", - "black": "#000000", - "red": "#b6214a", - "green": "#00a600", - "yellow": "#bfbf00", - "blue": "#246eb2", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#86a93e", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#84c138", - "background": "#100b05", - "cursorColor": "#84c138" - }, - { - "name": "Homebrew", - "black": "#000000", - "red": "#990000", - "green": "#00a600", - "yellow": "#999900", - "blue": "#0000b2", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#00d900", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#00ff00", - "background": "#000000", - "cursorColor": "#00ff00" - }, - { - "name": "HorizonBright", - "black": "#16161C", - "red": "#DA103F", - "green": "#1EB980", - "yellow": "#F6661E", - "blue": "#26BBD9", - "purple": "#EE64AE", - "cyan": "#1D8991", - "white": "#FADAD1", - "brightBlack": "#1A1C23", - "brightRed": "#F43E5C", - "brightGreen": "#07DA8C", - "brightYellow": "#F77D26", - "brightBlue": "#3FC6DE", - "brightPurple": "#F075B7", - "brightCyan": "#1EAEAE", - "brightWhite": "#FDF0ED", - "foreground": "#1C1E26", - "background": "#FDF0ED", - "cursorColor": "#1C1E26" - }, - { - "name": "HorizonDark", - "black": "#16161C", - "red": "#E95678", - "green": "#29D398", - "yellow": "#FAB795", - "blue": "#26BBD9", - "purple": "#EE64AE", - "cyan": "#59E3E3", - "white": "#FADAD1", - "brightBlack": "#232530", - "brightRed": "#EC6A88", - "brightGreen": "#3FDAA4", - "brightYellow": "#FBC3A7", - "brightBlue": "#3FC6DE", - "brightPurple": "#F075B7", - "brightCyan": "#6BE6E6", - "brightWhite": "#FDF0ED", - "foreground": "#FDF0ED", - "background": "#1C1E26", - "cursorColor": "#FDF0ED" - }, - { - "name": "Hurtado", - "black": "#575757", - "red": "#ff1b00", - "green": "#a5e055", - "yellow": "#fbe74a", - "blue": "#496487", - "purple": "#fd5ff1", - "cyan": "#86e9fe", - "white": "#cbcccb", - "brightBlack": "#262626", - "brightRed": "#d51d00", - "brightGreen": "#a5df55", - "brightYellow": "#fbe84a", - "brightBlue": "#89beff", - "brightPurple": "#c001c1", - "brightCyan": "#86eafe", - "brightWhite": "#dbdbdb", - "foreground": "#dbdbdb", - "background": "#000000", - "cursorColor": "#dbdbdb" - }, - { - "name": "Hybrid", - "black": "#282a2e", - "red": "#A54242", - "green": "#8C9440", - "yellow": "#de935f", - "blue": "#5F819D", - "purple": "#85678F", - "cyan": "#5E8D87", - "white": "#969896", - "brightBlack": "#373b41", - "brightRed": "#cc6666", - "brightGreen": "#b5bd68", - "brightYellow": "#f0c674", - "brightBlue": "#81a2be", - "brightPurple": "#b294bb", - "brightCyan": "#8abeb7", - "brightWhite": "#c5c8c6", - "foreground": "#94a3a5", - "background": "#141414", - "cursorColor": "#94a3a5" - }, - { - "name": "IBM3270(HighContrast)", - "black": "#000000", - "red": "#FF0000", - "green": "#00FF00", - "yellow": "#FFFF00", - "blue": "#00BFFF", - "purple": "#FFC0CB", - "cyan": "#40E0D0", - "white": "#BEBEBE", - "brightBlack": "#414141", - "brightRed": "#FFA500", - "brightGreen": "#98FB98", - "brightYellow": "#FFFF00", - "brightBlue": "#0000CD", - "brightPurple": "#A020F0", - "brightCyan": "#AEEEEE", - "brightWhite": "#FFFFFF", - "foreground": "#FDFDFD", - "background": "#000000", - "cursorColor": "#FDFDFD" - }, - { - "name": "ibm3270", - "black": "#222222", - "red": "#F01818", - "green": "#24D830", - "yellow": "#F0D824", - "blue": "#7890F0", - "purple": "#F078D8", - "cyan": "#54E4E4", - "white": "#A5A5A5", - "brightBlack": "#888888", - "brightRed": "#EF8383", - "brightGreen": "#7ED684", - "brightYellow": "#EFE28B", - "brightBlue": "#B3BFEF", - "brightPurple": "#EFB3E3", - "brightCyan": "#9CE2E2", - "brightWhite": "#FFFFFF", - "foreground": "#FDFDFD", - "background": "#000000", - "cursorColor": "#FDFDFD" - }, - { - "name": "ICGreenPPL", - "black": "#1f1f1f", - "red": "#fb002a", - "green": "#339c24", - "yellow": "#659b25", - "blue": "#149b45", - "purple": "#53b82c", - "cyan": "#2cb868", - "white": "#e0ffef", - "brightBlack": "#032710", - "brightRed": "#a7ff3f", - "brightGreen": "#9fff6d", - "brightYellow": "#d2ff6d", - "brightBlue": "#72ffb5", - "brightPurple": "#50ff3e", - "brightCyan": "#22ff71", - "brightWhite": "#daefd0", - "foreground": "#d9efd3", - "background": "#3a3d3f", - "cursorColor": "#d9efd3" - }, - { - "name": "ICOrangePPL", - "black": "#000000", - "red": "#c13900", - "green": "#a4a900", - "yellow": "#caaf00", - "blue": "#bd6d00", - "purple": "#fc5e00", - "cyan": "#f79500", - "white": "#ffc88a", - "brightBlack": "#6a4f2a", - "brightRed": "#ff8c68", - "brightGreen": "#f6ff40", - "brightYellow": "#ffe36e", - "brightBlue": "#ffbe55", - "brightPurple": "#fc874f", - "brightCyan": "#c69752", - "brightWhite": "#fafaff", - "foreground": "#ffcb83", - "background": "#262626", - "cursorColor": "#ffcb83" - }, - { - "name": "IdleToes", - "black": "#323232", - "red": "#d25252", - "green": "#7fe173", - "yellow": "#ffc66d", - "blue": "#4099ff", - "purple": "#f680ff", - "cyan": "#bed6ff", - "white": "#eeeeec", - "brightBlack": "#535353", - "brightRed": "#f07070", - "brightGreen": "#9dff91", - "brightYellow": "#ffe48b", - "brightBlue": "#5eb7f7", - "brightPurple": "#ff9dff", - "brightCyan": "#dcf4ff", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#323232", - "cursorColor": "#ffffff" - }, - { - "name": "IrBlack", - "black": "#4e4e4e", - "red": "#ff6c60", - "green": "#a8ff60", - "yellow": "#ffffb6", - "blue": "#69cbfe", - "purple": "#ff73Fd", - "cyan": "#c6c5fe", - "white": "#eeeeee", - "brightBlack": "#7c7c7c", - "brightRed": "#ffb6b0", - "brightGreen": "#ceffac", - "brightYellow": "#ffffcb", - "brightBlue": "#b5dcfe", - "brightPurple": "#ff9cfe", - "brightCyan": "#dfdffe", - "brightWhite": "#ffffff", - "foreground": "#eeeeee", - "background": "#000000", - "cursorColor": "#ffa560" - }, - { - "name": "JackieBrown", - "black": "#2c1d16", - "red": "#ef5734", - "green": "#2baf2b", - "yellow": "#bebf00", - "blue": "#246eb2", - "purple": "#d05ec1", - "cyan": "#00acee", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#86a93e", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#ffcc2f", - "background": "#2c1d16", - "cursorColor": "#ffcc2f" - }, - { - "name": "Japanesque", - "black": "#343935", - "red": "#cf3f61", - "green": "#7bb75b", - "yellow": "#e9b32a", - "blue": "#4c9ad4", - "purple": "#a57fc4", - "cyan": "#389aad", - "white": "#fafaf6", - "brightBlack": "#595b59", - "brightRed": "#d18fa6", - "brightGreen": "#767f2c", - "brightYellow": "#78592f", - "brightBlue": "#135979", - "brightPurple": "#604291", - "brightCyan": "#76bbca", - "brightWhite": "#b2b5ae", - "foreground": "#f7f6ec", - "background": "#1e1e1e", - "cursorColor": "#f7f6ec" - }, - { - "name": "Jellybeans", - "black": "#929292", - "red": "#e27373", - "green": "#94b979", - "yellow": "#ffba7b", - "blue": "#97bedc", - "purple": "#e1c0fa", - "cyan": "#00988e", - "white": "#dedede", - "brightBlack": "#bdbdbd", - "brightRed": "#ffa1a1", - "brightGreen": "#bddeab", - "brightYellow": "#ffdca0", - "brightBlue": "#b1d8f6", - "brightPurple": "#fbdaff", - "brightCyan": "#1ab2a8", - "brightWhite": "#ffffff", - "foreground": "#dedede", - "background": "#121212", - "cursorColor": "#dedede" - }, - { - "name": "Jup", - "black": "#000000", - "red": "#dd006f", - "green": "#6fdd00", - "yellow": "#dd6f00", - "blue": "#006fdd", - "purple": "#6f00dd", - "cyan": "#00dd6f", - "white": "#f2f2f2", - "brightBlack": "#7d7d7d", - "brightRed": "#ff74b9", - "brightGreen": "#b9ff74", - "brightYellow": "#ffb974", - "brightBlue": "#74b9ff", - "brightPurple": "#b974ff", - "brightCyan": "#74ffb9", - "brightWhite": "#ffffff", - "foreground": "#23476a", - "background": "#758480", - "cursorColor": "#23476a" - }, - { - "name": "Kibble", - "black": "#4d4d4d", - "red": "#c70031", - "green": "#29cf13", - "yellow": "#d8e30e", - "blue": "#3449d1", - "purple": "#8400ff", - "cyan": "#0798ab", - "white": "#e2d1e3", - "brightBlack": "#5a5a5a", - "brightRed": "#f01578", - "brightGreen": "#6ce05c", - "brightYellow": "#f3f79e", - "brightBlue": "#97a4f7", - "brightPurple": "#c495f0", - "brightCyan": "#68f2e0", - "brightWhite": "#ffffff", - "foreground": "#f7f7f7", - "background": "#0e100a", - "cursorColor": "#f7f7f7" - }, - { - "name": "kokuban", - "black": "#2E8744", - "red": "#D84E4C", - "green": "#95DA5A", - "yellow": "#D6E264", - "blue": "#4B9ED7", - "purple": "#945FC5", - "cyan": "#D89B25", - "white": "#D8E2D7", - "brightBlack": "#34934F", - "brightRed": "#FF4F59", - "brightGreen": "#AFF56A", - "brightYellow": "#FCFF75", - "brightBlue": "#57AEFF", - "brightPurple": "#AE63E9", - "brightCyan": "#FFAA2B", - "brightWhite": "#FFFEFE", - "foreground": "#D8E2D7", - "background": "#0D4A08", - "cursorColor": "#D8E2D7" - }, - { - "name": "laserwave", - "black": "#39243A", - "red": "#EB64B9", - "green": "#AFD686", - "yellow": "#FEAE87", - "blue": "#40B4C4", - "purple": "#B381C5", - "cyan": "#215969", - "white": "#91889b", - "brightBlack": "#716485", - "brightRed": "#FC2377", - "brightGreen": "#50FA7B", - "brightYellow": "#FFE261", - "brightBlue": "#74DFC4", - "brightPurple": "#6D75E0", - "brightCyan": "#B4DCE7", - "brightWhite": "#FFFFFF", - "foreground": "#E0E0E0", - "background": "#1F1926", - "cursorColor": "#C7C7C7" - }, - { - "name": "LaterThisEvening", - "black": "#2b2b2b", - "red": "#d45a60", - "green": "#afba67", - "yellow": "#e5d289", - "blue": "#a0bad6", - "purple": "#c092d6", - "cyan": "#91bfb7", - "white": "#3c3d3d", - "brightBlack": "#454747", - "brightRed": "#d3232f", - "brightGreen": "#aabb39", - "brightYellow": "#e5be39", - "brightBlue": "#6699d6", - "brightPurple": "#ab53d6", - "brightCyan": "#5fc0ae", - "brightWhite": "#c1c2c2", - "foreground": "#959595", - "background": "#222222", - "cursorColor": "#959595" - }, - { - "name": "Lavandula", - "black": "#230046", - "red": "#7d1625", - "green": "#337e6f", - "yellow": "#7f6f49", - "blue": "#4f4a7f", - "purple": "#5a3f7f", - "cyan": "#58777f", - "white": "#736e7d", - "brightBlack": "#372d46", - "brightRed": "#e05167", - "brightGreen": "#52e0c4", - "brightYellow": "#e0c386", - "brightBlue": "#8e87e0", - "brightPurple": "#a776e0", - "brightCyan": "#9ad4e0", - "brightWhite": "#8c91fa", - "foreground": "#736e7d", - "background": "#050014", - "cursorColor": "#736e7d" - }, - { - "name": "LiquidCarbonTransparent", - "black": "#000000", - "red": "#ff3030", - "green": "#559a70", - "yellow": "#ccac00", - "blue": "#0099cc", - "purple": "#cc69c8", - "cyan": "#7ac4cc", - "white": "#bccccc", - "brightBlack": "#000000", - "brightRed": "#ff3030", - "brightGreen": "#559a70", - "brightYellow": "#ccac00", - "brightBlue": "#0099cc", - "brightPurple": "#cc69c8", - "brightCyan": "#7ac4cc", - "brightWhite": "#bccccc", - "foreground": "#afc2c2", - "background": "#000000", - "cursorColor": "#afc2c2" - }, - { - "name": "LiquidCarbon", - "black": "#000000", - "red": "#ff3030", - "green": "#559a70", - "yellow": "#ccac00", - "blue": "#0099cc", - "purple": "#cc69c8", - "cyan": "#7ac4cc", - "white": "#bccccc", - "brightBlack": "#000000", - "brightRed": "#ff3030", - "brightGreen": "#559a70", - "brightYellow": "#ccac00", - "brightBlue": "#0099cc", - "brightPurple": "#cc69c8", - "brightCyan": "#7ac4cc", - "brightWhite": "#bccccc", - "foreground": "#afc2c2", - "background": "#303030", - "cursorColor": "#afc2c2" - }, - { - "name": "LunariaDark", - "black": "#36464E", - "red": "#846560", - "green": "#809984", - "yellow": "#A79A79", - "blue": "#555673", - "purple": "#866C83", - "cyan": "#7E98B4", - "white": "#CACED8", - "brightBlack": "#404F56", - "brightRed": "#BB928B", - "brightGreen": "#BFDCC2", - "brightYellow": "#F1DFB6", - "brightBlue": "#777798", - "brightPurple": "#BF9DB9", - "brightCyan": "#BDDCFF", - "brightWhite": "#DFE2ED", - "foreground": "#CACED8", - "background": "#36464E", - "cursorColor": "#CACED8" - }, - { - "name": "LunariaEclipse", - "black": "#323F46", - "red": "#83615B", - "green": "#7F9781", - "yellow": "#A69875", - "blue": "#53516F", - "purple": "#856880", - "cyan": "#7D96B2", - "white": "#C9CDD7", - "brightBlack": "#3D4950", - "brightRed": "#BA9088", - "brightGreen": "#BEDBC1", - "brightYellow": "#F1DFB4", - "brightBlue": "#767495", - "brightPurple": "#BE9CB8", - "brightCyan": "#BCDBFF", - "brightWhite": "#DFE2ED", - "foreground": "#C9CDD7", - "background": "#323F46", - "cursorColor": "#C9CDD7" - }, - { - "name": "LunariaLight", - "black": "#3E3C3D", - "red": "#783C1F", - "green": "#497D46", - "yellow": "#8F750B", - "blue": "#3F3566", - "purple": "#793F62", - "cyan": "#3778A9", - "white": "#D5CFCC", - "brightBlack": "#484646", - "brightRed": "#B06240", - "brightGreen": "#7BC175", - "brightYellow": "#DCB735", - "brightBlue": "#5C4F89", - "brightPurple": "#B56895", - "brightCyan": "#64BAFF", - "brightWhite": "#EBE4E1", - "foreground": "#484646", - "background": "#EBE4E1", - "cursorColor": "#484646" - }, - { - "name": "Maia", - "black": "#232423", - "red": "#BA2922", - "green": "#7E807E", - "yellow": "#4C4F4D", - "blue": "#16A085", - "purple": "#43746A", - "cyan": "#00CCCC", - "white": "#E0E0E0", - "brightBlack": "#282928", - "brightRed": "#CC372C", - "brightGreen": "#8D8F8D", - "brightYellow": "#4E524F", - "brightBlue": "#13BF9D", - "brightPurple": "#487D72", - "brightCyan": "#00D1D1", - "brightWhite": "#E8E8E8", - "foreground": "#BDC3C7", - "background": "#31363B", - "cursorColor": "#BDC3C7" - }, - { - "name": "ManPage", - "black": "#000000", - "red": "#cc0000", - "green": "#00a600", - "yellow": "#999900", - "blue": "#0000b2", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#cccccc", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#00d900", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#000000", - "background": "#fef49c", - "cursorColor": "#000000" - }, - { - "name": "Mar", - "black": "#000000", - "red": "#b5407b", - "green": "#7bb540", - "yellow": "#b57b40", - "blue": "#407bb5", - "purple": "#7b40b5", - "cyan": "#40b57b", - "white": "#f8f8f8", - "brightBlack": "#737373", - "brightRed": "#cd73a0", - "brightGreen": "#a0cd73", - "brightYellow": "#cda073", - "brightBlue": "#73a0cd", - "brightPurple": "#a073cd", - "brightCyan": "#73cda0", - "brightWhite": "#ffffff", - "foreground": "#23476a", - "background": "#ffffff", - "cursorColor": "#23476a" - }, - { - "name": "Material", - "black": "#073641", - "red": "#EB606B", - "green": "#C3E88D", - "yellow": "#F7EB95", - "blue": "#80CBC3", - "purple": "#FF2490", - "cyan": "#AEDDFF", - "white": "#FFFFFF", - "brightBlack": "#002B36", - "brightRed": "#EB606B", - "brightGreen": "#C3E88D", - "brightYellow": "#F7EB95", - "brightBlue": "#7DC6BF", - "brightPurple": "#6C71C3", - "brightCyan": "#34434D", - "brightWhite": "#FFFFFF", - "foreground": "#C3C7D1", - "background": "#1E282C", - "cursorColor": "#657B83" - }, - { - "name": "Mathias", - "black": "#000000", - "red": "#e52222", - "green": "#a6e32d", - "yellow": "#fc951e", - "blue": "#c48dff", - "purple": "#fa2573", - "cyan": "#67d9f0", - "white": "#f2f2f2", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#55ff55", - "brightYellow": "#ffff55", - "brightBlue": "#5555ff", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#bbbbbb", - "background": "#000000", - "cursorColor": "#bbbbbb" - }, - { - "name": "Medallion", - "black": "#000000", - "red": "#b64c00", - "green": "#7c8b16", - "yellow": "#d3bd26", - "blue": "#616bb0", - "purple": "#8c5a90", - "cyan": "#916c25", - "white": "#cac29a", - "brightBlack": "#5e5219", - "brightRed": "#ff9149", - "brightGreen": "#b2ca3b", - "brightYellow": "#ffe54a", - "brightBlue": "#acb8ff", - "brightPurple": "#ffa0ff", - "brightCyan": "#ffbc51", - "brightWhite": "#fed698", - "foreground": "#cac296", - "background": "#1d1908", - "cursorColor": "#cac296" - }, - { - "name": "Misterioso", - "black": "#000000", - "red": "#ff4242", - "green": "#74af68", - "yellow": "#ffad29", - "blue": "#338f86", - "purple": "#9414e6", - "cyan": "#23d7d7", - "white": "#e1e1e0", - "brightBlack": "#555555", - "brightRed": "#ff3242", - "brightGreen": "#74cd68", - "brightYellow": "#ffb929", - "brightBlue": "#23d7d7", - "brightPurple": "#ff37ff", - "brightCyan": "#00ede1", - "brightWhite": "#ffffff", - "foreground": "#e1e1e0", - "background": "#2d3743", - "cursorColor": "#e1e1e0" - }, - { - "name": "Miu", - "black": "#000000", - "red": "#b87a7a", - "green": "#7ab87a", - "yellow": "#b8b87a", - "blue": "#7a7ab8", - "purple": "#b87ab8", - "cyan": "#7ab8b8", - "white": "#d9d9d9", - "brightBlack": "#262626", - "brightRed": "#dbbdbd", - "brightGreen": "#bddbbd", - "brightYellow": "#dbdbbd", - "brightBlue": "#bdbddb", - "brightPurple": "#dbbddb", - "brightCyan": "#bddbdb", - "brightWhite": "#ffffff", - "foreground": "#d9e6f2", - "background": "#0d1926", - "cursorColor": "#d9e6f2" - }, - { - "name": "Molokai", - "black": "#1b1d1e", - "red": "#7325FA", - "green": "#23E298", - "yellow": "#60D4DF", - "blue": "#D08010", - "purple": "#FF0087", - "cyan": "#D0A843", - "white": "#BBBBBB", - "brightBlack": "#555555", - "brightRed": "#9D66F6", - "brightGreen": "#5FE0B1", - "brightYellow": "#6DF2FF", - "brightBlue": "#FFAF00", - "brightPurple": "#FF87AF", - "brightCyan": "#FFCE51", - "brightWhite": "#FFFFFF", - "foreground": "#BBBBBB", - "background": "#1b1d1e", - "cursorColor": "#BBBBBB" - }, - { - "name": "MonaLisa", - "black": "#351b0e", - "red": "#9b291c", - "green": "#636232", - "yellow": "#c36e28", - "blue": "#515c5d", - "purple": "#9b1d29", - "cyan": "#588056", - "white": "#f7d75c", - "brightBlack": "#874228", - "brightRed": "#ff4331", - "brightGreen": "#b4b264", - "brightYellow": "#ff9566", - "brightBlue": "#9eb2b4", - "brightPurple": "#ff5b6a", - "brightCyan": "#8acd8f", - "brightWhite": "#ffe598", - "foreground": "#f7d66a", - "background": "#120b0d", - "cursorColor": "#f7d66a" - }, - { - "name": "mono-amber", - "black": "#402500", - "red": "#FF9400", - "green": "#FF9400", - "yellow": "#FF9400", - "blue": "#FF9400", - "purple": "#FF9400", - "cyan": "#FF9400", - "white": "#FF9400", - "brightBlack": "#FF9400", - "brightRed": "#FF9400", - "brightGreen": "#FF9400", - "brightYellow": "#FF9400", - "brightBlue": "#FF9400", - "brightPurple": "#FF9400", - "brightCyan": "#FF9400", - "brightWhite": "#FF9400", - "foreground": "#FF9400", - "background": "#2B1900", - "cursorColor": "#FF9400" - }, - { - "name": "mono-cyan", - "black": "#003340", - "red": "#00CCFF", - "green": "#00CCFF", - "yellow": "#00CCFF", - "blue": "#00CCFF", - "purple": "#00CCFF", - "cyan": "#00CCFF", - "white": "#00CCFF", - "brightBlack": "#00CCFF", - "brightRed": "#00CCFF", - "brightGreen": "#00CCFF", - "brightYellow": "#00CCFF", - "brightBlue": "#00CCFF", - "brightPurple": "#00CCFF", - "brightCyan": "#00CCFF", - "brightWhite": "#00CCFF", - "foreground": "#00CCFF", - "background": "#00222B", - "cursorColor": "#00CCFF" - }, - { - "name": "mono-green", - "black": "#034000", - "red": "#0BFF00", - "green": "#0BFF00", - "yellow": "#0BFF00", - "blue": "#0BFF00", - "purple": "#0BFF00", - "cyan": "#0BFF00", - "white": "#0BFF00", - "brightBlack": "#0BFF00", - "brightRed": "#0BFF00", - "brightGreen": "#0BFF00", - "brightYellow": "#0BFF00", - "brightBlue": "#0BFF00", - "brightPurple": "#0BFF00", - "brightCyan": "#0BFF00", - "brightWhite": "#0BFF00", - "foreground": "#0BFF00", - "background": "#022B00", - "cursorColor": "#0BFF00" - }, - { - "name": "mono-red", - "black": "#401200", - "red": "#FF3600", - "green": "#FF3600", - "yellow": "#FF3600", - "blue": "#FF3600", - "purple": "#FF3600", - "cyan": "#FF3600", - "white": "#FF3600", - "brightBlack": "#FF3600", - "brightRed": "#FF3600", - "brightGreen": "#FF3600", - "brightYellow": "#FF3600", - "brightBlue": "#FF3600", - "brightPurple": "#FF3600", - "brightCyan": "#FF3600", - "brightWhite": "#FF3600", - "foreground": "#FF3600", - "background": "#2B0C00", - "cursorColor": "#FF3600" - }, - { - "name": "mono-white", - "black": "#3B3B3B", - "red": "#FAFAFA", - "green": "#FAFAFA", - "yellow": "#FAFAFA", - "blue": "#FAFAFA", - "purple": "#FAFAFA", - "cyan": "#FAFAFA", - "white": "#FAFAFA", - "brightBlack": "#FAFAFA", - "brightRed": "#FAFAFA", - "brightGreen": "#FAFAFA", - "brightYellow": "#FAFAFA", - "brightBlue": "#FAFAFA", - "brightPurple": "#FAFAFA", - "brightCyan": "#FAFAFA", - "brightWhite": "#FAFAFA", - "foreground": "#FAFAFA", - "background": "#262626", - "cursorColor": "#FAFAFA" - }, - { - "name": "mono-yellow", - "black": "#403500", - "red": "#FFD300", - "green": "#FFD300", - "yellow": "#FFD300", - "blue": "#FFD300", - "purple": "#FFD300", - "cyan": "#FFD300", - "white": "#FFD300", - "brightBlack": "#FFD300", - "brightRed": "#FFD300", - "brightGreen": "#FFD300", - "brightYellow": "#FFD300", - "brightBlue": "#FFD300", - "brightPurple": "#FFD300", - "brightCyan": "#FFD300", - "brightWhite": "#FFD300", - "foreground": "#FFD300", - "background": "#2B2400", - "cursorColor": "#FFD300" - }, - { - "name": "MonokaiDark", - "black": "#75715e", - "red": "#f92672", - "green": "#a6e22e", - "yellow": "#f4bf75", - "blue": "#66d9ef", - "purple": "#ae81ff", - "cyan": "#2AA198", - "white": "#f9f8f5", - "brightBlack": "#272822", - "brightRed": "#f92672", - "brightGreen": "#a6e22e", - "brightYellow": "#f4bf75", - "brightBlue": "#66d9ef", - "brightPurple": "#ae81ff", - "brightCyan": "#2AA198", - "brightWhite": "#f8f8f2", - "foreground": "#f8f8f2", - "background": "#272822", - "cursorColor": "#f8f8f2" - }, - { - "name": "MonokaiProRistretto", - "black": "#3E3838", - "red": "#DF7484", - "green": "#BBD87E", - "yellow": "#EDCE73", - "blue": "#DC9373", - "purple": "#A9AAE9", - "cyan": "#A4D7CC", - "white": "#FBF2F3", - "brightBlack": "#70696A", - "brightRed": "#DF7484", - "brightGreen": "#BBD87E", - "brightYellow": "#EDCE73", - "brightBlue": "#DC9373", - "brightPurple": "#A9AAE9", - "brightCyan": "#A4D7CC", - "brightWhite": "#FBF2F3", - "foreground": "#FBF2F3", - "background": "#3E3838", - "cursorColor": "#FBF2F3" - }, - { - "name": "MonokaiPro", - "black": "#363537", - "red": "#FF6188", - "green": "#A9DC76", - "yellow": "#FFD866", - "blue": "#FC9867", - "purple": "#AB9DF2", - "cyan": "#78DCE8", - "white": "#FDF9F3", - "brightBlack": "#908E8F", - "brightRed": "#FF6188", - "brightGreen": "#A9DC76", - "brightYellow": "#FFD866", - "brightBlue": "#FC9867", - "brightPurple": "#AB9DF2", - "brightCyan": "#78DCE8", - "brightWhite": "#FDF9F3", - "foreground": "#FDF9F3", - "background": "#363537", - "cursorColor": "#FDF9F3" - }, - { - "name": "MonokaiSoda", - "black": "#1a1a1a", - "red": "#f4005f", - "green": "#98e024", - "yellow": "#fa8419", - "blue": "#9d65ff", - "purple": "#f4005f", - "cyan": "#58d1eb", - "white": "#c4c5b5", - "brightBlack": "#625e4c", - "brightRed": "#f4005f", - "brightGreen": "#98e024", - "brightYellow": "#e0d561", - "brightBlue": "#9d65ff", - "brightPurple": "#f4005f", - "brightCyan": "#58d1eb", - "brightWhite": "#f6f6ef", - "foreground": "#c4c5b5", - "background": "#1a1a1a", - "cursorColor": "#c4c5b5" - }, - { - "name": "Morada", - "black": "#040404", - "red": "#0f49c4", - "green": "#48b117", - "yellow": "#e87324", - "blue": "#bc0116", - "purple": "#665b93", - "cyan": "#70a699", - "white": "#f5dcbe", - "brightBlack": "#4f7cbf", - "brightRed": "#1c96c7", - "brightGreen": "#3bff6f", - "brightYellow": "#efc31c", - "brightBlue": "#fb605b", - "brightPurple": "#975b5a", - "brightCyan": "#1eff8e", - "brightWhite": "#f6f5fb", - "foreground": "#ffffff", - "background": "#211f46", - "cursorColor": "#ffffff" - }, - { - "name": "N0tch2k", - "black": "#383838", - "red": "#a95551", - "green": "#666666", - "yellow": "#a98051", - "blue": "#657d3e", - "purple": "#767676", - "cyan": "#c9c9c9", - "white": "#d0b8a3", - "brightBlack": "#474747", - "brightRed": "#a97775", - "brightGreen": "#8c8c8c", - "brightYellow": "#a99175", - "brightBlue": "#98bd5e", - "brightPurple": "#a3a3a3", - "brightCyan": "#dcdcdc", - "brightWhite": "#d8c8bb", - "foreground": "#a0a0a0", - "background": "#222222", - "cursorColor": "#a0a0a0" - }, - { - "name": "neon-night", - "black": "#20242d", - "red": "#FF8E8E", - "green": "#7EFDD0", - "yellow": "#FCAD3F", - "blue": "#69B4F9", - "purple": "#DD92F6", - "cyan": "#8CE8ff", - "white": "#C9CCCD", - "brightBlack": "#20242d", - "brightRed": "#FF8E8E", - "brightGreen": "#7EFDD0", - "brightYellow": "#FCAD3F", - "brightBlue": "#69B4F9", - "brightPurple": "#DD92F6", - "brightCyan": "#8CE8ff", - "brightWhite": "#C9CCCD", - "foreground": "#C7C8FF", - "background": "#20242d", - "cursorColor": "#C7C8FF" - }, - { - "name": "Neopolitan", - "black": "#000000", - "red": "#800000", - "green": "#61ce3c", - "yellow": "#fbde2d", - "blue": "#253b76", - "purple": "#ff0080", - "cyan": "#8da6ce", - "white": "#f8f8f8", - "brightBlack": "#000000", - "brightRed": "#800000", - "brightGreen": "#61ce3c", - "brightYellow": "#fbde2d", - "brightBlue": "#253b76", - "brightPurple": "#ff0080", - "brightCyan": "#8da6ce", - "brightWhite": "#f8f8f8", - "foreground": "#ffffff", - "background": "#271f19", - "cursorColor": "#ffffff" - }, - { - "name": "Nep", - "black": "#000000", - "red": "#dd6f00", - "green": "#00dd6f", - "yellow": "#6fdd00", - "blue": "#6f00dd", - "purple": "#dd006f", - "cyan": "#006fdd", - "white": "#f2f2f2", - "brightBlack": "#7d7d7d", - "brightRed": "#ffb974", - "brightGreen": "#74ffb9", - "brightYellow": "#b9ff74", - "brightBlue": "#b974ff", - "brightPurple": "#ff74b9", - "brightCyan": "#74b9ff", - "brightWhite": "#ffffff", - "foreground": "#23476a", - "background": "#758480", - "cursorColor": "#23476a" - }, - { - "name": "Neutron", - "black": "#23252b", - "red": "#b54036", - "green": "#5ab977", - "yellow": "#deb566", - "blue": "#6a7c93", - "purple": "#a4799d", - "cyan": "#3f94a8", - "white": "#e6e8ef", - "brightBlack": "#23252b", - "brightRed": "#b54036", - "brightGreen": "#5ab977", - "brightYellow": "#deb566", - "brightBlue": "#6a7c93", - "brightPurple": "#a4799d", - "brightCyan": "#3f94a8", - "brightWhite": "#ebedf2", - "foreground": "#e6e8ef", - "background": "#1c1e22", - "cursorColor": "#e6e8ef" - }, - { - "name": "NightOwl", - "black": "#011627", - "red": "#EF5350", - "green": "#22da6e", - "yellow": "#addb67", - "blue": "#82aaff", - "purple": "#c792ea", - "cyan": "#21c7a8", - "white": "#ffffff", - "brightBlack": "#575656", - "brightRed": "#ef5350", - "brightGreen": "#22da6e", - "brightYellow": "#ffeb95", - "brightBlue": "#82aaff", - "brightPurple": "#c792ea", - "brightCyan": "#7fdbca", - "brightWhite": "#ffffff", - "foreground": "#d6deeb", - "background": "#011627", - "cursorColor": "#d6deeb" - }, - { - "name": "NightlionV1", - "black": "#4c4c4c", - "red": "#bb0000", - "green": "#5fde8f", - "yellow": "#f3f167", - "blue": "#276bd8", - "purple": "#bb00bb", - "cyan": "#00dadf", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#55ff55", - "brightYellow": "#ffff55", - "brightBlue": "#5555ff", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#bbbbbb", - "background": "#000000", - "cursorColor": "#bbbbbb" - }, - { - "name": "NightlionV2", - "black": "#4c4c4c", - "red": "#bb0000", - "green": "#04f623", - "yellow": "#f3f167", - "blue": "#64d0f0", - "purple": "#ce6fdb", - "cyan": "#00dadf", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#7df71d", - "brightYellow": "#ffff55", - "brightBlue": "#62cbe8", - "brightPurple": "#ff9bf5", - "brightCyan": "#00ccd8", - "brightWhite": "#ffffff", - "foreground": "#bbbbbb", - "background": "#171717", - "cursorColor": "#bbbbbb" - }, - { - "name": "nighty", - "black": "#373D48", - "red": "#9B3E46", - "green": "#095B32", - "yellow": "#808020", - "blue": "#1D3E6F", - "purple": "#823065", - "cyan": "#3A7458", - "white": "#828282", - "brightBlack": "#5C6370", - "brightRed": "#D0555F", - "brightGreen": "#119955", - "brightYellow": "#DFE048", - "brightBlue": "#4674B8", - "brightPurple": "#ED86C9", - "brightCyan": "#70D2A4", - "brightWhite": "#DFDFDF", - "foreground": "#DFDFDF", - "background": "#2F2F2F", - "cursorColor": "#DFDFDF" - }, - { - "name": "NordLight", - "black": "#003B4E", - "red": "#E64569", - "green": "#069F5F", - "yellow": "#DAB752", - "blue": "#439ECF", - "purple": "#D961DC", - "cyan": "#00B1BE", - "white": "#B3B3B3", - "brightBlack": "#3E89A1", - "brightRed": "#E4859A", - "brightGreen": "#A2CCA1", - "brightYellow": "#E1E387", - "brightBlue": "#6FBBE2", - "brightPurple": "#E586E7", - "brightCyan": "#96DCDA", - "brightWhite": "#DEDEDE", - "foreground": "#004f7c", - "background": "#ebeaf2", - "cursorColor": "#439ECF" - }, - { - "name": "Nord", - "black": "#3B4252", - "red": "#BF616A", - "green": "#A3BE8C", - "yellow": "#EBCB8B", - "blue": "#81A1C1", - "purple": "#B48EAD", - "cyan": "#88C0D0", - "white": "#E5E9F0", - "brightBlack": "#4C566A", - "brightRed": "#BF616A", - "brightGreen": "#A3BE8C", - "brightYellow": "#EBCB8B", - "brightBlue": "#81A1C1", - "brightPurple": "#B48EAD", - "brightCyan": "#8FBCBB", - "brightWhite": "#ECEFF4", - "foreground": "#D8DEE9", - "background": "#2E3440", - "cursorColor": "#D8DEE9" - }, - { - "name": "Novel", - "black": "#000000", - "red": "#cc0000", - "green": "#009600", - "yellow": "#d06b00", - "blue": "#0000cc", - "purple": "#cc00cc", - "cyan": "#0087cc", - "white": "#cccccc", - "brightBlack": "#808080", - "brightRed": "#cc0000", - "brightGreen": "#009600", - "brightYellow": "#d06b00", - "brightBlue": "#0000cc", - "brightPurple": "#cc00cc", - "brightCyan": "#0087cc", - "brightWhite": "#ffffff", - "foreground": "#3b2322", - "background": "#dfdbc3", - "cursorColor": "#3b2322" - }, - { - "name": "Obsidian", - "black": "#000000", - "red": "#a60001", - "green": "#00bb00", - "yellow": "#fecd22", - "blue": "#3a9bdb", - "purple": "#bb00bb", - "cyan": "#00bbbb", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#ff0003", - "brightGreen": "#93c863", - "brightYellow": "#fef874", - "brightBlue": "#a1d7ff", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#cdcdcd", - "background": "#283033", - "cursorColor": "#cdcdcd" - }, - { - "name": "OceanDark", - "black": "#4F4F4F", - "red": "#AF4B57", - "green": "#AFD383", - "yellow": "#E5C079", - "blue": "#7D90A4", - "purple": "#A4799D", - "cyan": "#85A6A5", - "white": "#EEEDEE", - "brightBlack": "#7B7B7B", - "brightRed": "#AF4B57", - "brightGreen": "#CEFFAB", - "brightYellow": "#FFFECC", - "brightBlue": "#B5DCFE", - "brightPurple": "#FB9BFE", - "brightCyan": "#DFDFFD", - "brightWhite": "#FEFFFE", - "foreground": "#979CAC", - "background": "#1C1F27", - "cursorColor": "#979CAC" - }, - { - "name": "Ocean", - "black": "#000000", - "red": "#990000", - "green": "#00a600", - "yellow": "#999900", - "blue": "#0000b2", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#00d900", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#ffffff", - "background": "#224fbc", - "cursorColor": "#ffffff" - }, - { - "name": "OceanicNext", - "black": "#121C21", - "red": "#E44754", - "green": "#89BD82", - "yellow": "#F7BD51", - "blue": "#5486C0", - "purple": "#B77EB8", - "cyan": "#50A5A4", - "white": "#FFFFFF", - "brightBlack": "#52606B", - "brightRed": "#E44754", - "brightGreen": "#89BD82", - "brightYellow": "#F7BD51", - "brightBlue": "#5486C0", - "brightPurple": "#B77EB8", - "brightCyan": "#50A5A4", - "brightWhite": "#FFFFFF", - "foreground": "#b3b8c3", - "background": "#121b21", - "cursorColor": "#b3b8c3" - }, - { - "name": "Ollie", - "black": "#000000", - "red": "#ac2e31", - "green": "#31ac61", - "yellow": "#ac4300", - "blue": "#2d57ac", - "purple": "#b08528", - "cyan": "#1fa6ac", - "white": "#8a8eac", - "brightBlack": "#5b3725", - "brightRed": "#ff3d48", - "brightGreen": "#3bff99", - "brightYellow": "#ff5e1e", - "brightBlue": "#4488ff", - "brightPurple": "#ffc21d", - "brightCyan": "#1ffaff", - "brightWhite": "#5b6ea7", - "foreground": "#8a8dae", - "background": "#222125", - "cursorColor": "#8a8dae" - }, - { - "name": "Omni", - "black": "#191622", - "red": "#E96379", - "green": "#67e480", - "yellow": "#E89E64", - "blue": "#78D1E1", - "purple": "#988BC7", - "cyan": "#FF79C6", - "white": "#ABB2BF", - "brightBlack": "#000000", - "brightRed": "#E96379", - "brightGreen": "#67e480", - "brightYellow": "#E89E64", - "brightBlue": "#78D1E1", - "brightPurple": "#988BC7", - "brightCyan": "#FF79C6", - "brightWhite": "#ffffff", - "foreground": "#ABB2BF", - "background": "#191622", - "cursorColor": "#ABB2BF" - }, - { - "name": "OneDark", - "black": "#000000", - "red": "#E06C75", - "green": "#98C379", - "yellow": "#D19A66", - "blue": "#61AFEF", - "purple": "#C678DD", - "cyan": "#56B6C2", - "white": "#ABB2BF", - "brightBlack": "#5C6370", - "brightRed": "#E06C75", - "brightGreen": "#98C379", - "brightYellow": "#D19A66", - "brightBlue": "#61AFEF", - "brightPurple": "#C678DD", - "brightCyan": "#56B6C2", - "brightWhite": "#FFFEFE", - "foreground": "#5C6370", - "background": "#1E2127", - "cursorColor": "#5C6370" - }, - { - "name": "OneHalfBlack", - "black": "#282c34", - "red": "#e06c75", - "green": "#98c379", - "yellow": "#e5c07b", - "blue": "#61afef", - "purple": "#c678dd", - "cyan": "#56b6c2", - "white": "#dcdfe4", - "brightBlack": "#282c34", - "brightRed": "#e06c75", - "brightGreen": "#98c379", - "brightYellow": "#e5c07b", - "brightBlue": "#61afef", - "brightPurple": "#c678dd", - "brightCyan": "#56b6c2", - "brightWhite": "#dcdfe4", - "foreground": "#dcdfe4", - "background": "#000000", - "cursorColor": "#dcdfe4" - }, - { - "name": "OneLight", - "black": "#000000", - "red": "#DA3E39", - "green": "#41933E", - "yellow": "#855504", - "blue": "#315EEE", - "purple": "#930092", - "cyan": "#0E6FAD", - "white": "#8E8F96", - "brightBlack": "#2A2B32", - "brightRed": "#DA3E39", - "brightGreen": "#41933E", - "brightYellow": "#855504", - "brightBlue": "#315EEE", - "brightPurple": "#930092", - "brightCyan": "#0E6FAD", - "brightWhite": "#FFFEFE", - "foreground": "#2A2B32", - "background": "#F8F8F8", - "cursorColor": "#2A2B32" - }, - { - "name": "palenight", - "black": "#292D3E", - "red": "#F07178", - "green": "#C3E88D", - "yellow": "#FFCB6B", - "blue": "#82AAFF", - "purple": "#C792EA", - "cyan": "#60ADEC", - "white": "#ABB2BF", - "brightBlack": "#959DCB", - "brightRed": "#F07178", - "brightGreen": "#C3E88D", - "brightYellow": "#FF5572", - "brightBlue": "#82AAFF", - "brightPurple": "#FFCB6B", - "brightCyan": "#676E95", - "brightWhite": "#FFFEFE", - "foreground": "#BFC7D5", - "background": "#292D3E", - "cursorColor": "#BFC7D5" - }, - { - "name": "Pali", - "black": "#0a0a0a", - "red": "#ab8f74", - "green": "#74ab8f", - "yellow": "#8fab74", - "blue": "#8f74ab", - "purple": "#ab748f", - "cyan": "#748fab", - "white": "#F2F2F2", - "brightBlack": "#5D5D5D", - "brightRed": "#FF1D62", - "brightGreen": "#9cc3af", - "brightYellow": "#FFD00A", - "brightBlue": "#af9cc3", - "brightPurple": "#FF1D62", - "brightCyan": "#4BB8FD", - "brightWhite": "#A020F0", - "foreground": "#d9e6f2", - "background": "#232E37", - "cursorColor": "#d9e6f2" - }, - { - "name": "Panda", - "black": "#1F1F20", - "red": "#FB055A", - "green": "#26FFD4", - "yellow": "#FDAA5A", - "blue": "#5C9FFF", - "purple": "#FC59A6", - "cyan": "#26FFD4", - "white": "#F0F0F0", - "brightBlack": "#5C6370", - "brightRed": "#FB055A", - "brightGreen": "#26FFD4", - "brightYellow": "#FEBE7E", - "brightBlue": "#55ADFF", - "brightPurple": "#FD95D0", - "brightCyan": "#26FFD4", - "brightWhite": "#F0F0F0", - "foreground": "#F0F0F0", - "background": "#1D1E20", - "cursorColor": "#F0F0F0" - }, - { - "name": "PaperColorDark", - "black": "#1C1C1C", - "red": "#AF005F", - "green": "#5FAF00", - "yellow": "#D7AF5F", - "blue": "#5FAFD7", - "purple": "#808080", - "cyan": "#D7875F", - "white": "#D0D0D0", - "brightBlack": "#585858", - "brightRed": "#5FAF5F", - "brightGreen": "#AFD700", - "brightYellow": "#AF87D7", - "brightBlue": "#FFAF00", - "brightPurple": "#FF5FAF", - "brightCyan": "#00AFAF", - "brightWhite": "#5F8787", - "foreground": "#D0D0D0", - "background": "#1C1C1C", - "cursorColor": "#D0D0D0" - }, - { - "name": "PaperColorLight", - "black": "#EEEEEE", - "red": "#AF0000", - "green": "#008700", - "yellow": "#5F8700", - "blue": "#0087AF", - "purple": "#878787", - "cyan": "#005F87", - "white": "#444444", - "brightBlack": "#BCBCBC", - "brightRed": "#D70000", - "brightGreen": "#D70087", - "brightYellow": "#8700AF", - "brightBlue": "#D75F00", - "brightPurple": "#D75F00", - "brightCyan": "#005FAF", - "brightWhite": "#005F87", - "foreground": "#444444", - "background": "#EEEEEE", - "cursorColor": "#444444" - }, - { - "name": "ParaisoDark", - "black": "#2f1e2e", - "red": "#ef6155", - "green": "#48b685", - "yellow": "#fec418", - "blue": "#06b6ef", - "purple": "#815ba4", - "cyan": "#5bc4bf", - "white": "#a39e9b", - "brightBlack": "#776e71", - "brightRed": "#ef6155", - "brightGreen": "#48b685", - "brightYellow": "#fec418", - "brightBlue": "#06b6ef", - "brightPurple": "#815ba4", - "brightCyan": "#5bc4bf", - "brightWhite": "#e7e9db", - "foreground": "#a39e9b", - "background": "#2f1e2e", - "cursorColor": "#a39e9b" - }, - { - "name": "PaulMillr", - "black": "#2a2a2a", - "red": "#ff0000", - "green": "#79ff0f", - "yellow": "#d3bf00", - "blue": "#396bd7", - "purple": "#b449be", - "cyan": "#66ccff", - "white": "#bbbbbb", - "brightBlack": "#666666", - "brightRed": "#ff0080", - "brightGreen": "#66ff66", - "brightYellow": "#f3d64e", - "brightBlue": "#709aed", - "brightPurple": "#db67e6", - "brightCyan": "#7adff2", - "brightWhite": "#ffffff", - "foreground": "#f2f2f2", - "background": "#000000", - "cursorColor": "#f2f2f2" - }, - { - "name": "PencilDark", - "black": "#212121", - "red": "#c30771", - "green": "#10a778", - "yellow": "#a89c14", - "blue": "#008ec4", - "purple": "#523c79", - "cyan": "#20a5ba", - "white": "#d9d9d9", - "brightBlack": "#424242", - "brightRed": "#fb007a", - "brightGreen": "#5fd7af", - "brightYellow": "#f3e430", - "brightBlue": "#20bbfc", - "brightPurple": "#6855de", - "brightCyan": "#4fb8cc", - "brightWhite": "#f1f1f1", - "foreground": "#f1f1f1", - "background": "#212121", - "cursorColor": "#f1f1f1" - }, - { - "name": "PencilLight", - "black": "#212121", - "red": "#c30771", - "green": "#10a778", - "yellow": "#a89c14", - "blue": "#008ec4", - "purple": "#523c79", - "cyan": "#20a5ba", - "white": "#d9d9d9", - "brightBlack": "#424242", - "brightRed": "#fb007a", - "brightGreen": "#5fd7af", - "brightYellow": "#f3e430", - "brightBlue": "#20bbfc", - "brightPurple": "#6855de", - "brightCyan": "#4fb8cc", - "brightWhite": "#f1f1f1", - "foreground": "#424242", - "background": "#f1f1f1", - "cursorColor": "#424242" - }, - { - "name": "Peppermint", - "black": "#353535", - "red": "#E64569", - "green": "#89D287", - "yellow": "#DAB752", - "blue": "#439ECF", - "purple": "#D961DC", - "cyan": "#64AAAF", - "white": "#B3B3B3", - "brightBlack": "#535353", - "brightRed": "#E4859A", - "brightGreen": "#A2CCA1", - "brightYellow": "#E1E387", - "brightBlue": "#6FBBE2", - "brightPurple": "#E586E7", - "brightCyan": "#96DCDA", - "brightWhite": "#DEDEDE", - "foreground": "#C7C7C7", - "background": "#000000", - "cursorColor": "#BBBBBB" - }, - { - "name": "Pixiefloss", - "black": "#2f2942", - "red": "#ff857f", - "green": "#48b685", - "yellow": "#e6c000", - "blue": "#ae81ff", - "purple": "#ef6155", - "cyan": "#c2ffdf", - "white": "#f8f8f2", - "brightBlack": "#75507b", - "brightRed": "#f1568e", - "brightGreen": "#5adba2", - "brightYellow": "#d5a425", - "brightBlue": "#c5a3ff", - "brightPurple": "#ef6155", - "brightCyan": "#c2ffff", - "brightWhite": "#f8f8f0", - "foreground": "#d1cae8", - "background": "#241f33", - "cursorColor": "#d1cae8" - }, - { - "name": "Pnevma", - "black": "#2f2e2d", - "red": "#a36666", - "green": "#90a57d", - "yellow": "#d7af87", - "blue": "#7fa5bd", - "purple": "#c79ec4", - "cyan": "#8adbb4", - "white": "#d0d0d0", - "brightBlack": "#4a4845", - "brightRed": "#d78787", - "brightGreen": "#afbea2", - "brightYellow": "#e4c9af", - "brightBlue": "#a1bdce", - "brightPurple": "#d7beda", - "brightCyan": "#b1e7dd", - "brightWhite": "#efefef", - "foreground": "#d0d0d0", - "background": "#1c1c1c", - "cursorColor": "#d0d0d0" - }, - { - "name": "PowerShell", - "black": "#000000", - "red": "#7E0008", - "green": "#098003", - "yellow": "#C4A000", - "blue": "#010083", - "purple": "#D33682", - "cyan": "#0E807F", - "white": "#7F7C7F", - "brightBlack": "#808080", - "brightRed": "#EF2929", - "brightGreen": "#1CFE3C", - "brightYellow": "#FEFE45", - "brightBlue": "#268AD2", - "brightPurple": "#FE13FA", - "brightCyan": "#29FFFE", - "brightWhite": "#C2C1C3", - "foreground": "#F6F6F7", - "background": "#052454", - "cursorColor": "#F6F6F7" - }, - { - "name": "Pro", - "black": "#000000", - "red": "#990000", - "green": "#00a600", - "yellow": "#999900", - "blue": "#2009db", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#00d900", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#f2f2f2", - "background": "#000000", - "cursorColor": "#f2f2f2" - }, - { - "name": "PurplePeopleEater", - "black": "#0d1117", - "red": "#e34c26", - "green": "#238636", - "yellow": "#ed9a51", - "blue": "#a5d6ff", - "purple": "#6eb0e8", - "cyan": "#c09aeb", - "white": "#c9d1d9", - "brightBlack": "#0d1117", - "brightRed": "#ff7b72", - "brightGreen": "#3bab4a", - "brightYellow": "#ffa657", - "brightBlue": "#a5d6ff", - "brightPurple": "#79c0ff", - "brightCyan": "#b694df", - "brightWhite": "#c9d1d9", - "foreground": "#c9d1d9", - "background": "#161b22", - "cursorColor": "#c9d1d9" - }, - { - "name": "RedAlert", - "black": "#000000", - "red": "#d62e4e", - "green": "#71be6b", - "yellow": "#beb86b", - "blue": "#489bee", - "purple": "#e979d7", - "cyan": "#6bbeb8", - "white": "#d6d6d6", - "brightBlack": "#262626", - "brightRed": "#e02553", - "brightGreen": "#aff08c", - "brightYellow": "#dfddb7", - "brightBlue": "#65aaf1", - "brightPurple": "#ddb7df", - "brightCyan": "#b7dfdd", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#762423", - "cursorColor": "#ffffff" - }, - { - "name": "RedSands", - "black": "#000000", - "red": "#ff3f00", - "green": "#00bb00", - "yellow": "#e7b000", - "blue": "#0072ff", - "purple": "#bb00bb", - "cyan": "#00bbbb", - "white": "#bbbbbb", - "brightBlack": "#555555", - "brightRed": "#bb0000", - "brightGreen": "#00bb00", - "brightYellow": "#e7b000", - "brightBlue": "#0072ae", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#d7c9a7", - "background": "#7a251e", - "cursorColor": "#d7c9a7" - }, - { - "name": "Relaxed", - "black": "#151515", - "red": "#BC5653", - "green": "#909D63", - "yellow": "#EBC17A", - "blue": "#6A8799", - "purple": "#B06698", - "cyan": "#C9DFFF", - "white": "#D9D9D9", - "brightBlack": "#636363", - "brightRed": "#BC5653", - "brightGreen": "#A0AC77", - "brightYellow": "#EBC17A", - "brightBlue": "#7EAAC7", - "brightPurple": "#B06698", - "brightCyan": "#ACBBD0", - "brightWhite": "#F7F7F7", - "foreground": "#D9D9D9", - "background": "#353A44", - "cursorColor": "#D9D9D9" - }, - { - "name": "Rippedcasts", - "black": "#000000", - "red": "#cdaf95", - "green": "#a8ff60", - "yellow": "#bfbb1f", - "blue": "#75a5b0", - "purple": "#ff73fd", - "cyan": "#5a647e", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#eecbad", - "brightGreen": "#bcee68", - "brightYellow": "#e5e500", - "brightBlue": "#86bdc9", - "brightPurple": "#e500e5", - "brightCyan": "#8c9bc4", - "brightWhite": "#e5e5e5", - "foreground": "#ffffff", - "background": "#2b2b2b", - "cursorColor": "#ffffff" - }, - { - "name": "Royal", - "black": "#241f2b", - "red": "#91284c", - "green": "#23801c", - "yellow": "#b49d27", - "blue": "#6580b0", - "purple": "#674d96", - "cyan": "#8aaabe", - "white": "#524966", - "brightBlack": "#312d3d", - "brightRed": "#d5356c", - "brightGreen": "#2cd946", - "brightYellow": "#fde83b", - "brightBlue": "#90baf9", - "brightPurple": "#a479e3", - "brightCyan": "#acd4eb", - "brightWhite": "#9e8cbd", - "foreground": "#514968", - "background": "#100815", - "cursorColor": "#514968" - }, - { - "name": "Sat", - "black": "#000000", - "red": "#dd0007", - "green": "#07dd00", - "yellow": "#ddd600", - "blue": "#0007dd", - "purple": "#d600dd", - "cyan": "#00ddd6", - "white": "#f2f2f2", - "brightBlack": "#7d7d7d", - "brightRed": "#ff7478", - "brightGreen": "#78ff74", - "brightYellow": "#fffa74", - "brightBlue": "#7478ff", - "brightPurple": "#fa74ff", - "brightCyan": "#74fffa", - "brightWhite": "#ffffff", - "foreground": "#23476a", - "background": "#758480", - "cursorColor": "#23476a" - }, - { - "name": "SeaShells", - "black": "#17384c", - "red": "#d15123", - "green": "#027c9b", - "yellow": "#fca02f", - "blue": "#1e4950", - "purple": "#68d4f1", - "cyan": "#50a3b5", - "white": "#deb88d", - "brightBlack": "#434b53", - "brightRed": "#d48678", - "brightGreen": "#628d98", - "brightYellow": "#fdd39f", - "brightBlue": "#1bbcdd", - "brightPurple": "#bbe3ee", - "brightCyan": "#87acb4", - "brightWhite": "#fee4ce", - "foreground": "#deb88d", - "background": "#09141b", - "cursorColor": "#deb88d" - }, - { - "name": "SeafoamPastel", - "black": "#757575", - "red": "#825d4d", - "green": "#728c62", - "yellow": "#ada16d", - "blue": "#4d7b82", - "purple": "#8a7267", - "cyan": "#729494", - "white": "#e0e0e0", - "brightBlack": "#8a8a8a", - "brightRed": "#cf937a", - "brightGreen": "#98d9aa", - "brightYellow": "#fae79d", - "brightBlue": "#7ac3cf", - "brightPurple": "#d6b2a1", - "brightCyan": "#ade0e0", - "brightWhite": "#e0e0e0", - "foreground": "#d4e7d4", - "background": "#243435", - "cursorColor": "#d4e7d4" - }, - { - "name": "Seti", - "black": "#323232", - "red": "#c22832", - "green": "#8ec43d", - "yellow": "#e0c64f", - "blue": "#43a5d5", - "purple": "#8b57b5", - "cyan": "#8ec43d", - "white": "#eeeeee", - "brightBlack": "#323232", - "brightRed": "#c22832", - "brightGreen": "#8ec43d", - "brightYellow": "#e0c64f", - "brightBlue": "#43a5d5", - "brightPurple": "#8b57b5", - "brightCyan": "#8ec43d", - "brightWhite": "#ffffff", - "foreground": "#cacecd", - "background": "#111213", - "cursorColor": "#cacecd" - }, - { - "name": "Shaman", - "black": "#012026", - "red": "#b2302d", - "green": "#00a941", - "yellow": "#5e8baa", - "blue": "#449a86", - "purple": "#00599d", - "cyan": "#5d7e19", - "white": "#405555", - "brightBlack": "#384451", - "brightRed": "#ff4242", - "brightGreen": "#2aea5e", - "brightYellow": "#8ed4fd", - "brightBlue": "#61d5ba", - "brightPurple": "#1298ff", - "brightCyan": "#98d028", - "brightWhite": "#58fbd6", - "foreground": "#405555", - "background": "#001015", - "cursorColor": "#405555" - }, - { - "name": "Shel", - "black": "#2c2423", - "red": "#ab2463", - "green": "#6ca323", - "yellow": "#ab6423", - "blue": "#2c64a2", - "purple": "#6c24a2", - "cyan": "#2ca363", - "white": "#918988", - "brightBlack": "#918988", - "brightRed": "#f588b9", - "brightGreen": "#c2ee86", - "brightYellow": "#f5ba86", - "brightBlue": "#8fbaec", - "brightPurple": "#c288ec", - "brightCyan": "#8feeb9", - "brightWhite": "#f5eeec", - "foreground": "#4882cd", - "background": "#2a201f", - "cursorColor": "#4882cd" - }, - { - "name": "Slate", - "black": "#222222", - "red": "#e2a8bf", - "green": "#81d778", - "yellow": "#c4c9c0", - "blue": "#264b49", - "purple": "#a481d3", - "cyan": "#15ab9c", - "white": "#02c5e0", - "brightBlack": "#ffffff", - "brightRed": "#ffcdd9", - "brightGreen": "#beffa8", - "brightYellow": "#d0ccca", - "brightBlue": "#7ab0d2", - "brightPurple": "#c5a7d9", - "brightCyan": "#8cdfe0", - "brightWhite": "#e0e0e0", - "foreground": "#35b1d2", - "background": "#222222", - "cursorColor": "#35b1d2" - }, - { - "name": "Smyck", - "black": "#000000", - "red": "#C75646", - "green": "#8EB33B", - "yellow": "#D0B03C", - "blue": "#72B3CC", - "purple": "#C8A0D1", - "cyan": "#218693", - "white": "#B0B0B0", - "brightBlack": "#5D5D5D", - "brightRed": "#E09690", - "brightGreen": "#CDEE69", - "brightYellow": "#FFE377", - "brightBlue": "#9CD9F0", - "brightPurple": "#FBB1F9", - "brightCyan": "#77DFD8", - "brightWhite": "#F7F7F7", - "foreground": "#F7F7F7", - "background": "#242424", - "cursorColor": "#F7F7F7" - }, - { - "name": "Snazzy", - "black": "#282A36", - "red": "#FF5C57", - "green": "#5AF78E", - "yellow": "#F3F99D", - "blue": "#57C7FF", - "purple": "#FF6AC1", - "cyan": "#9AEDFE", - "white": "#F1F1F0", - "brightBlack": "#686868", - "brightRed": "#FF5C57", - "brightGreen": "#5AF78E", - "brightYellow": "#F3F99D", - "brightBlue": "#57C7FF", - "brightPurple": "#FF6AC1", - "brightCyan": "#9AEDFE", - "brightWhite": "#EFF0EB", - "foreground": "#EFF0EB", - "background": "#282A36", - "cursorColor": "#97979B" - }, - { - "name": "SoftServer", - "black": "#000000", - "red": "#a2686a", - "green": "#9aa56a", - "yellow": "#a3906a", - "blue": "#6b8fa3", - "purple": "#6a71a3", - "cyan": "#6ba58f", - "white": "#99a3a2", - "brightBlack": "#666c6c", - "brightRed": "#dd5c60", - "brightGreen": "#bfdf55", - "brightYellow": "#deb360", - "brightBlue": "#62b1df", - "brightPurple": "#606edf", - "brightCyan": "#64e39c", - "brightWhite": "#d2e0de", - "foreground": "#99a3a2", - "background": "#242626", - "cursorColor": "#99a3a2" - }, - { - "name": "SolarizedDarcula", - "black": "#25292a", - "red": "#f24840", - "green": "#629655", - "yellow": "#b68800", - "blue": "#2075c7", - "purple": "#797fd4", - "cyan": "#15968d", - "white": "#d2d8d9", - "brightBlack": "#25292a", - "brightRed": "#f24840", - "brightGreen": "#629655", - "brightYellow": "#b68800", - "brightBlue": "#2075c7", - "brightPurple": "#797fd4", - "brightCyan": "#15968d", - "brightWhite": "#d2d8d9", - "foreground": "#d2d8d9", - "background": "#3d3f41", - "cursorColor": "#d2d8d9" - }, - { - "name": "SolarizedDarkHigherContrast", - "black": "#002831", - "red": "#d11c24", - "green": "#6cbe6c", - "yellow": "#a57706", - "blue": "#2176c7", - "purple": "#c61c6f", - "cyan": "#259286", - "white": "#eae3cb", - "brightBlack": "#006488", - "brightRed": "#f5163b", - "brightGreen": "#51ef84", - "brightYellow": "#b27e28", - "brightBlue": "#178ec8", - "brightPurple": "#e24d8e", - "brightCyan": "#00b39e", - "brightWhite": "#fcf4dc", - "foreground": "#9cc2c3", - "background": "#001e27", - "cursorColor": "#9cc2c3" - }, - { - "name": "SolarizedDark", - "black": "#073642", - "red": "#DC322F", - "green": "#859900", - "yellow": "#CF9A6B", - "blue": "#268BD2", - "purple": "#D33682", - "cyan": "#2AA198", - "white": "#EEE8D5", - "brightBlack": "#657B83", - "brightRed": "#D87979", - "brightGreen": "#88CF76", - "brightYellow": "#657B83", - "brightBlue": "#2699FF", - "brightPurple": "#D33682", - "brightCyan": "#43B8C3", - "brightWhite": "#FDF6E3", - "foreground": "#839496", - "background": "#002B36", - "cursorColor": "#839496" - }, - { - "name": "SolarizedLight", - "black": "#073642", - "red": "#DC322F", - "green": "#859900", - "yellow": "#B58900", - "blue": "#268BD2", - "purple": "#D33682", - "cyan": "#2AA198", - "white": "#EEE8D5", - "brightBlack": "#002B36", - "brightRed": "#CB4B16", - "brightGreen": "#586E75", - "brightYellow": "#657B83", - "brightBlue": "#839496", - "brightPurple": "#6C71C4", - "brightCyan": "#93A1A1", - "brightWhite": "#FDF6E3", - "foreground": "#657B83", - "background": "#FDF6E3", - "cursorColor": "#657B83" - }, - { - "name": "Sonokai", - "black": "#2C2E34", - "red": "#FC5D7C", - "green": "#9ED072", - "yellow": "#E7C664", - "blue": "#F39660", - "purple": "#B39DF3", - "cyan": "#76CCE0", - "white": "#E2E2E3", - "brightBlack": "#2C2E34", - "brightRed": "#FC5D7C", - "brightGreen": "#9ED072", - "brightYellow": "#E7C664", - "brightBlue": "#F39660", - "brightPurple": "#B39DF3", - "brightCyan": "#76CCE0", - "brightWhite": "#E2E2E3", - "foreground": "#E2E2E3", - "background": "#2C2E34", - "cursorColor": "#E2E2E3" - }, - { - "name": "Spacedust", - "black": "#6e5346", - "red": "#e35b00", - "green": "#5cab96", - "yellow": "#e3cd7b", - "blue": "#0f548b", - "purple": "#e35b00", - "cyan": "#06afc7", - "white": "#f0f1ce", - "brightBlack": "#684c31", - "brightRed": "#ff8a3a", - "brightGreen": "#aecab8", - "brightYellow": "#ffc878", - "brightBlue": "#67a0ce", - "brightPurple": "#ff8a3a", - "brightCyan": "#83a7b4", - "brightWhite": "#fefff1", - "foreground": "#ecf0c1", - "background": "#0a1e24", - "cursorColor": "#ecf0c1" - }, - { - "name": "SpaceGrayEightiesDull", - "black": "#15171c", - "red": "#b24a56", - "green": "#92b477", - "yellow": "#c6735a", - "blue": "#7c8fa5", - "purple": "#a5789e", - "cyan": "#80cdcb", - "white": "#b3b8c3", - "brightBlack": "#555555", - "brightRed": "#ec5f67", - "brightGreen": "#89e986", - "brightYellow": "#fec254", - "brightBlue": "#5486c0", - "brightPurple": "#bf83c1", - "brightCyan": "#58c2c1", - "brightWhite": "#ffffff", - "foreground": "#c9c6bc", - "background": "#222222", - "cursorColor": "#c9c6bc" - }, - { - "name": "SpaceGrayEighties", - "black": "#15171c", - "red": "#ec5f67", - "green": "#81a764", - "yellow": "#fec254", - "blue": "#5486c0", - "purple": "#bf83c1", - "cyan": "#57c2c1", - "white": "#efece7", - "brightBlack": "#555555", - "brightRed": "#ff6973", - "brightGreen": "#93d493", - "brightYellow": "#ffd256", - "brightBlue": "#4d84d1", - "brightPurple": "#ff55ff", - "brightCyan": "#83e9e4", - "brightWhite": "#ffffff", - "foreground": "#bdbaae", - "background": "#222222", - "cursorColor": "#bdbaae" - }, - { - "name": "SpaceGray", - "black": "#000000", - "red": "#b04b57", - "green": "#87b379", - "yellow": "#e5c179", - "blue": "#7d8fa4", - "purple": "#a47996", - "cyan": "#85a7a5", - "white": "#b3b8c3", - "brightBlack": "#000000", - "brightRed": "#b04b57", - "brightGreen": "#87b379", - "brightYellow": "#e5c179", - "brightBlue": "#7d8fa4", - "brightPurple": "#a47996", - "brightCyan": "#85a7a5", - "brightWhite": "#ffffff", - "foreground": "#b3b8c3", - "background": "#20242d", - "cursorColor": "#b3b8c3" - }, - { - "name": "Spring", - "black": "#000000", - "red": "#ff4d83", - "green": "#1f8c3b", - "yellow": "#1fc95b", - "blue": "#1dd3ee", - "purple": "#8959a8", - "cyan": "#3e999f", - "white": "#ffffff", - "brightBlack": "#000000", - "brightRed": "#ff0021", - "brightGreen": "#1fc231", - "brightYellow": "#d5b807", - "brightBlue": "#15a9fd", - "brightPurple": "#8959a8", - "brightCyan": "#3e999f", - "brightWhite": "#ffffff", - "foreground": "#ecf0c1", - "background": "#0a1e24", - "cursorColor": "#ecf0c1" - }, - { - "name": "Square", - "black": "#050505", - "red": "#e9897c", - "green": "#b6377d", - "yellow": "#ecebbe", - "blue": "#a9cdeb", - "purple": "#75507b", - "cyan": "#c9caec", - "white": "#f2f2f2", - "brightBlack": "#141414", - "brightRed": "#f99286", - "brightGreen": "#c3f786", - "brightYellow": "#fcfbcc", - "brightBlue": "#b6defb", - "brightPurple": "#ad7fa8", - "brightCyan": "#d7d9fc", - "brightWhite": "#e2e2e2", - "foreground": "#a1a1a1", - "background": "#0a1e24", - "cursorColor": "#a1a1a1" - }, - { - "name": "Srcery", - "black": "#1C1B19", - "red": "#FF3128", - "green": "#519F50", - "yellow": "#FBB829", - "blue": "#5573A3", - "purple": "#E02C6D", - "cyan": "#0AAEB3", - "white": "#918175", - "brightBlack": "#2D2B28", - "brightRed": "#F75341", - "brightGreen": "#98BC37", - "brightYellow": "#FED06E", - "brightBlue": "#8EB2F7", - "brightPurple": "#E35682", - "brightCyan": "#53FDE9", - "brightWhite": "#FCE8C3", - "foreground": "#ebdbb2", - "background": "#282828", - "cursorColor": "#ebdbb2" - }, - { - "name": "summer-pop", - "black": "#666666", - "red": "#FF1E8E", - "green": "#8EFF1E", - "yellow": "#FFFB00", - "blue": "#1E8EFF", - "purple": "#E500E5", - "cyan": "#00E5E5", - "white": "#E5E5E5", - "brightBlack": "#666666", - "brightRed": "#FF1E8E", - "brightGreen": "#8EFF1E", - "brightYellow": "#FFFB00", - "brightBlue": "#1E8EFF", - "brightPurple": "#E500E5", - "brightCyan": "#00E5E5", - "brightWhite": "#E5E5E5", - "foreground": "#FFFFFF", - "background": "#272822", - "cursorColor": "#FFFFFF" - }, - { - "name": "Sundried", - "black": "#302b2a", - "red": "#a7463d", - "green": "#587744", - "yellow": "#9d602a", - "blue": "#485b98", - "purple": "#864651", - "cyan": "#9c814f", - "white": "#c9c9c9", - "brightBlack": "#4d4e48", - "brightRed": "#aa000c", - "brightGreen": "#128c21", - "brightYellow": "#fc6a21", - "brightBlue": "#7999f7", - "brightPurple": "#fd8aa1", - "brightCyan": "#fad484", - "brightWhite": "#ffffff", - "foreground": "#c9c9c9", - "background": "#1a1818", - "cursorColor": "#c9c9c9" - }, - { - "name": "sweet-eliverlara", - "black": "#282C34", - "red": "#ED254E", - "green": "#71F79F", - "yellow": "#F9DC5C", - "blue": "#7CB7FF", - "purple": "#C74DED", - "cyan": "#00C1E4", - "white": "#DCDFE4", - "brightBlack": "#282C34", - "brightRed": "#ED254E", - "brightGreen": "#71F79F", - "brightYellow": "#F9DC5C", - "brightBlue": "#7CB7FF", - "brightPurple": "#C74DED", - "brightCyan": "#00C1E4", - "brightWhite": "#DCDFE4", - "foreground": "#C3C7D1", - "background": "#282C34", - "cursorColor": "#C3C7D1" - }, - { - "name": "SweetTerminal", - "black": "#3F3F54", - "red": "#f60055", - "green": "#06c993", - "yellow": "#9700be", - "blue": "#f69154", - "purple": "#ec89cb", - "cyan": "#60ADEC", - "white": "#ABB2BF", - "brightBlack": "#959DCB", - "brightRed": "#f60055", - "brightGreen": "#06c993", - "brightYellow": "#9700be", - "brightBlue": "#f69154", - "brightPurple": "#ec89cb", - "brightCyan": "#00dded", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#222235", - "cursorColor": "#ffffff" - }, - { - "name": "Symphonic", - "black": "#000000", - "red": "#dc322f", - "green": "#56db3a", - "yellow": "#ff8400", - "blue": "#0084d4", - "purple": "#b729d9", - "cyan": "#ccccff", - "white": "#ffffff", - "brightBlack": "#1b1d21", - "brightRed": "#dc322f", - "brightGreen": "#56db3a", - "brightYellow": "#ff8400", - "brightBlue": "#0084d4", - "brightPurple": "#b729d9", - "brightCyan": "#ccccff", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#000000", - "cursorColor": "#ffffff" - }, - { - "name": "SynthWave", - "black": "#011627", - "red": "#fe4450", - "green": "#72f1b8", - "yellow": "#fede5d", - "blue": "#03edf9", - "purple": "#ff7edb", - "cyan": "#03edf9", - "white": "#ffffff", - "brightBlack": "#575656", - "brightRed": "#fe4450", - "brightGreen": "#72f1b8", - "brightYellow": "#fede5d", - "brightBlue": "#03edf9", - "brightPurple": "#ff7edb", - "brightCyan": "#03edf9", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#262335", - "cursorColor": "#03edf9" - }, - { - "name": "Teerb", - "black": "#1c1c1c", - "red": "#d68686", - "green": "#aed686", - "yellow": "#d7af87", - "blue": "#86aed6", - "purple": "#d6aed6", - "cyan": "#8adbb4", - "white": "#d0d0d0", - "brightBlack": "#1c1c1c", - "brightRed": "#d68686", - "brightGreen": "#aed686", - "brightYellow": "#e4c9af", - "brightBlue": "#86aed6", - "brightPurple": "#d6aed6", - "brightCyan": "#b1e7dd", - "brightWhite": "#efefef", - "foreground": "#d0d0d0", - "background": "#262626", - "cursorColor": "#d0d0d0" - }, - { - "name": "Tender", - "black": "#1d1d1d", - "red": "#c5152f", - "green": "#c9d05c", - "yellow": "#ffc24b", - "blue": "#b3deef", - "purple": "#d3b987", - "cyan": "#73cef4", - "white": "#eeeeee", - "brightBlack": "#323232", - "brightRed": "#f43753", - "brightGreen": "#d9e066", - "brightYellow": "#facc72", - "brightBlue": "#c0eafb", - "brightPurple": "#efd093", - "brightCyan": "#a1d6ec", - "brightWhite": "#ffffff", - "foreground": "#EEEEEE", - "background": "#282828", - "cursorColor": "#EEEEEE" - }, - { - "name": "TerminalBasic", - "black": "#000000", - "red": "#990000", - "green": "#00a600", - "yellow": "#999900", - "blue": "#0000b2", - "purple": "#b200b2", - "cyan": "#00a6b2", - "white": "#bfbfbf", - "brightBlack": "#666666", - "brightRed": "#e50000", - "brightGreen": "#00d900", - "brightYellow": "#e5e500", - "brightBlue": "#0000ff", - "brightPurple": "#e500e5", - "brightCyan": "#00e5e5", - "brightWhite": "#e5e5e5", - "foreground": "#000000", - "background": "#ffffff", - "cursorColor": "#000000" - }, - { - "name": "TerminixDark", - "black": "#282a2e", - "red": "#a54242", - "green": "#a1b56c", - "yellow": "#de935f", - "blue": "#225555", - "purple": "#85678f", - "cyan": "#5e8d87", - "white": "#777777", - "brightBlack": "#373b41", - "brightRed": "#c63535", - "brightGreen": "#608360", - "brightYellow": "#fa805a", - "brightBlue": "#449da1", - "brightPurple": "#ba8baf", - "brightCyan": "#86c1b9", - "brightWhite": "#c5c8c6", - "foreground": "#868A8C", - "background": "#091116", - "cursorColor": "#868A8C" - }, - { - "name": "ThayerBright", - "black": "#1b1d1e", - "red": "#f92672", - "green": "#4df840", - "yellow": "#f4fd22", - "blue": "#2757d6", - "purple": "#8c54fe", - "cyan": "#38c8b5", - "white": "#ccccc6", - "brightBlack": "#505354", - "brightRed": "#ff5995", - "brightGreen": "#b6e354", - "brightYellow": "#feed6c", - "brightBlue": "#3f78ff", - "brightPurple": "#9e6ffe", - "brightCyan": "#23cfd5", - "brightWhite": "#f8f8f2", - "foreground": "#f8f8f8", - "background": "#1b1d1e", - "cursorColor": "#f8f8f8" - }, - { - "name": "Tin", - "black": "#000000", - "red": "#8d534e", - "green": "#4e8d53", - "yellow": "#888d4e", - "blue": "#534e8d", - "purple": "#8d4e88", - "cyan": "#4e888d", - "white": "#ffffff", - "brightBlack": "#000000", - "brightRed": "#b57d78", - "brightGreen": "#78b57d", - "brightYellow": "#b0b578", - "brightBlue": "#7d78b5", - "brightPurple": "#b578b0", - "brightCyan": "#78b0b5", - "brightWhite": "#ffffff", - "foreground": "#ffffff", - "background": "#2e2e35", - "cursorColor": "#ffffff" - }, - { - "name": "TokyoNightLight", - "black": "#0f0f14", - "red": "#8c4351", - "green": "#485e30", - "yellow": "#8f5e15", - "blue": "#34548a", - "purple": "#5a4a78", - "cyan": "#0f4b6e", - "white": "#343b58", - "brightBlack": "#9699a3", - "brightRed": "#8c4351", - "brightGreen": "#485e30", - "brightYellow": "#8f5e15", - "brightBlue": "#34548a", - "brightPurple": "#5a4a78", - "brightCyan": "#0f4b6e", - "brightWhite": "#343b58", - "foreground": "#565a6e", - "background": "#d5d6db", - "cursorColor": "#565a6e" - }, - { - "name": "TokyoNightStorm", - "black": "#414868", - "red": "#f7768e", - "green": "#9ece6a", - "yellow": "#e0af68", - "blue": "#7aa2f7", - "purple": "#bb9af7", - "cyan": "#7dcfff", - "white": "#c0caf5", - "brightBlack": "#414868", - "brightRed": "#f7768e", - "brightGreen": "#9ece6a", - "brightYellow": "#e0af68", - "brightBlue": "#7aa2f7", - "brightPurple": "#bb9af7", - "brightCyan": "#7dcfff", - "brightWhite": "#c0caf5", - "foreground": "#c0caf5", - "background": "#24283b", - "cursorColor": "#c0caf5" - }, - { - "name": "TokyoNight", - "black": "#414868", - "red": "#f7768e", - "green": "#9ece6a", - "yellow": "#e0af68", - "blue": "#7aa2f7", - "purple": "#bb9af7", - "cyan": "#7dcfff", - "white": "#a9b1d6", - "brightBlack": "#414868", - "brightRed": "#f7768e", - "brightGreen": "#9ece6a", - "brightYellow": "#e0af68", - "brightBlue": "#7aa2f7", - "brightPurple": "#bb9af7", - "brightCyan": "#7dcfff", - "brightWhite": "#c0caf5", - "foreground": "#c0caf5", - "background": "#1a1b26", - "cursorColor": "#c0caf5" - }, - { - "name": "TomorrowNightBlue", - "black": "#000000", - "red": "#FF9DA3", - "green": "#D1F1A9", - "yellow": "#FFEEAD", - "blue": "#BBDAFF", - "purple": "#EBBBFF", - "cyan": "#99FFFF", - "white": "#FFFEFE", - "brightBlack": "#000000", - "brightRed": "#FF9CA3", - "brightGreen": "#D0F0A8", - "brightYellow": "#FFEDAC", - "brightBlue": "#BADAFF", - "brightPurple": "#EBBAFF", - "brightCyan": "#99FFFF", - "brightWhite": "#FFFEFE", - "foreground": "#FFFEFE", - "background": "#002451", - "cursorColor": "#FFFEFE" - }, - { - "name": "TomorrowNightBright", - "black": "#000000", - "red": "#D54E53", - "green": "#B9CA49", - "yellow": "#E7C547", - "blue": "#79A6DA", - "purple": "#C397D8", - "cyan": "#70C0B1", - "white": "#FFFEFE", - "brightBlack": "#000000", - "brightRed": "#D44D53", - "brightGreen": "#B9C949", - "brightYellow": "#E6C446", - "brightBlue": "#79A6DA", - "brightPurple": "#C396D7", - "brightCyan": "#70C0B1", - "brightWhite": "#FFFEFE", - "foreground": "#E9E9E9", - "background": "#000000", - "cursorColor": "#E9E9E9" - }, - { - "name": "TomorrowNightEighties", - "black": "#000000", - "red": "#F27779", - "green": "#99CC99", - "yellow": "#FFCC66", - "blue": "#6699CC", - "purple": "#CC99CC", - "cyan": "#66CCCC", - "white": "#FFFEFE", - "brightBlack": "#000000", - "brightRed": "#F17779", - "brightGreen": "#99CC99", - "brightYellow": "#FFCC66", - "brightBlue": "#6699CC", - "brightPurple": "#CC99CC", - "brightCyan": "#66CCCC", - "brightWhite": "#FFFEFE", - "foreground": "#CCCCCC", - "background": "#2C2C2C", - "cursorColor": "#CCCCCC" - }, - { - "name": "TomorrowNight", - "black": "#000000", - "red": "#CC6666", - "green": "#B5BD68", - "yellow": "#F0C674", - "blue": "#81A2BE", - "purple": "#B293BB", - "cyan": "#8ABEB7", - "white": "#FFFEFE", - "brightBlack": "#000000", - "brightRed": "#CC6666", - "brightGreen": "#B5BD68", - "brightYellow": "#F0C574", - "brightBlue": "#80A1BD", - "brightPurple": "#B294BA", - "brightCyan": "#8ABDB6", - "brightWhite": "#FFFEFE", - "foreground": "#C5C8C6", - "background": "#1D1F21", - "cursorColor": "#C4C8C5" - }, - { - "name": "Tomorrow", - "black": "#000000", - "red": "#C82828", - "green": "#718C00", - "yellow": "#EAB700", - "blue": "#4171AE", - "purple": "#8959A8", - "cyan": "#3E999F", - "white": "#FFFEFE", - "brightBlack": "#000000", - "brightRed": "#C82828", - "brightGreen": "#708B00", - "brightYellow": "#E9B600", - "brightBlue": "#4170AE", - "brightPurple": "#8958A7", - "brightCyan": "#3D999F", - "brightWhite": "#FFFEFE", - "foreground": "#4D4D4C", - "background": "#FFFFFF", - "cursorColor": "#4C4C4C" - }, - { - "name": "ToyChest", - "black": "#2c3f58", - "red": "#be2d26", - "green": "#1a9172", - "yellow": "#db8e27", - "blue": "#325d96", - "purple": "#8a5edc", - "cyan": "#35a08f", - "white": "#23d183", - "brightBlack": "#336889", - "brightRed": "#dd5944", - "brightGreen": "#31d07b", - "brightYellow": "#e7d84b", - "brightBlue": "#34a6da", - "brightPurple": "#ae6bdc", - "brightCyan": "#42c3ae", - "brightWhite": "#d5d5d5", - "foreground": "#31d07b", - "background": "#24364b", - "cursorColor": "#31d07b" - }, - { - "name": "Treehouse", - "black": "#321300", - "red": "#b2270e", - "green": "#44a900", - "yellow": "#aa820c", - "blue": "#58859a", - "purple": "#97363d", - "cyan": "#b25a1e", - "white": "#786b53", - "brightBlack": "#433626", - "brightRed": "#ed5d20", - "brightGreen": "#55f238", - "brightYellow": "#f2b732", - "brightBlue": "#85cfed", - "brightPurple": "#e14c5a", - "brightCyan": "#f07d14", - "brightWhite": "#ffc800", - "foreground": "#786b53", - "background": "#191919", - "cursorColor": "#786b53" - }, - { - "name": "Twilight", - "black": "#141414", - "red": "#c06d44", - "green": "#afb97a", - "yellow": "#c2a86c", - "blue": "#44474a", - "purple": "#b4be7c", - "cyan": "#778385", - "white": "#ffffd4", - "brightBlack": "#262626", - "brightRed": "#de7c4c", - "brightGreen": "#ccd88c", - "brightYellow": "#e2c47e", - "brightBlue": "#5a5e62", - "brightPurple": "#d0dc8e", - "brightCyan": "#8a989b", - "brightWhite": "#ffffd4", - "foreground": "#ffffd4", - "background": "#141414", - "cursorColor": "#ffffd4" - }, - { - "name": "Ura", - "black": "#000000", - "red": "#c21b6f", - "green": "#6fc21b", - "yellow": "#c26f1b", - "blue": "#1b6fc2", - "purple": "#6f1bc2", - "cyan": "#1bc26f", - "white": "#808080", - "brightBlack": "#808080", - "brightRed": "#ee84b9", - "brightGreen": "#b9ee84", - "brightYellow": "#eeb984", - "brightBlue": "#84b9ee", - "brightPurple": "#b984ee", - "brightCyan": "#84eeb9", - "brightWhite": "#e5e5e5", - "foreground": "#23476a", - "background": "#feffee", - "cursorColor": "#23476a" - }, - { - "name": "Urple", - "black": "#000000", - "red": "#b0425b", - "green": "#37a415", - "yellow": "#ad5c42", - "blue": "#564d9b", - "purple": "#6c3ca1", - "cyan": "#808080", - "white": "#87799c", - "brightBlack": "#5d3225", - "brightRed": "#ff6388", - "brightGreen": "#29e620", - "brightYellow": "#f08161", - "brightBlue": "#867aed", - "brightPurple": "#a05eee", - "brightCyan": "#eaeaea", - "brightWhite": "#bfa3ff", - "foreground": "#877a9b", - "background": "#1b1b23", - "cursorColor": "#877a9b" - }, - { - "name": "Vag", - "black": "#303030", - "red": "#a87139", - "green": "#39a871", - "yellow": "#71a839", - "blue": "#7139a8", - "purple": "#a83971", - "cyan": "#3971a8", - "white": "#8a8a8a", - "brightBlack": "#494949", - "brightRed": "#b0763b", - "brightGreen": "#3bb076", - "brightYellow": "#76b03b", - "brightBlue": "#763bb0", - "brightPurple": "#b03b76", - "brightCyan": "#3b76b0", - "brightWhite": "#cfcfcf", - "foreground": "#d9e6f2", - "background": "#191f1d", - "cursorColor": "#d9e6f2" - }, - { - "name": "Vaughn", - "black": "#25234f", - "red": "#705050", - "green": "#60b48a", - "yellow": "#dfaf8f", - "blue": "#5555ff", - "purple": "#f08cc3", - "cyan": "#8cd0d3", - "white": "#709080", - "brightBlack": "#709080", - "brightRed": "#dca3a3", - "brightGreen": "#60b48a", - "brightYellow": "#f0dfaf", - "brightBlue": "#5555ff", - "brightPurple": "#ec93d3", - "brightCyan": "#93e0e3", - "brightWhite": "#ffffff", - "foreground": "#dcdccc", - "background": "#25234f", - "cursorColor": "#dcdccc" - }, - { - "name": "VibrantInk", - "black": "#878787", - "red": "#ff6600", - "green": "#ccff04", - "yellow": "#ffcc00", - "blue": "#44b4cc", - "purple": "#9933cc", - "cyan": "#44b4cc", - "white": "#f5f5f5", - "brightBlack": "#555555", - "brightRed": "#ff0000", - "brightGreen": "#00ff00", - "brightYellow": "#ffff00", - "brightBlue": "#0000ff", - "brightPurple": "#ff00ff", - "brightCyan": "#00ffff", - "brightWhite": "#e5e5e5", - "foreground": "#ffffff", - "background": "#000000", - "cursorColor": "#ffffff" - }, - { - "name": "VSCodeDark+", - "black": "#6A787A", - "red": "#E9653B", - "green": "#39E9A8", - "yellow": "#E5B684", - "blue": "#44AAE6", - "purple": "#E17599", - "cyan": "#3DD5E7", - "white": "#C3DDE1", - "brightBlack": "#598489", - "brightRed": "#E65029", - "brightGreen": "#00FF9A", - "brightYellow": "#E89440", - "brightBlue": "#009AFB", - "brightPurple": "#FF578F", - "brightCyan": "#5FFFFF", - "brightWhite": "#D9FBFF", - "foreground": "#CCCCCC", - "background": "#1E1E1E", - "cursorColor": "#CCCCCC" - }, - { - "name": "VSCodeLight+", - "black": "#020202", - "red": "#CD3232", - "green": "#00BC00", - "yellow": "#A5A900", - "blue": "#0752A8", - "purple": "#BC05BC", - "cyan": "#0598BC", - "white": "#343434", - "brightBlack": "#5E5E5E", - "brightRed": "#cd3333", - "brightGreen": "#1BCE1A", - "brightYellow": "#ADBB5B", - "brightBlue": "#0752A8", - "brightPurple": "#C451CE", - "brightCyan": "#52A8C7", - "brightWhite": "#A6A3A6", - "foreground": "#020202", - "background": "#f9f9f9", - "cursorColor": "#020202" - }, - { - "name": "WarmNeon", - "black": "#000000", - "red": "#e24346", - "green": "#39b13a", - "yellow": "#dae145", - "blue": "#4261c5", - "purple": "#f920fb", - "cyan": "#2abbd4", - "white": "#d0b8a3", - "brightBlack": "#fefcfc", - "brightRed": "#e97071", - "brightGreen": "#9cc090", - "brightYellow": "#ddda7a", - "brightBlue": "#7b91d6", - "brightPurple": "#f674ba", - "brightCyan": "#5ed1e5", - "brightWhite": "#d8c8bb", - "foreground": "#afdab6", - "background": "#404040", - "cursorColor": "#afdab6" - }, - { - "name": "Wez", - "black": "#000000", - "red": "#cc5555", - "green": "#55cc55", - "yellow": "#cdcd55", - "blue": "#5555cc", - "purple": "#cc55cc", - "cyan": "#7acaca", - "white": "#cccccc", - "brightBlack": "#555555", - "brightRed": "#ff5555", - "brightGreen": "#55ff55", - "brightYellow": "#ffff55", - "brightBlue": "#5555ff", - "brightPurple": "#ff55ff", - "brightCyan": "#55ffff", - "brightWhite": "#ffffff", - "foreground": "#b3b3b3", - "background": "#000000", - "cursorColor": "#b3b3b3" - }, - { - "name": "WildCherry", - "black": "#000507", - "red": "#d94085", - "green": "#2ab250", - "yellow": "#ffd16f", - "blue": "#883cdc", - "purple": "#ececec", - "cyan": "#c1b8b7", - "white": "#fff8de", - "brightBlack": "#009cc9", - "brightRed": "#da6bac", - "brightGreen": "#f4dca5", - "brightYellow": "#eac066", - "brightBlue": "#308cba", - "brightPurple": "#ae636b", - "brightCyan": "#ff919d", - "brightWhite": "#e4838d", - "foreground": "#dafaff", - "background": "#1f1726", - "cursorColor": "#dafaff" - }, - { - "name": "Wombat", - "black": "#000000", - "red": "#ff615a", - "green": "#b1e969", - "yellow": "#ebd99c", - "blue": "#5da9f6", - "purple": "#e86aff", - "cyan": "#82fff7", - "white": "#dedacf", - "brightBlack": "#313131", - "brightRed": "#f58c80", - "brightGreen": "#ddf88f", - "brightYellow": "#eee5b2", - "brightBlue": "#a5c7ff", - "brightPurple": "#ddaaff", - "brightCyan": "#b7fff9", - "brightWhite": "#ffffff", - "foreground": "#dedacf", - "background": "#171717", - "cursorColor": "#dedacf" - }, - { - "name": "Wryan", - "black": "#333333", - "red": "#8c4665", - "green": "#287373", - "yellow": "#7c7c99", - "blue": "#395573", - "purple": "#5e468c", - "cyan": "#31658c", - "white": "#899ca1", - "brightBlack": "#3d3d3d", - "brightRed": "#bf4d80", - "brightGreen": "#53a6a6", - "brightYellow": "#9e9ecb", - "brightBlue": "#477ab3", - "brightPurple": "#7e62b3", - "brightCyan": "#6096bf", - "brightWhite": "#c0c0c0", - "foreground": "#999993", - "background": "#101010", - "cursorColor": "#999993" - }, - { - "name": "Wzoreck", - "black": "#2E3436", - "red": "#FC6386", - "green": "#424043", - "yellow": "#FCE94F", - "blue": "#FB976B", - "purple": "#75507B", - "cyan": "#34E2E2", - "white": "#FFFFFF", - "brightBlack": "#989595", - "brightRed": "#FC6386", - "brightGreen": "#A9DC76", - "brightYellow": "#FCE94F", - "brightBlue": "#FB976B", - "brightPurple": "#AB9DF2", - "brightCyan": "#34E2E2", - "brightWhite": "#D1D1C0", - "foreground": "#FCFCFA", - "background": "#424043", - "cursorColor": "#FCFCFA" - }, - { - "name": "Zenburn", - "black": "#4d4d4d", - "red": "#705050", - "green": "#60b48a", - "yellow": "#f0dfaf", - "blue": "#506070", - "purple": "#dc8cc3", - "cyan": "#8cd0d3", - "white": "#dcdccc", - "brightBlack": "#709080", - "brightRed": "#dca3a3", - "brightGreen": "#c3bf9f", - "brightYellow": "#e0cf9f", - "brightBlue": "#94bff3", - "brightPurple": "#ec93d3", - "brightCyan": "#93e0e3", - "brightWhite": "#ffffff", - "foreground": "#dcdccc", - "background": "#3f3f3f", - "cursorColor": "#dcdccc" - } -] diff --git a/dimos/web/dimos_interface/tsconfig.json b/dimos/web/dimos_interface/tsconfig.json deleted file mode 100644 index 4bf29f39d2..0000000000 --- a/dimos/web/dimos_interface/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "extends": "@tsconfig/svelte/tsconfig.json", - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "resolveJsonModule": true, - "allowJs": true, - "checkJs": true, - "isolatedModules": true, - "types": [ - "node" - ] - }, - "include": [ - "src/**/*.ts", - "src/**/*.js", - "src/**/*.svelte" - ], - "references": [ - { - "path": "./tsconfig.node.json" - } - ] -} diff --git a/dimos/web/dimos_interface/tsconfig.node.json b/dimos/web/dimos_interface/tsconfig.node.json deleted file mode 100644 index ad883d0eb4..0000000000 --- a/dimos/web/dimos_interface/tsconfig.node.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler" - }, - "include": [ - "vite.config.ts" - ] -} diff --git a/dimos/web/dimos_interface/vite.config.ts b/dimos/web/dimos_interface/vite.config.ts deleted file mode 100644 index 29be79dd4a..0000000000 --- a/dimos/web/dimos_interface/vite.config.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Copyright 2025 Dimensional Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { defineConfig } from 'vite'; -import { svelte } from '@sveltejs/vite-plugin-svelte'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [svelte()], - server: { - port: 3000, - host: '0.0.0.0', - watch: { - // Exclude node_modules, .git and other large directories - ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**', 'lambda/**'], - // Use polling instead of filesystem events (less efficient but uses fewer watchers) - usePolling: true, - }, - proxy: { - '/api': { - target: 'https://0rqz7w5rvf.execute-api.us-east-2.amazonaws.com', - changeOrigin: true, - rewrite: (path) => path.replace(/^\/api/, '/default/getGenesis'), - configure: (proxy, _options) => { - proxy.on('error', (err, _req, _res) => { - console.log('proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, _res) => { - console.log('Sending Request to the Target:', req.method, req.url); - }); - proxy.on('proxyRes', (proxyRes, req, _res) => { - console.log('Received Response from the Target:', proxyRes.statusCode, req.url); - }); - }, - }, - '/unitree': { - target: 'http://0.0.0.0:5555', - changeOrigin: true, - configure: (proxy, _options) => { - proxy.on('error', (err, _req, _res) => { - console.log('unitree proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, _res) => { - console.log('Sending Unitree Request:', req.method, req.url); - }); - proxy.on('proxyRes', (proxyRes, req, _res) => { - console.log('Received Unitree Response:', proxyRes.statusCode, req.url); - }); - }, - }, - '/text_streams': { - target: 'http://0.0.0.0:5555', - changeOrigin: true, - configure: (proxy, _options) => { - proxy.on('error', (err, _req, _res) => { - console.log('text streams proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, _res) => { - console.log('Sending Text Streams Request:', req.method, req.url); - }); - proxy.on('proxyRes', (proxyRes, req, _res) => { - console.log('Received Text Streams Response:', proxyRes.statusCode, req.url); - }); - }, - }, - '/simulation': { - target: '', // Will be set dynamically - changeOrigin: true, - configure: (proxy, _options) => { - proxy.on('error', (err, _req, _res) => { - console.log('proxy error', err); - }); - proxy.on('proxyReq', (proxyReq, req, _res) => { - console.log('Sending Simulation Request:', req.method, req.url); - }); - }, - } - }, - cors: true - }, - define: { - 'process.env': process.env - } -}); diff --git a/dimos/web/edge_io.py b/dimos/web/edge_io.py deleted file mode 100644 index 28ccae8733..0000000000 --- a/dimos/web/edge_io.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from reactivex.disposable import CompositeDisposable - - -class EdgeIO: - def __init__(self, dev_name: str = "NA", edge_type: str = "Base") -> None: - self.dev_name = dev_name - self.edge_type = edge_type - self.disposables = CompositeDisposable() - - def dispose_all(self) -> None: - """Disposes of all active subscriptions managed by this agent.""" - self.disposables.dispose() diff --git a/dimos/web/fastapi_server.py b/dimos/web/fastapi_server.py deleted file mode 100644 index 606e081fb3..0000000000 --- a/dimos/web/fastapi_server.py +++ /dev/null @@ -1,226 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Working FastAPI/Uvicorn Impl. - -# Notes: Do not use simultaneously with Flask, this includes imports. -# Workers are not yet setup, as this requires a much more intricate -# reorganization. There appears to be possible signalling issues when -# opening up streams on multiple windows/reloading which will need to -# be fixed. Also note, Chrome only supports 6 simultaneous web streams, -# and its advised to test threading/worker performance with another -# browser like Safari. - -# Fast Api & Uvicorn -import asyncio -from pathlib import Path -from queue import Empty, Queue -from threading import Lock - -import cv2 -from fastapi import FastAPI, Form, HTTPException, Request -from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse -from fastapi.templating import Jinja2Templates -import reactivex as rx -from reactivex import operators as ops -from reactivex.disposable import SingleAssignmentDisposable -from sse_starlette.sse import EventSourceResponse -import uvicorn - -from dimos.web.edge_io import EdgeIO - -# TODO: Resolve threading, start/stop stream functionality. - - -class FastAPIServer(EdgeIO): - def __init__( # type: ignore[no-untyped-def] - self, - dev_name: str = "FastAPI Server", - edge_type: str = "Bidirectional", - host: str = "0.0.0.0", - port: int = 5555, - text_streams=None, - **streams, - ) -> None: - super().__init__(dev_name, edge_type) - self.app = FastAPI() - self.port = port - self.host = host - BASE_DIR = Path(__file__).resolve().parent - self.templates = Jinja2Templates(directory=str(BASE_DIR / "templates")) - self.streams = streams - self.active_streams = {} - self.stream_locks = {key: Lock() for key in self.streams} - self.stream_queues = {} # type: ignore[var-annotated] - self.stream_disposables = {} # type: ignore[var-annotated] - - # Initialize text streams - self.text_streams = text_streams or {} - self.text_queues = {} # type: ignore[var-annotated] - self.text_disposables = {} - self.text_clients = set() # type: ignore[var-annotated] - - # Create a Subject for text queries - self.query_subject = rx.subject.Subject() # type: ignore[var-annotated] - self.query_stream = self.query_subject.pipe(ops.share()) - - for key in self.streams: - if self.streams[key] is not None: - self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_fastapi), ops.share() - ) - - # Set up text stream subscriptions - for key, stream in self.text_streams.items(): - if stream is not None: - self.text_queues[key] = Queue(maxsize=100) - disposable = stream.subscribe( - lambda text, k=key: self.text_queues[k].put(text) if text is not None else None, - lambda e, k=key: self.text_queues[k].put(None), - lambda k=key: self.text_queues[k].put(None), - ) - self.text_disposables[key] = disposable - self.disposables.add(disposable) - - self.setup_routes() - - def process_frame_fastapi(self, frame): # type: ignore[no-untyped-def] - """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode(".jpg", frame) - return buffer.tobytes() - - def stream_generator(self, key): # type: ignore[no-untyped-def] - """Generate frames for a given video stream.""" - - def generate(): # type: ignore[no-untyped-def] - if key not in self.stream_queues: - self.stream_queues[key] = Queue(maxsize=10) - - frame_queue = self.stream_queues[key] - - # Clear any existing disposable for this stream - if key in self.stream_disposables: - self.stream_disposables[key].dispose() - - disposable = SingleAssignmentDisposable() - self.stream_disposables[key] = disposable - self.disposables.add(disposable) - - if key in self.active_streams: - with self.stream_locks[key]: - # Clear the queue before starting new subscription - while not frame_queue.empty(): - try: - frame_queue.get_nowait() - except Empty: - break - - disposable.disposable = self.active_streams[key].subscribe( - lambda frame: frame_queue.put(frame) if frame is not None else None, - lambda e: frame_queue.put(None), - lambda: frame_queue.put(None), - ) - - try: - while True: - try: - frame = frame_queue.get(timeout=1) - if frame is None: - break - yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") - except Empty: - # Instead of breaking, continue waiting for new frames - continue - finally: - if key in self.stream_disposables: - self.stream_disposables[key].dispose() - - return generate - - def create_video_feed_route(self, key): # type: ignore[no-untyped-def] - """Create a video feed route for a specific stream.""" - - async def video_feed(): # type: ignore[no-untyped-def] - return StreamingResponse( - self.stream_generator(key)(), # type: ignore[no-untyped-call] - media_type="multipart/x-mixed-replace; boundary=frame", - ) - - return video_feed - - async def text_stream_generator(self, key): # type: ignore[no-untyped-def] - """Generate SSE events for text stream.""" - client_id = id(object()) - self.text_clients.add(client_id) - - try: - while True: - if key in self.text_queues: - try: - text = self.text_queues[key].get(timeout=1) - if text is not None: - yield {"event": "message", "id": key, "data": text} - except Empty: - # Send a keep-alive comment - yield {"event": "ping", "data": ""} - await asyncio.sleep(0.1) - finally: - self.text_clients.remove(client_id) - - def setup_routes(self) -> None: - """Set up FastAPI routes.""" - - @self.app.get("/", response_class=HTMLResponse) - async def index(request: Request): # type: ignore[no-untyped-def] - stream_keys = list(self.streams.keys()) - text_stream_keys = list(self.text_streams.keys()) - return self.templates.TemplateResponse( - "index_fastapi.html", - { - "request": request, - "stream_keys": stream_keys, - "text_stream_keys": text_stream_keys, - }, - ) - - @self.app.post("/submit_query") - async def submit_query(query: str = Form(...)): # type: ignore[no-untyped-def] - # Using Form directly as a dependency ensures proper form handling - try: - if query: - # Emit the query through our Subject - self.query_subject.on_next(query) - return JSONResponse({"success": True, "message": "Query received"}) - return JSONResponse({"success": False, "message": "No query provided"}) - except Exception as e: - # Ensure we always return valid JSON even on error - return JSONResponse( - status_code=500, - content={"success": False, "message": f"Server error: {e!s}"}, - ) - - @self.app.get("/text_stream/{key}") - async def text_stream(key: str): # type: ignore[no-untyped-def] - if key not in self.text_streams: - raise HTTPException(status_code=404, detail=f"Text stream '{key}' not found") - return EventSourceResponse(self.text_stream_generator(key)) # type: ignore[no-untyped-call] - - for key in self.streams: - self.app.get(f"/video_feed/{key}")(self.create_video_feed_route(key)) # type: ignore[no-untyped-call] - - def run(self) -> None: - """Run the FastAPI server.""" - uvicorn.run( - self.app, host=self.host, port=self.port - ) # TODO: Translate structure to enable in-built workers' diff --git a/dimos/web/flask_server.py b/dimos/web/flask_server.py deleted file mode 100644 index 4cd6d0a5e0..0000000000 --- a/dimos/web/flask_server.py +++ /dev/null @@ -1,105 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from queue import Queue - -import cv2 -from flask import Flask, Response, render_template -from reactivex import operators as ops -from reactivex.disposable import SingleAssignmentDisposable - -from dimos.web.edge_io import EdgeIO - - -class FlaskServer(EdgeIO): - def __init__( # type: ignore[no-untyped-def] - self, - dev_name: str = "Flask Server", - edge_type: str = "Bidirectional", - port: int = 5555, - **streams, - ) -> None: - super().__init__(dev_name, edge_type) - self.app = Flask(__name__) - self.port = port - self.streams = streams - self.active_streams = {} - - # Initialize shared stream references with ref_count - for key in self.streams: - if self.streams[key] is not None: - # Apply share and ref_count to manage subscriptions - self.active_streams[key] = self.streams[key].pipe( - ops.map(self.process_frame_flask), ops.share() - ) - - self.setup_routes() - - def process_frame_flask(self, frame): # type: ignore[no-untyped-def] - """Convert frame to JPEG format for streaming.""" - _, buffer = cv2.imencode(".jpg", frame) - return buffer.tobytes() - - def setup_routes(self) -> None: - @self.app.route("/") - def index(): # type: ignore[no-untyped-def] - stream_keys = list(self.streams.keys()) # Get the keys from the streams dictionary - return render_template("index_flask.html", stream_keys=stream_keys) - - # Function to create a streaming response - def stream_generator(key): # type: ignore[no-untyped-def] - def generate(): # type: ignore[no-untyped-def] - frame_queue = Queue() # type: ignore[var-annotated] - disposable = SingleAssignmentDisposable() - - # Subscribe to the shared, ref-counted stream - if key in self.active_streams: - disposable.disposable = self.active_streams[key].subscribe( - lambda frame: frame_queue.put(frame) if frame is not None else None, - lambda e: frame_queue.put(None), - lambda: frame_queue.put(None), - ) - - try: - while True: - frame = frame_queue.get() - if frame is None: - break - yield (b"--frame\r\nContent-Type: image/jpeg\r\n\r\n" + frame + b"\r\n") - finally: - disposable.dispose() - - return generate - - def make_response_generator(key): # type: ignore[no-untyped-def] - def response_generator(): # type: ignore[no-untyped-def] - return Response( - stream_generator(key)(), # type: ignore[no-untyped-call] - mimetype="multipart/x-mixed-replace; boundary=frame", - ) - - return response_generator - - # Dynamically adding routes using add_url_rule - for key in self.streams: - endpoint = f"video_feed_{key}" - self.app.add_url_rule( - f"/video_feed/{key}", - endpoint, - view_func=make_response_generator(key), # type: ignore[no-untyped-call] - ) - - def run(self, host: str = "0.0.0.0", port: int = 5555, threaded: bool = True) -> None: - self.port = port - self.app.run(host=host, port=self.port, debug=False, threaded=threaded) diff --git a/dimos/web/robot_web_interface.py b/dimos/web/robot_web_interface.py deleted file mode 100644 index f45319f1d2..0000000000 --- a/dimos/web/robot_web_interface.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Robot Web Interface wrapper for DIMOS. -Provides a clean interface to the dimensional-interface FastAPI server. -""" - -from dimos.web.dimos_interface.api.server import FastAPIServer - - -class RobotWebInterface(FastAPIServer): - """Wrapper class for the dimos-interface FastAPI server.""" - - def __init__(self, port: int = 5555, text_streams=None, audio_subject=None, **streams) -> None: # type: ignore[no-untyped-def] - super().__init__( - dev_name="Robot Web Interface", - edge_type="Bidirectional", - host="0.0.0.0", - port=port, - text_streams=text_streams, - audio_subject=audio_subject, - **streams, - ) diff --git a/dimos/web/templates/index_fastapi.html b/dimos/web/templates/index_fastapi.html deleted file mode 100644 index 75b0c1c179..0000000000 --- a/dimos/web/templates/index_fastapi.html +++ /dev/null @@ -1,389 +0,0 @@ - - - - - - - - Video Stream Example - - - -

Live Video Streams

- -
-

Ask a Question

-
- - -
-
-
- - - {% if text_stream_keys %} -
-

Text Streams

- {% for key in text_stream_keys %} -
-

{{ key.replace('_', ' ').title() }}

-
-
- - - -
-
- {% endfor %} -
- {% endif %} - -
- {% for key in stream_keys %} -
-

{{ key.replace('_', ' ').title() }}

- {{ key }} Feed -
- - -
-
- {% endfor %} -
- - - - - - diff --git a/dimos/web/templates/index_flask.html b/dimos/web/templates/index_flask.html deleted file mode 100644 index e41665e588..0000000000 --- a/dimos/web/templates/index_flask.html +++ /dev/null @@ -1,118 +0,0 @@ - - - - - - - - Video Stream Example - - - -

Live Video Streams

- -
- {% for key in stream_keys %} -
-

{{ key.replace('_', ' ').title() }}

- {{ key }} Feed -
- {% endfor %} -
- - - - - diff --git a/dimos/web/templates/rerun_dashboard.html b/dimos/web/templates/rerun_dashboard.html deleted file mode 100644 index f0792079e3..0000000000 --- a/dimos/web/templates/rerun_dashboard.html +++ /dev/null @@ -1,83 +0,0 @@ - - - - Dimos Dashboard - - - -
- - - -
- - - diff --git a/dimos/web/websocket_vis/README.md b/dimos/web/websocket_vis/README.md deleted file mode 100644 index c04235958e..0000000000 --- a/dimos/web/websocket_vis/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# WebSocket Visualization Module - -The `WebsocketVisModule` provides a real-time data for visualization and control of the robot in Foxglove (see `dimos/web/command-center-extension/README.md`). - -## Overview - -Visualization: - -- Robot position and orientation -- Navigation paths -- Costmaps - -Control: - -- Set navigation goal -- Set GPS location goal -- Keyboard teleop (WASD) -- Trigger exploration - -## What it Provides - -### Inputs (Subscribed Topics) -- `robot_pose` (PoseStamped): Current robot position and orientation -- `gps_location` (LatLon): GPS coordinates of the robot -- `path` (Path): Planned navigation path -- `global_costmap` (OccupancyGrid): Global costmap for visualization - -### Outputs (Published Topics) -- `click_goal` (PoseStamped): Goal positions set by user clicks in the web interface -- `gps_goal` (LatLon): GPS goal coordinates set through the interface -- `explore_cmd` (Bool): Command to start autonomous exploration -- `stop_explore_cmd` (Bool): Command to stop exploration -- `movecmd` (Twist): Direct movement commands from the interface -- `movecmd_stamped` (TwistStamped): Timestamped movement commands - -## How to Use - -### Basic Usage - -```python -from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule -from dimos import core - -# Deploy the WebSocket visualization module -websocket_vis = dimos.deploy(WebsocketVisModule, port=7779) - -# Receive control from the Foxglove plugin. -websocket_vis.click_goal.transport = core.LCMTransport("/goal_request", PoseStamped) -websocket_vis.explore_cmd.transport = core.LCMTransport("/explore_cmd", Bool) -websocket_vis.stop_explore_cmd.transport = core.LCMTransport("/stop_explore_cmd", Bool) -websocket_vis.movecmd.transport = core.LCMTransport("/cmd_vel", Twist) -websocket_vis.gps_goal.transport = core.pLCMTransport("/gps_goal") - -# Send visualization data to the Foxglove plugin. -websocket_vis.robot_pose.connect(connection.odom) -websocket_vis.path.connect(global_planner.path) -websocket_vis.global_costmap.connect(mapper.global_costmap) -websocket_vis.gps_location.connect(connection.gps_location) - -# Start the module -websocket_vis.start() -``` - -### Accessing the Interface - -See `dimos/web/command-center-extension/README.md` for how to add the command-center plugin in Foxglove. diff --git a/dimos/web/websocket_vis/costmap_viz.py b/dimos/web/websocket_vis/costmap_viz.py deleted file mode 100644 index 21309c94bc..0000000000 --- a/dimos/web/websocket_vis/costmap_viz.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple costmap wrapper for visualization purposes. -This is a minimal implementation to support websocket visualization. -""" - -import numpy as np - -from dimos.msgs.nav_msgs import OccupancyGrid - - -class CostmapViz: - """A wrapper around OccupancyGrid for visualization compatibility.""" - - def __init__(self, occupancy_grid: OccupancyGrid | None = None) -> None: - """Initialize from an OccupancyGrid.""" - self.occupancy_grid = occupancy_grid - - @property - def data(self) -> np.ndarray | None: # type: ignore[type-arg] - """Get the costmap data as a numpy array.""" - if self.occupancy_grid: - return self.occupancy_grid.grid - return None - - @property - def width(self) -> int: - """Get the width of the costmap.""" - if self.occupancy_grid: - return self.occupancy_grid.width - return 0 - - @property - def height(self) -> int: - """Get the height of the costmap.""" - if self.occupancy_grid: - return self.occupancy_grid.height - return 0 - - @property - def resolution(self) -> float: - """Get the resolution of the costmap.""" - if self.occupancy_grid: - return self.occupancy_grid.resolution - return 1.0 - - @property - def origin(self): # type: ignore[no-untyped-def] - """Get the origin pose of the costmap.""" - if self.occupancy_grid: - return self.occupancy_grid.origin - return None diff --git a/dimos/web/websocket_vis/optimized_costmap.py b/dimos/web/websocket_vis/optimized_costmap.py deleted file mode 100644 index 34502744c4..0000000000 --- a/dimos/web/websocket_vis/optimized_costmap.py +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2025-2026 Dimensional Inc. - -import base64 -import hashlib -import time -from typing import Any -import zlib - -import numpy as np - - -class OptimizedCostmapEncoder: - """Handles optimized encoding of costmaps with delta compression.""" - - def __init__(self, chunk_size: int = 64) -> None: - self.chunk_size = chunk_size - self.last_full_grid: np.ndarray | None = None # type: ignore[type-arg] - self.last_full_sent_time: float = 0 # Track when last full update was sent - self.chunk_hashes: dict[tuple[int, int], str] = {} - self.full_update_interval = 3.0 # Send full update every 3 seconds - - def encode_costmap(self, grid: np.ndarray, force_full: bool = False) -> dict[str, Any]: # type: ignore[type-arg] - """Encode a costmap grid with optimizations. - - Args: - grid: The costmap grid as numpy array - force_full: Force sending a full update - - Returns: - Encoded costmap data - """ - current_time = time.time() - - # Determine if we need a full update - send_full = ( - force_full - or self.last_full_grid is None - or self.last_full_grid.shape != grid.shape - or (current_time - self.last_full_sent_time) > self.full_update_interval - ) - - if send_full: - return self._encode_full(grid, current_time) - else: - return self._encode_delta(grid, current_time) - - def _encode_full(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: # type: ignore[type-arg] - height, width = grid.shape - - # Convert to uint8 for better compression (costmap values are -1 to 100) - # Map -1 to 255 for unknown cells - grid_uint8 = grid.astype(np.int16) - grid_uint8[grid_uint8 == -1] = 255 - grid_uint8 = grid_uint8.astype(np.uint8) - - # Compress the data - compressed = zlib.compress(grid_uint8.tobytes(), level=6) - - # Base64 encode - encoded = base64.b64encode(compressed).decode("ascii") - - # Update state - self.last_full_grid = grid.copy() - self.last_full_sent_time = current_time - self._update_chunk_hashes(grid) - - return { - "update_type": "full", - "shape": [height, width], - "dtype": "u8", # uint8 - "compressed": True, - "compression": "zlib", - "data": encoded, - } - - def _encode_delta(self, grid: np.ndarray, current_time: float) -> dict[str, Any]: # type: ignore[type-arg] - height, width = grid.shape - changed_chunks = [] - - # Divide grid into chunks and check for changes - for y in range(0, height, self.chunk_size): - for x in range(0, width, self.chunk_size): - # Get chunk bounds - y_end = min(y + self.chunk_size, height) - x_end = min(x + self.chunk_size, width) - - # Extract chunk - chunk = grid[y:y_end, x:x_end] - - # Compute hash of chunk - chunk_hash = hashlib.md5(chunk.tobytes()).hexdigest() - chunk_key = (y, x) - - # Check if chunk has changed - if chunk_key not in self.chunk_hashes or self.chunk_hashes[chunk_key] != chunk_hash: - # Chunk has changed, encode it - chunk_uint8 = chunk.astype(np.int16) - chunk_uint8[chunk_uint8 == -1] = 255 - chunk_uint8 = chunk_uint8.astype(np.uint8) - - # Compress chunk - compressed = zlib.compress(chunk_uint8.tobytes(), level=6) - encoded = base64.b64encode(compressed).decode("ascii") - - changed_chunks.append( - {"pos": [y, x], "size": [y_end - y, x_end - x], "data": encoded} - ) - - # Update hash - self.chunk_hashes[chunk_key] = chunk_hash - - # Update state - only update the grid, not the timer - self.last_full_grid = grid.copy() - - # If too many chunks changed, send full update instead - total_chunks = ((height + self.chunk_size - 1) // self.chunk_size) * ( - (width + self.chunk_size - 1) // self.chunk_size - ) - - if len(changed_chunks) > total_chunks * 0.5: - # More than 50% changed, send full update - return self._encode_full(grid, current_time) - - return { - "update_type": "delta", - "shape": [height, width], - "dtype": "u8", - "compressed": True, - "compression": "zlib", - "chunks": changed_chunks, - } - - def _update_chunk_hashes(self, grid: np.ndarray) -> None: # type: ignore[type-arg] - """Update all chunk hashes for the grid.""" - self.chunk_hashes.clear() - height, width = grid.shape - - for y in range(0, height, self.chunk_size): - for x in range(0, width, self.chunk_size): - y_end = min(y + self.chunk_size, height) - x_end = min(x + self.chunk_size, width) - chunk = grid[y:y_end, x:x_end] - chunk_hash = hashlib.md5(chunk.tobytes()).hexdigest() - self.chunk_hashes[(y, x)] = chunk_hash diff --git a/dimos/web/websocket_vis/path_history.py b/dimos/web/websocket_vis/path_history.py deleted file mode 100644 index 39b6be08a3..0000000000 --- a/dimos/web/websocket_vis/path_history.py +++ /dev/null @@ -1,75 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple path history class for visualization purposes. -This is a minimal implementation to support websocket visualization. -""" - -from dimos.msgs.geometry_msgs import Vector3 - - -class PathHistory: - """A simple container for storing a history of positions for visualization.""" - - def __init__(self, points: list[Vector3 | tuple | list] | None = None) -> None: # type: ignore[type-arg] - """Initialize with optional list of points.""" - self.points: list[Vector3] = [] - if points: - for p in points: - if isinstance(p, Vector3): - self.points.append(p) - else: - self.points.append(Vector3(*p)) - - def ipush(self, point: Vector3 | tuple | list) -> "PathHistory": # type: ignore[type-arg] - """Add a point to the history (in-place) and return self.""" - if isinstance(point, Vector3): - self.points.append(point) - else: - self.points.append(Vector3(*point)) - return self - - def iclip_tail(self, max_length: int) -> "PathHistory": - """Keep only the last max_length points (in-place) and return self.""" - if max_length > 0 and len(self.points) > max_length: - self.points = self.points[-max_length:] - return self - - def last(self) -> Vector3 | None: - """Return the last point in the history, or None if empty.""" - return self.points[-1] if self.points else None - - def length(self) -> float: - """Calculate the total length of the path.""" - if len(self.points) < 2: - return 0.0 - - total = 0.0 - for i in range(1, len(self.points)): - p1 = self.points[i - 1] - p2 = self.points[i] - dx = p2.x - p1.x - dy = p2.y - p1.y - dz = p2.z - p1.z - total += (dx * dx + dy * dy + dz * dz) ** 0.5 - return total - - def __len__(self) -> int: - """Return the number of points in the history.""" - return len(self.points) - - def __getitem__(self, index: int) -> Vector3: - """Get a point by index.""" - return self.points[index] diff --git a/dimos/web/websocket_vis/websocket_vis_module.py b/dimos/web/websocket_vis/websocket_vis_module.py deleted file mode 100644 index ad93af5c96..0000000000 --- a/dimos/web/websocket_vis/websocket_vis_module.py +++ /dev/null @@ -1,402 +0,0 @@ -#!/usr/bin/env python3 - -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -WebSocket Visualization Module for Dimos navigation and mapping. - -This module provides a WebSocket data server for real-time visualization. -The frontend is served from a separate HTML file. -""" - -import asyncio -from pathlib import Path as FilePath -import threading -import time -from typing import Any -import webbrowser - -from dimos_lcm.std_msgs import Bool # type: ignore[import-untyped] -from reactivex.disposable import Disposable -import socketio # type: ignore[import-untyped] -from starlette.applications import Starlette -from starlette.responses import FileResponse, RedirectResponse, Response -from starlette.routing import Route -import uvicorn - -from dimos.utils.data import get_data - -# Path to the frontend HTML templates and command-center build -_TEMPLATES_DIR = FilePath(__file__).parent.parent / "templates" -_DASHBOARD_HTML = _TEMPLATES_DIR / "rerun_dashboard.html" -_COMMAND_CENTER_DIR = ( - FilePath(__file__).parent.parent / "command-center-extension" / "dist-standalone" -) - -from dimos.core import In, Module, Out, rpc -from dimos.core.global_config import GlobalConfig, global_config -from dimos.mapping.occupancy.gradient import gradient -from dimos.mapping.occupancy.inflation import simple_inflate -from dimos.mapping.types import LatLon -from dimos.msgs.geometry_msgs import PoseStamped, Twist, TwistStamped, Vector3 -from dimos.msgs.nav_msgs import OccupancyGrid, Path -from dimos.utils.logging_config import setup_logger - -from .optimized_costmap import OptimizedCostmapEncoder - -logger = setup_logger() - -_browser_open_lock = threading.Lock() -_browser_opened = False - - -class WebsocketVisModule(Module): - """ - WebSocket-based visualization module for real-time navigation data. - - This module provides a web interface for visualizing: - - Robot position and orientation - - Navigation paths - - Costmaps - - Interactive goal setting via mouse clicks - - Inputs: - - robot_pose: Current robot position - - path: Navigation path - - global_costmap: Global costmap for visualization - - Outputs: - - click_goal: Goal position from user clicks - """ - - # LCM inputs - odom: In[PoseStamped] - gps_location: In[LatLon] - path: In[Path] - global_costmap: In[OccupancyGrid] - - # LCM outputs - goal_request: Out[PoseStamped] - gps_goal: Out[LatLon] - explore_cmd: Out[Bool] - stop_explore_cmd: Out[Bool] - cmd_vel: Out[Twist] - movecmd_stamped: Out[TwistStamped] - - def __init__( - self, - port: int = 7779, - cfg: GlobalConfig = global_config, - **kwargs: Any, - ) -> None: - """Initialize the WebSocket visualization module. - - Args: - port: Port to run the web server on - cfg: Optional global config for viewer backend settings - """ - super().__init__(**kwargs) - self._global_config = cfg - - self.port = port - self._uvicorn_server_thread: threading.Thread | None = None - self.sio: socketio.AsyncServer | None = None - self.app = None - self._broadcast_loop = None - self._broadcast_thread = None - self._uvicorn_server: uvicorn.Server | None = None - - self.vis_state = {} # type: ignore[var-annotated] - self.state_lock = threading.Lock() - self.costmap_encoder = OptimizedCostmapEncoder(chunk_size=64) - - # Track GPS goal points for visualization - self.gps_goal_points: list[dict[str, float]] = [] - logger.info( - f"WebSocket visualization module initialized on port {port}, GPS goal tracking enabled" - ) - - def _start_broadcast_loop(self) -> None: - def websocket_vis_loop() -> None: - self._broadcast_loop = asyncio.new_event_loop() # type: ignore[assignment] - asyncio.set_event_loop(self._broadcast_loop) - try: - self._broadcast_loop.run_forever() # type: ignore[attr-defined] - except Exception as e: - logger.error(f"Broadcast loop error: {e}") - finally: - self._broadcast_loop.close() # type: ignore[attr-defined] - - self._broadcast_thread = threading.Thread(target=websocket_vis_loop, daemon=True) # type: ignore[assignment] - self._broadcast_thread.start() # type: ignore[attr-defined] - - @rpc - def start(self) -> None: - super().start() - - self._create_server() - - self._start_broadcast_loop() - - self._uvicorn_server_thread = threading.Thread(target=self._run_uvicorn_server, daemon=True) - self._uvicorn_server_thread.start() - - # Auto-open browser only for rerun-web (dashboard with Rerun iframe + command center) - # For rerun and foxglove, users access the command center manually if needed - if self._global_config.viewer_backend == "rerun-web": - url = f"http://localhost:{self.port}/" - logger.info(f"Dimensional Command Center: {url}") - - global _browser_opened - with _browser_open_lock: - if not _browser_opened: - try: - webbrowser.open_new_tab(url) - _browser_opened = True - except Exception as e: - logger.debug(f"Failed to open browser: {e}") - - try: - unsub = self.odom.subscribe(self._on_robot_pose) - self._disposables.add(Disposable(unsub)) - except Exception: - ... - - try: - unsub = self.gps_location.subscribe(self._on_gps_location) - self._disposables.add(Disposable(unsub)) - except Exception: - ... - - try: - unsub = self.path.subscribe(self._on_path) - self._disposables.add(Disposable(unsub)) - except Exception: - ... - - try: - unsub = self.global_costmap.subscribe(self._on_global_costmap) - self._disposables.add(Disposable(unsub)) - except Exception: - ... - - @rpc - def stop(self) -> None: - if self._uvicorn_server: - self._uvicorn_server.should_exit = True - - if self.sio and self._broadcast_loop and not self._broadcast_loop.is_closed(): - - async def _disconnect_all() -> None: - await self.sio.disconnect() - - asyncio.run_coroutine_threadsafe(_disconnect_all(), self._broadcast_loop) - - if self._broadcast_loop and not self._broadcast_loop.is_closed(): - self._broadcast_loop.call_soon_threadsafe(self._broadcast_loop.stop) - - if self._broadcast_thread and self._broadcast_thread.is_alive(): - self._broadcast_thread.join(timeout=1.0) - - if self._uvicorn_server_thread and self._uvicorn_server_thread.is_alive(): - self._uvicorn_server_thread.join(timeout=2.0) - - super().stop() - - @rpc - def set_gps_travel_goal_points(self, points: list[LatLon]) -> None: - json_points = [{"lat": x.lat, "lon": x.lon} for x in points] - self.vis_state["gps_travel_goal_points"] = json_points - self._emit("gps_travel_goal_points", json_points) - - def _create_server(self) -> None: - # Create SocketIO server - self.sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") - - async def serve_index(request): # type: ignore[no-untyped-def] - """Serve appropriate HTML based on viewer mode.""" - # If running native Rerun, redirect to standalone command center - if self._global_config.viewer_backend != "rerun-web": - return RedirectResponse(url="/command-center") - - # Otherwise serve full dashboard with Rerun iframe - return FileResponse(_DASHBOARD_HTML, media_type="text/html") - - async def serve_command_center(request): # type: ignore[no-untyped-def] - """Serve the command center 2D visualization (built React app).""" - index_file = get_data("command_center.html") - if index_file.exists(): - return FileResponse(index_file, media_type="text/html") - else: - return Response( - content="Command center not built. Run: cd dimos/web/command-center-extension && npm install && npm run build:standalone", - status_code=503, - media_type="text/plain", - ) - - routes = [ - Route("/", serve_index), - Route("/command-center", serve_command_center), - ] - - starlette_app = Starlette(routes=routes) - - self.app = socketio.ASGIApp(self.sio, starlette_app) - - # Register SocketIO event handlers - @self.sio.event # type: ignore[untyped-decorator] - async def connect(sid, environ) -> None: # type: ignore[no-untyped-def] - with self.state_lock: - current_state = dict(self.vis_state) - - # Include GPS goal points in the initial state - if self.gps_goal_points: - current_state["gps_travel_goal_points"] = self.gps_goal_points - - # Force full costmap update on new connection - self.costmap_encoder.last_full_grid = None - - await self.sio.emit("full_state", current_state, room=sid) # type: ignore[union-attr] - logger.info( - f"Client {sid} connected, sent state with {len(self.gps_goal_points)} GPS goal points" - ) - - @self.sio.event # type: ignore[untyped-decorator] - async def click(sid, position) -> None: # type: ignore[no-untyped-def] - goal = PoseStamped( - position=(position[0], position[1], 0), - orientation=(0, 0, 0, 1), # Default orientation - frame_id="world", - ) - self.goal_request.publish(goal) - logger.info( - "Click goal published", x=round(goal.position.x, 3), y=round(goal.position.y, 3) - ) - - @self.sio.event # type: ignore[untyped-decorator] - async def gps_goal(sid: str, goal: dict[str, float]) -> None: - logger.info(f"Received GPS goal: {goal}") - - # Publish the goal to LCM - self.gps_goal.publish(LatLon(lat=goal["lat"], lon=goal["lon"])) - - # Add to goal points list for visualization - self.gps_goal_points.append(goal) - logger.info(f"Added GPS goal to list. Total goals: {len(self.gps_goal_points)}") - - # Emit updated goal points back to all connected clients - if self.sio is not None: - await self.sio.emit("gps_travel_goal_points", self.gps_goal_points) - logger.debug( - f"Emitted gps_travel_goal_points with {len(self.gps_goal_points)} points: {self.gps_goal_points}" - ) - - @self.sio.event # type: ignore[untyped-decorator] - async def start_explore(sid: str) -> None: - logger.info("Starting exploration") - self.explore_cmd.publish(Bool(data=True)) - - @self.sio.event # type: ignore[untyped-decorator] - async def stop_explore(sid) -> None: # type: ignore[no-untyped-def] - logger.info("Stopping exploration") - self.stop_explore_cmd.publish(Bool(data=True)) - - @self.sio.event # type: ignore[untyped-decorator] - async def clear_gps_goals(sid: str) -> None: - logger.info("Clearing all GPS goal points") - self.gps_goal_points.clear() - if self.sio is not None: - await self.sio.emit("gps_travel_goal_points", self.gps_goal_points) - logger.info("GPS goal points cleared and updated clients") - - @self.sio.event # type: ignore[untyped-decorator] - async def move_command(sid: str, data: dict[str, Any]) -> None: - # Publish Twist if transport is configured - if self.cmd_vel and self.cmd_vel.transport: - twist = Twist( - linear=Vector3(data["linear"]["x"], data["linear"]["y"], data["linear"]["z"]), - angular=Vector3( - data["angular"]["x"], data["angular"]["y"], data["angular"]["z"] - ), - ) - self.cmd_vel.publish(twist) - - # Publish TwistStamped if transport is configured - if self.movecmd_stamped and self.movecmd_stamped.transport: - twist_stamped = TwistStamped( - ts=time.time(), - frame_id="base_link", - linear=Vector3(data["linear"]["x"], data["linear"]["y"], data["linear"]["z"]), - angular=Vector3( - data["angular"]["x"], data["angular"]["y"], data["angular"]["z"] - ), - ) - self.movecmd_stamped.publish(twist_stamped) - - def _run_uvicorn_server(self) -> None: - config = uvicorn.Config( - self.app, # type: ignore[arg-type] - host="0.0.0.0", - port=self.port, - log_level="error", # Reduce verbosity - ) - self._uvicorn_server = uvicorn.Server(config) - self._uvicorn_server.run() - - def _on_robot_pose(self, msg: PoseStamped) -> None: - pose_data = {"type": "vector", "c": [msg.position.x, msg.position.y, msg.position.z]} - self.vis_state["robot_pose"] = pose_data - self._emit("robot_pose", pose_data) - - def _on_gps_location(self, msg: LatLon) -> None: - pose_data = {"lat": msg.lat, "lon": msg.lon} - self.vis_state["gps_location"] = pose_data - self._emit("gps_location", pose_data) - - def _on_path(self, msg: Path) -> None: - points = [[pose.position.x, pose.position.y] for pose in msg.poses] - path_data = {"type": "path", "points": points} - self.vis_state["path"] = path_data - self._emit("path", path_data) - - def _on_global_costmap(self, msg: OccupancyGrid) -> None: - costmap_data = self._process_costmap(msg) - self.vis_state["costmap"] = costmap_data - self._emit("costmap", costmap_data) - - def _process_costmap(self, costmap: OccupancyGrid) -> dict[str, Any]: - """Convert OccupancyGrid to visualization format.""" - costmap = gradient(simple_inflate(costmap, 0.1), max_distance=1.0) - grid_data = self.costmap_encoder.encode_costmap(costmap.grid) - - return { - "type": "costmap", - "grid": grid_data, - "origin": { - "type": "vector", - "c": [costmap.origin.position.x, costmap.origin.position.y, 0], - }, - "resolution": costmap.resolution, - "origin_theta": 0, # Assuming no rotation for now - } - - def _emit(self, event: str, data: Any) -> None: - if self._broadcast_loop and not self._broadcast_loop.is_closed(): - asyncio.run_coroutine_threadsafe(self.sio.emit(event, data), self._broadcast_loop) - - -websocket_vis = WebsocketVisModule.blueprint - -__all__ = ["WebsocketVisModule", "websocket_vis"] diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile deleted file mode 100644 index ef80b70e1d..0000000000 --- a/docker/dev/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -ARG FROM_IMAGE=ghcr.io/dimensionalos/ros-python:dev -FROM ${FROM_IMAGE} - -ARG GIT_COMMIT=unknown -ARG GIT_BRANCH=unknown - -RUN apt-get update && apt-get install -y \ - git \ - git-lfs \ - nano \ - vim \ - ccze \ - tmux \ - htop \ - iputils-ping \ - wget \ - net-tools \ - sudo \ - pre-commit - - -# Configure git to trust any directory (resolves dubious ownership issues in containers) -RUN git config --global --add safe.directory '*' - -WORKDIR /app - -# Install UV for fast Python package management -ENV UV_SYSTEM_PYTHON=1 -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -ENV PATH="/root/.local/bin:$PATH" - -# Install dependencies with UV -RUN uv pip install .[dev] - -# Copy files and add version to motd -COPY /assets/dimensionalascii.txt /etc/motd -COPY /docker/dev/bash.sh /root/.bash.sh -COPY /docker/dev/tmux.conf /root/.tmux.conf - -# Install nodejs (for random devtooling like copilot etc) -RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash -ENV NVM_DIR=/root/.nvm -RUN bash -c "source $NVM_DIR/nvm.sh && nvm install 24" - -# This doesn't work atm -RUN echo " v_${GIT_BRANCH}:${GIT_COMMIT} | $(date)" >> /etc/motd -RUN echo "echo -e '\033[34m$(cat /etc/motd)\033[0m\n'" >> /root/.bashrc - -RUN echo "source /root/.bash.sh" >> /root/.bashrc - -COPY /docker/dev/entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - -ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/dev/bash.sh b/docker/dev/bash.sh deleted file mode 100755 index c5248841d9..0000000000 --- a/docker/dev/bash.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/bin/bash -# history -shopt -s histappend -export HISTCONTROL="ignoredups" -export HISTSIZE=100000 -export HISTFILESIZE=100000 -export HISTIGNORE='ls' - -# basic vars -export EDITOR="nano" -export LESS='-R' - -# basic aliases -alias ta='tmux a' -alias ccze='ccze -o nolookups -A' -alias pd='p d' -alias t='tmux' -alias g='grep' -alias f='find' -alias ..="cd .." -alias ka="killall" -alias la="ls -al" -alias l="ls" -alias sl="ls" -alias ls="ls --color" -alias c="clear" -alias psa="ps aux" -alias grep="grep --color=auto" -alias p="ping -c 1 -w 1" -alias psg="ps aux | grep" -alias unitg="systemctl list-unit-files | grep" -alias ug="unitg" -alias unit="echo 'systemctl list-unit-files'; systemctl list-unit-files" -alias scr="echo 'sudo systemctl daemon-reload'; sudo systemctl daemon-reload" -alias psac="ps aux | ccze -Ao nolookups" -alias psa="ps aux" -alias pdn="p dns" -alias s="sudo -iu root" -alias m="mount" -alias oip="wget -qO- http://www.ipaddr.de/?plain" -alias getlogin="echo genpass 6 : genpass 20" -alias rscp="rsync -vrt --size-only --partial --progress " -alias rscpd="rsync --delete-after -vrt --size-only --partial --progress " -alias v="vim" -alias npm="export PYTHON=python2; npm" -alias ssh="ssh -o ConnectTimeout=1" -alias gp="git push" -alias rh="history -a; history -c; history -r" -alias gs="git status" -alias gd="git diff" -alias ipy="python -c 'import IPython; IPython.terminal.ipapp.launch_new_instance()'" - -function npmg -{ - echo 'global npm install' - tmpUmask u=rwx,g=rx,o=rx npm $@ -} - -function tmpUmask -{ - oldUmask=$(umask) - newUmask=$1 - - shift - umask $newUmask - echo umask $(umask -S) - echo "$@" - eval $@ - umask $oldUmask - echo umask $(umask -S) - -} - -function newloginuser -{ - read user - pass=$(genpass 20) - - echo $user : $pass - echo site? - read site - echo site: $site - - echo $site : $user : $pass >> ~/.p -} - -function newlogin -{ - user=$(genpass 6) - pass=$(genpass 20) - - echo $user : $pass - echo site? - read site - echo site: $site - - echo $site : $user : $pass >> ~/.p - -} - - -function newlogin -{ - pass=$(genpass 30) - echo $pass -} - - -function getpass { - echo $(genpass 20) -} - -function genpass -{ - newpass=$(cat /dev/urandom | base64 | tr -d "0" | tr -d "y" | tr -d "Y" | tr -d "z" | tr -d "Z" | tr -d "I" | tr -d "l" | tr -d "//" | head -c$1) - echo -n $newpass -} - -function sx -{ - if [ -z $1 ] - then - screen -x $(cat /tmp/sx) - else - echo -n $1 > /tmp/sx - screen -x $1 - fi -} - -function loopy -{ - while [ 1 ]; do - eval "$1" - if [ "$2" ]; then sleep $2; else sleep 1; fi - done -} - - -function we -{ - eval "$@" - until [ $? -eq 0 ]; do - sleep 1; eval "$@" - done -} - -alias wf='waitfor' -function waitfor -{ - eval "$1" - until [ $? -eq 0 ]; do - sleep 1; eval "$1" - done - eval "$2" -} - -function waitnot -{ - eval "$1" - until [ $? -ne 0 ]; do - sleep 1; eval "$1" - done - eval "$2" -} - -function wrscp -{ - echo rscp $@ - waitfor "rscp $1 $2" -} - -function waitfornot -{ - eval "$1" - until [ $? -ne 0 ]; do - sleep 1 - eval "$1" - done - eval "$2" -} - - -function watchFile -{ - tail -F $1 2>&1 | sed -e "$(echo -e "s/^\(tail: .\+: file truncated\)$/\1\e[2J \e[0f/")" -} - -PS1='${debian_chroot:+($debian_chroot)}\[\033[32m\]\u@dimos\[\033[00m\]:\[\033[34m\]\w\[\033[00m\] \$ ' - -export PATH="/app/bin:${PATH}" - -# we store history in the container so rebuilding doesn't lose it -export HISTFILE=/app/.bash_history - -# export all .env variables -set -a -source /app/.env -set +a diff --git a/docker/dev/docker-compose-cuda.yaml b/docker/dev/docker-compose-cuda.yaml deleted file mode 100644 index 5def3fb6c3..0000000000 --- a/docker/dev/docker-compose-cuda.yaml +++ /dev/null @@ -1,32 +0,0 @@ -services: - dev-environment: - image: ghcr.io/dimensionalos/dev:${DEV_IMAGE_TAG:-latest} - container_name: dimos-dev-${DEV_IMAGE_TAG:-latest} - network_mode: "host" - volumes: - - ../../../:/app - - # X11 forwarding - - /tmp/.X11-unix:/tmp/.X11-unix - - ${HOME}/.Xauthority:/root/.Xauthority:rw - - runtime: nvidia - environment: - - PYTHONUNBUFFERED=1 - - PYTHONPATH=/app - - DISPLAY=${DISPLAY:-} - - # NVIDIA - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - - # X11 and XDG runtime - - XAUTHORITY=/root/.Xauthority - - XDG_RUNTIME_DIR=/tmp/xdg-runtime - - ports: - - "5555:5555" - - "3000:3000" - stdin_open: true - tty: true - command: /bin/bash diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml deleted file mode 100644 index 8175e26c69..0000000000 --- a/docker/dev/docker-compose.yaml +++ /dev/null @@ -1,23 +0,0 @@ -services: - dev-environment: - image: ghcr.io/dimensionalos/dev:${DEV_IMAGE_TAG:-latest} - container_name: dimos-dev-${DEV_IMAGE_TAG:-latest} - network_mode: "host" - volumes: - - ../../:/app - - # X11 forwarding - - /tmp/.X11-unix:/tmp/.X11-unix - - ${HOME}/.Xauthority:/root/.Xauthority:rw - - environment: - - PYTHONUNBUFFERED=1 - - PYTHONPATH=/app - - DISPLAY=${DISPLAY:-} - - ports: - - "5555:5555" - - "3000:3000" - stdin_open: true - tty: true - command: /bin/bash diff --git a/docker/dev/entrypoint.sh b/docker/dev/entrypoint.sh deleted file mode 100644 index d48bea16e3..0000000000 --- a/docker/dev/entrypoint.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -if [ -d "/opt/ros/${ROS_DISTRO}" ]; then - source /opt/ros/${ROS_DISTRO}/setup.bash -else - echo "ROS is not available in this env" -fi - -exec "$@" diff --git a/docker/dev/tmux.conf b/docker/dev/tmux.conf deleted file mode 100644 index ecf6b22ced..0000000000 --- a/docker/dev/tmux.conf +++ /dev/null @@ -1,84 +0,0 @@ -# set-option -g pane-active-border-fg yellow -# set-option -g pane-active-border-bg blue -# set-option -g pane-border-fg blue -# set-option -g pane-border-bg blue -# set-option -g message-fg black -# set-option -g message-bg green -set-option -g status-bg blue -set-option -g status-fg cyan -set-option -g history-limit 5000 - -set-option -g prefix C-q - -bind | split-window -h -c "#{pane_current_path}" -bind "-" split-window -v -c "#{pane_current_path}" -bind k kill-pane -#bind C-Tab select-pane -t :.+ -#bind-key a send-prefix - -bind -n C-down new-window -c "#{pane_current_path}" -bind -n C-up new-window -c "#{pane_current_path}" -bind -n M-n new-window -c "#{pane_current_path}" -bind -n M-c new-window -c "#{pane_current_path}" -bind -n C-left prev -bind -n C-right next -bind -n M-C-n next -bind -n M-C-p prev -# bind -n C-\ new-window -c "#{pane_current_path}" -bind c new-window -c "#{pane_current_path}" - -#bind -n A-s resize-pane -#bind -n A-w resize-pane -U -#bind -n A-a resize-pane -L -#ind -n A-d resize-pane -R -#bind -n C-M-left swap-window -t -1 -#bind -n C-M-right swap-window -t +1 -#set -g default-terminal "screen-256color" -#set -g default-terminal "xterm" - -bind-key u capture-pane \; save-buffer /tmp/tmux-buffer \; run-shell "urxvtc --geometry 51x20 --title 'floatme' -e bash -c \"cat /tmp/tmux-buffer | urlview\" " -bind-key r source-file ~/.tmux.conf - -# set-window-option -g window-status-current-fg green -set -g status-fg white - -set-window-option -g aggressive-resize off -set-window-option -g automatic-rename on - -# bind-key -n C-\` select-window -t 0 -bind-key -n C-0 select-window -t 0 -bind-key -n C-1 select-window -t 1 -bind-key -n C-2 select-window -t 2 -bind-key -n C-3 select-window -t 3 -bind-key -n C-4 select-window -t 4 -bind-key -n C-5 select-window -t 5 -bind-key -n C-6 select-window -t 6 -bind-key -n C-7 select-window -t 7 -bind-key -n C-8 select-window -t 8 -bind-key -n C-9 select-window -t 9 - - -# statusbar settings - adopted from tmuxline.vim and vim-airline - Theme: murmur -set -g status-justify "left" -set -g status "on" -set -g status-left-style "none" -set -g message-command-style "fg=colour144,bg=colour237" -set -g status-right-style "none" -set -g status-style "bg=black" -set -g status-bg "black" -set -g message-style "fg=colour144,bg=colour237" -set -g pane-active-border-style "fg=colour248" -#set -g pane-border-style "fg=colour238" -#set -g pane-active-border-style "fg=colour241" -set -g pane-border-style "fg=colour0" -set -g status-right-length "100" -set -g status-left-length "100" -# setw -g window-status-activity-attr "none" -setw -g window-status-activity-style "fg=colour27,bg=colour234,none" -setw -g window-status-separator "#[bg=colour235]" -setw -g window-status-style "fg=colour253,bg=black,none" -set -g status-left "" -set -g status-right "#[bg=black]#[fg=colour244]#h#[fg=colour244]#[fg=colour3]/#[fg=colour244]#S" - -setw -g window-status-format " #[fg=colour3]#I#[fg=colour244] #W " -setw -g window-status-current-format " #[fg=color3]#I#[fg=colour254] #W " diff --git a/docker/navigation/.env.hardware b/docker/navigation/.env.hardware deleted file mode 100644 index 234e58545c..0000000000 --- a/docker/navigation/.env.hardware +++ /dev/null @@ -1,101 +0,0 @@ -# Hardware Configuration Environment Variables -# Copy this file to .env and customize for your hardware setup - -# ============================================ -# NVIDIA GPU Support -# ============================================ -# Set the Docker runtime to nvidia for GPU support (it's runc by default) -#DOCKER_RUNTIME=nvidia - -# ============================================ -# ROS Configuration -# ============================================ -# ROS domain ID for multi-robot setups -ROS_DOMAIN_ID=42 - -# Robot configuration ('mechanum_drive', 'unitree/unitree_g1', 'unitree/unitree_g1', etc) -ROBOT_CONFIG_PATH=mechanum_drive - -# Robot IP address on local network for connection over WebRTC -# For Unitree Go2, Unitree G1, if using WebRTCConnection -# This can be found in the unitree app under Device settings or via network scan -ROBOT_IP= - -# ============================================ -# Mid-360 Lidar Configuration -# ============================================ -# Network interface connected to the lidar (e.g., eth0, enp0s3) -# Find with: ip addr show -LIDAR_INTERFACE=eth0 - -# Processing computer IP address on the lidar subnet -# Must be on the same subnet as the lidar (e.g., 192.168.1.5) -# LIDAR_COMPUTER_IP=192.168.123.5 # FOR UNITREE G1 EDU -LIDAR_COMPUTER_IP=192.168.1.5 - -# Gateway IP address for the lidar subnet -# LIDAR_GATEWAY=192.168.123.1 # FOR UNITREE G1 EDU -LIDAR_GATEWAY=192.168.1.1 - -# Full IP address of your Mid-360 lidar -# This should match the IP configured on your lidar device -# Common patterns: 192.168.1.1XX or 192.168.123.1XX -# LIDAR_IP=192.168.123.120 # FOR UNITREE G1 EDU -LIDAR_IP=192.168.1.116 - -# ============================================ -# Motor Controller Configuration -# ============================================ -# Serial device for motor controller -# Check with: ls /dev/ttyACM* or ls /dev/ttyUSB* -MOTOR_SERIAL_DEVICE=/dev/ttyACM0 - -# ============================================ -# Network Communication (for base station) -# ============================================ -# Enable WiFi buffer optimization for data transmission -# Set to true if using wireless base station -ENABLE_WIFI_BUFFER=false - -# ============================================ -# Unitree Robot Configuration -# ============================================ -# Enable Unitree WebRTC control (for Go2, G1) -#USE_UNITREE=true - -# Unitree robot IP address -UNITREE_IP=192.168.12.1 - -# Unitree connection method (LocalAP or Ethernet) -UNITREE_CONN=LocalAP - -# ============================================ -# Navigation Options -# ============================================ -# Enable route planner (FAR planner for goal navigation) -USE_ROUTE_PLANNER=false - -# Enable RViz visualization -USE_RVIZ=false - -# Map path for localization mode (leave empty for SLAM/mapping mode) -# Set to file prefix (no .pcd extension), e.g., /ros2_ws/maps/warehouse -# The system will load: MAP_PATH.pcd for SLAM, MAP_PATH_tomogram.pickle for PCT planner -MAP_PATH= - -# ============================================ -# Device Group IDs -# ============================================ -# Group ID for /dev/input devices (joystick) -# Find with: getent group input | cut -d: -f3 -INPUT_GID=995 - -# Group ID for serial devices -# Find with: getent group dialout | cut -d: -f3 -DIALOUT_GID=20 - -# ============================================ -# Display Configuration -# ============================================ -# X11 display (usually auto-detected) -# DISPLAY=:0 diff --git a/docker/navigation/.gitignore b/docker/navigation/.gitignore deleted file mode 100644 index 0eaccbc740..0000000000 --- a/docker/navigation/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -# Cloned repository -ros-navigation-autonomy-stack/ - -# Unity models (large binary files) -unity_models/ - -# ROS bag files -bagfiles/ - -# Config files (may contain local settings) -config/ - -# Docker volumes -.docker/ - -# Temporary files -*.tmp -*.log -*.swp -*~ diff --git a/docker/navigation/Dockerfile b/docker/navigation/Dockerfile deleted file mode 100644 index fa51fd621c..0000000000 --- a/docker/navigation/Dockerfile +++ /dev/null @@ -1,508 +0,0 @@ -# ============================================================================= -# DimOS Navigation Docker Image -# ============================================================================= -# -# Multi-stage build for ROS 2 navigation with SLAM support. -# Includes both arise_slam and FASTLIO2 - select at runtime via LOCALIZATION_METHOD. -# -# Supported configurations: -# - ROS distributions: humble, jazzy -# - SLAM methods: arise_slam (default), fastlio (set LOCALIZATION_METHOD=fastlio) -# -# Build: -# ./build.sh --humble # Build for ROS 2 Humble -# ./build.sh --jazzy # Build for ROS 2 Jazzy -# -# Run: -# ./start.sh --hardware --route-planner # Uses arise_slam -# LOCALIZATION_METHOD=fastlio ./start.sh --hardware --route-planner # Uses FASTLIO2 -# -# ============================================================================= - -# Build argument for ROS distribution (default: humble) -ARG ROS_DISTRO=humble -ARG TARGETARCH - -# ----------------------------------------------------------------------------- -# Platform-specific base images -# - amd64: Use osrf/ros desktop-full (includes Gazebo, full GUI) -# - arm64: Use ros-base (desktop-full not available for ARM) -# ----------------------------------------------------------------------------- -FROM osrf/ros:${ROS_DISTRO}-desktop-full AS base-amd64 -FROM ros:${ROS_DISTRO}-ros-base AS base-arm64 - -# ----------------------------------------------------------------------------- -# STAGE 1: Build Stage - compile all C++ dependencies -# ----------------------------------------------------------------------------- -FROM base-${TARGETARCH} AS builder - -ARG ROS_DISTRO -ENV DEBIAN_FRONTEND=noninteractive -ENV ROS_DISTRO=${ROS_DISTRO} -ENV WORKSPACE=/ros2_ws - -# Install build dependencies only -RUN apt-get update && apt-get install -y --no-install-recommends \ - # Build tools - git \ - cmake \ - build-essential \ - python3-colcon-common-extensions \ - # Libraries needed for building - libpcl-dev \ - libgoogle-glog-dev \ - libgflags-dev \ - libatlas-base-dev \ - libeigen3-dev \ - libsuitesparse-dev \ - # ROS packages needed for build - ros-${ROS_DISTRO}-pcl-ros \ - ros-${ROS_DISTRO}-cv-bridge \ - && rm -rf /var/lib/apt/lists/* - -# On arm64, ros-base doesn't include rviz2 (unlike desktop-full on amd64) -# Install it separately for building rviz plugins -# Note: ARG must be re-declared after FROM; placed here to maximize layer caching above -ARG TARGETARCH -RUN if [ "${TARGETARCH}" = "arm64" ]; then \ - apt-get update && apt-get install -y --no-install-recommends \ - ros-${ROS_DISTRO}-rviz2 \ - && rm -rf /var/lib/apt/lists/*; \ - fi - -# On arm64, build open3d from source (no Linux aarch64 wheels on PyPI) -# Cached as a separate layer; the wheel is copied to the runtime stage -# mkdir runs unconditionally so COPY --from=builder works on all architectures -RUN mkdir -p /opt/open3d-wheel && \ - PYTHON_MINOR=$(python3 -c "import sys; print(sys.version_info.minor)") && \ - if [ "${TARGETARCH}" = "arm64" ] && [ "$PYTHON_MINOR" -ge 12 ]; then \ - echo "Building open3d from source for arm64 + Python 3.${PYTHON_MINOR} (no PyPI wheel)..." && \ - apt-get update && apt-get install -y --no-install-recommends \ - python3-dev \ - python3-pip \ - python3-setuptools \ - python3-wheel \ - libblas-dev \ - liblapack-dev \ - libgl1-mesa-dev \ - libglib2.0-dev \ - libxinerama-dev \ - libxcursor-dev \ - libxrandr-dev \ - libxi-dev \ - gfortran \ - && rm -rf /var/lib/apt/lists/* && \ - cd /tmp && \ - git clone --depth 1 --branch v0.19.0 https://github.com/isl-org/Open3D.git && \ - cd Open3D && \ - util/install_deps_ubuntu.sh assume-yes && \ - mkdir build && cd build && \ - cmake .. \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_CUDA_MODULE=OFF \ - -DBUILD_GUI=OFF \ - -DBUILD_TENSORFLOW_OPS=OFF \ - -DBUILD_PYTORCH_OPS=OFF \ - -DBUILD_UNIT_TESTS=OFF \ - -DBUILD_BENCHMARKS=OFF \ - -DBUILD_EXAMPLES=OFF \ - -DBUILD_WEBRTC=OFF && \ - make -j$(($(nproc) > 4 ? 4 : $(nproc))) && \ - make pip-package -j$(($(nproc) > 4 ? 4 : $(nproc))) && \ - mkdir -p /opt/open3d-wheel && \ - cp lib/python_package/pip_package/open3d*.whl /opt/open3d-wheel/ && \ - cd / && rm -rf /tmp/Open3D; \ - fi - -# On arm64, build or-tools from source (pre-built binaries are x86_64 only) -# This is cached as a separate layer since it takes significant time to build -ENV OR_TOOLS_VERSION=9.8 -RUN if [ "${TARGETARCH}" = "arm64" ]; then \ - echo "Building or-tools v${OR_TOOLS_VERSION} from source for arm64..." && \ - apt-get update && apt-get install -y --no-install-recommends \ - lsb-release \ - wget \ - && rm -rf /var/lib/apt/lists/* && \ - cd /tmp && \ - wget -q https://github.com/google/or-tools/archive/refs/tags/v${OR_TOOLS_VERSION}.tar.gz && \ - tar xzf v${OR_TOOLS_VERSION}.tar.gz && \ - cd or-tools-${OR_TOOLS_VERSION} && \ - cmake -S . -B build \ - -DCMAKE_BUILD_TYPE=Release \ - -DBUILD_DEPS=ON \ - -DBUILD_SAMPLES=OFF \ - -DBUILD_EXAMPLES=OFF \ - -DBUILD_FLATZINC=OFF \ - -DUSE_SCIP=OFF \ - -DUSE_COINOR=OFF && \ - cmake --build build --config Release -j$(($(nproc) > 4 ? 4 : $(nproc))) && \ - cmake --install build --prefix /opt/or-tools && \ - rm -rf /tmp/or-tools-${OR_TOOLS_VERSION} /tmp/v${OR_TOOLS_VERSION}.tar.gz; \ - fi - -# Create workspace -RUN mkdir -p ${WORKSPACE}/src - -# Copy autonomy stack source -COPY docker/navigation/ros-navigation-autonomy-stack ${WORKSPACE}/src/ros-navigation-autonomy-stack - -# On arm64, replace pre-built x86_64 or-tools with arm64 built version -RUN if [ "${TARGETARCH}" = "arm64" ] && [ -d "/opt/or-tools" ]; then \ - echo "Replacing x86_64 or-tools with arm64 build..." && \ - OR_TOOLS_DIR=${WORKSPACE}/src/ros-navigation-autonomy-stack/src/exploration_planner/tare_planner/or-tools && \ - rm -rf ${OR_TOOLS_DIR}/lib/*.so* ${OR_TOOLS_DIR}/lib/*.a && \ - cp -r /opt/or-tools/lib/* ${OR_TOOLS_DIR}/lib/ && \ - rm -rf ${OR_TOOLS_DIR}/include && \ - cp -r /opt/or-tools/include ${OR_TOOLS_DIR}/ && \ - ldconfig; \ - fi - -# Compatibility fix: In Humble, cv_bridge uses .h extension, but Jazzy uses .hpp -# Create a symlink so code written for Jazzy works on Humble -RUN if [ "${ROS_DISTRO}" = "humble" ]; then \ - CV_BRIDGE_DIR=$(find /opt/ros/humble/include -name "cv_bridge.h" -printf "%h\n" 2>/dev/null | head -1) && \ - if [ -n "$CV_BRIDGE_DIR" ]; then \ - ln -sf "$CV_BRIDGE_DIR/cv_bridge.h" "$CV_BRIDGE_DIR/cv_bridge.hpp"; \ - echo "Created cv_bridge.hpp symlink in $CV_BRIDGE_DIR"; \ - else \ - echo "Warning: cv_bridge.h not found, skipping symlink creation"; \ - fi; \ - fi - -# Build Livox-SDK2 -RUN cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/Livox-SDK2 && \ - mkdir -p build && cd build && \ - cmake .. && make -j$(nproc) && make install && ldconfig && \ - rm -rf ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/Livox-SDK2/build - -# Build Sophus -RUN cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/Sophus && \ - mkdir -p build && cd build && \ - cmake .. -DBUILD_TESTS=OFF && make -j$(nproc) && make install && \ - rm -rf ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/Sophus/build - -# Build Ceres Solver -RUN cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/ceres-solver && \ - mkdir -p build && cd build && \ - cmake .. && make -j$(nproc) && make install && \ - rm -rf ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/ceres-solver/build - -# Build GTSAM -RUN cd ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/gtsam && \ - mkdir -p build && cd build && \ - cmake .. -DGTSAM_USE_SYSTEM_EIGEN=ON -DGTSAM_BUILD_WITH_MARCH_NATIVE=OFF && \ - make -j$(nproc) && make install && ldconfig && \ - rm -rf ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/dependency/gtsam/build - -# Build ROS workspace with both SLAM systems (no --symlink-install for multi-stage build compatibility) -RUN /bin/bash -c "source /opt/ros/${ROS_DISTRO}/setup.bash && \ - cd ${WORKSPACE} && \ - echo 'Building with both arise_slam and FASTLIO2' && \ - colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release" - -# ----------------------------------------------------------------------------- -# STAGE 2: Runtime Stage - minimal image for running -# ----------------------------------------------------------------------------- -ARG ROS_DISTRO -ARG TARGETARCH -FROM base-${TARGETARCH} AS runtime - -ARG ROS_DISTRO -ENV DEBIAN_FRONTEND=noninteractive -ENV ROS_DISTRO=${ROS_DISTRO} -ENV WORKSPACE=/ros2_ws -ENV DIMOS_PATH=/workspace/dimos -# LOCALIZATION_METHOD: arise_slam (default) or fastlio -ENV LOCALIZATION_METHOD=arise_slam - -# DDS Configuration - Use FastDDS (default ROS 2 middleware) -ENV RMW_IMPLEMENTATION=rmw_fastrtps_cpp -ENV FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml - -# Install runtime dependencies only (no build tools) -RUN apt-get update && apt-get install -y --no-install-recommends \ - # ROS packages - ros-${ROS_DISTRO}-pcl-ros \ - ros-${ROS_DISTRO}-cv-bridge \ - ros-${ROS_DISTRO}-foxglove-bridge \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt* \ - ros-${ROS_DISTRO}-joy \ - # DDS middleware (FastDDS is default, just ensure it's installed) - ros-${ROS_DISTRO}-rmw-fastrtps-cpp \ - # Runtime libraries - libpcl-dev \ - libgoogle-glog-dev \ - libgflags-dev \ - libatlas-base-dev \ - libeigen3-dev \ - libsuitesparse-dev \ - # X11 for GUI (minimal) - libx11-6 \ - libxext6 \ - libxrender1 \ - libgl1 \ - libglib2.0-0 \ - # Networking tools - iputils-ping \ - net-tools \ - iproute2 \ - # Serial/USB for hardware - usbutils \ - # Python (minimal) - python3-pip \ - python3-venv \ - # Joystick support - joystick \ - # Time sync for multi-computer setups - chrony \ - && rm -rf /var/lib/apt/lists/* - -# Copy installed libraries from builder -COPY --from=builder /usr/local/lib /usr/local/lib -COPY --from=builder /usr/local/include /usr/local/include - -RUN ldconfig - -# Copy built ROS workspace from builder -COPY --from=builder ${WORKSPACE}/install ${WORKSPACE}/install - -# Copy only config/rviz files from src (not the large dependency folders) -# These are needed if running without volume mount -COPY --from=builder ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/rviz ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/rviz -COPY --from=builder ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/route_planner/far_planner/rviz ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/route_planner/far_planner/rviz -COPY --from=builder ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/exploration_planner/tare_planner/rviz ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/exploration_planner/tare_planner/rviz -# Copy SLAM config files based on SLAM_TYPE -COPY --from=builder ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/config ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/config - -# Copy config files for both SLAM systems -RUN --mount=from=builder,source=${WORKSPACE}/src/ros-navigation-autonomy-stack/src,target=/tmp/src \ - echo "Copying arise_slam configs" && \ - mkdir -p ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/arise_slam_mid360 && \ - cp -r /tmp/src/slam/arise_slam_mid360/config ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/arise_slam_mid360/ 2>/dev/null || true && \ - echo "Copying FASTLIO2 configs" && \ - mkdir -p ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/FASTLIO2_ROS2 && \ - for pkg in fastlio2 localizer pgo hba; do \ - if [ -d "/tmp/src/slam/FASTLIO2_ROS2/$pkg/config" ]; then \ - mkdir -p ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/FASTLIO2_ROS2/$pkg && \ - cp -r /tmp/src/slam/FASTLIO2_ROS2/$pkg/config ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/FASTLIO2_ROS2/$pkg/; \ - fi; \ - if [ -d "/tmp/src/slam/FASTLIO2_ROS2/$pkg/rviz" ]; then \ - cp -r /tmp/src/slam/FASTLIO2_ROS2/$pkg/rviz ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/slam/FASTLIO2_ROS2/$pkg/; \ - fi; \ - done - -# Copy simulation shell scripts (real robot mode uses volume mount) -COPY --from=builder ${WORKSPACE}/src/ros-navigation-autonomy-stack/system_simulation*.sh ${WORKSPACE}/src/ros-navigation-autonomy-stack/ - -# Create directories -RUN mkdir -p ${DIMOS_PATH} \ - ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity \ - ${WORKSPACE}/bagfiles \ - ${WORKSPACE}/logs \ - ${WORKSPACE}/config - -# Create FastDDS configuration file -RUN cat > ${WORKSPACE}/config/fastdds.xml <<'EOF' - - - - - - ros2_navigation_participant - - - SIMPLE - - 10 - 0 - - - 3 - 0 - - - - - 10485760 - 10485760 - true - - - - - - - udp_transport - UDPv4 - 10485760 - 10485760 - 65500 - - - - shm_transport - SHM - 10485760 - 1048576 - - - -EOF - -# Install portaudio for unitree-webrtc-connect (pyaudio dependency) -RUN apt-get update && apt-get install -y --no-install-recommends \ - portaudio19-dev \ - && rm -rf /var/lib/apt/lists/* - -# Create Python venv and install dependencies -RUN python3 -m venv /opt/dimos-venv && \ - /opt/dimos-venv/bin/pip install --no-cache-dir \ - pyyaml - -# On arm64, install open3d wheel built from source in the builder stage -COPY --from=builder /opt/open3d-wheel /opt/open3d-wheel -ARG TARGETARCH -RUN if [ "${TARGETARCH}" = "arm64" ] && ls /opt/open3d-wheel/open3d*.whl 1>/dev/null 2>&1; then \ - echo "Installing open3d from pre-built arm64 wheel..." && \ - /opt/dimos-venv/bin/pip install --no-cache-dir /opt/open3d-wheel/open3d*.whl && \ - rm -rf /opt/open3d-wheel; \ - fi - -# Copy dimos source and install as editable package -# The volume mount at runtime will overlay /workspace/dimos, but the editable -# install creates a link that will use the volume-mounted files -COPY pyproject.toml setup.py /workspace/dimos/ -COPY dimos /workspace/dimos/dimos -RUN /opt/dimos-venv/bin/pip install --no-cache-dir -e "/workspace/dimos[unitree]" - -# Set up shell environment -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> ~/.bashrc && \ - echo "source ${WORKSPACE}/install/setup.bash" >> ~/.bashrc && \ - echo "source /opt/dimos-venv/bin/activate" >> ~/.bashrc && \ - echo "export RMW_IMPLEMENTATION=rmw_fastrtps_cpp" >> ~/.bashrc && \ - echo "export FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml" >> ~/.bashrc - -# Copy helper scripts -COPY docker/navigation/run_both.sh /usr/local/bin/run_both.sh -COPY docker/navigation/ros_launch_wrapper.py /usr/local/bin/ros_launch_wrapper.py -COPY docker/navigation/foxglove_utility/twist_relay.py /usr/local/bin/twist_relay.py -COPY docker/navigation/foxglove_utility/goal_autonomy_relay.py /usr/local/bin/goal_autonomy_relay.py -RUN chmod +x /usr/local/bin/run_both.sh /usr/local/bin/ros_launch_wrapper.py /usr/local/bin/twist_relay.py /usr/local/bin/goal_autonomy_relay.py - -# Set up udev rules for motor controller -RUN mkdir -p /etc/udev/rules.d && \ - echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", MODE="0666", GROUP="dialout"' \ - > /etc/udev/rules.d/99-motor-controller.rules - -# Set up entrypoint script -RUN echo '#!/bin/bash\n\ -set -e\n\ -\n\ -# Mark git directories as safe\n\ -git config --global --add safe.directory /workspace/dimos 2>/dev/null || true\n\ -git config --global --add safe.directory /ros2_ws/src/ros-navigation-autonomy-stack 2>/dev/null || true\n\ -\n\ -# Source ROS setup\n\ -source /opt/ros/${ROS_DISTRO}/setup.bash\n\ -source ${WORKSPACE}/install/setup.bash\n\ -\n\ -# Activate Python virtual environment\n\ -source /opt/dimos-venv/bin/activate\n\ -\n\ -# DDS Configuration (FastDDS)\n\ -export RMW_IMPLEMENTATION=rmw_fastrtps_cpp\n\ -export FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml\n\ -\n\ -# Use custom DDS config if provided via mount\n\ -if [ -f "/ros2_ws/config/custom_fastdds.xml" ]; then\n\ - export FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/custom_fastdds.xml\n\ - echo "Using custom FastDDS configuration"\n\ -fi\n\ -\n\ -# Export ROBOT_CONFIG_PATH for autonomy stack\n\ -export ROBOT_CONFIG_PATH="${ROBOT_CONFIG_PATH:-mechanum_drive}"\n\ -\n\ -# Hardware-specific configurations\n\ -if [ "${HARDWARE_MODE}" = "true" ]; then\n\ - # Set network buffer sizes for WiFi data transmission\n\ - if [ "${ENABLE_WIFI_BUFFER}" = "true" ]; then\n\ - sysctl -w net.core.rmem_max=67108864 net.core.rmem_default=67108864 2>/dev/null || true\n\ - sysctl -w net.core.wmem_max=67108864 net.core.wmem_default=67108864 2>/dev/null || true\n\ - fi\n\ - \n\ - # Configure network interface for Mid-360 lidar if specified\n\ - if [ -n "${LIDAR_INTERFACE}" ] && [ -n "${LIDAR_COMPUTER_IP}" ]; then\n\ - ip addr add ${LIDAR_COMPUTER_IP}/24 dev ${LIDAR_INTERFACE} 2>/dev/null || true\n\ - ip link set ${LIDAR_INTERFACE} up 2>/dev/null || true\n\ - fi\n\ - \n\ - # Generate MID360_config.json if LIDAR_COMPUTER_IP and LIDAR_IP are set\n\ - if [ -n "${LIDAR_COMPUTER_IP}" ] && [ -n "${LIDAR_IP}" ]; then\n\ - cat > ${WORKSPACE}/src/ros-navigation-autonomy-stack/src/utilities/livox_ros_driver2/config/MID360_config.json </dev/null || true\n\ - echo "Generated MID360_config.json with LIDAR_COMPUTER_IP=${LIDAR_COMPUTER_IP} and LIDAR_IP=${LIDAR_IP}"\n\ - fi\n\ - \n\ - # Display Robot IP configuration if set\n\ - if [ -n "${ROBOT_IP}" ]; then\n\ - echo "Robot IP configured on local network: ${ROBOT_IP}"\n\ - fi\n\ -fi\n\ -\n\ -# Execute the command\n\ -exec "$@"' > /ros_entrypoint.sh && \ - chmod +x /ros_entrypoint.sh - -# Working directory -WORKDIR ${DIMOS_PATH} - -# Set the entrypoint -ENTRYPOINT ["/ros_entrypoint.sh"] - -# Default command -CMD ["bash"] diff --git a/docker/navigation/README.md b/docker/navigation/README.md deleted file mode 100644 index 32483b6512..0000000000 --- a/docker/navigation/README.md +++ /dev/null @@ -1,184 +0,0 @@ -# ROS Docker Integration for DimOS - -This directory contains Docker configuration files to run DimOS and the ROS autonomy stack in the same container, enabling communication between the two systems. - -## Prerequisites - -1. **Install Docker with `docker compose` support**. Follow the [official Docker installation guide](https://docs.docker.com/engine/install/). -2. **Install NVIDIA GPU drivers**. See [NVIDIA driver installation](https://www.nvidia.com/download/index.aspx). -3. **Install NVIDIA Container Toolkit**. Follow the [installation guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html). - -## Automated Quick Start - -This is an optimistic overview. Use the commands below for an in depth version. - -**Build the Docker image:** - -```bash -cd docker/navigation -./build.sh --humble # Build for ROS 2 Humble -./build.sh --jazzy # Build for ROS 2 Jazzy -``` - -This will: -- Clone the ros-navigation-autonomy-stack repository -- Build a Docker image with both arise_slam and FASTLIO2 -- Set up the environment for both ROS and DimOS - -The resulting image will be named `dimos_autonomy_stack:{distro}` (e.g., `humble`, `jazzy`). -Select SLAM method at runtime via `--localization arise_slam` or `--localization fastlio`. - -Note that the build will take a while and produce an image of approximately 24 GB. - -**Run the simulator to test it's working:** - -Use the same ROS distribution flag as your build: - -```bash -./start.sh --simulation --image humble # If built with --humble -# or -./start.sh --simulation --image jazzy # If built with --jazzy -``` - -
-

Manual build

- -Go to the docker dir and clone the ROS navigation stack (choose the branch matching your ROS distribution). - -```bash -cd docker/navigation -git clone -b humble git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git -# or -git clone -b jazzy git@github.com:dimensionalOS/ros-navigation-autonomy-stack.git -``` - -Download a [Unity environment model for the Mecanum wheel platform](https://drive.google.com/drive/folders/1G1JYkccvoSlxyySuTlPfvmrWoJUO8oSs?usp=sharing) and unzip the files to `unity_models`. - -Alternativelly, extract `office_building_1` from LFS: - -```bash -tar -xf ../../data/.lfs/office_building_1.tar.gz -mv office_building_1 unity_models -``` - -Then, go back to the root (from docker/navigation) and build the docker image: - -```bash -cd ../.. # Back to dimos root -ROS_DISTRO=humble docker compose -f docker/navigation/docker-compose.yml build -# or -ROS_DISTRO=jazzy docker compose -f docker/navigation/docker-compose.yml build -``` - -
- -## On Real Hardware - -### Configure the WiFi - -[Read this](https://github.com/dimensionalOS/ros-navigation-autonomy-stack/tree/jazzy?tab=readme-ov-file#transmitting-data-over-wifi) to see how to configure the WiFi. - -### Configure the Livox Lidar - -The MID360_config.json file is automatically generated on container startup based on your environment variables (LIDAR_COMPUTER_IP and LIDAR_IP). - -### Copy Environment Template -```bash -cp .env.hardware .env -``` - -### Edit `.env` File - -Key configuration parameters: - -```bash -# Robot Configuration -ROBOT_CONFIG_PATH=unitree/unitree_go2 # Robot type (mechanum_drive, unitree/unitree_go2, unitree/unitree_g1) - -# Lidar Configuration -LIDAR_INTERFACE=eth0 # Your ethernet interface (find with: ip link show) -LIDAR_COMPUTER_IP=192.168.1.5 # Computer IP on the lidar subnet -LIDAR_GATEWAY=192.168.1.1 # Gateway IP address for the lidar subnet -LIDAR_IP=192.168.1.1xx # xx = last two digits from lidar QR code serial number -ROBOT_IP= # IP addres of robot on local network (if using WebRTC connection) - -# Special Configuration for Unitree G1 EDU -# Special Configuration for Unitree G1 EDU -LIDAR_COMPUTER_IP=192.168.123.5 -LIDAR_GATEWAY=192.168.123.1 -LIDAR_IP=192.168.123.120 -ROBOT_IP=192.168.12.1 # For WebRTC local AP mode (optional, need additional wifi dongle) -``` - -### Start the Navigation Stack - -#### Start with Route Planner automatically - -```bash -# arise_slam (default) -./start.sh --hardware --route-planner -./start.sh --hardware --route-planner --rviz - -# FASTLIO2 -./start.sh --hardware --localization fastlio --route-planner -./start.sh --hardware --localization fastlio --route-planner --rviz - -# Jazzy image -./start.sh --hardware --image jazzy --route-planner - -# Development mode (mount src for config editing) -./start.sh --hardware --dev -``` - -[Foxglove Studio](https://foxglove.dev/download) is the default visualization tool. It's ideal for remote operation - SSH with port forwarding to the robot's mini PC and run commands there: - -```bash -ssh -L 8765:localhost:8765 user@robot-ip -``` - -Then on your local machine: -1. Open Foxglove and connect to `ws://localhost:8765` -2. Load the layout from `dimos/assets/foxglove_dashboards/Overwatch.json` (Layout menu → Import) -3. Click in the 3D panel to drop a target pose (similar to RViz). The "Autonomy ON" indicator should be green, and "Goal Reached" will show when the robot arrives. - -
-

Start manually

- -Start the container and leave it open. Use the same ROS distribution flag as your build: - -```bash -./start.sh --hardware --image humble # If built with --humble -# or -./start.sh --hardware --image jazzy # If built with --jazzy -``` - -It doesn't do anything by default. You have to run commands on it by `exec`-ing: - -To enter the container from another terminal: - -```bash -docker exec -it dimos_hardware_container bash -``` - -##### In the container - -In the container to run the full navigation stack you must run both the dimensional python runfile with connection module and the navigation stack. - -###### Dimensional Python + Connection Module - -For the Unitree G1 -```bash -dimos run unitree-g1 -ROBOT_IP=XX.X.X.XXX dimos run unitree-g1 # If ROBOT_IP env variable is not set in .env -``` - -###### Navigation Stack - -```bash -cd /ros2_ws/src/ros-navigation-autonomy-stack -./system_real_robot_with_route_planner.sh -``` - -Now you can place goal points/poses in RVIZ by clicking the "Goalpoint" button. The robot will navigate to the point, running both local and global planners for dynamic obstacle avoidance. - -
diff --git a/docker/navigation/build.sh b/docker/navigation/build.sh deleted file mode 100755 index 371db08b49..0000000000 --- a/docker/navigation/build.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/bin/bash - -set -e - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -# Default ROS distribution -ROS_DISTRO="humble" - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - --humble) - ROS_DISTRO="humble" - shift - ;; - --jazzy) - ROS_DISTRO="jazzy" - shift - ;; - --help|-h) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --humble Build with ROS 2 Humble (default)" - echo " --jazzy Build with ROS 2 Jazzy" - echo " --help, -h Show this help message" - echo "" - echo "The image includes both arise_slam and FASTLIO2." - echo "Select SLAM method at runtime via LOCALIZATION_METHOD env var." - echo "" - echo "Examples:" - echo " $0 # Build with ROS Humble" - echo " $0 --jazzy # Build with ROS Jazzy" - exit 0 - ;; - *) - echo -e "${RED}Unknown option: $1${NC}" - echo "Run '$0 --help' for usage information" - exit 1 - ;; - esac -done - -export ROS_DISTRO -export IMAGE_TAG="${ROS_DISTRO}" - -echo -e "${GREEN}================================================${NC}" -echo -e "${GREEN}Building DimOS + ROS Autonomy Stack Docker Image${NC}" -echo -e "${GREEN}ROS Distribution: ${ROS_DISTRO}${NC}" -echo -e "${GREEN}Image Tag: ${IMAGE_TAG}${NC}" -echo -e "${GREEN}SLAM: arise_slam + FASTLIO2 (both included)${NC}" -echo -e "${GREEN}================================================${NC}" -echo "" - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$SCRIPT_DIR" - -# Use fastlio2 branch which has both arise_slam and FASTLIO2 -TARGET_BRANCH="fastlio2" -TARGET_REMOTE="origin" -CLONE_URL="https://github.com/dimensionalOS/ros-navigation-autonomy-stack.git" - -# Clone or checkout ros-navigation-autonomy-stack -if [ ! -d "ros-navigation-autonomy-stack" ]; then - echo -e "${YELLOW}Cloning ros-navigation-autonomy-stack repository (${TARGET_BRANCH} branch)...${NC}" - git clone -b ${TARGET_BRANCH} ${CLONE_URL} ros-navigation-autonomy-stack - echo -e "${GREEN}Repository cloned successfully!${NC}" -else - # Directory exists, ensure we're on the correct branch - cd ros-navigation-autonomy-stack - - CURRENT_BRANCH=$(git branch --show-current) - if [ "$CURRENT_BRANCH" != "${TARGET_BRANCH}" ]; then - echo -e "${YELLOW}Switching from ${CURRENT_BRANCH} to ${TARGET_BRANCH} branch...${NC}" - # Stash any local changes (e.g., auto-generated config files) - if git stash --quiet 2>/dev/null; then - echo -e "${YELLOW}Stashed local changes${NC}" - fi - git fetch ${TARGET_REMOTE} ${TARGET_BRANCH} - git checkout -B ${TARGET_BRANCH} ${TARGET_REMOTE}/${TARGET_BRANCH} - echo -e "${GREEN}Switched to ${TARGET_BRANCH} branch${NC}" - else - echo -e "${GREEN}Already on ${TARGET_BRANCH} branch${NC}" - # Check for local changes before pulling latest - if ! git diff --quiet || ! git diff --cached --quiet; then - echo -e "${RED}Local changes detected in ros-navigation-autonomy-stack.${NC}" - echo -e "${RED}Please commit or discard them before building.${NC}" - git status --short - exit 1 - fi - git fetch ${TARGET_REMOTE} ${TARGET_BRANCH} - git reset --hard ${TARGET_REMOTE}/${TARGET_BRANCH} - fi - cd .. -fi - -if [ ! -d "unity_models" ]; then - echo -e "${YELLOW}Using office_building_1 as the Unity environment...${NC}" - tar -xf ../../data/.lfs/office_building_1.tar.gz - mv office_building_1 unity_models -fi - -echo "" -echo -e "${YELLOW}Building Docker image with docker compose...${NC}" -echo "This will take a while as it needs to:" -echo " - Download base ROS ${ROS_DISTRO^} image" -echo " - Install ROS packages and dependencies" -echo " - Build the autonomy stack (arise_slam + FASTLIO2)" -echo " - Build Livox-SDK2 for Mid-360 lidar" -echo " - Build SLAM dependencies (Sophus, Ceres, GTSAM)" -echo " - Install Python dependencies for DimOS" -echo "" - -cd ../.. - -docker compose -f docker/navigation/docker-compose.yml build - -echo "" -echo -e "${GREEN}============================================${NC}" -echo -e "${GREEN}Docker image built successfully!${NC}" -echo -e "${GREEN}Image: dimos_autonomy_stack:${IMAGE_TAG}${NC}" -echo -e "${GREEN}SLAM: arise_slam + FASTLIO2 (both included)${NC}" -echo -e "${GREEN}============================================${NC}" -echo "" -echo "To run in SIMULATION mode:" -echo -e "${YELLOW} ./start.sh --simulation --${ROS_DISTRO}${NC}" -echo "" -echo "To run in HARDWARE mode:" -echo " 1. Configure your hardware settings in .env file" -echo " (copy from .env.hardware if needed)" -echo " 2. Run the hardware container:" -echo -e "${YELLOW} ./start.sh --hardware --${ROS_DISTRO}${NC}" -echo "" -echo "To use FASTLIO2 instead of arise_slam, set LOCALIZATION_METHOD:" -echo -e "${YELLOW} LOCALIZATION_METHOD=fastlio ./start.sh --hardware --${ROS_DISTRO}${NC}" -echo "" diff --git a/docker/navigation/docker-compose.dev.yml b/docker/navigation/docker-compose.dev.yml deleted file mode 100644 index defbdae846..0000000000 --- a/docker/navigation/docker-compose.dev.yml +++ /dev/null @@ -1,23 +0,0 @@ -# ============================================================================= -# DEVELOPMENT OVERRIDES - Mount source for live editing -# ============================================================================= -# -# Usage: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -# -# This file adds development-specific volume mounts for editing ROS configs -# without rebuilding the image. -# -# ============================================================================= - -services: - dimos_simulation: - volumes: - # Mount ROS navigation stack source for config editing - - ./ros-navigation-autonomy-stack/src/slam/arise_slam_mid360/config:/ros2_ws/src/ros-navigation-autonomy-stack/src/slam/arise_slam_mid360/config:rw - - ./ros-navigation-autonomy-stack/src/base_autonomy/local_planner/config:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/local_planner/config:rw - - dimos_hardware: - volumes: - # Mount ROS navigation stack source for config editing - - ./ros-navigation-autonomy-stack/src/slam/arise_slam_mid360/config:/ros2_ws/src/ros-navigation-autonomy-stack/src/slam/arise_slam_mid360/config:rw - - ./ros-navigation-autonomy-stack/src/base_autonomy/local_planner/config:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/local_planner/config:rw diff --git a/docker/navigation/docker-compose.yml b/docker/navigation/docker-compose.yml deleted file mode 100644 index 6546968757..0000000000 --- a/docker/navigation/docker-compose.yml +++ /dev/null @@ -1,353 +0,0 @@ -services: - # Simulation profile - dimos_simulation: - build: - context: ../.. - dockerfile: docker/navigation/Dockerfile - network: host - args: - ROS_DISTRO: ${ROS_DISTRO:-humble} - image: dimos_autonomy_stack:${IMAGE_TAG:-humble} - container_name: dimos_simulation_container - profiles: ["", "simulation"] # Active by default (empty profile) AND with --profile simulation - - # Shared memory size for ROS 2 FastDDS - shm_size: '8gb' - - # Enable interactive terminal - stdin_open: true - tty: true - - # Network configuration - required for ROS communication - network_mode: host - - # Allow `ip link set ...` (needed by DimOS LCM autoconf) without requiring sudo - cap_add: - - NET_ADMIN - - # Use nvidia runtime for GPU acceleration (falls back to runc if not available) - runtime: ${DOCKER_RUNTIME:-nvidia} - - # Environment variables for display and ROS - environment: - - DISPLAY=${DISPLAY} - - QT_X11_NO_MITSHM=1 - - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all} - - NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES:-all} - - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} - - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} - - ROBOT_IP=${ROBOT_IP:-} - - HARDWARE_MODE=false - # DDS Configuration (FastDDS) - - RMW_IMPLEMENTATION=rmw_fastrtps_cpp - - FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml - # Localization method: arise_slam (default) or fastlio - - LOCALIZATION_METHOD=${LOCALIZATION_METHOD:-arise_slam} - - # Volume mounts - volumes: - # X11 socket for GUI - - /tmp/.X11-unix:/tmp/.X11-unix:rw - - ${HOME}/.Xauthority:/root/.Xauthority:rw - - # Mount Unity environment models (if available) - - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw - - # Mount entire dimos directory for live development - - ../..:/workspace/dimos:rw - - # Mount bagfiles directory - - ./bagfiles:/ros2_ws/bagfiles:rw - - # Mount config files for easy editing - - ./config:/ros2_ws/config:rw - - # Device access (for joystick controllers) - devices: - - /dev/input:/dev/input - - /dev/dri:/dev/dri - - # Working directory - working_dir: /workspace/dimos - - # Command to run both ROS and DimOS - command: /usr/local/bin/run_both.sh - - # Hardware profile - for real robot - dimos_hardware: - build: - context: ../.. - dockerfile: docker/navigation/Dockerfile - network: host - args: - ROS_DISTRO: ${ROS_DISTRO:-humble} - image: dimos_autonomy_stack:${IMAGE_TAG:-humble} - container_name: dimos_hardware_container - profiles: ["hardware"] - - # Shared memory size for ROS 2 FastDDS - shm_size: '8gb' - - # Load environment from .env file - env_file: - - .env - - # Enable interactive terminal - stdin_open: true - tty: true - - # Network configuration - MUST be host for hardware access - network_mode: host - - # Privileged mode REQUIRED for hardware access - privileged: true - - # Override runtime for GPU support - runtime: ${DOCKER_RUNTIME:-runc} - - # Add host groups for device access (input for joystick, dialout for serial) - group_add: - - ${INPUT_GID:-995} - - ${DIALOUT_GID:-20} - - # Hardware environment variables - environment: - - DISPLAY=${DISPLAY:-:0} - - QT_X11_NO_MITSHM=1 - - NVIDIA_VISIBLE_DEVICES=all - - NVIDIA_DRIVER_CAPABILITIES=all - - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} - - ROBOT_CONFIG_PATH=${ROBOT_CONFIG_PATH:-mechanum_drive} - - ROBOT_IP=${ROBOT_IP:-} - - HARDWARE_MODE=true - # DDS Configuration (FastDDS) - - RMW_IMPLEMENTATION=rmw_fastrtps_cpp - - FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml - # Localization method: arise_slam (default) or fastlio - - LOCALIZATION_METHOD=${LOCALIZATION_METHOD:-arise_slam} - # Mid-360 Lidar configuration - - LIDAR_INTERFACE=${LIDAR_INTERFACE:-} - - LIDAR_COMPUTER_IP=${LIDAR_COMPUTER_IP:-192.168.1.5} - - LIDAR_GATEWAY=${LIDAR_GATEWAY:-192.168.1.1} - - LIDAR_IP=${LIDAR_IP:-192.168.1.116} - # Motor controller - - MOTOR_SERIAL_DEVICE=${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} - # Network optimization - - ENABLE_WIFI_BUFFER=true - # Route planner option - - USE_ROUTE_PLANNER=${USE_ROUTE_PLANNER:-false} - # RViz option - - USE_RVIZ=${USE_RVIZ:-false} - # Unitree robot configuration - - UNITREE_IP=${UNITREE_IP:-192.168.12.1} - - UNITREE_CONN=${UNITREE_CONN:-LocalAP} - # Map path for localization mode (e.g., /ros2_ws/maps/warehouse) - - MAP_PATH=${MAP_PATH:-} - - # Volume mounts - volumes: - # X11 socket for GUI - - /tmp/.X11-unix:/tmp/.X11-unix:rw - - ${HOME}/.Xauthority:/root/.Xauthority:rw - # Mount Unity environment models (optional for hardware) - - ./unity_models:/ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/mesh/unity:rw - # Mount entire dimos directory - - ../..:/workspace/dimos:rw - # Mount bagfiles directory - - ./bagfiles:/ros2_ws/bagfiles:rw - # Mount config files for easy editing - - ./config:/ros2_ws/config:rw - # Mount maps directory for localization - - ./maps:/ros2_ws/maps:rw - # Hardware-specific volumes - - ./logs:/ros2_ws/logs:rw - - /etc/localtime:/etc/localtime:ro - - /etc/timezone:/etc/timezone:ro - - /dev/bus/usb:/dev/bus/usb:rw - - /sys:/sys:ro - - # Device access for hardware - devices: - # Joystick controller (specific device to avoid permission issues) - - /dev/input/js0:/dev/input/js0 - # GPU access - - /dev/dri:/dev/dri - # Motor controller serial ports - - ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}:${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0} - # Additional serial ports (can be enabled via environment) - # - /dev/ttyUSB0:/dev/ttyUSB0 - # - /dev/ttyUSB1:/dev/ttyUSB1 - # Cameras (can be enabled via environment) - # - /dev/video0:/dev/video0 - - # Working directory - working_dir: /workspace/dimos - - # Command - launch the real robot system with foxglove_bridge - command: - - bash - - -c - - | - echo "Checking joystick..." - ls -la /dev/input/js0 2>/dev/null || echo "Warning: No joystick found at /dev/input/js0" - cd /ros2_ws - source install/setup.bash - source /opt/dimos-venv/bin/activate - # Launch with SLAM method based on LOCALIZATION_METHOD - if [ "$LOCALIZATION_METHOD" = "fastlio" ]; then - echo "Using FASTLIO2 localization" - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Starting real robot system WITH route planner..." - ros2 launch vehicle_simulator system_real_robot_with_route_planner.launch.py use_fastlio2:=true & - else - echo "Starting real robot system (base autonomy)..." - ros2 launch vehicle_simulator system_real_robot.launch.py use_fastlio2:=true & - fi - else - echo "Using arise_slam localization" - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Starting real robot system WITH route planner..." - ros2 launch vehicle_simulator system_real_robot_with_route_planner.launch.py & - else - echo "Starting real robot system (base autonomy)..." - ros2 launch vehicle_simulator system_real_robot.launch.py & - fi - fi - sleep 2 - if [ "$USE_RVIZ" = "true" ]; then - echo "Starting RViz2..." - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - ros2 run rviz2 rviz2 -d /ros2_ws/src/ros-navigation-autonomy-stack/src/route_planner/far_planner/rviz/default.rviz & - else - ros2 run rviz2 rviz2 -d /ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz & - fi - fi - # Launch Unitree control if ROBOT_CONFIG_PATH contains "unitree" - if [[ "$ROBOT_CONFIG_PATH" == *"unitree"* ]]; then - echo "Starting Unitree WebRTC control (IP: $UNITREE_IP, Method: $UNITREE_CONN)..." - ros2 launch unitree_webrtc_ros unitree_control.launch.py robot_ip:=$UNITREE_IP connection_method:=$UNITREE_CONN & - fi - # Start twist relay for Foxglove Teleop (converts Twist -> TwistStamped) - echo "Starting Twist relay for Foxglove Teleop..." - python3 /usr/local/bin/twist_relay.py & - # Start goal autonomy relay (publishes Joy to enable autonomy when goal_pose received) - echo "Starting Goal Autonomy relay for Foxglove..." - python3 /usr/local/bin/goal_autonomy_relay.py & - echo "Starting Foxglove Bridge on port 8765..." - echo "Connect via Foxglove Studio: ws://$(hostname -I | awk '{print $1}'):8765" - ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765 - - # Capabilities for hardware operations - cap_add: - - NET_ADMIN # Network interface configuration - - SYS_ADMIN # System operations - - SYS_TIME # Time synchronization - - # Bagfile profile - for bagfile playback with use_sim_time=true - dimos_bagfile: - build: - context: ../.. - dockerfile: docker/navigation/Dockerfile - network: host - args: - ROS_DISTRO: ${ROS_DISTRO:-humble} - image: dimos_autonomy_stack:${IMAGE_TAG:-humble} - container_name: dimos_bagfile_container - profiles: ["bagfile"] - - # Shared memory size for ROS 2 FastDDS - shm_size: '8gb' - - # Enable interactive terminal - stdin_open: true - tty: true - - # Network configuration - network_mode: host - - # Use nvidia runtime for GPU acceleration (falls back to runc if not available) - runtime: ${DOCKER_RUNTIME:-nvidia} - - # Environment variables - environment: - - DISPLAY=${DISPLAY} - - QT_X11_NO_MITSHM=1 - - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all} - - NVIDIA_DRIVER_CAPABILITIES=${NVIDIA_DRIVER_CAPABILITIES:-all} - - ROS_DOMAIN_ID=${ROS_DOMAIN_ID:-42} - # DDS Configuration (FastDDS) - - RMW_IMPLEMENTATION=rmw_fastrtps_cpp - - FASTRTPS_DEFAULT_PROFILES_FILE=/ros2_ws/config/fastdds.xml - # Localization method: arise_slam (default) or fastlio - - LOCALIZATION_METHOD=${LOCALIZATION_METHOD:-arise_slam} - # Route planner option - - USE_ROUTE_PLANNER=${USE_ROUTE_PLANNER:-false} - # RViz option - - USE_RVIZ=${USE_RVIZ:-false} - # Map path for localization mode (e.g., /ros2_ws/maps/warehouse) - - MAP_PATH=${MAP_PATH:-} - - # Volume mounts - volumes: - # X11 socket for GUI - - /tmp/.X11-unix:/tmp/.X11-unix:rw - - ${HOME}/.Xauthority:/root/.Xauthority:rw - # Mount bagfiles directory - - ./bagfiles:/ros2_ws/bagfiles:rw - # Mount config files for easy editing - - ./config:/ros2_ws/config:rw - # Mount maps directory for localization - - ./maps:/ros2_ws/maps:rw - - # Device access (for joystick controllers) - devices: - - /dev/input:/dev/input - - /dev/dri:/dev/dri - - # Working directory - working_dir: /ros2_ws - - # Command - launch bagfile system (use_sim_time=true by default in launch files) - command: - - bash - - -c - - | - source install/setup.bash - echo "Bagfile playback mode (use_sim_time=true)" - echo "" - echo "Launch files ready. Play your bagfile with:" - echo " ros2 bag play --clock /ros2_ws/bagfiles/" - echo "" - # Launch with SLAM method based on LOCALIZATION_METHOD - if [ "$LOCALIZATION_METHOD" = "fastlio" ]; then - echo "Using FASTLIO2 localization" - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Starting bagfile system WITH route planner..." - ros2 launch vehicle_simulator system_bagfile_with_route_planner.launch.py use_fastlio2:=true & - else - echo "Starting bagfile system (base autonomy)..." - ros2 launch vehicle_simulator system_bagfile.launch.py use_fastlio2:=true & - fi - else - echo "Using arise_slam localization" - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Starting bagfile system WITH route planner..." - ros2 launch vehicle_simulator system_bagfile_with_route_planner.launch.py & - else - echo "Starting bagfile system (base autonomy)..." - ros2 launch vehicle_simulator system_bagfile.launch.py & - fi - fi - sleep 2 - if [ "$USE_RVIZ" = "true" ]; then - echo "Starting RViz2..." - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - ros2 run rviz2 rviz2 -d /ros2_ws/src/ros-navigation-autonomy-stack/src/route_planner/far_planner/rviz/default.rviz & - else - ros2 run rviz2 rviz2 -d /ros2_ws/src/ros-navigation-autonomy-stack/src/base_autonomy/vehicle_simulator/rviz/vehicle_simulator.rviz & - fi - fi - # Keep container running - echo "" - echo "Container ready. Waiting for bagfile playback..." - wait diff --git a/docker/navigation/foxglove_utility/goal_autonomy_relay.py b/docker/navigation/foxglove_utility/goal_autonomy_relay.py deleted file mode 100755 index 44ed59008c..0000000000 --- a/docker/navigation/foxglove_utility/goal_autonomy_relay.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Relay node that publishes Joy message to enable autonomy mode when goal_pose is received. -Mimics the behavior of the goalpoint_rviz_plugin for Foxglove compatibility. -""" - -from geometry_msgs.msg import PointStamped, PoseStamped -import rclpy -from rclpy.node import Node -from rclpy.qos import DurabilityPolicy, HistoryPolicy, QoSProfile, ReliabilityPolicy -from sensor_msgs.msg import Joy - - -class GoalAutonomyRelay(Node): - def __init__(self): - super().__init__("goal_autonomy_relay") - - # QoS for goal topics (match foxglove_bridge) - goal_qos = QoSProfile( - reliability=ReliabilityPolicy.RELIABLE, - history=HistoryPolicy.KEEP_LAST, - durability=DurabilityPolicy.VOLATILE, - depth=5, - ) - - # Subscribe to goal_pose (PoseStamped from Foxglove) - self.pose_sub = self.create_subscription( - PoseStamped, "/goal_pose", self.goal_pose_callback, goal_qos - ) - - # Subscribe to way_point (PointStamped from Foxglove) - self.point_sub = self.create_subscription( - PointStamped, "/way_point", self.way_point_callback, goal_qos - ) - - # Publisher for Joy message to enable autonomy - self.joy_pub = self.create_publisher(Joy, "/joy", 5) - - self.get_logger().info( - "Goal autonomy relay started - will publish Joy to enable autonomy when goals are received" - ) - - def publish_autonomy_joy(self): - """Publish Joy message that enables autonomy mode (mimics goalpoint_rviz_plugin)""" - joy = Joy() - joy.header.stamp = self.get_clock().now().to_msg() - joy.header.frame_id = "goal_autonomy_relay" - - # axes[2] = -1.0 enables autonomy mode in pathFollower - # axes[4] = 1.0 sets forward speed - # axes[5] = 1.0 enables obstacle checking - joy.axes = [0.0, 0.0, -1.0, 0.0, 1.0, 1.0, 0.0, 0.0] - - # buttons[7] = 1 (same as RViz plugin) - joy.buttons = [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] - - self.joy_pub.publish(joy) - self.get_logger().info("Published Joy message to enable autonomy mode") - - def goal_pose_callback(self, msg: PoseStamped): - self.get_logger().info( - f"Received goal_pose at ({msg.pose.position.x:.2f}, {msg.pose.position.y:.2f})" - ) - self.publish_autonomy_joy() - - def way_point_callback(self, msg: PointStamped): - self.get_logger().info(f"Received way_point at ({msg.point.x:.2f}, {msg.point.y:.2f})") - self.publish_autonomy_joy() - - -def main(args=None): - rclpy.init(args=args) - node = GoalAutonomyRelay() - try: - rclpy.spin(node) - except KeyboardInterrupt: - pass - finally: - node.destroy_node() - rclpy.shutdown() - - -if __name__ == "__main__": - main() diff --git a/docker/navigation/foxglove_utility/twist_relay.py b/docker/navigation/foxglove_utility/twist_relay.py deleted file mode 100644 index 6e72d5104b..0000000000 --- a/docker/navigation/foxglove_utility/twist_relay.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Simple relay node that converts geometry_msgs/Twist to geometry_msgs/TwistStamped. -Used for Foxglove Teleop panel which only publishes Twist. -""" - -from geometry_msgs.msg import Twist, TwistStamped -import rclpy -from rclpy.node import Node -from rclpy.qos import HistoryPolicy, QoSProfile, ReliabilityPolicy - - -class TwistRelay(Node): - def __init__(self): - super().__init__("twist_relay") - - # Declare parameters - self.declare_parameter("input_topic", "/foxglove_teleop") - self.declare_parameter("output_topic", "/cmd_vel") - self.declare_parameter("frame_id", "vehicle") - - input_topic = self.get_parameter("input_topic").value - output_topic = self.get_parameter("output_topic").value - self.frame_id = self.get_parameter("frame_id").value - - # QoS for real-time control - qos = QoSProfile( - reliability=ReliabilityPolicy.BEST_EFFORT, history=HistoryPolicy.KEEP_LAST, depth=1 - ) - - # Subscribe to Twist (from Foxglove Teleop) - self.subscription = self.create_subscription(Twist, input_topic, self.twist_callback, qos) - - # Publish TwistStamped - self.publisher = self.create_publisher(TwistStamped, output_topic, qos) - - self.get_logger().info( - f"Twist relay: {input_topic} (Twist) -> {output_topic} (TwistStamped)" - ) - - def twist_callback(self, msg: Twist): - stamped = TwistStamped() - stamped.header.stamp = self.get_clock().now().to_msg() - stamped.header.frame_id = self.frame_id - stamped.twist = msg - self.publisher.publish(stamped) - - -def main(args=None): - rclpy.init(args=args) - node = TwistRelay() - try: - rclpy.spin(node) - except KeyboardInterrupt: - pass - finally: - node.destroy_node() - rclpy.shutdown() - - -if __name__ == "__main__": - main() diff --git a/docker/navigation/ros_launch_wrapper.py b/docker/navigation/ros_launch_wrapper.py deleted file mode 100755 index dc28eabe72..0000000000 --- a/docker/navigation/ros_launch_wrapper.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -Wrapper script to properly handle ROS2 launch file shutdown. -This script ensures clean shutdown of all ROS nodes when receiving SIGINT. -""" - -import os -import signal -import subprocess -import sys -import time - - -class ROSLaunchWrapper: - def __init__(self): - self.ros_process = None - self.dimos_process = None - self.shutdown_in_progress = False - - def signal_handler(self, _signum, _frame): - """Handle shutdown signals gracefully""" - if self.shutdown_in_progress: - return - - self.shutdown_in_progress = True - print("\n\nShutdown signal received. Stopping services gracefully...") - - # Stop DimOS first - if self.dimos_process and self.dimos_process.poll() is None: - print("Stopping DimOS...") - self.dimos_process.terminate() - try: - self.dimos_process.wait(timeout=5) - print("DimOS stopped cleanly.") - except subprocess.TimeoutExpired: - print("Force stopping DimOS...") - self.dimos_process.kill() - self.dimos_process.wait() - - # Stop ROS - send SIGINT first for graceful shutdown - if self.ros_process and self.ros_process.poll() is None: - print("Stopping ROS nodes (this may take a moment)...") - - # Send SIGINT to trigger graceful ROS shutdown - self.ros_process.send_signal(signal.SIGINT) - - # Wait for graceful shutdown with timeout - try: - self.ros_process.wait(timeout=15) - print("ROS stopped cleanly.") - except subprocess.TimeoutExpired: - print("ROS is taking too long to stop. Sending SIGTERM...") - self.ros_process.terminate() - try: - self.ros_process.wait(timeout=5) - except subprocess.TimeoutExpired: - print("Force stopping ROS...") - self.ros_process.kill() - self.ros_process.wait() - - # Clean up any remaining processes - print("Cleaning up any remaining processes...") - cleanup_commands = [ - "pkill -f 'ros2' || true", - "pkill -f 'localPlanner' || true", - "pkill -f 'pathFollower' || true", - "pkill -f 'terrainAnalysis' || true", - "pkill -f 'sensorScanGeneration' || true", - "pkill -f 'vehicleSimulator' || true", - "pkill -f 'visualizationTools' || true", - "pkill -f 'far_planner' || true", - "pkill -f 'graph_decoder' || true", - ] - - for cmd in cleanup_commands: - subprocess.run(cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - print("All services stopped.") - sys.exit(0) - - def run(self): - # Register signal handlers - signal.signal(signal.SIGINT, self.signal_handler) - signal.signal(signal.SIGTERM, self.signal_handler) - - print("Starting ROS route planner and DimOS...") - - # Change to the ROS workspace directory - os.chdir("/ros2_ws/src/ros-navigation-autonomy-stack") - - # Start ROS route planner - print("Starting ROS route planner...") - self.ros_process = subprocess.Popen( - ["bash", "./system_simulation_with_route_planner.sh"], - preexec_fn=os.setsid, # Create new process group - ) - - print("Waiting for ROS to initialize...") - time.sleep(5) - - print("Starting DimOS navigation bot...") - - nav_bot_path = "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" - venv_python = "/opt/dimos-venv/bin/python" - - if not os.path.exists(nav_bot_path): - print(f"ERROR: demo_ros_navigation.py not found at {nav_bot_path}") - nav_dir = "/workspace/dimos/dimos/navigation/" - if os.path.exists(nav_dir): - print(f"Contents of {nav_dir}:") - for item in os.listdir(nav_dir): - print(f" - {item}") - else: - print(f"Directory not found: {nav_dir}") - return - - if not os.path.exists(venv_python): - print(f"ERROR: venv Python not found at {venv_python}, using system Python") - return - - print(f"Using Python: {venv_python}") - print(f"Starting script: {nav_bot_path}") - - # Use the venv Python explicitly - try: - self.dimos_process = subprocess.Popen( - [venv_python, nav_bot_path], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - bufsize=1, - universal_newlines=True, - ) - - # Give it a moment to start and check if it's still running - time.sleep(2) - poll_result = self.dimos_process.poll() - if poll_result is not None: - # Process exited immediately - stdout, stderr = self.dimos_process.communicate(timeout=1) - print(f"ERROR: DimOS failed to start (exit code: {poll_result})") - if stdout: - print(f"STDOUT: {stdout}") - if stderr: - print(f"STDERR: {stderr}") - self.dimos_process = None - else: - print(f"DimOS started successfully (PID: {self.dimos_process.pid})") - - except Exception as e: - print(f"ERROR: Failed to start DimOS: {e}") - self.dimos_process = None - - if self.dimos_process: - print("Both systems are running. Press Ctrl+C to stop.") - else: - print("ROS is running (DimOS failed to start). Press Ctrl+C to stop.") - print("") - - # Wait for processes - try: - # Monitor both processes - while True: - # Check if either process has died - if self.ros_process.poll() is not None: - print("ROS process has stopped unexpectedly.") - self.signal_handler(signal.SIGTERM, None) - break - if self.dimos_process and self.dimos_process.poll() is not None: - print("DimOS process has stopped.") - # DimOS stopping is less critical, but we should still clean up ROS - self.signal_handler(signal.SIGTERM, None) - break - time.sleep(1) - except KeyboardInterrupt: - pass # Signal handler will take care of cleanup - - -if __name__ == "__main__": - wrapper = ROSLaunchWrapper() - wrapper.run() diff --git a/docker/navigation/run_both.sh b/docker/navigation/run_both.sh deleted file mode 100755 index 022ccd607c..0000000000 --- a/docker/navigation/run_both.sh +++ /dev/null @@ -1,202 +0,0 @@ -#!/bin/bash -# Script to run both ROS route planner and DimOS together - -echo "Starting ROS route planner and DimOS..." - -# Variables for process IDs -ROS_PID="" -DIMOS_PID="" -RVIZ_PID="" -UNITY_PID="" -SHUTDOWN_IN_PROGRESS=false - -# Function to handle cleanup -cleanup() { - if [ "$SHUTDOWN_IN_PROGRESS" = true ]; then - return - fi - SHUTDOWN_IN_PROGRESS=true - - echo "" - echo "Shutdown initiated. Stopping services..." - - # First, stop RViz - if [ -n "$RVIZ_PID" ] && kill -0 $RVIZ_PID 2>/dev/null; then - echo "Stopping RViz..." - kill -TERM $RVIZ_PID 2>/dev/null || true - sleep 1 - if kill -0 $RVIZ_PID 2>/dev/null; then - kill -9 $RVIZ_PID 2>/dev/null || true - fi - fi - - # Stop Unity simulator - if [ -n "$UNITY_PID" ] && kill -0 $UNITY_PID 2>/dev/null; then - echo "Stopping Unity simulator..." - kill -TERM $UNITY_PID 2>/dev/null || true - sleep 1 - if kill -0 $UNITY_PID 2>/dev/null; then - kill -9 $UNITY_PID 2>/dev/null || true - fi - fi - - # Then, try to gracefully stop DimOS - if [ -n "$DIMOS_PID" ] && kill -0 $DIMOS_PID 2>/dev/null; then - echo "Stopping DimOS..." - kill -TERM $DIMOS_PID 2>/dev/null || true - - # Wait up to 5 seconds for DimOS to stop - for i in {1..10}; do - if ! kill -0 $DIMOS_PID 2>/dev/null; then - echo "DimOS stopped cleanly." - break - fi - sleep 0.5 - done - - # Force kill if still running - if kill -0 $DIMOS_PID 2>/dev/null; then - echo "Force stopping DimOS..." - kill -9 $DIMOS_PID 2>/dev/null || true - fi - fi - - # Then handle ROS - send SIGINT to the launch process group - if [ -n "$ROS_PID" ] && kill -0 $ROS_PID 2>/dev/null; then - echo "Stopping ROS nodes (this may take a moment)..." - - # Send SIGINT to the process group to properly trigger ROS shutdown - kill -INT -$ROS_PID 2>/dev/null || kill -INT $ROS_PID 2>/dev/null || true - - # Wait up to 15 seconds for graceful shutdown - for i in {1..30}; do - if ! kill -0 $ROS_PID 2>/dev/null; then - echo "ROS stopped cleanly." - break - fi - sleep 0.5 - done - - # If still running, send SIGTERM - if kill -0 $ROS_PID 2>/dev/null; then - echo "Sending SIGTERM to ROS..." - kill -TERM -$ROS_PID 2>/dev/null || kill -TERM $ROS_PID 2>/dev/null || true - sleep 2 - fi - - # Final resort: SIGKILL - if kill -0 $ROS_PID 2>/dev/null; then - echo "Force stopping ROS..." - kill -9 -$ROS_PID 2>/dev/null || kill -9 $ROS_PID 2>/dev/null || true - fi - fi - - # Clean up any remaining ROS2 processes - echo "Cleaning up any remaining processes..." - pkill -f "rviz2" 2>/dev/null || true - pkill -f "Model.x86_64" 2>/dev/null || true - pkill -f "ros2" 2>/dev/null || true - pkill -f "localPlanner" 2>/dev/null || true - pkill -f "pathFollower" 2>/dev/null || true - pkill -f "terrainAnalysis" 2>/dev/null || true - pkill -f "sensorScanGeneration" 2>/dev/null || true - pkill -f "vehicleSimulator" 2>/dev/null || true - pkill -f "visualizationTools" 2>/dev/null || true - pkill -f "far_planner" 2>/dev/null || true - pkill -f "graph_decoder" 2>/dev/null || true - - echo "All services stopped." -} - -# Set up trap to call cleanup on exit -trap cleanup EXIT INT TERM - -# Source ROS environment -echo "Sourcing ROS environment..." -source /opt/ros/${ROS_DISTRO:-humble}/setup.bash -source /ros2_ws/install/setup.bash - -# Start ROS route planner in background (in new process group) -echo "Starting ROS route planner..." -cd /ros2_ws/src/ros-navigation-autonomy-stack - -# Run Unity simulation if available -UNITY_EXECUTABLE="./src/base_autonomy/vehicle_simulator/mesh/unity/environment/Model.x86_64" -if [ -f "$UNITY_EXECUTABLE" ]; then - echo "Starting Unity simulation environment..." - "$UNITY_EXECUTABLE" & - UNITY_PID=$! -else - echo "Warning: Unity environment not found at $UNITY_EXECUTABLE" - echo "Continuing without Unity simulation (you may need to provide sensor data)" - UNITY_PID="" -fi -sleep 3 -setsid bash -c 'ros2 launch vehicle_simulator system_simulation_with_route_planner.launch.py' & -ROS_PID=$! -ros2 run rviz2 rviz2 -d src/route_planner/far_planner/rviz/default.rviz & -RVIZ_PID=$! - -# Wait a bit for ROS to initialize -echo "Waiting for ROS to initialize..." -sleep 5 - -# Start DimOS -echo "Starting DimOS navigation bot..." - -# Check if the script exists -if [ ! -f "/workspace/dimos/dimos/navigation/demo_ros_navigation.py" ]; then - echo "ERROR: demo_ros_navigation.py not found at /workspace/dimos/dimos/navigation/demo_ros_navigation.py" - echo "Available files in /workspace/dimos/dimos/navigation/:" - ls -la /workspace/dimos/dimos/navigation/ 2>/dev/null || echo "Directory not found" -else - echo "Found demo_ros_navigation.py, activating virtual environment..." - if [ -f "/opt/dimos-venv/bin/activate" ]; then - source /opt/dimos-venv/bin/activate - echo "Python path: $(which python)" - echo "Python version: $(python --version)" - - # Install dimos package if not already installed - if ! python -c "import dimos" 2>/dev/null; then - echo "Installing dimos package..." - if [ -f "/workspace/dimos/setup.py" ] || [ -f "/workspace/dimos/pyproject.toml" ]; then - # Install Unitree extra (includes agents stack + unitree deps used by demo) - pip install -e "/workspace/dimos[unitree]" --quiet - else - echo "WARNING: dimos package not found at /workspace/dimos" - fi - fi - else - echo "WARNING: Virtual environment not found at /opt/dimos-venv, using system Python" - fi - - echo "Starting demo_ros_navigation.py..." - # Capture any startup errors - python /workspace/dimos/dimos/navigation/demo_ros_navigation.py 2>&1 & - DIMOS_PID=$! - - # Give it a moment to start and check if it's still running - sleep 2 - if kill -0 $DIMOS_PID 2>/dev/null; then - echo "DimOS started successfully with PID: $DIMOS_PID" - else - echo "ERROR: DimOS failed to start (process exited immediately)" - echo "Check the logs above for error messages" - DIMOS_PID="" - fi -fi - -echo "" -if [ -n "$DIMOS_PID" ]; then - echo "Both systems are running. Press Ctrl+C to stop." -else - echo "ROS is running (DimOS failed to start). Press Ctrl+C to stop." -fi -echo "" - -# Wait for processes -if [ -n "$DIMOS_PID" ]; then - wait $ROS_PID $DIMOS_PID 2>/dev/null || true -else - wait $ROS_PID 2>/dev/null || true -fi diff --git a/docker/navigation/start.sh b/docker/navigation/start.sh deleted file mode 100755 index be45908a33..0000000000 --- a/docker/navigation/start.sh +++ /dev/null @@ -1,389 +0,0 @@ -#!/bin/bash - -set -e - -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' - -# Parse command line arguments -MODE="simulation" -USE_ROUTE_PLANNER="false" -USE_RVIZ="false" -DEV_MODE="false" -ROS_DISTRO="humble" -LOCALIZATION_METHOD="${LOCALIZATION_METHOD:-arise_slam}" -while [[ $# -gt 0 ]]; do - case $1 in - --hardware) - MODE="hardware" - shift - ;; - --simulation) - MODE="simulation" - shift - ;; - --bagfile) - MODE="bagfile" - shift - ;; - --route-planner) - USE_ROUTE_PLANNER="true" - shift - ;; - --rviz) - USE_RVIZ="true" - shift - ;; - --dev) - DEV_MODE="true" - shift - ;; - --image) - if [ -z "$2" ] || [[ "$2" == --* ]]; then - echo -e "${RED}--image requires a value (humble or jazzy)${NC}" - exit 1 - fi - ROS_DISTRO="$2" - shift 2 - ;; - --localization) - if [ -z "$2" ] || [[ "$2" == --* ]]; then - echo -e "${RED}--localization requires a value (arise_slam or fastlio)${NC}" - exit 1 - fi - LOCALIZATION_METHOD="$2" - shift 2 - ;; - --help|-h) - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Mode (mutually exclusive):" - echo " --simulation Start simulation container (default)" - echo " --hardware Start hardware container" - echo " --bagfile Start bagfile playback container (use_sim_time=true)" - echo "" - echo "Image and localization:" - echo " --image ROS 2 distribution: humble (default), jazzy" - echo " --localization SLAM method: arise_slam (default), fastlio" - echo "" - echo "Additional options:" - echo " --route-planner Enable FAR route planner (for hardware mode)" - echo " --rviz Launch RViz2 visualization" - echo " --dev Development mode (mount src for config editing)" - echo " --help, -h Show this help message" - echo "" - echo "Examples:" - echo " $0 --simulation # Start simulation" - echo " $0 --hardware --image jazzy # Hardware with Jazzy" - echo " $0 --hardware --localization fastlio # Hardware with FASTLIO2" - echo " $0 --hardware --route-planner --rviz # Hardware with route planner + RViz" - echo " $0 --hardware --dev # Hardware with src mounted" - echo " $0 --bagfile # Bagfile playback" - echo " $0 --bagfile --localization fastlio --route-planner # Bagfile with FASTLIO2 + route planner" - echo "" - echo "Press Ctrl+C to stop the container" - exit 0 - ;; - *) - echo -e "${RED}Unknown option: $1${NC}" - echo "Run '$0 --help' for usage information" - exit 1 - ;; - esac -done - -export ROS_DISTRO -export LOCALIZATION_METHOD -export IMAGE_TAG="${ROS_DISTRO}" - -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$SCRIPT_DIR" - -echo -e "${GREEN}================================================${NC}" -echo -e "${GREEN}Starting DimOS Docker Container${NC}" -echo -e "${GREEN}Mode: ${MODE}${NC}" -echo -e "${GREEN}ROS Distribution: ${ROS_DISTRO}${NC}" -echo -e "${GREEN}ROS Domain ID: ${ROS_DOMAIN_ID:-42}${NC}" -echo -e "${GREEN}Localization: ${LOCALIZATION_METHOD}${NC}" -echo -e "${GREEN}Image Tag: ${IMAGE_TAG}${NC}" -echo -e "${GREEN}================================================${NC}" -echo "" - -# Pull image option removed - use build.sh to build locally - -# Hardware-specific checks -if [ "$MODE" = "hardware" ]; then - # Check if .env file exists - if [ ! -f ".env" ]; then - if [ -f ".env.hardware" ]; then - echo -e "${YELLOW}Creating .env from .env.hardware template...${NC}" - cp .env.hardware .env - echo -e "${RED}Please edit .env file with your hardware configuration:${NC}" - echo " - LIDAR_IP: Full IP address of your Mid-360 lidar" - echo " - LIDAR_COMPUTER_IP: IP address of this computer on the lidar subnet" - echo " - LIDAR_INTERFACE: Network interface connected to lidar" - echo " - MOTOR_SERIAL_DEVICE: Serial device for motor controller" - echo "" - echo "After editing, run this script again." - exit 1 - fi - fi - - # Source the environment file - if [ -f ".env" ]; then - set -a - source .env - set +a - fi - - # Auto-detect group IDs for device permissions - echo -e "${GREEN}Detecting device group IDs...${NC}" - export INPUT_GID=$(getent group input | cut -d: -f3 || echo "995") - export DIALOUT_GID=$(getent group dialout | cut -d: -f3 || echo "20") - # Warn if fallback values are being used - if ! getent group input > /dev/null 2>&1; then - echo -e "${YELLOW}Warning: input group not found, using fallback GID ${INPUT_GID}${NC}" - fi - if ! getent group dialout > /dev/null 2>&1; then - echo -e "${YELLOW}Warning: dialout group not found, using fallback GID ${DIALOUT_GID}${NC}" - fi - echo -e " input group GID: ${INPUT_GID}" - echo -e " dialout group GID: ${DIALOUT_GID}" - - if [ -f ".env" ]; then - # Check for required environment variables - if [ -z "$LIDAR_IP" ] || [ "$LIDAR_IP" = "192.168.1.116" ]; then - echo -e "${YELLOW}Warning: LIDAR_IP still using default value in .env${NC}" - echo "Set LIDAR_IP to the actual IP address of your Mid-360 lidar" - fi - - if [ -z "$LIDAR_GATEWAY" ]; then - echo -e "${YELLOW}Warning: LIDAR_GATEWAY not configured in .env${NC}" - echo "Set LIDAR_GATEWAY to the gateway IP address for the lidar subnet" - fi - - # Check for robot IP configuration - if [ -n "$ROBOT_IP" ]; then - echo -e "${GREEN}Robot IP configured: $ROBOT_IP${NC}" - else - echo -e "${YELLOW}Note: ROBOT_IP not configured in .env${NC}" - echo "Set ROBOT_IP if using network connection to robot" - fi - - # Check for serial devices - echo -e "${GREEN}Checking for serial devices...${NC}" - if [ -e "${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" ]; then - echo -e " Found device at: ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}" - else - echo -e "${YELLOW} Warning: Device not found at ${MOTOR_SERIAL_DEVICE:-/dev/ttyACM0}${NC}" - echo -e "${YELLOW} Available serial devices:${NC}" - ls /dev/ttyACM* /dev/ttyUSB* 2>/dev/null || echo " None found" - fi - - # Check network interface for lidar - echo -e "${GREEN}Checking network interface for lidar...${NC}" - - # Get available ethernet interfaces - AVAILABLE_ETH="" - for i in /sys/class/net/*; do - if [ "$(cat $i/type 2>/dev/null)" = "1" ] && [ "$i" != "/sys/class/net/lo" ]; then - interface=$(basename $i) - if [ -z "$AVAILABLE_ETH" ]; then - AVAILABLE_ETH="$interface" - else - AVAILABLE_ETH="$AVAILABLE_ETH, $interface" - fi - fi - done - - if [ -z "$LIDAR_INTERFACE" ]; then - # No interface configured - echo -e "${RED}================================================================${NC}" - echo -e "${RED} ERROR: ETHERNET INTERFACE NOT CONFIGURED!${NC}" - echo -e "${RED}================================================================${NC}" - echo -e "${YELLOW} LIDAR_INTERFACE not set in .env file${NC}" - echo "" - echo -e "${YELLOW} Your ethernet interfaces: ${GREEN}${AVAILABLE_ETH}${NC}" - echo "" - echo -e "${YELLOW} ACTION REQUIRED:${NC}" - echo -e " 1. Edit the .env file and set:" - echo -e " ${GREEN}LIDAR_INTERFACE=${NC}" - echo -e " 2. Run this script again" - echo -e "${RED}================================================================${NC}" - exit 1 - elif ! ip link show "$LIDAR_INTERFACE" &>/dev/null; then - # Interface configured but doesn't exist - echo -e "${RED}================================================================${NC}" - echo -e "${RED} ERROR: ETHERNET INTERFACE '$LIDAR_INTERFACE' NOT FOUND!${NC}" - echo -e "${RED}================================================================${NC}" - echo -e "${YELLOW} You configured: LIDAR_INTERFACE=$LIDAR_INTERFACE${NC}" - echo -e "${YELLOW} But this interface doesn't exist on your system${NC}" - echo "" - echo -e "${YELLOW} Your ethernet interfaces: ${GREEN}${AVAILABLE_ETH}${NC}" - echo "" - echo -e "${YELLOW} ACTION REQUIRED:${NC}" - echo -e " 1. Edit the .env file and change to one of your interfaces:" - echo -e " ${GREEN}LIDAR_INTERFACE=${NC}" - echo -e " 2. Run this script again" - echo -e "${RED}================================================================${NC}" - exit 1 - else - # Interface exists and is configured correctly - echo -e " ${GREEN}✓${NC} Network interface $LIDAR_INTERFACE found" - echo -e " ${GREEN}✓${NC} Will configure static IP: ${LIDAR_COMPUTER_IP}/24" - echo -e " ${GREEN}✓${NC} Will set gateway: ${LIDAR_GATEWAY}" - echo "" - echo -e "${YELLOW} Network configuration mode: Static IP (Manual)${NC}" - echo -e " This will temporarily replace DHCP with static IP assignment" - echo -e " Configuration reverts when container stops" - fi - fi - -fi - -# Check if the image exists -if ! docker images --format '{{.Repository}}:{{.Tag}}' | grep -q "^dimos_autonomy_stack:${IMAGE_TAG}$"; then - echo -e "${RED}Docker image dimos_autonomy_stack:${IMAGE_TAG} not found.${NC}" - echo -e "${YELLOW}Please build it first with:${NC}" - echo -e " ./build.sh --${ROS_DISTRO}" - exit 1 -fi - -# Check for X11 display -if [ -z "$DISPLAY" ]; then - echo -e "${YELLOW}Warning: DISPLAY not set. GUI applications may not work.${NC}" - export DISPLAY=:0 -else - echo -e "${GREEN}Using DISPLAY: $DISPLAY${NC}" -fi -export DISPLAY - -# Allow X11 connections from Docker -echo -e "${GREEN}Configuring X11 access...${NC}" -xhost +local:docker 2>/dev/null || true - -# Setup X11 auth for remote/SSH connections -XAUTH=/tmp/.docker.xauth -touch $XAUTH 2>/dev/null || true -if [ -n "$DISPLAY" ]; then - xauth nlist $DISPLAY 2>/dev/null | sed -e 's/^..../ffff/' | xauth -f $XAUTH nmerge - 2>/dev/null || true - chmod 644 $XAUTH 2>/dev/null || true - echo -e "${GREEN}X11 auth configured for display: $DISPLAY${NC}" -fi - -cleanup() { - xhost -local:docker 2>/dev/null || true -} - -trap cleanup EXIT - -# Check for NVIDIA runtime -if docker info 2>/dev/null | grep -q nvidia; then - echo -e "${GREEN}NVIDIA Docker runtime detected${NC}" - export DOCKER_RUNTIME=nvidia - if [ "$MODE" = "hardware" ]; then - export NVIDIA_VISIBLE_DEVICES=all - export NVIDIA_DRIVER_CAPABILITIES=all - fi -else - echo -e "${YELLOW}NVIDIA Docker runtime not found. GPU acceleration disabled.${NC}" - export DOCKER_RUNTIME=runc -fi - -# Set container name for reference -if [ "$MODE" = "hardware" ]; then - CONTAINER_NAME="dimos_hardware_container" -elif [ "$MODE" = "bagfile" ]; then - CONTAINER_NAME="dimos_bagfile_container" -else - CONTAINER_NAME="dimos_simulation_container" -fi - -# Export settings for docker-compose -export USE_ROUTE_PLANNER -export USE_RVIZ - -# Print helpful info before starting -echo "" -if [ "$MODE" = "hardware" ]; then - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Hardware mode - Auto-starting ROS real robot system WITH route planner" - echo "" - echo "The container will automatically run:" - echo " - ROS navigation stack (system_real_robot_with_route_planner.launch)" - echo " - FAR Planner for goal-based navigation" - echo " - Foxglove Bridge" - else - echo "Hardware mode - Auto-starting ROS real robot system (base autonomy)" - echo "" - echo "The container will automatically run:" - echo " - ROS navigation stack (system_real_robot.launch)" - echo " - Foxglove Bridge" - fi - if [ "$USE_RVIZ" = "true" ]; then - echo " - RViz2 visualization" - fi - if [ "$DEV_MODE" = "true" ]; then - echo "" - echo -e " ${YELLOW}Development mode: src folder mounted for config editing${NC}" - fi - echo "" - echo "To enter the container from another terminal:" - echo -e " ${YELLOW}docker exec -it ${CONTAINER_NAME} bash${NC}" -elif [ "$MODE" = "bagfile" ]; then - if [ "$USE_ROUTE_PLANNER" = "true" ]; then - echo "Bagfile mode - Starting bagfile playback system WITH route planner" - echo "" - echo "The container will run (use_sim_time=true):" - echo " - ROS navigation stack (system_bagfile_with_route_planner.launch)" - echo " - FAR Planner for goal-based navigation" - else - echo "Bagfile mode - Starting bagfile playback system (base autonomy)" - echo "" - echo "The container will run (use_sim_time=true):" - echo " - ROS navigation stack (system_bagfile.launch)" - fi - if [ "$USE_RVIZ" = "true" ]; then - echo " - RViz2 visualization" - fi - echo "" - echo -e "${YELLOW}Remember to play bagfile with: ros2 bag play --clock ${NC}" - echo "" - echo "To enter the container from another terminal:" - echo -e " ${YELLOW}docker exec -it ${CONTAINER_NAME} bash${NC}" -else - echo "Simulation mode - Auto-starting ROS simulation and DimOS" - echo "" - echo "The container will automatically run:" - echo " - ROS navigation stack with route planner" - echo " - DimOS navigation demo" - echo "" - echo "To enter the container from another terminal:" - echo " docker exec -it ${CONTAINER_NAME} bash" -fi - -# Note: DISPLAY is now passed directly via environment variable -# No need to write RUNTIME_DISPLAY to .env for local host running - -# Create required directories -if [ "$MODE" = "hardware" ]; then - mkdir -p bagfiles config logs maps -elif [ "$MODE" = "bagfile" ]; then - mkdir -p bagfiles config maps -fi - -# Build compose command -COMPOSE_CMD="docker compose -f docker-compose.yml" -if [ "$DEV_MODE" = "true" ]; then - COMPOSE_CMD="$COMPOSE_CMD -f docker-compose.dev.yml" -fi - -if [ "$MODE" = "hardware" ]; then - $COMPOSE_CMD --profile hardware up -elif [ "$MODE" = "bagfile" ]; then - $COMPOSE_CMD --profile bagfile up -else - $COMPOSE_CMD up -fi diff --git a/docker/python/Dockerfile b/docker/python/Dockerfile deleted file mode 100644 index 30c9fda8eb..0000000000 --- a/docker/python/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -ARG FROM_IMAGE=ghcr.io/dimensionalos/ros:dev -FROM ${FROM_IMAGE} - -# Install basic requirements -RUN apt-get update && apt-get install -y \ - python-is-python3 \ - curl \ - gnupg2 \ - lsb-release \ - python3-pip \ - clang \ - portaudio19-dev \ - git \ - mesa-utils \ - libgl1 \ - libgl1-mesa-dri \ - software-properties-common \ - libxcb1-dev \ - libxcb-keysyms1-dev \ - libxcb-util0-dev \ - libxcb-icccm4-dev \ - libxcb-image0-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xinerama0-dev \ - libxcb-xkb-dev \ - libxkbcommon-x11-dev \ - qtbase5-dev \ - qtchooser \ - qt5-qmake \ - qtbase5-dev-tools \ - supervisor \ - iproute2 # for LCM networking system config \ - liblcm-dev - -# Fix distutils-installed packages that block pip upgrades -RUN apt-get purge -y python3-blinker python3-sympy python3-oauthlib || true - -# Install UV for fast Python package management -ENV UV_SYSTEM_PYTHON=1 -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -ENV PATH="/root/.local/bin:$PATH" - -WORKDIR /app - -# Copy entire project first to ensure proper package installation -COPY . /app/ - -# Install dependencies with UV (10-100x faster than pip) -RUN uv pip install --upgrade 'pip>=24' 'setuptools>=70' 'wheel' 'packaging>=24' && \ - uv pip install '.[misc,cpu,sim,drone,unitree,web,perception,visualization,manipulation]' - -# Remove pydrake .pyi stubs that use Python 3.12 syntax (breaks mypy on 3.10) -RUN find /usr/local/lib/python3.10/dist-packages/pydrake -name '*.pyi' -delete diff --git a/docker/python/module-install.sh b/docker/python/module-install.sh deleted file mode 100644 index ab0aea1032..0000000000 --- a/docker/python/module-install.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash -# DimOS Module Install (generic) -# Converts any Dockerfile into a DimOS module container -# -# Usage in Dockerfile: -# RUN --mount=from=ghcr.io/dimensionalos/ros-python:dev,source=/app,target=/tmp/d \ -# bash /tmp/d/docker/python/module-install.sh /tmp/d -# ENTRYPOINT ["/dimos/entrypoint.sh"] - -set -euo pipefail - -SRC="${1:-/tmp/d}" - -# ---- Copy source into image (skip if already at /dimos/source) ---- -if [ "${SRC}" != "/dimos/source" ]; then - mkdir -p /dimos/source - cp -r "${SRC}/dimos" "${SRC}/pyproject.toml" /dimos/source/ - [ -f "${SRC}/README.md" ] && cp "${SRC}/README.md" /dimos/source/ || true -fi - -# ---- Find Python + Pip (conda env > venv > uv > system) ---- -PYTHON="" -PIP="" - -# 1. Check for Conda environment -if [ -z "$PYTHON" ] && command -v conda >/dev/null 2>&1; then - DIMOS_CONDA_ENV="${DIMOS_CONDA_ENV:-app}" - if conda env list 2>/dev/null | awk '{print $1}' | grep -qx "${DIMOS_CONDA_ENV}"; then - PYTHON="conda run --no-capture-output -n ${DIMOS_CONDA_ENV} python" - PIP="conda run -n ${DIMOS_CONDA_ENV} pip" - echo "Using Conda env: ${DIMOS_CONDA_ENV}" - fi -fi - -# 2. Check for venv (including uv's .venv) -if [ -z "$PYTHON" ]; then - for v in /opt/venv /app/venv /venv /app/.venv /.venv; do - if [ -x "${v}/bin/python" ] && [ -x "${v}/bin/pip" ]; then - PYTHON="${v}/bin/python" - PIP="${v}/bin/pip" - echo "Using venv: ${v}" - break - fi - done -fi - -# 3. Check for uv (uses system python but manages deps) -if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then - PYTHON="python" - PIP="uv pip" - echo "Using uv" -fi - -# 4. Fallback to system Python -if [ -z "$PYTHON" ]; then - PYTHON="python" - PIP="pip" - echo "Using system Python" -fi - -# ---- Install DimOS (deps from pyproject.toml[docker]) ---- -${PIP} install --no-cache-dir -e "/dimos/source[docker]" - -# ---- Create entrypoint ---- -cat > /dimos/entrypoint.sh < /dev/null - -# Install ROS2 packages and dependencies -RUN apt-get update && apt-get install -y \ - ros-${ROS_DISTRO}-desktop \ - ros-${ROS_DISTRO}-ros-base \ - ros-${ROS_DISTRO}-image-tools \ - ros-${ROS_DISTRO}-compressed-image-transport \ - ros-${ROS_DISTRO}-vision-msgs \ - ros-${ROS_DISTRO}-rviz2 \ - ros-${ROS_DISTRO}-rqt \ - ros-${ROS_DISTRO}-rqt-common-plugins \ - ros-${ROS_DISTRO}-twist-mux \ - ros-${ROS_DISTRO}-joy \ - ros-${ROS_DISTRO}-teleop-twist-joy \ - ros-${ROS_DISTRO}-navigation2 \ - ros-${ROS_DISTRO}-nav2-bringup \ - ros-${ROS_DISTRO}-nav2-amcl \ - ros-${ROS_DISTRO}-nav2-map-server \ - ros-${ROS_DISTRO}-nav2-util \ - ros-${ROS_DISTRO}-pointcloud-to-laserscan \ - ros-${ROS_DISTRO}-slam-toolbox \ - ros-${ROS_DISTRO}-foxglove-bridge \ - python3-rosdep \ - python3-rosinstall \ - python3-rosinstall-generator \ - python3-wstool \ - python3-colcon-common-extensions \ - python3-vcstool \ - build-essential \ - screen \ - tmux - -# Initialize rosdep -RUN rosdep init -RUN rosdep update - -# Source ROS2 and workspace in bashrc -RUN echo "source /opt/ros/${ROS_DISTRO}/setup.bash" >> /root/.bashrc diff --git a/docker/ros/install-nix.sh b/docker/ros/install-nix.sh deleted file mode 100644 index 879e2149e1..0000000000 --- a/docker/ros/install-nix.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if nix_path="$(type -p nix)" ; then - echo "Aborting: Nix is already installed at ${nix_path}" - exit -fi - -if [[ ($OSTYPE =~ linux) && ($INPUT_ENABLE_KVM == 'true') ]]; then - enable_kvm() { - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-install-nix-action-kvm.rules - sudo udevadm control --reload-rules && sudo udevadm trigger --name-match=kvm - } - - echo '::group::Enabling KVM support' - enable_kvm && echo 'Enabled KVM' || echo 'KVM is not available' - echo '::endgroup::' -fi - -# GitHub command to put the following log messages into a group which is collapsed by default -echo "::group::Installing Nix" - -# Create a temporary workdir -workdir=$(mktemp -d) -trap 'rm -rf "$workdir"' EXIT - -# Configure Nix -add_config() { - echo "$1" >> "$workdir/nix.conf" -} -add_config "show-trace = true" -# Set jobs to number of cores -add_config "max-jobs = auto" -if [[ $OSTYPE =~ darwin ]]; then - add_config "ssl-cert-file = /etc/ssl/cert.pem" -fi -# Allow binary caches specified at user level -if [[ $INPUT_SET_AS_TRUSTED_USER == 'true' ]]; then - add_config "trusted-users = root ${USER:-}" -fi -# Add a GitHub access token. -# Token-less access is subject to lower rate limits. -if [[ -n "${INPUT_GITHUB_ACCESS_TOKEN:-}" ]]; then - echo "::debug::Using the provided github_access_token for github.com" - add_config "access-tokens = github.com=$INPUT_GITHUB_ACCESS_TOKEN" -# Use the default GitHub token if available. -# Skip this step if running an Enterprise instance. The default token there does not work for github.com. -elif [[ -n "${GITHUB_TOKEN:-}" && $GITHUB_SERVER_URL == "https://github.com" ]]; then - echo "::debug::Using the default GITHUB_TOKEN for github.com" - add_config "access-tokens = github.com=$GITHUB_TOKEN" -else - echo "::debug::Continuing without a GitHub access token" -fi -# Append extra nix configuration if provided -if [[ -n "${INPUT_EXTRA_NIX_CONFIG:-}" ]]; then - add_config "$INPUT_EXTRA_NIX_CONFIG" -fi -if [[ ! $INPUT_EXTRA_NIX_CONFIG =~ "experimental-features" ]]; then - add_config "experimental-features = nix-command flakes" -fi -# Always allow substituting from the cache, even if the derivation has `allowSubstitutes = false`. -# This is a CI optimisation to avoid having to download the inputs for already-cached derivations to rebuild trivial text files. -if [[ ! $INPUT_EXTRA_NIX_CONFIG =~ "always-allow-substitutes" ]]; then - add_config "always-allow-substitutes = true" -fi - -# Nix installer flags -installer_options=( - --no-channel-add - --nix-extra-conf-file "$workdir/nix.conf" -) - -# only use the nix-daemon settings if on darwin (which get ignored) or systemd is supported -if [[ (! $INPUT_INSTALL_OPTIONS =~ "--no-daemon") && ($OSTYPE =~ darwin || -e /run/systemd/system) ]]; then - installer_options+=( - --daemon - --daemon-user-count "$(python3 -c 'import multiprocessing as mp; print(mp.cpu_count() * 2)')" - ) -else - # "fix" the following error when running nix* - # error: the group 'nixbld' specified in 'build-users-group' does not exist - add_config "build-users-group =" - sudo mkdir -p /etc/nix - sudo chmod 0755 /etc/nix - sudo cp "$workdir/nix.conf" /etc/nix/nix.conf -fi - -if [[ -n "${INPUT_INSTALL_OPTIONS:-}" ]]; then - IFS=' ' read -r -a extra_installer_options <<< "$INPUT_INSTALL_OPTIONS" - installer_options=("${extra_installer_options[@]}" "${installer_options[@]}") -fi - -echo "installer options: ${installer_options[*]}" - -# There is --retry-on-errors, but only newer curl versions support that -curl_retries=5 -while ! curl -sS -o "$workdir/install" -v --fail -L "${INPUT_INSTALL_URL:-https://releases.nixos.org/nix/nix-2.28.3/install}" -do - sleep 1 - ((curl_retries--)) - if [[ $curl_retries -le 0 ]]; then - echo "curl retries failed" >&2 - exit 1 - fi -done - -sh "$workdir/install" "${installer_options[@]}" - -# Set paths -echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" -# new path for nix 2.14 -echo "$HOME/.nix-profile/bin" >> "$GITHUB_PATH" - -if [[ -n "${INPUT_NIX_PATH:-}" ]]; then - echo "NIX_PATH=${INPUT_NIX_PATH}" >> "$GITHUB_ENV" -fi - -# Set temporary directory (if not already set) to fix https://github.com/cachix/install-nix-action/issues/197 -if [[ -z "${TMPDIR:-}" ]]; then - echo "TMPDIR=${RUNNER_TEMP}" >> "$GITHUB_ENV" -fi - -# Close the log message group which was opened above -echo "::endgroup::" diff --git a/docs/agents/docs/assets/codeblocks_example.svg b/docs/agents/docs/assets/codeblocks_example.svg deleted file mode 100644 index 3ba6c37a4b..0000000000 --- a/docs/agents/docs/assets/codeblocks_example.svg +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - -A - -A - - - -B - -B - - - -A->B - - - - - -C - -C - - - -A->C - - - - - -B->C - - - - - diff --git a/docs/agents/docs/assets/pikchr_basic.svg b/docs/agents/docs/assets/pikchr_basic.svg deleted file mode 100644 index 5410d35577..0000000000 --- a/docs/agents/docs/assets/pikchr_basic.svg +++ /dev/null @@ -1,12 +0,0 @@ - - -Step 1 - - - -Step 2 - - - -Step 3 - diff --git a/docs/agents/docs/assets/pikchr_branch.svg b/docs/agents/docs/assets/pikchr_branch.svg deleted file mode 100644 index e7b2b86596..0000000000 --- a/docs/agents/docs/assets/pikchr_branch.svg +++ /dev/null @@ -1,16 +0,0 @@ - - -Input - - - -Process - - - -Path A - - - -Path B - diff --git a/docs/agents/docs/assets/pikchr_explicit.svg b/docs/agents/docs/assets/pikchr_explicit.svg deleted file mode 100644 index a6a913fcb4..0000000000 --- a/docs/agents/docs/assets/pikchr_explicit.svg +++ /dev/null @@ -1,8 +0,0 @@ - - -Step 1 - - - -Step 2 - diff --git a/docs/agents/docs/assets/pikchr_labels.svg b/docs/agents/docs/assets/pikchr_labels.svg deleted file mode 100644 index b11fe64bca..0000000000 --- a/docs/agents/docs/assets/pikchr_labels.svg +++ /dev/null @@ -1,5 +0,0 @@ - - -Box -label below - diff --git a/docs/agents/docs/assets/pikchr_sizing.svg b/docs/agents/docs/assets/pikchr_sizing.svg deleted file mode 100644 index 3a0c433cb1..0000000000 --- a/docs/agents/docs/assets/pikchr_sizing.svg +++ /dev/null @@ -1,13 +0,0 @@ - - -short - - - -.subscribe() - - - -two lines -of text - diff --git a/docs/agents/docs/codeblocks.md b/docs/agents/docs/codeblocks.md deleted file mode 100644 index 323f1c0c50..0000000000 --- a/docs/agents/docs/codeblocks.md +++ /dev/null @@ -1,314 +0,0 @@ -# Executable Code Blocks - -We use [md-babel-py](https://github.com/leshy/md-babel-py/) to execute code blocks in markdown and insert results. - -## Golden Rule - -**All code blocks must be executable.** Never write illustrative/pseudo code blocks. If you're showing an API usage pattern, create a minimal working example that actually runs. This ensures documentation stays correct as the codebase evolves. - -## Running - -```sh skip -md-babel-py run document.md # edit in-place -md-babel-py run document.md --stdout # preview to stdout -md-babel-py run document.md --dry-run # show what would run -``` - -## Supported Languages - -Python, Shell (sh), Node.js, plus visualization: Matplotlib, Graphviz, Pikchr, Asymptote, OpenSCAD, Diagon. - -## Code Block Flags - -Add flags after the language identifier: - -| Flag | Effect | -|------|--------| -| `session=NAME` | Share state between blocks with same session name | -| `output=path.png` | Write output to file instead of inline | -| `no-result` | Execute but don't insert result | -| `skip` | Don't execute this block | -| `expected-error` | Block is expected to fail | - -## Examples - -# md-babel-py - -Execute code blocks in markdown files and insert the results. - -![Demo](assets/screencast.gif) - -**Use cases:** -- Keep documentation examples up-to-date automatically -- Validate code snippets in docs actually work -- Generate diagrams and charts from code in markdown -- Literate programming with executable documentation - -## Languages - -### Shell - -```sh -echo "cwd: $(pwd)" -``` - - -``` -cwd: /work -``` - -### Python - -```python session=example -a = "hello world" -print(a) -``` - - -``` -hello world -``` - -Sessions preserve state between code blocks: - -```python session=example -print(a, "again") -``` - - -``` -hello world again -``` - -### Node.js - -```node -console.log("Hello from Node.js"); -console.log(`Node version: ${process.version}`); -``` - - -``` -Hello from Node.js -Node version: v22.21.1 -``` - -### Matplotlib - -```python output=assets/matplotlib-demo.svg -import matplotlib.pyplot as plt -import numpy as np -plt.style.use('dark_background') -x = np.linspace(0, 4 * np.pi, 200) -plt.figure(figsize=(8, 4)) -plt.plot(x, np.sin(x), label='sin(x)', linewidth=2) -plt.plot(x, np.cos(x), label='cos(x)', linewidth=2) -plt.xlabel('x') -plt.ylabel('y') -plt.legend() -plt.grid(alpha=0.3) -plt.savefig('{output}', transparent=True) -``` - - -![output](assets/matplotlib-demo.svg) - -### Pikchr - -SQLite's diagram language: - -
-diagram source - -```pikchr fold output=assets/pikchr-demo.svg -color = white -fill = none -linewid = 0.4in - -# Input file -In: file "README.md" fit -arrow - -# Processing -Parse: box "Parse" rad 5px fit -arrow -Exec: box "Execute" rad 5px fit - -# Fan out to languages -arrow from Exec.e right 0.3in then up 0.4in then right 0.3in -Sh: oval "Shell" fit -arrow from Exec.e right 0.3in then right 0.3in -Node: oval "Node" fit -arrow from Exec.e right 0.3in then down 0.4in then right 0.3in -Py: oval "Python" fit - -# Merge back -X: dot at (Py.e.x + 0.3in, Node.e.y) invisible -line from Sh.e right until even with X then down to X -line from Node.e to X -line from Py.e right until even with X then up to X -Out: file "README.md" fit with .w at (X.x + 0.3in, X.y) -arrow from X to Out.w -``` - -
- - -![output](assets/pikchr-demo.svg) - -### Asymptote - -Vector graphics: - -```asymptote output=assets/histogram.svg -import graph; -import stats; - -size(400,200,IgnoreAspect); -defaultpen(white); - -int n=10000; -real[] a=new real[n]; -for(int i=0; i < n; ++i) a[i]=Gaussrand(); - -draw(graph(Gaussian,min(a),max(a)),orange); - -int N=bins(a); - -histogram(a,min(a),max(a),N,normalize=true,low=0,rgb(0.4,0.6,0.8),rgb(0.2,0.4,0.6),bars=true); - -xaxis("$x$",BottomTop,LeftTicks,p=white); -yaxis("$dP/dx$",LeftRight,RightTicks(trailingzero),p=white); -``` - - -![output](assets/histogram.svg) - -### Graphviz - -```dot output=assets/graph.svg -A -> B -> C -A -> C -``` - - -![output](assets/graph.svg) - -### OpenSCAD - -```openscad output=assets/cube-sphere.png -cube([10, 10, 10]); -sphere(r=7); -``` - - -![output](assets/cube-sphere.png) - -### Diagon - -ASCII art diagrams: - -```diagon mode=Math -1 + 1/2 + sum(i,0,10) -``` - - -``` - 10 - ___ - 1 ╲ -1 + ─ + ╱ i - 2 ‾‾‾ - 0 -``` - -```diagon mode=GraphDAG -A -> B -> C -A -> C -``` - - -``` -┌───┐ -│A │ -└┬─┬┘ - │┌▽┐ - ││B│ - │└┬┘ -┌▽─▽┐ -│C │ -└───┘ -``` - -## Install - -### Nix (recommended) - -```sh skip -# Run directly from GitHub -nix run github:leshy/md-babel-py -- run README.md --stdout - -# Or clone and run locally -nix run . -- run README.md --stdout -``` - -### Docker - -```sh skip -# Pull from Docker Hub -docker run -v $(pwd):/work lesh/md-babel-py:main run /work/README.md --stdout - -# Or build locally via Nix -nix build .#docker # builds tarball to ./result -docker load < result # loads image from tarball -docker run -v $(pwd):/work md-babel-py:latest run /work/file.md --stdout -``` - -### pipx - -```sh skip -pipx install md-babel-py -# or: uv pip install md-babel-py -md-babel-py run README.md --stdout -``` - -If not using nix or docker, evaluators require system dependencies: - -| Language | System packages | -|-----------|-----------------------------| -| python | python3 | -| node | nodejs | -| dot | graphviz | -| asymptote | asymptote, texlive, dvisvgm | -| pikchr | pikchr | -| openscad | openscad, xvfb, imagemagick | -| diagon | diagon | - -```sh skip -# Arch Linux -sudo pacman -S python nodejs graphviz asymptote texlive-basic openscad xorg-server-xvfb imagemagick - -# Debian/Ubuntu -sudo apt-get install python3 nodejs graphviz asymptote texlive xvfb imagemagick openscad -``` - -Note: pikchr and diagon may need to be built from source. Use Docker or Nix for full evaluator support. - -## Usage - -```sh skip -# Edit file in-place -md-babel-py run document.md - -# Output to separate file -md-babel-py run document.md --output result.md - -# Print to stdout -md-babel-py run document.md --stdout - -# Only run specific languages -md-babel-py run document.md --lang python,sh - -# Dry run - show what would execute -md-babel-py run document.md --dry-run -``` diff --git a/docs/agents/docs/doclinks.md b/docs/agents/docs/doclinks.md deleted file mode 100644 index d5533c5983..0000000000 --- a/docs/agents/docs/doclinks.md +++ /dev/null @@ -1,21 +0,0 @@ -When writing or editing markdown documentation, use `doclinks` tool to resolve file references. - -Full documentation if needed: [`utils/docs/doclinks.md`](/dimos/utils/docs/doclinks.md) - -## Syntax - - -| Pattern | Example | -|-------------|-----------------------------------------------------| -| Code file | `[`service/spec.py`]()` → resolves path | -| With symbol | `Configurable` in `[`spec.py`]()` → adds `#L` | -| Doc link | `[Configuration](.md)` → resolves to doc | - - -## Usage - -```bash -doclinks docs/guide.md # single file -doclinks docs/ # directory -doclinks --dry-run ... # preview only -``` diff --git a/docs/agents/docs/index.md b/docs/agents/docs/index.md deleted file mode 100644 index bec2ce79e6..0000000000 --- a/docs/agents/docs/index.md +++ /dev/null @@ -1,192 +0,0 @@ - -# Code Blocks - -**All code blocks must be executable.** -Never write illustrative/pseudo code blocks. -If you're showing an API usage pattern, create a minimal working example that actually runs. This ensures documentation stays correct as the codebase evolves. - -After writing a code block in your markdown file, you can run it by executing -`md-babel-py run document.md` - -more information on this tool is in [codeblocks](/docs/agents/docs_agent/codeblocks.md) - - -# Code or Docs Links - -After adding a link to a doc run - -`doclinks document.md` - -### Code file references -```markdown -See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. -``` - -After running doclinks, becomes: -```markdown -See [`service/spec.py`](/dimos/protocol/service/spec.py) for the implementation. -``` - -### Symbol auto-linking -Mention a symbol on the same line to auto-link to its line number: -```markdown -The `Configurable` class is defined in [`service/spec.py`](/dimos/protocol/service/spec.py#L22). -``` - -Becomes: -```markdown -The `Configurable` class is defined in [`service/spec.py`](/dimos/protocol/service/spec.py#L22). -``` -### Doc-to-doc references -Use `.md` as the link target: -```markdown -See [Configuration](/docs/api/configuration.md) for more details. -``` - -Becomes: -```markdown -See [Configuration](/docs/concepts/configuration.md) for more details. -``` - -More information on this in [doclinks](/docs/agents/docs_agent/doclinks.md) - - -# Pikchr - -[Pikchr](https://pikchr.org/) is a diagram language from SQLite. Use it for flowcharts and architecture diagrams. - -**Important:** Always wrap pikchr blocks in `
` tags so the source is collapsed by default on GitHub. The rendered SVG stays visible outside the fold. Code blocks (Python, etc.) should NOT be folded—they're meant to be read. - -## Basic syntax - -
-diagram source - -```pikchr fold output=assets/pikchr_basic.svg -color = white -fill = none - -A: box "Step 1" rad 5px fit wid 170% ht 170% -arrow right 0.3in -B: box "Step 2" rad 5px fit wid 170% ht 170% -arrow right 0.3in -C: box "Step 3" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/pikchr_basic.svg) - -## Box sizing - -Use `fit` with percentage scaling to auto-size boxes with padding: - -
-diagram source - -```pikchr fold output=assets/pikchr_sizing.svg -color = white -fill = none - -# fit wid 170% ht 170% = auto-size + padding -A: box "short" rad 5px fit wid 170% ht 170% -arrow right 0.3in -B: box ".subscribe()" rad 5px fit wid 170% ht 170% -arrow right 0.3in -C: box "two lines" "of text" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/pikchr_sizing.svg) - -The pattern `fit wid 170% ht 170%` means: auto-size to text, then scale width by 170% and height by 170%. - -For explicit sizing (when you need consistent box sizes): - -
-diagram source - -```pikchr fold output=assets/pikchr_explicit.svg -color = white -fill = none - -A: box "Step 1" rad 5px fit wid 170% ht 170% -arrow right 0.3in -B: box "Step 2" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/pikchr_explicit.svg) - -## Common settings - -Always start with: - -``` -color = white # text color -fill = none # transparent box fill -``` - -## Branching paths - -
-diagram source - -```pikchr fold output=assets/pikchr_branch.svg -color = white -fill = none - -A: box "Input" rad 5px fit wid 170% ht 170% -arrow -B: box "Process" rad 5px fit wid 170% ht 170% - -# Branch up -arrow from B.e right 0.3in then up 0.35in then right 0.3in -C: box "Path A" rad 5px fit wid 170% ht 170% - -# Branch down -arrow from B.e right 0.3in then down 0.35in then right 0.3in -D: box "Path B" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/pikchr_branch.svg) - -**Tip:** For tree/hierarchy diagrams, prefer left-to-right layout (root on left, children branching right). This reads more naturally and avoids awkward vertical stacking. - -## Adding labels - -
-diagram source - -```pikchr fold output=assets/pikchr_labels.svg -color = white -fill = none - -A: box "Box" rad 5px fit wid 170% ht 170% -text "label below" at (A.x, A.y - 0.4in) -``` - -
- - -![output](assets/pikchr_labels.svg) - -## Reference - -| Element | Syntax | -|---------|--------| -| Box | `box "text" rad 5px wid Xin ht Yin` | -| Arrow | `arrow right 0.3in` | -| Oval | `oval "text" wid Xin ht Yin` | -| Text | `text "label" at (X, Y)` | -| Named point | `A: box ...` then reference `A.e`, `A.n`, `A.x`, `A.y` | - -See [pikchr.org/home/doc/trunk/doc/userman.md](https://pikchr.org/home/doc/trunk/doc/userman.md) for full documentation. diff --git a/docs/agents/index.md b/docs/agents/index.md deleted file mode 100644 index ec9d66e886..0000000000 --- a/docs/agents/index.md +++ /dev/null @@ -1,19 +0,0 @@ -# For Agents - -These docs are mostly for coding agents - -```sh -tree . -P '*.md' --prune -``` - - -``` -. -├── docs -│   ├── codeblocks.md -│   ├── doclinks.md -│   └── index.md -└── index.md - -2 directories, 4 files -``` diff --git a/docs/capabilities/agents/readme.md b/docs/capabilities/agents/readme.md deleted file mode 100644 index 57be659e9c..0000000000 --- a/docs/capabilities/agents/readme.md +++ /dev/null @@ -1 +0,0 @@ -# Agents diff --git a/docs/capabilities/manipulation/readme.md b/docs/capabilities/manipulation/readme.md deleted file mode 100644 index 91dada0395..0000000000 --- a/docs/capabilities/manipulation/readme.md +++ /dev/null @@ -1,112 +0,0 @@ -# Manipulation - -Motion planning and teleoperation for robotic manipulators. Uses Drake for physics simulation and Meshcat for 3D visualization. - -## Quick Start - -### Keyboard Teleop (single command) - -Each blueprint launches the full stack — keyboard UI, mock controller, IK solver, and Drake visualization: - -```bash -dimos run keyboard-teleop-piper # Piper 6-DOF -dimos run keyboard-teleop-xarm6 # XArm6 6-DOF -dimos run keyboard-teleop-xarm7 # XArm7 7-DOF -``` - -Open the Meshcat URL printed in the terminal (default `http://localhost:7000`) to see the robot. - -Keyboard controls: - -| Key | Action | -|-----|--------| -| W/S | +X/-X (forward/back) | -| A/D | -Y/+Y (left/right) | -| Q/E | +Z/-Z (up/down) | -| R/F | +Roll/-Roll | -| T/G | +Pitch/-Pitch | -| Y/H | +Yaw/-Yaw | -| SPACE | Reset to home pose | -| ESC | Quit | - -### Motion Planning (two terminals) - -```bash -# Terminal 1: Mock coordinator -dimos run coordinator-mock - -# Terminal 2: Planner with Drake visualization -dimos run xarm7-planner-coordinator -``` - -Then use the IPython client: - -```bash -python -m dimos.manipulation.planning.examples.manipulation_client -``` - -```python -joints() # Get current joints -plan([0.1] * 7) # Plan to target -preview() # Preview in Meshcat -execute() # Execute via coordinator -``` - -### Perception + Agent - -```bash -# Terminal 1: Coordinator with real xarm7 -dimos run coordinator-xarm7 - -# Terminal 2: Perception + manipulation + LLM agent -dimos run xarm-perception-agent -``` - -## Architecture - -``` -KeyboardTeleopModule ──→ ControlCoordinator ──→ ManipulationModule - (pygame UI) (100Hz tick loop) (Drake + Meshcat) - │ │ │ - PoseStamped CartesianIK task RRT planner - commands (Pinocchio IK) JacobianIK - │ DrakeWorld - JointState ────────────→ (visualization) -``` - -- **KeyboardTeleopModule** — Pygame UI publishing cartesian pose commands -- **ControlCoordinator** — 100Hz control loop with mock or real hardware adapters -- **ManipulationModule** — Drake physics, Meshcat viz, RRT motion planning, obstacle management - -## Blueprints - -| Blueprint | Description | -|-----------|-------------| -| `keyboard-teleop-piper` | Piper 6-DOF keyboard teleop with Drake viz | -| `keyboard-teleop-xarm6` | XArm6 6-DOF keyboard teleop with Drake viz | -| `keyboard-teleop-xarm7` | XArm7 7-DOF keyboard teleop with Drake viz | -| `xarm6-planner-only` | XArm6 standalone planner (no coordinator) | -| `xarm7-planner-coordinator` | XArm7 planner with coordinator integration | -| `dual-xarm6-planner` | Dual XArm6 planning | -| `xarm-perception` | XArm7 + RealSense camera for perception | -| `xarm-perception-agent` | XArm7 perception + LLM agent | - -## Supported Robots - -| Robot | DOF | Teleop | Planning | Perception | -|-------|-----|--------|----------|------------| -| Piper | 6 | Y | Y | — | -| XArm6 | 6 | Y | Y | — | -| XArm7 | 7 | Y | Y | Y | - -## Key Files - -| File | Description | -|------|-------------| -| [`manipulation_module.py`](/dimos/manipulation/manipulation_module.py) | Main module (RPC interface, state machine) | -| [`manipulation_blueprints.py`](/dimos/manipulation/manipulation_blueprints.py) | Planner and perception blueprints | -| [`robot/manipulators/piper/blueprints.py`](/dimos/robot/manipulators/piper/blueprints.py) | Piper keyboard teleop blueprint | -| [`robot/manipulators/xarm/blueprints.py`](/dimos/robot/manipulators/xarm/blueprints.py) | XArm keyboard teleop blueprints | -| [`teleop/keyboard/keyboard_teleop_module.py`](/dimos/teleop/keyboard/keyboard_teleop_module.py) | Keyboard teleop module | -| [`planning/world/drake_world.py`](/dimos/manipulation/planning/world/drake_world.py) | Drake physics backend | -| [`planning/planners/rrt_planner.py`](/dimos/manipulation/planning/planners/rrt_planner.py) | RRT-Connect motion planner | diff --git a/docs/capabilities/navigation/native/assets/1-lidar.png b/docs/capabilities/navigation/native/assets/1-lidar.png deleted file mode 100644 index 6584ee90cb..0000000000 --- a/docs/capabilities/navigation/native/assets/1-lidar.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d76742ada18d20dc0e3a3be04159d3412e7df6acee8596ff37916f0f269d3e0 -size 597386 diff --git a/docs/capabilities/navigation/native/assets/2-globalmap.png b/docs/capabilities/navigation/native/assets/2-globalmap.png deleted file mode 100644 index 55541a8fcb..0000000000 --- a/docs/capabilities/navigation/native/assets/2-globalmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc2f27ec2dcc4048acde6b53229c7596b3a7f6ed6afad30c4cd062cf5751bd24 -size 1104485 diff --git a/docs/capabilities/navigation/native/assets/3-globalcostmap.png b/docs/capabilities/navigation/native/assets/3-globalcostmap.png deleted file mode 100644 index 907d0b0448..0000000000 --- a/docs/capabilities/navigation/native/assets/3-globalcostmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1f9e6c142b220f1a4be7b08950f628a2d34e26caba8a1f5c100726bec6c88ef -size 793366 diff --git a/docs/capabilities/navigation/native/assets/4-navcostmap.png b/docs/capabilities/navigation/native/assets/4-navcostmap.png deleted file mode 100644 index 6c40bce0e0..0000000000 --- a/docs/capabilities/navigation/native/assets/4-navcostmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9ee4332e3d92162ddf41a0137c2ab5b6a885d758aa5a27037e413cdd4d946436 -size 741912 diff --git a/docs/capabilities/navigation/native/assets/5-all.png b/docs/capabilities/navigation/native/assets/5-all.png deleted file mode 100644 index 655be72c1c..0000000000 --- a/docs/capabilities/navigation/native/assets/5-all.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a777d315beac6f4773adcb5c27384fd983720083941b4f62060958ddf6c16d2 -size 1209867 diff --git a/docs/capabilities/navigation/native/assets/go2_blueprint.svg b/docs/capabilities/navigation/native/assets/go2_blueprint.svg deleted file mode 100644 index 51b0e7c40f..0000000000 --- a/docs/capabilities/navigation/native/assets/go2_blueprint.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - -modules - -cluster_mapping - -mapping - - -cluster_navigation - -navigation - - -cluster_robot - -robot - - -cluster_visualization - -visualization - - - -CostMapper - -CostMapper - - - -chan_global_costmap_OccupancyGrid - - - -global_costmap:OccupancyGrid - - - -CostMapper->chan_global_costmap_OccupancyGrid - - - - -VoxelGridMapper - -VoxelGridMapper - - - -chan_global_map_PointCloud2 - - - -global_map:PointCloud2 - - - -VoxelGridMapper->chan_global_map_PointCloud2 - - - - -ReplanningAStarPlanner - -ReplanningAStarPlanner - - - -chan_cmd_vel_Twist - - - -cmd_vel:Twist - - - -ReplanningAStarPlanner->chan_cmd_vel_Twist - - - - -chan_goal_reached_Bool - - - -goal_reached:Bool - - - -ReplanningAStarPlanner->chan_goal_reached_Bool - - - - -WavefrontFrontierExplorer - -WavefrontFrontierExplorer - - - -chan_goal_request_PoseStamped - - - -goal_request:PoseStamped - - - -WavefrontFrontierExplorer->chan_goal_request_PoseStamped - - - - -GO2Connection - -GO2Connection - - - -chan_lidar_PointCloud2 - - - -lidar:PointCloud2 - - - -GO2Connection->chan_lidar_PointCloud2 - - - - -RerunBridgeModule - -RerunBridgeModule - - - -chan_cmd_vel_Twist->GO2Connection - - - - - -chan_global_costmap_OccupancyGrid->ReplanningAStarPlanner - - - - - -chan_global_costmap_OccupancyGrid->WavefrontFrontierExplorer - - - - - -chan_global_map_PointCloud2->CostMapper - - - - - -chan_goal_reached_Bool->WavefrontFrontierExplorer - - - - - -chan_goal_request_PoseStamped->ReplanningAStarPlanner - - - - - -chan_lidar_PointCloud2->VoxelGridMapper - - - - - diff --git a/docs/capabilities/navigation/native/assets/go2nav_dataflow.svg b/docs/capabilities/navigation/native/assets/go2nav_dataflow.svg deleted file mode 100644 index 94bb3e39ee..0000000000 --- a/docs/capabilities/navigation/native/assets/go2nav_dataflow.svg +++ /dev/null @@ -1,22 +0,0 @@ - - -Go2 - - - -VoxelGridMapper - - - -CostMapper - - - -Navigation -PointCloud2 -PointCloud2 -OccupancyGrid - - -Twist - diff --git a/docs/capabilities/navigation/native/assets/noros_nav.gif b/docs/capabilities/navigation/native/assets/noros_nav.gif deleted file mode 100644 index ab47bb9cb5..0000000000 --- a/docs/capabilities/navigation/native/assets/noros_nav.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:60f842cd2fda539338443b3c501197fbb875f5c5f3883ba3ffdd17005e9bd786 -size 612786 diff --git a/docs/capabilities/navigation/native/index.md b/docs/capabilities/navigation/native/index.md deleted file mode 100644 index 115c6f0ee2..0000000000 --- a/docs/capabilities/navigation/native/index.md +++ /dev/null @@ -1,144 +0,0 @@ -# Go2 Non-ROS Navigation - - - -The Go2 navigation stack runs entirely without ROS. It uses a **column-carving voxel map** strategy: each new LiDAR frame replaces the corresponding region of the global map entirely, ensuring the map always reflects the latest observations. - -## Data Flow - -
-diagram source - -```pikchr fold output=assets/go2nav_dataflow.svg -color = white -fill = none - -Go2: box "Go2" rad 5px fit wid 170% ht 170% -arrow right 0.5in -Vox: box "VoxelGridMapper" rad 5px fit wid 170% ht 170% -arrow right 0.5in -Cost: box "CostMapper" rad 5px fit wid 170% ht 170% -arrow right 0.5in -Nav: box "Navigation" rad 5px fit wid 170% ht 170% - -M1: dot at 1/2 way between Go2.e and Vox.w invisible -text "PointCloud2" italic at (M1.x, Go2.n.y + 0.15in) - -M2: dot at 1/2 way between Vox.e and Cost.w invisible -text "PointCloud2" italic at (M2.x, Vox.n.y + 0.15in) - -M3: dot at 1/2 way between Cost.e and Nav.w invisible -text "OccupancyGrid" italic at (M3.x, Cost.n.y + 0.15in) - -arrow dashed from Nav.s down 0.3in then left until even with Go2.s then to Go2.s -M4: dot at 1/2 way between Go2.s and Nav.s invisible -text "Twist" italic at (M4.x, Nav.s.y - 0.45in) -``` - -
- - -![output](assets/go2nav_dataflow.svg) -## Pipeline Steps - -### 1. LiDAR Frame — [`GO2Connection`](/dimos/robot/unitree/go2/connection.py) - -We don't connect to the LiDAR directly — instead we use Unitree's WebRTC client (via [legion's webrtc driver](https://github.com/legion1581/unitree_webrtc_connect)), which streams a heavily preprocessed 5cm voxel grid rather than raw point cloud data. This allows us to support stock, unjailbroken Go2 Air and Pro models out of the box. - -![LiDAR frame](assets/1-lidar.png) - -### 2. Global Voxel Map — [`VoxelGridMapper`](/dimos/mapping/voxels.py) - -The [`VoxelGridMapper`](/dimos/mapping/voxels.py) maintains a sparse 3D occupancy grid using Open3D's `VoxelBlockGrid` backed by a hash map. Each voxel is a 5cm cube by default. - -Voxel hash map provides O(1) insert/erase/lookup, so this is efficient even with millions of voxels. The grid runs on **CUDA** by default for speed, with CPU fallback. - -Each incoming LiDAR frame is spliced into the global map via column carving. We consider any previously mapped voxels in the space of a received LiDAR frame stale, by erasing entire Z-columns in the footprint, we guarantee: - -- No ghost obstacles from previous passes -- Dynamic objects (people, doors) get cleared automatically -- The latest observation always wins - -We don't have proper loop closure and stable odometry, we trust the data go2 odom reports, which is surprisingly stable but does drift eventually, You will reliably map and nav through very large spaces (500sqm in our tests) but you won't go down the street to a super market. - - -#### Configuration - -| Parameter | Default | Description | -|--------------------|-----------|---------------------------------------------------------| -| `voxel_size` | 0.05 | Voxel cube size in meters | -| `block_count` | 2,000,000 | Max voxels in hash map | -| `device` | `CUDA:0` | Compute device (`CUDA:0` or `CPU:0`) | -| `carve_columns` | `true` | Enable column carving (disable for append-only mapping) | -| `publish_interval` | 0 | Seconds between map publishes (0 = every frame) | - -![Global map](assets/2-globalmap.png) - -### 3. Global Costmap — [`CostMapper`](/dimos/mapping/costmapper.py) - -The [`CostMapper`](/dimos/mapping/costmapper.py) converts the 3D voxel map into a 2D occupancy grid. The default algorithm (`height_cost`) maps rate of change of Z, with some smoothing. - -algo settings are in [`occupancy.py`](/dimos/mapping/pointclouds/occupancy.py) and can be configured per robot - - -#### Configuration - -```python skip -@dataclass(frozen=True) -class HeightCostConfig(OccupancyConfig): - """Config for height-cost based occupancy (terrain slope analysis).""" - can_pass_under: float = 0.6 - can_climb: float = 0.15 - ignore_noise: float = 0.05 - smoothing: float = 1.0 -``` - -| Cost | Meaning | -|------|----------------------------------------------------------| -| 0 | Flat, easy to traverse | -| 50 | Moderate slope (~7.5cm rise per cell in case of go2) | -| 100 | Steep or impassable (≥15cm rise per cell in case of go2) | -| -1 | Unknown (no observations) | - -![Global costmap](assets/3-globalcostmap.png) - -### 4. Navigation Costmap — [`ReplanningAStarPlanner`](/dimos/navigation/replanning_a_star/module.py) - -The planner will process the terrain gradient and compute it's own algo-relevant costmap, prioritizing safe free paths, while be willing to path aggressively through tight spaces if it has to - -We run the planner in a constant loop so it will dynamically react to obstacles encountered. - -![Navigation costmap with path](assets/4-navcostmap.png) - -### 5. All Layers Combined - -All visualization layers shown together - -![All layers](assets/5-all.png) - -## Blueprint Composition - -The navigation stack is composed in the [`unitree_go2`](/dimos/robot/unitree/go2/blueprints/__init__.py) blueprint: - -```python fold output=assets/go2_blueprint.svg -from dimos.core.blueprints import autoconnect -from dimos.core.introspection import to_svg -from dimos.mapping.costmapper import cost_mapper -from dimos.mapping.voxels import voxel_mapper -from dimos.navigation.frontier_exploration import wavefront_frontier_explorer -from dimos.navigation.replanning_a_star.module import replanning_a_star_planner -from dimos.robot.unitree.go2.blueprints.basic.unitree_go2_basic import unitree_go2_basic - -unitree_go2 = autoconnect( - unitree_go2_basic, # robot connection + visualization - voxel_mapper(voxel_size=0.05), # 3D voxel mapping - cost_mapper(), # 2D costmap generation - replanning_a_star_planner(), # path planning - wavefront_frontier_explorer(), # exploration -).global_config(n_dask_workers=6, robot_model="unitree_go2") - -to_svg(unitree_go2, "assets/go2_blueprint.svg") -``` - - -![output](assets/go2_blueprint.svg) diff --git a/docs/capabilities/navigation/readme.md b/docs/capabilities/navigation/readme.md deleted file mode 100644 index af26c07f94..0000000000 --- a/docs/capabilities/navigation/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# Navigation - - -## Non-ROS - -- [Go2 Navigation](native/index.md) — column-carving voxel mapping + slope-based costmap - -## ROS - -See [ROS Transports](/docs/api/transports.md) for bridging DimOS streams to ROS topics. diff --git a/docs/capabilities/perception/readme.md b/docs/capabilities/perception/readme.md deleted file mode 100644 index 5d6e089dbf..0000000000 --- a/docs/capabilities/perception/readme.md +++ /dev/null @@ -1,3 +0,0 @@ -# Perception - -## Detections diff --git a/docs/development/README.md b/docs/development/README.md deleted file mode 100644 index 130e86fdaa..0000000000 --- a/docs/development/README.md +++ /dev/null @@ -1,273 +0,0 @@ -# Development Guide - -1. [How to set up your system](#1-setup) (pick one: system install, nix flake + direnv, pure nix flake) -2. [How to hack on DimOS](#2-how-to-hack-on-dimos) (which files to edit, debugging help, etc) -3. [How to make a PR](#3-how-to-make-a-pr) (our expectations for a PR) - -
- -# 1. Setup - -All the setup options are for your convenience. If you can get DimOS running on TempleOS with a package manager you wrote yourself, all the power to you. - ---- - -## Setup Option A: System Install - -### Why pick this option? (pros/cons/when-to-use) - -* Downside: mutates your global system, which can create side effects and make it less reliable -* Upside: Often good for a quick hack or exploring -* Upside: Sometimes easier for CUDA/GPU acceleration -* Use when: you understand system package management (arch linux user) or you don't care about making changes to your system - -### How to set up DimOS - -```bash -# System dependencies - -# On Ubuntu 22.04 or 24.04 -if [ "$OSTYPE" = "linux-gnu" ]; then - sudo apt-get update - sudo apt-get install -y curl g++ portaudio19-dev git-lfs libturbojpeg python3-dev pre-commit -# On macOS (12.6 or newer) -elif [ "$(uname)" = "Darwin" ]; then - # install homebrew - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - # install dependencies - brew install gnu-sed gcc portaudio git-lfs libjpeg-turbo python pre-commit -fi - -# install uv for python -curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH="$HOME/.local/bin:$PATH" - -# this allows getting large files on-demand -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - - -# create & activate a virtualenv (needed for dimos) -uv venv && . .venv/bin/activate - -# install dimos's python package with everything enabled -uv pip install -e '.[base,dev,manipulation,misc,unitree,drone]' - -# setup pre-commit -pre-commit install - -# test the install (takes about 1 minute) -uv run pytest dimos -``` - -Note, a few dependencies do not have PyPI packages and need to be installed from their Git repositories. These are only required for specific features: - -- **CLIP** and **detectron2**: Required for the Detic open-vocabulary object detector - -You can install them with: - -```bash -uv add git+https://github.com/openai/CLIP.git -uv add git+https://github.com/facebookresearch/detectron2.git -``` - -### Why pick this option? (pros/cons/when-to-use) - -* Upside: Reliable and consistent across OS's -* Upside: Unified formatting, linting and type-checking. -* Upside: Other than Docker, it won't touch your operating system (no side effects) -* Downside: It runs in a VM: slower, issues with GPU/CUDA, issues with hardware access like Webcam access, Networking, etc -* Upside: Your IDE-integrated vibe coding agent will "just work" -* Use when: You're not sure what option to pick - -### Quickstart - -First [install Docker](https://docs.docker.com/get-started/get-docker/) if you haven't already. - -Install the *Dev Containers* plug-in for VS Code, Cursor, or your IDE of choice. Clone the repo, open it in your IDE, and the IDE should prompt you to open in using a Dev Container. - -### Don't like IDE's? Use devcontainer CLI directly - -Terminal within your IDE should use devcontainer transparently given you installed the plugin, but in case you want to run our shell without an IDE, you can use `./bin/dev` -(it depends on npm/node being installed) - -
-Click to see how to use it in the command line - -```sh -./bin/dev -devcontainer CLI (https://github.com/devcontainers/cli) not found. Install into repo root? (y/n): y - -added 1 package, and audited 2 packages in 8s -found 0 vulnerabilities - -[1 ms] @devcontainers/cli 0.76.0. Node.js v20.19.0. linux 6.12.27-amd64 x64. -[4838 ms] Start: Run: docker start f0355b6574d9bd277d6eb613e1dc32e3bc18e7493e5b170e335d0e403578bcdb -[5299 ms] f0355b6574d9bd277d6eb613e1dc32e3bc18e7493e5b170e335d0e403578bcdb -{"outcome":"success","containerId":"f0355b6574d9bd277d6eb613e1dc32e3bc18e7493e5b170e335d0e403578bcdb","remoteUser":"root","remoteWorkspaceFolder":"/workspaces/dimos"} - - ██████╗ ██╗███╗ ███╗███████╗███╗ ██╗███████╗██╗ ██████╗ ███╗ ██╗ █████╗ ██╗ - ██╔══██╗██║████╗ ████║██╔════╝████╗ ██║██╔════╝██║██╔═══██╗████╗ ██║██╔══██╗██║ - ██║ ██║██║██╔████╔██║█████╗ ██╔██╗ ██║███████╗██║██║ ██║██╔██╗ ██║███████║██║ - ██║ ██║██║██║╚██╔╝██║██╔══╝ ██║╚██╗██║╚════██║██║██║ ██║██║╚██╗██║██╔══██║██║ - ██████╔╝██║██║ ╚═╝ ██║███████╗██║ ╚████║███████║██║╚██████╔╝██║ ╚████║██║ ██║███████╗ - ╚═════╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ - - v_unknown:unknown | Wed May 28 09:23:33 PM UTC 2025 - -root@dimos:/workspaces/dimos # -``` - -The script will: - -* Offer to npm install `@devcontainers/cli` locally (if not available globally) on first run. -* Pull `ghcr.io/dimensionalos/dev:dev` if not present (external contributors: we plan to mirror to Docker Hub). - -You’ll land in the workspace as **root** with all project tooling available. - -## Setup Option B: Nix Flake + direnv - -### Why pick this option? (pros/cons/when-to-use) - -* Upside: Faster and more reliable than Dev Containers (no emulation) -* Upside: Nearly as isolated as Docker, but has full hardware access (CUDA, Webcam, networking) -* Downside: Not hard, but you need to install/understand [direnv](https://direnv.net/) (which you probably should do anyway) -* Downside: Nix is not user-friendly (IDE integration is not as good as Dev Containers) -* Use when: you need reliability and don't mind a one-time startup delay - -### Quickstart - -Install and activate [direnv](https://direnv.net/). - -```sh -# Install Nix -curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install -. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh -# make sure flakes are enabled -mkdir -p "$HOME/.config/nix"; echo "experimental-features = nix-command flakes" >> "$HOME/.config/nix/nix.conf" - -# this allows getting large files on-demand -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -# activate the nix .envrc -cp .envrc.nix .envrc -# this is going to take a while -direnv allow -direnv reload -direnv status - -# create virtualenv (needed for dimos) -uv venv && . .venv/bin/activate -# install dimos's python package with everything enabled -uv pip install -e '.[base,dev,manipulation,misc,unitree,drone]' -# test the install (takes about 3 minutes) -uv run pytest dimos -``` - -## Setup Option C: Nix Flake - Isolated/Reliable - -### Why pick this option? (pros/cons/when-to-use) - -* Use when: you need absolute reliability (use this if you want it to work first try) and don't mind a startup delay -* Upside: Doesn't need direnv, and has most of the other benefits of Nix -* Downside: Have to manually enter the environment (like `./venv/bin/activate` but slower) -* Upside: If you're using a basic shell, you'll get a nicely customized shell -* Downside: If you have hyper-customized your shell (fish, riced zsh, etc), you'll have to deal with someone else's preferences -* Downside: Your vibe coding agent will basically be unable to run tests for you (they don't understand how to enter the environment) - -### Quickstart - -```sh -# Install Nix -curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install -# make sure flakes are enabled -mkdir -p "$HOME/.config/nix"; echo "experimental-features = nix-command flakes" >> "$HOME/.config/nix/nix.conf" - -# this allows getting large files on-demand -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -# activate the nix development shell -nix develop '.#isolated' -``` - -Once inside the shell, run: - -```sh -# create virtualenv (needed for dimos) -uv venv && . .venv/bin/activate -# install dimos's python package with everything enabled -uv pip install -e '.[base,dev,manipulation,misc,unitree,drone]' -# test the install (takes about 3 minutes) -uv run pytest dimos -``` - -
- -# 2. How to Hack on DimOS - -## Debugging - -Enable maximum logging: by adding `DIMOS_LOG_LEVEL=DEBUG RERUN_SAVE=1` as a prefix to the command. For example: - -```bash -DIMOS_LOG_LEVEL=DEBUG RERUN_SAVE=1 dimos run unitree-go2 -``` - -This will save the rerun data to `rerun.json` in the current directory. - -## Where is `` located? (Architecture) - -* If you want to add a `dimos run ` command see [dimos_run.md](/docs/development/dimos_run.md) -* If you want to add a camera driver see [depth_camera_integration.md](/docs/development/depth_camera_integration.md) -* For edits to manipulation see [manipulation](/dimos/hardware/manipulators/README.md) and the related modules under `dimos/manipulation/`. -* `dimos/core/`: Is where stuff like `Module`, `In`, `Out`, and `RPC` live. -* `dimos/robot/`: Robot-specific modules live here. -* `dimos/hardware/`: Are for sensors, end-effectors, and related individual hardware pieces. -* `dimos/msgs/`: If you're trying to find a message type to send over a stream, look here. -* `dimos/dashboard/`: Contains code related to visualization. -* `dimos/protocol/`: Defines low level stuff for communication between modules. -* See `dimos/` for the remainder - -## Testing - -We use both pytest and manual testing. - -```sh -pytest # run all tests at or below the current directory -``` - -### Testing Cheatsheet - -| Action | Command | -| --------------------------- | ---------------------------- | -| Run tests in current path | `pytest` | -| Filter tests by name | `pytest -k ""` | -| Enable stdout in tests | `pytest -s` | -| Run tagged tests | `pytest -m ` | - -We use tags for special tests, like `tool` for things that aren't meant to be run in CI and for cases that require hardware or visual inspection (pointcloud merging visualization, etc). - -You can enable a tag by selecting -m - these are configured in `./pyproject.toml` - -
- -# 3. How to Make a PR -- Open the PR against the `dev` branch (not `main`). -- **No matter what, provide a few-lines that, when run, let a reviewer test the feature you added** (assuming you changed functional python code). -- Less changed files = better. -- If you're writing documentation, see [writing docs](/docs/development/writing_docs.md) -- If you get mypy errors, please fix them. Don't just add # type: ignore. Please first understand why mypy is complaining and try to fix it. It's only okay to ignore if the issue cannot be fixed. -- If you made a change that is likely going to involve a debate, open the github UI and add a graphical comment on that code. Justify your choice and explain downsides of alternatives. -- We don't require 100% test coverage, but if you're making a PR of notable python changes you should probably either have unit tests or good reason why not (ex: visualization stuff is hard to test so we don't). -- Have the name of your PR start with `WIP:` if its not ready to merge but you want to show someone the changes. -- If you have large (>500kb) files, see [large file management](/docs/development/large_file_management.md) for how to store and load them (don't just commit them). -- So long as you don't disable pre-commit hooks the formatting, license headers, EOLs, LFS checks, etc will be handled automatically by [pre-commit](https://pre-commit.com). If something goes wrong with the hooks you can run the step manually with `pre-commit run --all-files`. -- If you're a new hire at DimOS: - - Did we mention smaller PR's are better? Smaller PR's are better. - - Only open a PR when you're okay with us spending AI tokens reviewing it (don't open a half-done PR and then fix it, wait till the code is mostly done). - - If there are 3 highly-intertwined bugs, make 3 PRs, not 1 PR. Yes it is more dev work, but review time is the bottleneck (not dev time). One line PR's are the easiest thing to review. - - When the AI (currently Greptile) comments on the code, respond. Sometimes Greptile is dumb as rocks but, as a reviewer, it's nice to see a finished conversation. diff --git a/docs/development/adding_a_custom_arm.md b/docs/development/adding_a_custom_arm.md deleted file mode 100644 index 2b435a50fe..0000000000 --- a/docs/development/adding_a_custom_arm.md +++ /dev/null @@ -1,730 +0,0 @@ -# How to Integrate a New Manipulator Arm - -This guide walks through integrating a new robot arm with DimOS, from writing the hardware adapter to creating blueprints for planning and control. - -## Architecture Overview - -DimOS uses a **Protocol-based adapter pattern** — no base class inheritance required. Your adapter wraps the vendor SDK and exposes a standard interface that the rest of the system consumes: - -``` -┌──────────────────────────────────────────────────────────────┐ -│ ManipulationModule (Planning) │ -│ - Plans collision-free trajectories using Drake │ -│ - Sends trajectories to coordinator via RPC │ -└───────────────────────┬──────────────────────────────────────┘ - │ RPC: execute trajectory -┌───────────────────────▼──────────────────────────────────────┐ -│ ControlCoordinator (100Hz control loop) │ -│ - Reads state from all adapters │ -│ - Runs tasks (trajectory, servo, velocity) │ -│ - Arbitrates per-joint conflicts (priority-based) │ -│ - Routes commands to the correct adapter │ -│ - Publishes aggregated joint state │ -└───────────────────────┬──────────────────────────────────────┘ - │ uses -┌───────────────────────▼──────────────────────────────────────┐ -│ Your Adapter (implements Protocol) │ -│ - Wraps vendor SDK (TCP/IP, CAN, serial, etc.) │ -│ - Converts between vendor units and SI units │ -│ - Handles connection lifecycle │ -└──────────────────────────────────────────────────────────────┘ -``` - -> See also: `dimos/hardware/manipulators/README.md` for a quick reference. - -## Prerequisites - -1. **Vendor SDK** — The Python SDK for your robot arm (e.g., `xarm-python-sdk`, `piper-sdk`) -2. **URDF/xacro** — A robot description file (only needed if you want motion planning) -3. **Connection info** — IP address, CAN port, serial device, etc. - -## Step 1: Create the Adapter - -Create a new directory for your arm under `dimos/hardware/manipulators/`: - -``` -dimos/hardware/manipulators/ -├── spec.py # ManipulatorAdapter Protocol (don't modify) -├── registry.py # Auto-discovery registry (don't modify) -├── mock/ -├── xarm/ -├── piper/ -└── yourarm/ # ← New directory - ├── __init__.py - └── adapter.py -``` - -### adapter.py — Full Skeleton - -Below is a complete annotated adapter. Implement each method by wrapping your vendor SDK calls. All values crossing the adapter boundary **must use SI units**. - -| Quantity | SI Unit | -|------------------|----------| -| Angles | radians | -| Angular velocity | rad/s | -| Torque | Nm | -| Position | meters | -| Force | Newtons | - -```python -"""YourArm adapter — implements ManipulatorAdapter protocol. - -SDK Units: -DimOS Units: angles=radians, distance=meters, velocity=rad/s -""" - -from __future__ import annotations - -import math -from typing import TYPE_CHECKING - -# Import your vendor SDK -from yourarm_sdk import YourArmSDK - -if TYPE_CHECKING: - from dimos.hardware.manipulators.registry import AdapterRegistry - -from dimos.hardware.manipulators.spec import ( - ControlMode, - JointLimits, - ManipulatorInfo, -) - -# Unit conversion constants (if your SDK doesn't use SI units) -MM_TO_M = 0.001 -M_TO_MM = 1000.0 - - -class YourArmAdapter: - """YourArm hardware adapter. - - Implements ManipulatorAdapter protocol via duck typing. - No inheritance required — just match the method signatures in spec.py. - """ - - def __init__(self, address: str, dof: int = 6) -> None: - """Initialize the adapter. - - Args: - address: Connection address (IP, CAN port, serial device, etc.) - dof: Degrees of freedom. - """ - if not address: - raise ValueError("address is required for YourArmAdapter") - self._address = address - self._dof = dof - self._sdk: YourArmSDK | None = None - self._control_mode: ControlMode = ControlMode.POSITION - - # ========================================================================= - # Connection - # ========================================================================= - - def connect(self) -> bool: - """Connect to hardware. Returns True on success.""" - try: - self._sdk = YourArmSDK(self._address) - self._sdk.connect() - # Verify connection succeeded - if not self._sdk.is_alive(): - print(f"ERROR: Arm at {self._address} not reachable") - return False - return True - except Exception as e: - print(f"ERROR: Failed to connect to arm at {self._address}: {e}") - return False - - def disconnect(self) -> None: - """Disconnect from hardware.""" - if self._sdk: - self._sdk.disconnect() - self._sdk = None - - def is_connected(self) -> bool: - """Check if connected.""" - return self._sdk is not None and self._sdk.is_alive() - - # ========================================================================= - # Info - # ========================================================================= - - def get_info(self) -> ManipulatorInfo: - """Get manipulator info (vendor, model, DOF).""" - return ManipulatorInfo( - vendor="YourVendor", - model="YourModel", - dof=self._dof, - firmware_version=None, # Optional: query from SDK if available - serial_number=None, # Optional: query from SDK if available - ) - - def get_dof(self) -> int: - """Get degrees of freedom.""" - return self._dof - - def get_limits(self) -> JointLimits: - """Get joint position and velocity limits in SI units. - - Either hardcode known limits or query them from the SDK. - """ - return JointLimits( - position_lower=[-math.pi] * self._dof, # radians - position_upper=[math.pi] * self._dof, # radians - velocity_max=[math.pi] * self._dof, # rad/s - ) - - # ========================================================================= - # Control Mode - # ========================================================================= - - def set_control_mode(self, mode: ControlMode) -> bool: - """Set control mode. - - Map DimOS ControlMode enum values to your SDK's mode codes. - Return False for modes your arm doesn't support. - """ - if not self._sdk: - return False - - mode_map = { - ControlMode.POSITION: 0, # Your SDK's position mode code - ControlMode.SERVO_POSITION: 1, # High-frequency servo mode - ControlMode.VELOCITY: 4, # Velocity mode - # Add other supported modes... - } - - sdk_mode = mode_map.get(mode) - if sdk_mode is None: - return False # Unsupported mode - - success = self._sdk.set_mode(sdk_mode) - if success: - self._control_mode = mode - return success - - def get_control_mode(self) -> ControlMode: - """Get current control mode.""" - return self._control_mode - - # ========================================================================= - # State Reading - # ========================================================================= - - def read_joint_positions(self) -> list[float]: - """Read current joint positions in radians. - - Convert from SDK units to radians. - """ - if not self._sdk: - raise RuntimeError("Not connected") - raw_positions = self._sdk.get_joint_positions() - return [math.radians(p) for p in raw_positions[:self._dof]] - - def read_joint_velocities(self) -> list[float]: - """Read current joint velocities in rad/s. - - If your SDK doesn't provide velocity feedback, return zeros. - The coordinator can estimate velocity via finite differences. - """ - if not self._sdk: - return [0.0] * self._dof - # If SDK supports velocity reading: - # raw_velocities = self._sdk.get_joint_velocities() - # return [math.radians(v) for v in raw_velocities[:self._dof]] - return [0.0] * self._dof - - def read_joint_efforts(self) -> list[float]: - """Read current joint torques in Nm. - - If your SDK doesn't provide torque feedback, return zeros. - """ - if not self._sdk: - return [0.0] * self._dof - # If SDK supports torque reading: - # return list(self._sdk.get_joint_torques()[:self._dof]) - return [0.0] * self._dof - - def read_state(self) -> dict[str, int]: - """Read robot state (mode, state code, etc).""" - if not self._sdk: - return {"state": 0, "mode": 0} - return { - "state": self._sdk.get_state(), - "mode": self._sdk.get_mode(), - } - - def read_error(self) -> tuple[int, str]: - """Read error code and message. (0, '') means no error.""" - if not self._sdk: - return 0, "" - code = self._sdk.get_error_code() - if code == 0: - return 0, "" - return code, f"YourArm error {code}" - - # ========================================================================= - # Motion Control (Joint Space) - # ========================================================================= - - def write_joint_positions( - self, - positions: list[float], - velocity: float = 1.0, - ) -> bool: - """Command joint positions in radians. - - Args: - positions: Target positions in radians. - velocity: Speed as fraction of max (0-1). - - Convert from radians to SDK units before sending. - """ - if not self._sdk: - return False - sdk_positions = [math.degrees(p) for p in positions] - return self._sdk.set_joint_positions(sdk_positions) - - def write_joint_velocities(self, velocities: list[float]) -> bool: - """Command joint velocities in rad/s. - - Return False if velocity control is not supported. - """ - if not self._sdk: - return False - sdk_velocities = [math.degrees(v) for v in velocities] - return self._sdk.set_joint_velocities(sdk_velocities) - - def write_stop(self) -> bool: - """Stop all motion immediately.""" - if not self._sdk: - return False - return self._sdk.emergency_stop() - - # ========================================================================= - # Servo Control - # ========================================================================= - - def write_enable(self, enable: bool) -> bool: - """Enable or disable servos.""" - if not self._sdk: - return False - return self._sdk.enable_motors(enable) - - def read_enabled(self) -> bool: - """Check if servos are enabled.""" - if not self._sdk: - return False - return self._sdk.motors_enabled() - - def write_clear_errors(self) -> bool: - """Clear error state.""" - if not self._sdk: - return False - return self._sdk.clear_errors() - - # ========================================================================= - # Optional: Cartesian Control - # Return None/False if not supported by your arm. - # ========================================================================= - - def read_cartesian_position(self) -> dict[str, float] | None: - """Read end-effector pose. - - Returns dict with keys: x, y, z (meters), roll, pitch, yaw (radians). - Return None if not supported. - """ - return None # Or implement if your SDK supports it - - def write_cartesian_position( - self, - pose: dict[str, float], - velocity: float = 1.0, - ) -> bool: - """Command end-effector pose. Return False if not supported.""" - return False - - # ========================================================================= - # Optional: Gripper - # ========================================================================= - - def read_gripper_position(self) -> float | None: - """Read gripper position in meters. Return None if no gripper.""" - return None - - def write_gripper_position(self, position: float) -> bool: - """Command gripper position in meters. Return False if no gripper.""" - return False - - # ========================================================================= - # Optional: Force/Torque Sensor - # ========================================================================= - - def read_force_torque(self) -> list[float] | None: - """Read F/T sensor data [fx, fy, fz, tx, ty, tz]. None if no sensor.""" - return None - - -# ── Registry hook (required for auto-discovery) ─────────────────── -def register(registry: AdapterRegistry) -> None: - """Register this adapter with the registry.""" - registry.register("yourarm", YourArmAdapter) - - -__all__ = ["YourArmAdapter"] -``` - -### Key implementation notes - -- **Unsupported features** — Return `None` for reads and `False` for writes. Never raise exceptions for optional features. -- **Velocity/effort feedback** — If your SDK doesn't provide these, return zeros. The coordinator handles this gracefully. -- **Lazy SDK import** — If the vendor SDK is an optional dependency, you can import it inside `connect()` instead of at module level (see Piper adapter for this pattern): - ```python - def connect(self) -> bool: - try: - from yourarm_sdk import YourArmSDK - self._sdk = YourArmSDK(self._address) - ... - except ImportError: - print("ERROR: yourarm-sdk not installed. Run: pip install yourarm-sdk") - return False - ``` - -## Step 2: Create Package Files - -### \_\_init\_\_.py - -```python -"""YourArm manipulator hardware adapter. - -Usage: - >>> from dimos.hardware.manipulators.yourarm import YourArmAdapter - >>> adapter = YourArmAdapter(address="192.168.1.100", dof=6) - >>> adapter.connect() - >>> positions = adapter.read_joint_positions() -""" - -from dimos.hardware.manipulators.yourarm.adapter import YourArmAdapter - -__all__ = ["YourArmAdapter"] -``` - -### How auto-discovery works - -The `AdapterRegistry` in `dimos/hardware/manipulators/registry.py` automatically discovers your adapter at import time: - -1. It iterates over all subpackages under `dimos/hardware/manipulators/` -2. For each subpackage, it tries to import `.adapter` -3. If that module has a `register()` function, it calls it - -This means **no manual registration is needed** — just having the `register()` function in your `adapter.py` is sufficient. - -You can verify discovery works: - -```python -from dimos.hardware.manipulators.registry import adapter_registry -print(adapter_registry.available()) # Should include "yourarm" -``` - -## Step 3: Create Your Robot Folder and Blueprints - -Each robot in DimOS gets its own folder under `dimos/robot/`. This is where you define all blueprints for your arm — coordinator, planning, perception, etc. This follows the same pattern as Unitree robots (`dimos/robot/unitree/`). - -### 3a. Create the robot directory - -``` -dimos/robot/ -├── unitree/ # Unitree robots (reference example) -│ ├── go2/ -│ │ └── blueprints/ -│ └── g1/ -│ └── blueprints/ -└── yourarm/ # ← New directory for your robot - ├── __init__.py - └── blueprints.py -``` - -### 3b. Define your blueprints - -Create `dimos/robot/yourarm/blueprints.py` with your coordinator and (optionally) planning blueprints: - -```python -"""Blueprints for YourArm robot. - -Usage: - # Run via CLI: - dimos run coordinator-yourarm # Start coordinator with real hardware - dimos run yourarm-planner # Start planner (optional, for motion planning) - - # Or programmatically: - from dimos.robot.yourarm.blueprints import coordinator_yourarm - coordinator = coordinator_yourarm.build() - coordinator.loop() -""" - -from __future__ import annotations - -from pathlib import Path - -from dimos.control.components import HardwareComponent, HardwareType, make_joints -from dimos.control.coordinator import TaskConfig, control_coordinator -from dimos.core.transport import LCMTransport -from dimos.msgs.sensor_msgs import JointState - -# ============================================================================= -# Coordinator Blueprints -# ============================================================================= - -# YourArm (6-DOF) — real hardware -coordinator_yourarm = control_coordinator( - tick_rate=100.0, # Control loop frequency (Hz) - publish_joint_state=True, # Publish aggregated joint state - joint_state_frame_id="coordinator", - hardware=[ - HardwareComponent( - hardware_id="arm", # Unique ID for this hardware - hardware_type=HardwareType.MANIPULATOR, - joints=make_joints("arm", 6), # Creates ["arm_joint1", ..., "arm_joint6"] - adapter_type="yourarm", # Must match registry name - address="192.168.1.100", # Passed to adapter __init__ - auto_enable=True, # Auto-enable servos on start - ), - ], - tasks=[ - TaskConfig( - name="traj_arm", # Task name (used by ManipulationModule RPC) - type="trajectory", # Trajectory execution task - joint_names=[f"arm_joint{i+1}" for i in range(6)], - priority=10, # Higher priority wins arbitration - ), - ], -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) - - -``` - -### Blueprint field reference - -| Field | Description | -|-------|-------------| -| `hardware_id` | Unique name for this hardware component. Used to route commands. | -| `adapter_type` | Name registered with `adapter_registry` (e.g., `"yourarm"`). | -| `address` | Connection info passed to adapter's `__init__` as `address` kwarg. | -| `joints` | List of joint names. `make_joints("arm", 6)` creates `["arm_joint1", ..., "arm_joint6"]`. | -| `auto_enable` | If `True`, servos are enabled automatically when the coordinator starts. | -| `task.name` | Name used by the ManipulationModule to invoke trajectory execution via RPC. | -| `task.type` | Task type: `"trajectory"`, `"servo"`, `"velocity"`, or `"cartesian_ik"`. | -| `task.priority` | Priority for per-joint arbitration. Higher number wins. | - -## Step 4: Add URDF and Planning Integration (Optional) - -If you want motion planning (collision-free trajectories via Drake), you need a URDF and a planning blueprint. Add these to your robot's own `blueprints.py`. - -### 4a. Add your URDF - -Place your URDF/xacro files under LFS data so they can be resolved via `LfsPath`. `LfsPath` is a `Path` subclass that lazily downloads LFS data on first access — this avoids downloading at import time when the blueprint module is loaded. - -```python -from dimos.utils.data import LfsPath -from dimos.manipulation.manipulation_module import manipulation_module -from dimos.manipulation.planning.spec import RobotModelConfig -from dimos.msgs.geometry_msgs import PoseStamped, Quaternion, Vector3 - -# LfsPath defers download until the path is actually accessed -_YOURARM_URDF_PATH = LfsPath("yourarm_description/urdf/yourarm.urdf") -_YOURARM_PACKAGE_PATH = LfsPath("yourarm_description") - - -def _make_base_pose(x=0.0, y=0.0, z=0.0) -> PoseStamped: - return PoseStamped( - position=Vector3(x=x, y=y, z=z), - orientation=Quaternion(0.0, 0.0, 0.0, 1.0), - ) -``` - -### 4b. Create a robot model config helper - -```python -def _make_yourarm_config( - name: str = "arm", - y_offset: float = 0.0, - joint_prefix: str = "", - coordinator_task: str | None = None, -) -> RobotModelConfig: - """Create YourArm robot config for planning. - - Args: - name: Robot name in the Drake planning world. - y_offset: Y-axis offset for multi-arm setups. - joint_prefix: Prefix for joint name mapping to coordinator namespace. - coordinator_task: Coordinator task name for trajectory execution via RPC. - """ - # These must match the joint names in your URDF - joint_names = ["joint1", "joint2", "joint3", "joint4", "joint5", "joint6"] - joint_mapping = {f"{joint_prefix}{j}": j for j in joint_names} if joint_prefix else {} - - return RobotModelConfig( - name=name, - urdf_path=_YOURARM_URDF_PATH, - base_pose=_make_base_pose(y=y_offset), - joint_names=joint_names, - end_effector_link="link6", # Last link in your URDF's kinematic chain - base_link="base_link", # Root link of your URDF - package_paths={"yourarm_description": _YOURARM_PACKAGE_PATH}, - xacro_args={}, # Xacro arguments if using .xacro files - collision_exclusion_pairs=[], # Pairs of links that can touch (e.g., gripper fingers) - auto_convert_meshes=True, # Convert DAE/STL meshes for Drake - max_velocity=1.0, # Max velocity scaling factor - max_acceleration=2.0, # Max acceleration scaling factor - joint_name_mapping=joint_mapping, - coordinator_task_name=coordinator_task, - ) -``` - -### 4c. Create a planning blueprint - -Add this to your `dimos/robot/yourarm/blueprints.py` alongside the coordinator blueprint: - -```python -# ============================================================================= -# Planner Blueprints (requires URDF) -# ============================================================================= - -yourarm_planner = manipulation_module( - robots=[_make_yourarm_config("arm", joint_prefix="arm_", coordinator_task="traj_arm")], - planning_timeout=10.0, - enable_viz=True, -).transports( - { - ("joint_state", JointState): LCMTransport("/coordinator/joint_state", JointState), - } -) -``` - -### Key config fields - -| Field | Description | -|-------|-------------| -| `urdf_path` | Path to `.urdf` or `.xacro` file | -| `joint_names` | Ordered list of controlled joints (must match URDF) | -| `end_effector_link` | Link to use as the end-effector for IK | -| `base_link` | Root link of the robot model | -| `package_paths` | Maps `package://` URIs to filesystem paths (for xacro) | -| `joint_name_mapping` | Maps coordinator names (e.g., `"arm_joint1"`) to URDF names (e.g., `"joint1"`) | -| `coordinator_task_name` | Must match the `TaskConfig.name` in your coordinator blueprint | -| `collision_exclusion_pairs` | List of `(link_a, link_b)` tuples for links that may legitimately touch (e.g., gripper fingers) | - -## Step 5: Register Blueprints - -The blueprint registry in `dimos/robot/all_blueprints.py` is **auto-generated** by scanning the codebase for blueprint declarations. After adding your blueprints: - -1. Run the generation test to update the registry: - ```bash - pytest dimos/robot/test_all_blueprints_generation.py - ``` -3. Now you can run your arm via CLI: - ```bash - dimos run coordinator-yourarm - dimos run yourarm-planner # If you added a planning blueprint - ``` - -## Step 6: Testing - -### Verify adapter registration - -```python -from dimos.hardware.manipulators.registry import adapter_registry - -# Check your adapter shows up -assert "yourarm" in adapter_registry.available() - -# Create an instance via registry (same path the coordinator uses) -adapter = adapter_registry.create("yourarm", address="192.168.1.100", dof=6) -``` - -### Unit test with mock - -You can test coordinator logic without hardware by using `unittest.mock`: - -```python -import pytest -from unittest.mock import MagicMock -from dimos.hardware.manipulators.spec import ManipulatorAdapter - -@pytest.fixture -def mock_adapter(): - adapter = MagicMock(spec=ManipulatorAdapter) - adapter.get_dof.return_value = 6 - adapter.read_joint_positions.return_value = [0.0] * 6 - adapter.read_joint_velocities.return_value = [0.0] * 6 - adapter.read_joint_efforts.return_value = [0.0] * 6 - adapter.write_joint_positions.return_value = True - adapter.read_enabled.return_value = True - adapter.is_connected.return_value = True - return adapter - -def test_read_positions(mock_adapter): - assert mock_adapter.read_joint_positions() == [0.0] * 6 - -def test_write_positions(mock_adapter): - target = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] - assert mock_adapter.write_joint_positions(target) is True -``` - -### Integration test with coordinator - -```python -from dimos.control.blueprints import coordinator_mock - -# Build and start coordinator with mock hardware -coordinator = coordinator_mock.build() -coordinator.start() - -# Your adapter is tested through the same coordinator interface -# Just swap adapter_type="mock" to adapter_type="yourarm" in a blueprint -``` - -### Test the real adapter standalone - -```python -from dimos.hardware.manipulators.yourarm import YourArmAdapter - -adapter = YourArmAdapter(address="192.168.1.100", dof=6) -assert adapter.connect() is True -assert adapter.is_connected() is True - -# Read state -positions = adapter.read_joint_positions() -assert len(positions) == 6 -print(f"Joint positions (rad): {positions}") - -# Enable and move -adapter.write_enable(True) -adapter.write_joint_positions([0.0] * 6) - -# Cleanup -adapter.write_stop() -adapter.disconnect() -``` - -## Quick Reference Checklist - -Files to create: - -- [ ] `dimos/hardware/manipulators/yourarm/__init__.py` -- [ ] `dimos/hardware/manipulators/yourarm/adapter.py` (implements Protocol + `register()`) -- [ ] `dimos/robot/yourarm/__init__.py` -- [ ] `dimos/robot/yourarm/blueprints.py` (coordinator + planning blueprints) - -Files to modify: - -- [ ] `pyproject.toml` — Add vendor SDK to optional dependencies *(if applicable)* - -Verification: - -- [ ] `adapter_registry.available()` includes `"yourarm"` -- [ ] `pytest dimos/robot/test_all_blueprints_generation.py` passes (regenerates `all_blueprints.py`) -- [ ] `dimos run coordinator-yourarm` starts successfully diff --git a/docs/development/assets/docker-hierarchy.svg b/docs/development/assets/docker-hierarchy.svg deleted file mode 100644 index 7c84d6aa9f..0000000000 --- a/docs/development/assets/docker-hierarchy.svg +++ /dev/null @@ -1,31 +0,0 @@ - - -ubuntu:22.04 - -ubuntu:22.04 -Non-ROS Track -ROS Track - - - -python - - - -dev - - - -ros - - - -ros-python - - - -ros-dev - - -same dockerfiles - diff --git a/docs/development/assets/get_data_flow.svg b/docs/development/assets/get_data_flow.svg deleted file mode 100644 index d875e1dadb..0000000000 --- a/docs/development/assets/get_data_flow.svg +++ /dev/null @@ -1,25 +0,0 @@ - - -get_data(name) - - - -Check -data/{name} - - - -Return path - - - -Pull LFS - - - -Decompress - - - -Return path - diff --git a/docs/development/depth_camera_integration.md b/docs/development/depth_camera_integration.md deleted file mode 100644 index e152394262..0000000000 --- a/docs/development/depth_camera_integration.md +++ /dev/null @@ -1,147 +0,0 @@ -# Depth Camera Integration Guide - -This folder contains camera drivers and modules for RGB-D (depth) cameras such as RealSense and ZED. -Use this guide to add a new depth camera, wire TF correctly, and publish the required streams. - -## Add a New Depth Camera - -1) **Create a new driver module** - - Path: `dimos/hardware/sensors/camera//camera.py` - - Export a blueprint in `/__init__.py` (match the `realsense` / `zed` pattern). - -2) **Define config** - - Inherit from `ModuleConfig` and `DepthCameraConfig`: - ```python - @dataclass - class MyDepthCameraConfig(ModuleConfig, DepthCameraConfig): - width: int = 1280 - height: int = 720 - fps: int = 15 - camera_name: str = "camera" - base_frame_id: str = "base_link" - base_transform: Transform | None = field(default_factory=default_base_transform) - align_depth_to_color: bool = True - enable_depth: bool = True - enable_pointcloud: bool = False - pointcloud_fps: float = 5.0 - camera_info_fps: float = 1.0 - ``` - -3) **Implement the module** - - Inherit from `DepthCameraHardware` and `Module` (see `RealSenseCamera` / `ZEDCamera`). - - Provide these outputs (matching `RealSenseCamera` / `ZEDCamera`): - - `color_image: Out[Image]` - - `depth_image: Out[Image]` - - `pointcloud: Out[PointCloud2]` (optional, can be disabled by config) - - `camera_info: Out[CameraInfo]` - - `depth_camera_info: Out[CameraInfo]` - - Implement RPCs: - - `start()` / `stop()` - - `get_color_camera_info()` / `get_depth_camera_info()` - - `get_depth_scale()` (meters per depth unit) - -4) **Publish frames** - - Color images: `Image(format=ImageFormat.RGB, frame_id=_color_optical_frame)` - - Depth images: - - If `align_depth_to_color`: use `_color_optical_frame` - - Else: use `_depth_optical_frame` - - CameraInfo frame_id must match the image frame_id you publish. - -5) **Publish camera info** - - Build `CameraInfo` from camera intrinsics. - - Publish at `camera_info_fps`. - -6) **Publish pointcloud (optional)** - - Use `PointCloud2.from_rgbd(color_image, depth_image, camera_info, depth_scale)`. - - Publish at `pointcloud_fps`. - -## TF: Required Frames and Transforms - -Frame names are defined by the abstract depth camera spec (`dimos/hardware/sensors/camera/spec.py`). -Use the properties below to ensure consistent naming: - -- `_camera_link`: base link for the camera module (usually `{camera_name}_link`) -- `_color_frame`: non-optical color frame -- `_color_optical_frame`: optical color frame -- `_depth_frame`: non-optical depth frame -- `_depth_optical_frame`: optical depth frame - -Recommended transform chain (publish every frame or at your preferred TF rate): - -1) **Mounting transform** (from config): - - `base_frame_id -> _camera_link` - - Use `config.base_transform` if provided - -2) **Depth frame** - - `_camera_link -> _depth_frame` (identity unless the camera provides extrinsics) - - `_depth_frame -> _depth_optical_frame` using `OPTICAL_ROTATION` - -3) **Color frame** - - `_camera_link -> _color_frame` (from extrinsics, or identity if unavailable) - - `_color_frame -> _color_optical_frame` using `OPTICAL_ROTATION` - -Notes: -- If you align depth to color, keep TFs the same but publish depth images in `_color_optical_frame`. -- Ensure `color_image.frame_id` and `camera_info.frame_id` match. Same for depth. - -## Required Streams / Topics - -Use these stream names in your module and attach transports as needed. -Default LCM topics in `realsense` / `zed` demos are shown below. - -| Stream name | Type | Suggested topic | Frame ID source | -|-------------------|--------------|-------------------------|-----------------| -| `color_image` | `Image` | `/camera/color` | `_color_optical_frame` | -| `depth_image` | `Image` | `/camera/depth` | `_depth_optical_frame` or `_color_optical_frame` | -| `pointcloud` | `PointCloud2`| `/camera/pointcloud` | (derived from CameraInfo) | -| `camera_info` | `CameraInfo` | `/camera/color_info` | matches `color_image` | -| `depth_camera_info` | `CameraInfo` | `/camera/depth_info` | matches `depth_image` | - -For `ObjectSceneRegistrationModule`, the required inputs are: -- `color_image` -- `depth_image` -- `camera_info` -- TF tree resolving `target_frame` to `color_image.frame_id` - -## Object Scene Registration (Brief Overview) - -`ObjectSceneRegistrationModule` consumes synchronized RGB + depth + camera intrinsics and produces: -- 2D detections (YOLO‑E) -- 3D detections (projected via depth + intrinsics + TF) -- Overlay annotations and aggregated pointclouds - -See: -- `dimos/perception/object_scene_registration.py` -- `dimos/perception/demo_object_scene_registration.py` - -Quick wiring example: - -```python -from dimos.core.blueprints import autoconnect -from dimos.hardware.sensors.camera.realsense import realsense_camera -from dimos.perception.object_scene_registration import object_scene_registration_module - -pipeline = autoconnect( - realsense_camera(enable_pointcloud=False), - object_scene_registration_module(target_frame="world"), -) -``` - -Run the demo via CLI: -```bash -dimos run demo-object-scene-registration -``` - -## Foxglove (Viewer) - -Install Foxglove from: -- https://foxglove.dev/download - -## Modules and Skills (Short Intro) - -- **Modules** are typed components with `In[...]` / `Out[...]` streams and `start()` / `stop()` lifecycles. -- **Skills** are callable methods (decorated with `@skill`) on any `Module`, automatically discovered by agents. - -Reference: -- Modules overview: `/docs/usage/modules.md` -- TF fundamentals: `/docs/usage/transforms.md` diff --git a/docs/development/dimos_run.md b/docs/development/dimos_run.md deleted file mode 100644 index 3e6bee65e6..0000000000 --- a/docs/development/dimos_run.md +++ /dev/null @@ -1,79 +0,0 @@ -# DimOS Run - -#### Warning: If you just want to run a blueprint you don't need to add it to `dimos run`: - -`your_code.py` -```python -from dimos.robot.unitree_webrtc.unitree_go2_blueprints import basic as example_blueprint - -if __name__ == "__main__": - example_blueprint.build().loop() -``` - -```sh -python ./your_code.py -``` - -## Usage - -For example, to run the standard Unitree Go2 blueprint run: - -```bash -dimos run unitree-go2 -``` - -For the one with agents run: - -```bash -dimos run unitree-go2-agentic -``` - -You can dynamically connect additional modules. For example: - -```bash -dimos run unitree-go2 --extra-module agent --extra-module navigation_skill -``` - -## Adding your own - -Blueprints can be defined anywhere, but they're all linked together in `dimos/robot/all_blueprints.py`. E.g.: - -```python -all_blueprints = { - "unitree-go2": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:standard", - "unitree-go2-agentic": "dimos.robot.unitree_webrtc.unitree_go2_blueprints:agentic", - ... -} -``` - -(They are defined as imports to avoid triggering unrelated imports.) - -## `GlobalConfig` - -This tool also initializes the global config and passes it to the blueprint. - -`GlobalConfig` contains configuration options that are useful across many modules. For example: - -```python -class GlobalConfig(BaseSettings): - robot_ip: str | None = None - simulation: bool = False - replay: bool = False - n_dask_workers: int = 2 -``` - -Configuration values can be set from multiple places in order of precedence (later entries override earlier ones): - -- Default value defined on GlobalConfig. (`simulation = False`) -- Value defined in `.env` (`SIMULATION=true`) -- Value in the environment variable (`SIMULATION=true`) -- Value defined on the blueprint (`blueprint.global_config(simulation=True)`) -- Value coming from the CLI (`--simulation` or `--no-simulation`) - -For environment variables/`.env` values, you have to prefix the name with `DIMOS_`. - -For the command line, you call it like this: - -```bash -dimos --simulation run unitree-go2 -``` diff --git a/docs/development/docker.md b/docs/development/docker.md deleted file mode 100644 index 2f3f4a98ec..0000000000 --- a/docs/development/docker.md +++ /dev/null @@ -1,162 +0,0 @@ -# Docker Images - -Dimos uses parallel Docker image hierarchies for ROS and non-ROS builds, allowing you to choose the environment that fits your use case. - -## Image Hierarchy - -
Pikchr - -```pikchr fold output=assets/docker-hierarchy.svg -color = white -fill = none - -# Base images -U1: box "ubuntu:22.04" rad 5px fit wid 170% ht 170% -U2: box "ubuntu:22.04" rad 5px fit wid 170% ht 170% at (U1.x + 2.5in, U1.y) - -# Labels -text "Non-ROS Track" at (U1.x, U1.y + 0.5in) -text "ROS Track" at (U2.x, U2.y + 0.5in) - -# Non-ROS track -arrow from U1.s down 0.4in -P: box "python" rad 5px fit wid 170% ht 170% -arrow from P.s down 0.4in -D: box "dev" rad 5px fit wid 170% ht 170% - -# ROS track -arrow from U2.s down 0.4in -R: box "ros" rad 5px fit wid 170% ht 170% -arrow from R.s down 0.4in -RP: box "ros-python" rad 5px fit wid 170% ht 170% -arrow from RP.s down 0.4in -RD: box "ros-dev" rad 5px fit wid 170% ht 170% - -# Cross-reference: same dockerfiles reused -line dashed from P.e right 0.3in then down until even with RP then right to RP.w -line dashed from D.e right 0.3in then down until even with RD then right to RD.w -text "same dockerfiles" at (D.e.x + 1.2in, D.e.y + 0.4in) -``` - -
- - -![output](assets/docker-hierarchy.svg) - - -## Images - -All images are published to `ghcr.io/dimensionalos/`. - -| Image | Base | Purpose | -|--------------|-----------------------------|----------------------------------------------------| -| `python` | ubuntu:22.04 | Core dimos with Python dependencies, no ROS | -| `dev` | python | Development environment (editors, git, pre-commit) | -| `ros` | ubuntu:22.04 | ROS2 Humble with navigation packages | -| `ros-python` | ros | ROS + dimos Python dependencies | -| `ros-dev` | ros-python | Full ROS development environment | - -## Tags - -Images are tagged based on the git branch: - -| Branch | Tag | -|------------------|-------------------------------------------------| -| `main` | `latest` | -| `dev` | `dev` | -| feature branches | sanitized branch name (e.g., `feature_foo_bar`) | - -## When to Use Each Image - -### Non-ROS Track (`python` → `dev`) - -```sh skip -docker run -it ghcr.io/dimensionalos/dev:latest bash -``` - -### ROS Track (`ros` → `ros-python` → `ros-dev`) - -Use when you need ROS2 integration: -- Robot hardware control via ROS topics -- Navigation stack integration -- ROS message passing between components -- Running ROS tests (`pytest -m ros`) - -```sh skip -docker run -it ghcr.io/dimensionalos/ros-dev:latest bash -``` - -## Local Development - -### Building Images Locally - -Use the helper script: - -```sh skip -./bin/dockerbuild python # Build python image -./bin/dockerbuild dev # Build dev image -./bin/dockerbuild ros # Build ros image -``` - -## CI/CD Pipeline - -The workflow in [`.github/workflows/docker.yml`](/.github/workflows/docker.yml) handles: - -1. **Change detection** - Only rebuilds images when relevant files change -2. **Parallel builds** - ROS and non-ROS tracks build independently -3. **Cascade rebuilds** - Changes to base images trigger downstream rebuilds -4. **Test execution** - Tests run in the freshly built images - -### Trigger Paths - -| Image | Triggers on changes to | -|----------|------------------------------------------------------| -| `ros` | `docker/ros/**`, workflow files | -| `python` | `docker/python/**`, `pyproject.toml`, workflow files | -| `dev` | `docker/dev/**` | - -### Test Jobs - -After images build, tests run in parallel: - -| Job | Image | Command | -|-------------------------|---------|---------------------------| -| `run-tests` | dev | `pytest` | -| `run-ros-tests` | ros-dev | `pytest && pytest -m ros` | -| `run-heavy-tests` | dev | `pytest -m heavy` | -| `run-lcm-tests` | dev | `pytest -m lcm` | -| `run-integration-tests` | dev | `pytest -m integration` | -| `run-mypy` | ros-dev | `mypy dimos` | - -## Dockerfile Structure - -### Common Patterns - -All Dockerfiles accept a `FROM_IMAGE` build arg for flexibility: - -```dockerfile skip -ARG FROM_IMAGE=ubuntu:22.04 -FROM ${FROM_IMAGE} -``` - -This allows the same Dockerfile (e.g., `python`) to build on different bases. - -### Python Package Installation - -Images use [uv](https://github.com/astral-sh/uv) for fast dependency installation: - -```dockerfile skip -ENV UV_SYSTEM_PYTHON=1 -RUN curl -LsSf https://astral.sh/uv/install.sh | sh -RUN uv pip install '.[misc,cpu,sim,drone,unitree,web,perception,visualization]' -``` - -### Dev Image Features - -The dev image ([`docker/dev/Dockerfile`](/docker/dev/Dockerfile)) adds: -- Git, git-lfs, pre-commit -- Editors (nano, vim) -- tmux with custom config -- Node.js (via nvm) -- Custom bash prompt with version info -- Entrypoint script that sources ROS setup diff --git a/docs/development/grid_testing.md b/docs/development/grid_testing.md deleted file mode 100644 index e5daab7b32..0000000000 --- a/docs/development/grid_testing.md +++ /dev/null @@ -1,116 +0,0 @@ -# Grid Testing Strategy - -Grid tests run the same test logic across multiple implementations or configurations using pytest's parametrize feature. - -## Case Type Pattern - -Define a `Case` dataclass that holds everything needed to run tests against a specific implementation: - -```python -from collections.abc import Callable, Iterator -from contextlib import AbstractContextManager -from dataclasses import dataclass, field -from typing import Any, Generic - -@dataclass -class Case(Generic[TopicT, MsgT]): - name: str # For pytest id - pubsub_context: Callable[[], AbstractContextManager[...]] # Context manager factory - topic_values: list[tuple[TopicT, MsgT]] # Pre-generated test data (always 3 pairs) - tags: set[str] = field(default_factory=set) # Capability tags for filtering - - def __iter__(self) -> Iterator[Any]: - """Makes Case work with pytest.parametrize unpacking.""" - return iter((self.pubsub_context, self.topic_values)) -``` - -## Capability Tags - -Use tags to indicate what features each implementation supports: - -```python -testcases = [ - Case( - name="lcm_typed", - pubsub_context=lcm_typed_context, - topic_values=[...], - tags={"all", "glob", "regex"}, # LCM supports all pattern types - ), - Case( - name="shm_pickle", - pubsub_context=shm_context, - topic_values=[...], - tags={"all"}, # SharedMemory only supports subscribe_all - ), -] -``` - -## Filtered Test Lists - -Build separate lists for each capability to use with parametrize: - -```python -all_cases = [c for c in testcases if "all" in c.tags] -glob_cases = [c for c in testcases if "glob" in c.tags] -regex_cases = [c for c in testcases if "regex" in c.tags] -``` - -## Test Functions - -Use the filtered lists in parametrize decorators: - -```python -@pytest.mark.parametrize("case", all_cases, ids=lambda c: c.name) -def test_subscribe_all(case: Case) -> None: - with case.pubsub_context() as pubsub: - # Test logic using case.topic_values - ... - -@pytest.mark.parametrize("case", glob_cases, ids=lambda c: c.name) -def test_subscribe_glob(case: Case) -> None: - if not glob_cases: - pytest.skip("no implementations support glob") - with case.pubsub_context() as pubsub: - ... -``` - -## Context Managers - -Each implementation provides a context manager factory: - -```python -@contextmanager -def lcm_typed_context() -> Generator[LCM, None, None]: - lcm = LCM(autoconf=True) - lcm.start() - yield lcm - lcm.stop() -``` - -## Test Data Guidelines - -- Always provide exactly 3 topic/value pairs for consistency -- For typed implementations, use different types per topic to verify type handling -- For bytes implementations, use simple distinguishable byte strings - -```python -# Typed test data - different types per topic -typed_topic_values = [ - (Topic("/sensor/position", Vector3), Vector3(1, 2, 3)), - (Topic("/sensor/orientation", Quaternion), Quaternion(0, 0, 0, 1)), - (Topic("/robot/pose", Pose), Pose(...)), -] - -# Bytes test data -bytes_topic_values = [ - (Topic("/topic1"), b"msg1"), - (Topic("/topic2"), b"msg2"), - (Topic("/topic3"), b"msg3"), -] -``` - -## Examples - -- `dimos/protocol/pubsub/test_spec.py` - Basic pubsub operations -- `dimos/protocol/pubsub/test_subscribe_all.py` - Pattern subscriptions -- `dimos/protocol/pubsub/benchmark/testdata.py` - Benchmark cases diff --git a/docs/development/large_file_management.md b/docs/development/large_file_management.md deleted file mode 100644 index 18eedd23bb..0000000000 --- a/docs/development/large_file_management.md +++ /dev/null @@ -1,206 +0,0 @@ -# Data Loading - -The [`get_data`](/dimos/utils/data.py) function provides access to test data and model files, handling Git LFS downloads automatically. - -## Basic Usage - -```python -from dimos.utils.data import get_data - -# Get path to a data file/directory -data_path = get_data("cafe.jpg") -print(f"Path: {data_path}") -print(f"Exists: {data_path.exists()}") -``` - - -``` -Path: /home/lesh/coding/dimos/data/cafe.jpg -Exists: True -``` - -## How It Works - -
Pikchr - -```pikchr fold output=assets/get_data_flow.svg -color = white -fill = none - -A: box "get_data(name)" rad 5px fit wid 170% ht 170% -arrow right 0.4in -B: box "Check" "data/{name}" rad 5px fit wid 170% ht 170% - -# Branch: exists -arrow from B.e right 0.3in then up 0.4in then right 0.3in -C: box "Return path" rad 5px fit wid 170% ht 170% - -# Branch: missing -arrow from B.e right 0.3in then down 0.4in then right 0.3in -D: box "Pull LFS" rad 5px fit wid 170% ht 170% -arrow right 0.3in -E: box "Decompress" rad 5px fit wid 170% ht 170% -arrow right 0.3in -F: box "Return path" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/get_data_flow.svg) - -1. Checks if `data/{name}` already exists locally -2. If missing, pulls the `.tar.gz` archive from Git LFS -3. Decompresses the archive to `data/` -4. Returns the `Path` to the extracted file/directory - -## Common Patterns - -### Loading Images - -```python -from dimos.utils.data import get_data -from dimos.msgs.sensor_msgs import Image - -image = Image.from_file(get_data("cafe.jpg")) -print(f"Image shape: {image.data.shape}") -``` - - -``` -Image shape: (771, 1024, 3) -``` - -### Loading Model Checkpoints - -```python -from dimos.utils.data import get_data - -model_dir = get_data("models_yolo") -checkpoint = model_dir / "yolo11n.pt" -print(f"Checkpoint: {checkpoint.name} ({checkpoint.stat().st_size // 1024}KB)") -``` - - -``` -Checkpoint: yolo11n.pt (5482KB) -``` - -### Loading Recorded Data for Replay - -```python -from dimos.utils.data import get_data -from dimos.utils.testing.replay import TimedSensorReplay - -data_dir = get_data("unitree_office_walk") -replay = TimedSensorReplay(data_dir / "lidar") -print(f"Replay {replay} loaded from: {data_dir.name}") -print(replay.find_closest_seek(1)) -``` - - -``` -Replay loaded from: unitree_office_walk -{'type': 'msg', 'topic': 'rt/utlidar/voxel_map_compressed', 'data': {'stamp': 1751591000.0, 'frame_id': 'odom', 'resolution': 0.05, 'src_size': 77824, 'origin': [-3.625, -3.275, -0.575], 'width': [128, 128, 38], 'data': {'points': array([[ 2.725, -1.025, -0.575], - [ 2.525, -0.275, -0.575], - [ 2.575, -0.275, -0.575], - ..., - [ 2.675, -0.525, 0.775], - [ 2.375, 1.175, 0.775], - [ 2.325, 1.225, 0.775]], shape=(22730, 3))}}} -``` - -### Loading Point Clouds - -```python -from dimos.utils.data import get_data -from dimos.mapping.pointclouds.util import read_pointcloud - -pointcloud = read_pointcloud(get_data("apartment") / "sum.ply") -print(f"Loaded pointcloud with {len(pointcloud.points)} points") -``` - - -``` -Loaded pointcloud with 63672 points -``` - -## Data Directory Structure - -Data files live in `data/` at the repo root. Large files are stored in `data/.lfs/` as `.tar.gz` archives tracked by Git LFS. - -
Diagram - -```diagon fold mode=Tree -data/ - cafe.jpg - apartment/ - sum.ply - .lfs/ - cafe.jpg.tar.gz - apartment.tar.gz -``` - -
- - -``` -data/ - ├──cafe.jpg - ├──apartment/ - │ └──sum.ply - └──.lfs/ - ├──cafe.jpg.tar.gz - └──apartment.tar.gz -``` - - -## Adding New Data - -### Small Files (< 1MB) - -Commit directly to `data/`: - -```sh skip -cp my_image.jpg data/ - -# 2. Compress and upload to LFS -./bin/lfs_push - -git add data/.lfs/my_image.jpg.tar.gz - -git commit -m "Add test image" -``` - -### Large Files or Directories - -Use the LFS workflow: - -```sh skip -# 1. Copy data to data/ -cp -r my_dataset/ data/ - -# 2. Compress and upload to LFS -./bin/lfs_push - -git add data/.lfs/my_dataset.tar.gz - -# 3. Commit the .tar.gz reference -git commit -m "Add my_dataset test data" -``` - -The [`lfs_push`](/bin/lfs_push) script: -1. Compresses `data/my_dataset/` → `data/.lfs/my_dataset.tar.gz` -2. Uploads to Git LFS -3. Stages the compressed file - -A pre-commit hook ([`bin/hooks/lfs_check`](/bin/hooks/lfs_check#L26)) blocks commits if you have uncompressed directories in `data/` without a corresponding `.tar.gz` in `data/.lfs/`. - -## Location Resolution - -When running from: -- **Git repo**: Uses `{repo}/data/` -- **Installed package**: Clones repo to user data dir: - - Linux: `~/.local/share/dimos/repo/data/` - - macOS: `~/Library/Application Support/dimos/repo/data/` - - Fallback: `/tmp/dimos/repo/data/` diff --git a/docs/development/testing.md b/docs/development/testing.md deleted file mode 100644 index c27a8c5dec..0000000000 --- a/docs/development/testing.md +++ /dev/null @@ -1,122 +0,0 @@ -# Testing - -For development, you should install all dependencies so that tests have access to them. - -```bash -uv sync --all-extras --no-extra dds -``` - -## Types of tests - -There are different types of tests based on what their goal is: - -| Type | Description | Mocking | Speed | -|------|-------------|---------|-------| -| Unit | Test a small individual piece of code | All dependencies | Very fast | -| Integration | Test the integration between multiple units of code | Most dependencies | Some fast, some slow | -| Functional | Test a particular desired functionality | Some dependencies | Some fast, some slow | -| End-to-end | Test the entire system as a whole from the perspective of the user | None | Very slow | - -The distinction between unit, integration, and functional tests is often debated and rarely productive. - -Rather than waste time on classifying tests, it's better to separate tests by how they are used: - -* **fast tests**: tests which you can run after each code change (people often run them with filesystem watchers: whenever a file is saved, automatically run the tests) -* **slow tests**: tests which you run every once in a while to make sure you haven't broken anything (maybe every commit, but definitely before publishing a PR) - -The purpose of running tests in a loop is to get immediate feedback. The faster the loop, the easier it is to identify a problem since the source is the tiny bit of code you changed. - -## Usage - -### Fast tests - -Run the fast tests: - -```bash -./bin/pytest-fast -``` - -This is the same as: - -```bash -pytest dimos -``` - -The default `addopts` in `pyproject.toml` includes a `-m` filter that excludes slow markers (like `integration`, `heavy`, `e2e`, etc.), so plain `pytest dimos` only runs fast tests. - -### Slow tests - -Run the slow tests: - -```bash -./bin/pytest-slow -``` - -This overrides the default `-m` filter to include most markers. When writing or debugging a specific slow test, override `-m` yourself: - -```bash -pytest -m integration dimos/path/to/test_something.py -``` - -Note: passing `-m` on the command line overrides the default from `addopts`, so you get exactly the marker set you asked for. - -## Writing tests - -Test files live next to the code they test. If you have `dimos/core/pubsub.py`, its tests go in `dimos/core/test_pubsub.py`. - -When writing tests you probably want to limit the run to whatever tests you're writing: - -```bash -pytest -sv dimos/core/test_my_code.py -``` - -### Fixtures - -Pytest fixtures are very useful for making sure test failures don't affect other tests. - -Whenever you have something that needs to be cleaned up when the test is over (disconnect, close, delete temp files, etc.), you should use a fixture. - -Simple example code: - -```python -@pytest.fixture -def arm(): - arm = RobotArm(device="/dev/ttyUSB0") - arm.connect() - yield arm - arm.disconnect() - -def test_arm_moves_to_position(arm): - arm.move_to(x=0.5, y=0.3, z=0.1) - assert arm.position == (0.5, 0.3, 0.1) -``` - -The `yield` is key: everything before it is setup, everything after is teardown. The teardown runs even if the test fails, so you never leak resources between tests. - -### Mocking - -It's easier to use the `mocker` fixture instead of `unittest.mock`. It automatically undoes all patches when the test ends, so you don't need `with` blocks. - -Patching a method: - -```python -def test_uses_cached_position(mocker): - mocker.patch("dimos.hardware.RobotArm.get_position", return_value=(0.0, 0.0, 0.0)) - arm = RobotArm() - assert arm.get_position() == (0.0, 0.0, 0.0) -``` - -There are other useful things in `mocker`, like `mocker.MagicMock()` for creating fake objects. - -## Useful pytest options - -| Option | Description | -|--------|-------------| -| `-s` | Show stdout/stderr output | -| `-v` | More verbose test names | -| `-x` | Stop on first failure | -| `-k foo` | Only run tests matching `foo` | -| `--lf` | Rerun only the tests that failed last time | -| `--pdb` | Drop into the debugger when a test fails | -| `--tb=short` | Shorter tracebacks | -| `--durations=0` | Measure the speed of each test | diff --git a/docs/development/writing_docs.md b/docs/development/writing_docs.md deleted file mode 100644 index 58466d6592..0000000000 --- a/docs/development/writing_docs.md +++ /dev/null @@ -1,7 +0,0 @@ -# Writing Docs - -1. Where to put your docs: - - If it only matters to people who contribute to dimos (like this doc), put them in `docs/development` - - Otherwise put them in `docs/usage` -2. Run `bin/gen_diagrams` to generate the svg's for your diagrams. We use [pikchr](https://pikchr.org/home/doc/trunk/doc/userman.md) as a diagram language. -3. Use [md-babel-py](https://github.com/leshy/md-babel-py/) (`md-babel-py run thing.md`) to make sure your code examples work. diff --git a/docs/hardware/integration_guide.md b/docs/hardware/integration_guide.md deleted file mode 100644 index 805e6ad418..0000000000 --- a/docs/hardware/integration_guide.md +++ /dev/null @@ -1,3 +0,0 @@ -# New Hardware Integration Guide - -TODO: Document how to add support for new hardware platforms. diff --git a/docs/installation/nix.md b/docs/installation/nix.md deleted file mode 100644 index 557bb6608a..0000000000 --- a/docs/installation/nix.md +++ /dev/null @@ -1,57 +0,0 @@ -# Nix install (required for nix managed dimos) - -You need to have [nix](https://nixos.org/) installed and [flakes](https://nixos.wiki/wiki/Flakes) enabled, - -[official install docs](https://nixos.org/download/) recommended, but here is a quickstart: - -```sh -# Install Nix https://nixos.org/download/ -curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install -. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh - -# make sure nix-flakes are enabled -mkdir -p "$HOME/.config/nix"; echo "experimental-features = nix-command flakes" >> "$HOME/.config/nix/nix.conf" -``` - -# Using DimOS as a library - -```sh -mkdir myproject && cd myproject - -# pull the flake (needed for nix develop outside the repo) -wget https://raw.githubusercontent.com/dimensionalOS/dimos/refs/heads/main/flake.nix -wget https://raw.githubusercontent.com/dimensionalOS/dimos/refs/heads/main/flake.lock - -# enter the nix development shell (provides system deps) -nix develop - -python3 -m venv .venv -source .venv/bin/activate - -# install everything (depending on your use case you might not need all extras, -# check your respective platform guides) -pip install "dimos[misc,sim,visualization,agents,web,perception,unitree,manipulation,cpu,dev]" -``` - -# Developing on DimOS - -```sh -# this allows getting large files on-demand (and not pulling all immediately) -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -# enter the nix development shell (provides system deps) -nix develop - -python3 -m venv .venv -source .venv/bin/activate - -pip install -e ".[misc,sim,visualization,agents,web,perception,unitree,manipulation,cpu,dev]" - -# type check -mypy dimos - -# tests (around a minute to run) -pytest dimos -``` diff --git a/docs/installation/osx.md b/docs/installation/osx.md deleted file mode 100644 index da8916dd1a..0000000000 --- a/docs/installation/osx.md +++ /dev/null @@ -1,41 +0,0 @@ -# macOS Install (12.6 or newer) - -```sh -# install homebrew -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -# install dependencies -brew install gnu-sed gcc portaudio git-lfs libjpeg-turbo python pre-commit - -# install uv -curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH="$HOME/.local/bin:$PATH" -``` - -# Using DimOS as a library - -```sh -mkdir myproject && cd myproject - -uv venv --python 3.12 -source .venv/bin/activate - -# install everything (depending on your use case you might not need all extras, -# check your respective platform guides) -uv pip install dimos[misc,sim,visualization,agents,web,perception,unitree,manipulation,cpu,dev] -``` - -# Developing on DimOS - -```sh -# this allows getting large files on-demand (and not pulling all immediately) -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -uv sync --all-extras --no-extra dds - -# type check -uv run mypy dimos - -# tests (around a minute to run) -uv run pytest dimos -``` diff --git a/docs/installation/ubuntu.md b/docs/installation/ubuntu.md deleted file mode 100644 index 8ff47329d5..0000000000 --- a/docs/installation/ubuntu.md +++ /dev/null @@ -1,39 +0,0 @@ -# System Dependencies Install (Ubuntu 22.04 or 24.04) - -```sh -sudo apt-get update -sudo apt-get install -y curl g++ portaudio19-dev git-lfs libturbojpeg python3-dev pre-commit - -# install uv -curl -LsSf https://astral.sh/uv/install.sh | sh && export PATH="$HOME/.local/bin:$PATH" -``` - -# Using DimOS as a library - -```sh -mkdir myproject && cd myproject - -uv venv --python 3.12 -source .venv/bin/activate - -# install everything (depending on your use case you might not need all extras, -# check your respective platform guides) -uv pip install dimos[misc,sim,visualization,agents,web,perception,unitree,manipulation,cpu,dev] -``` - -# Developing on DimOS - -```sh -# this allows getting large files on-demand (and not pulling all immediately) -export GIT_LFS_SKIP_SMUDGE=1 -git clone -b dev https://github.com/dimensionalOS/dimos.git -cd dimos - -uv sync --all-extras --no-extra dds - -# type check -uv run mypy dimos - -# tests (around a minute to run) -uv run pytest dimos -``` diff --git a/docs/platforms/quadruped/go2/index.md b/docs/platforms/quadruped/go2/index.md deleted file mode 100644 index 40f32bcdd2..0000000000 --- a/docs/platforms/quadruped/go2/index.md +++ /dev/null @@ -1,113 +0,0 @@ -# Unitree Go2 — Getting Started - -The Unitree Go2 is DimOS's primary reference platform. Full autonomous navigation, mapping, and agentic control — no ROS required. - -## Requirements - -- Unitree Go2 Pro or Air (stock firmware 1.1.7+, no jailbreak needed) -- Ubuntu 22.04/24.04 with CUDA GPU (recommended), or macOS (experimental) -- Python 3.12 - -## Install - -First, install system dependencies for your platform: -- [Ubuntu](../../../installation/ubuntu.md) -- [macOS](../../../installation/osx.md) -- [Nix](../../../installation/nix.md) - -Then install DimOS: - -```bash -uv venv --python "3.12" -source .venv/bin/activate -uv pip install dimos[base,unitree] -``` - -## Try It — No Hardware Needed - -```bash -# Replay a recorded Go2 navigation session -# First run downloads ~2.4 GB of LiDAR/video data from LFS -dimos --replay run unitree-go2 -``` - -Opens the command center at [localhost:7779](http://localhost:7779) with Rerun 3D visualization — watch the Go2 map and navigate an office in real time. - -## Run on Your Go2 - -```bash -export ROBOT_IP= -dimos run unitree-go2 -``` - -That's it. DimOS connects via WebRTC (no jailbreak required), starts the full navigation stack, and opens the command center. - -> **Tip:** Keep the Unitree built-in obstacle avoidance enabled on the robot for now. DimOS handles path planning, but the onboard obstacle avoidance provides an extra safety layer. - -### What's Running - -| Module | What It Does | -|--------|-------------| -| **GO2Connection** | WebRTC connection to the robot — streams LiDAR, video, odometry | -| **VoxelGridMapper** | Builds a 3D voxel map using column-carving (CUDA accelerated) | -| **CostMapper** | Converts 3D map → 2D costmap via terrain slope analysis | -| **ReplanningAStarPlanner** | Continuous A* path planning with dynamic replanning | -| **WavefrontFrontierExplorer** | Autonomous exploration of unmapped areas | -| **RerunBridge** | 3D visualization in browser | -| **WebsocketVis** | Command center at localhost:7779 | - -### Send Goals - -From the command center ([localhost:7779](http://localhost:7779)): -- Click on the map to set navigation goals -- Toggle autonomous exploration -- Monitor robot pose, costmap, and planned path - -## MuJoCo Simulation - -```bash -uv pip install dimos[base,unitree,sim] -dimos --simulation run unitree-go2 -``` - -Full navigation stack in MuJoCo — same code, simulated robot. - -## Agentic Control - -Natural language control with an LLM agent that understands physical space: - -```bash -export OPENAI_API_KEY= -export ROBOT_IP= -dimos run unitree-go2-agentic -``` - -Then use the human CLI to talk to the agent: - -```bash -humancli -> explore the space -``` - -The agent subscribes to camera, LiDAR, and spatial memory streams — it sees what the robot sees. - -## Available Blueprints - -| Blueprint | Description | -|-----------|-------------| -| `unitree-go2-basic` | Connection + visualization (no navigation) | -| `unitree-go2` | Full navigation stack | -| `unitree-go2-agentic` | Navigation + LLM agent | -| `unitree-go2-agentic-ollama` | Agent with local Ollama models | -| `unitree-go2-agentic-mcp` | Agent with MCP tool access | -| `unitree-go2-spatial` | Navigation + spatial memory | -| `unitree-go2-detection` | Navigation + object detection | -| `unitree-go2-ros` | ROS 2 bridge mode | - -## Deep Dive - -- [Navigation Stack](../../../capabilities/navigation/native/index.md) — column-carving voxel mapping, costmap generation, A* planning -- [Visualization](../../../usage/visualization.md) — Rerun, Foxglove, performance tuning -- [Data Streams](../../../usage/data_streams/) — RxPY streams, backpressure, quality filtering -- [Transports](../../../usage/transports/index.md) — LCM, SHM, DDS -- [Blueprints](../../../usage/blueprints.md) — composing modules diff --git a/docs/todo.md b/docs/todo.md deleted file mode 100644 index 464090415c..0000000000 --- a/docs/todo.md +++ /dev/null @@ -1 +0,0 @@ -# TODO diff --git a/docs/usage/README.md b/docs/usage/README.md deleted file mode 100644 index 071b6fc0b2..0000000000 --- a/docs/usage/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Concepts - -This page explains general concepts. - -## Table of Contents - -- [Modules](/docs/usage/modules.md): The primary units of deployment in DimOS, modules run in parallel and are python classes. -- [Streams](/docs/usage/sensor_streams/README.md): How modules communicate, a Pub / Sub system. -- [Blueprints](/docs/usage/blueprints.md): a way to group modules together and define their connections to each other. -- [RPC](/docs/usage/blueprints.md#calling-the-methods-of-other-modules): how one module can call a method on another module (arguments get serialized to JSON-like binary data). -- [Skills](/docs/usage/blueprints.md#defining-skills): An RPC function, except it can be called by an AI agent (a tool for an AI). -- Agents: AI that has an objective, access to stream data, and is capable of calling skills as tools. diff --git a/docs/usage/assets/abstraction_layers.svg b/docs/usage/assets/abstraction_layers.svg deleted file mode 100644 index 0903cfbf27..0000000000 --- a/docs/usage/assets/abstraction_layers.svg +++ /dev/null @@ -1,20 +0,0 @@ - - -Blueprints - - - -Modules - - - -Transports - - - -PubSub -robot configs -camera, nav -LCM, SHM, ROS -pub/sub API - diff --git a/docs/usage/assets/camera_module.svg b/docs/usage/assets/camera_module.svg deleted file mode 100644 index 48cc4286db..0000000000 --- a/docs/usage/assets/camera_module.svg +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - -module - -cluster_outputs - - -cluster_rpcs - -RPCs - - -cluster_skills - -Skills - - - -CameraModule - -CameraModule - - - -out_color_image - - - -color_image:Image - - - -CameraModule->out_color_image - - - - - -out_camera_info - - - -camera_info:CameraInfo - - - -CameraModule->out_camera_info - - - - - -rpc_set_transport - -set_transport(stream_name: str, transport: Transport) -> bool - - - -CameraModule->rpc_set_transport - - - - -skill_video_stream - -video_stream stream=passive reducer=latest_reducer - - - -CameraModule->skill_video_stream - - - - -rpc_start - -start() - - - diff --git a/docs/usage/assets/go2_agentic.svg b/docs/usage/assets/go2_agentic.svg deleted file mode 100644 index f20c1b5ac5..0000000000 --- a/docs/usage/assets/go2_agentic.svg +++ /dev/null @@ -1,260 +0,0 @@ - - - - - - -modules - -cluster_agents - -agents - - -cluster_mapping - -mapping - - -cluster_navigation - -navigation - - -cluster_perception - -perception - - -cluster_robot - -robot - - - -HumanInput - -HumanInput - - - -LlmAgent - -LlmAgent - - - -NavigationSkillContainer - -NavigationSkillContainer - - - -SpeakSkill - -SpeakSkill - - - -WebInput - -WebInput - - - -CostMapper - -CostMapper - - - -chan_global_costmap_OccupancyGrid - - - -global_costmap:OccupancyGrid - - - -CostMapper->chan_global_costmap_OccupancyGrid - - - - -VoxelGridMapper - -VoxelGridMapper - - - -chan_global_map_LidarMessage - - - -global_map:LidarMessage - - - -VoxelGridMapper->chan_global_map_LidarMessage - - - - -ReplanningAStarPlanner - -ReplanningAStarPlanner - - - -chan_cmd_vel_Twist - - - -cmd_vel:Twist - - - -ReplanningAStarPlanner->chan_cmd_vel_Twist - - - - -chan_goal_reached_Bool - - - -goal_reached:Bool - - - -ReplanningAStarPlanner->chan_goal_reached_Bool - - - - -WavefrontFrontierExplorer - -WavefrontFrontierExplorer - - - -chan_goal_request_PoseStamped - - - -goal_request:PoseStamped - - - -WavefrontFrontierExplorer->chan_goal_request_PoseStamped - - - - -SpatialMemory - -SpatialMemory - - - -FoxgloveBridge - -FoxgloveBridge - - - -GO2Connection - -GO2Connection - - - -chan_color_image_Image - - - -color_image:Image - - - -GO2Connection->chan_color_image_Image - - - - -chan_lidar_LidarMessage - - - -lidar:LidarMessage - - - -GO2Connection->chan_lidar_LidarMessage - - - - -UnitreeSkillContainer - -UnitreeSkillContainer - - - -chan_cmd_vel_Twist->GO2Connection - - - - - -chan_color_image_Image->NavigationSkillContainer - - - - - -chan_color_image_Image->SpatialMemory - - - - - -chan_global_costmap_OccupancyGrid->ReplanningAStarPlanner - - - - - -chan_global_costmap_OccupancyGrid->WavefrontFrontierExplorer - - - - - -chan_global_map_LidarMessage->CostMapper - - - - - -chan_goal_reached_Bool->WavefrontFrontierExplorer - - - - - -chan_goal_request_PoseStamped->ReplanningAStarPlanner - - - - - -chan_lidar_LidarMessage->VoxelGridMapper - - - - - diff --git a/docs/usage/assets/go2_nav.svg b/docs/usage/assets/go2_nav.svg deleted file mode 100644 index 25adae5264..0000000000 --- a/docs/usage/assets/go2_nav.svg +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - -modules - -cluster_mapping - -mapping - - -cluster_navigation - -navigation - - -cluster_robot - -robot - - - -CostMapper - -CostMapper - - - -chan_global_costmap_OccupancyGrid - - - -global_costmap:OccupancyGrid - - - -CostMapper->chan_global_costmap_OccupancyGrid - - - - -VoxelGridMapper - -VoxelGridMapper - - - -chan_global_map_LidarMessage - - - -global_map:LidarMessage - - - -VoxelGridMapper->chan_global_map_LidarMessage - - - - -ReplanningAStarPlanner - -ReplanningAStarPlanner - - - -chan_cmd_vel_Twist - - - -cmd_vel:Twist - - - -ReplanningAStarPlanner->chan_cmd_vel_Twist - - - - -chan_goal_reached_Bool - - - -goal_reached:Bool - - - -ReplanningAStarPlanner->chan_goal_reached_Bool - - - - -WavefrontFrontierExplorer - -WavefrontFrontierExplorer - - - -chan_goal_request_PoseStamped - - - -goal_request:PoseStamped - - - -WavefrontFrontierExplorer->chan_goal_request_PoseStamped - - - - -FoxgloveBridge - -FoxgloveBridge - - - -GO2Connection - -GO2Connection - - - -chan_lidar_LidarMessage - - - -lidar:LidarMessage - - - -GO2Connection->chan_lidar_LidarMessage - - - - -chan_cmd_vel_Twist->GO2Connection - - - - - -chan_global_costmap_OccupancyGrid->ReplanningAStarPlanner - - - - - -chan_global_costmap_OccupancyGrid->WavefrontFrontierExplorer - - - - - -chan_global_map_LidarMessage->CostMapper - - - - - -chan_goal_reached_Bool->WavefrontFrontierExplorer - - - - - -chan_goal_request_PoseStamped->ReplanningAStarPlanner - - - - - -chan_lidar_LidarMessage->VoxelGridMapper - - - - - diff --git a/docs/usage/assets/lcmspy.png b/docs/usage/assets/lcmspy.png deleted file mode 100644 index 6e68fde03a..0000000000 --- a/docs/usage/assets/lcmspy.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:91da9ef9f7797cce332da448739e28591f7ecfc0fd674e8b4be973cf28331438 -size 7118 diff --git a/docs/usage/assets/pubsub_benchmark.png b/docs/usage/assets/pubsub_benchmark.png deleted file mode 100644 index 759a8b3977..0000000000 --- a/docs/usage/assets/pubsub_benchmark.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:728484a4358df18ced7b5763a88a962701c2b02b5d319eb9a8b28c6c72d009fe -size 23946 diff --git a/docs/usage/assets/transforms.png b/docs/usage/assets/transforms.png deleted file mode 100644 index 49dba4ab9a..0000000000 --- a/docs/usage/assets/transforms.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6597e0008197902e321a3ad3dfb1e838f860fa7ca1277c369ed6ff7da8bf757d -size 101102 diff --git a/docs/usage/assets/transforms_chain.svg b/docs/usage/assets/transforms_chain.svg deleted file mode 100644 index 3f6c21741b..0000000000 --- a/docs/usage/assets/transforms_chain.svg +++ /dev/null @@ -1,12 +0,0 @@ - - -base_link - - - -camera_link - - - -camera_optical - diff --git a/docs/usage/assets/transforms_modules.svg b/docs/usage/assets/transforms_modules.svg deleted file mode 100644 index 08e7c309a5..0000000000 --- a/docs/usage/assets/transforms_modules.svg +++ /dev/null @@ -1,20 +0,0 @@ - - -world - - - -base_link - - - -camera_link - - - -camera_optical - -RobotBaseModule - -CameraModule - diff --git a/docs/usage/assets/transforms_tree.svg b/docs/usage/assets/transforms_tree.svg deleted file mode 100644 index f95f1a6621..0000000000 --- a/docs/usage/assets/transforms_tree.svg +++ /dev/null @@ -1,26 +0,0 @@ - - -world - - - -robot_base - - - -camera_link - - - -camera_optical -mug here - - - -arm_base - - - -gripper -target here - diff --git a/docs/usage/blueprints.md b/docs/usage/blueprints.md deleted file mode 100644 index 54b52ba3c0..0000000000 --- a/docs/usage/blueprints.md +++ /dev/null @@ -1,316 +0,0 @@ -# Blueprints - -Blueprints (`_BlueprintAtom`) are instructions for how to initialize a `Module`. - -You don't typically want to run a single module, so multiple blueprints are handled together in `Blueprint`. - -You create a `Blueprint` from a single module (say `ConnectionModule`) with: - -```python session=blueprint-ex1 -from dimos.core.blueprints import Blueprint -from dimos.core import Module, rpc - -class ConnectionModule(Module): - def __init__(self, arg1, arg2, kwarg='value') -> None: - super().__init__() - -blueprint = Blueprint.create(ConnectionModule, 'arg1', 'arg2', kwarg='value') -``` - -But the same thing can be accomplished more succinctly as: - -```python session=blueprint-ex1 -connection = ConnectionModule.blueprint -``` - -Now you can create the blueprint with: - -```python session=blueprint-ex1 -blueprint = connection('arg1', 'arg2', kwarg='value') -``` - -## Linking blueprints - -You can link multiple blueprints together with `autoconnect`: - -```python session=blueprint-ex1 -from dimos.core.blueprints import autoconnect - -class Module1(Module): - def __init__(self, arg1) -> None: - super().__init__() - -class Module2(Module): - ... - -class Module3(Module): - ... - -module1 = Module1.blueprint -module2 = Module2.blueprint -module3 = Module3.blueprint - -blueprint = autoconnect( - module1(), - module2(), - module3(), -) -``` - -`blueprint` itself is a `Blueprint` so you can link it with other modules: - -```python session=blueprint-ex1 -class Module4(Module): - ... - -class Module5(Module): - ... - -module4 = Module4.blueprint -module5 = Module5.blueprint - -expanded_blueprint = autoconnect( - blueprint, - module4(), - module5(), -) -``` - -Blueprints are frozen data classes, and `autoconnect()` always constructs an expanded blueprint so you never have to worry about changes in one affecting the other. - -### Duplicate module handling - -If the same module appears multiple times in `autoconnect`, the **later blueprint wins** and overrides earlier ones: - -```python session=blueprint-ex1 -blueprint = autoconnect( - module1(arg1=1), - module2(), - module1(arg1=2), # This one is used, the first is discarded -) -``` - -This is so you can "inherit" from one blueprint but override something you need to change. - -## How transports are linked - -Imagine you have this code: - -```python session=blueprint-ex1 -from functools import partial - -from dimos.core.blueprints import Blueprint, autoconnect -from dimos.core import Module, rpc, Out, In -from dimos.msgs.sensor_msgs import Image - -class ModuleA(Module): - image: Out[Image] - start_explore: Out[bool] - -class ModuleB(Module): - image: In[Image] - begin_explore: In[bool] - -module_a = partial(Blueprint.create, ModuleA) -module_b = partial(Blueprint.create, ModuleB) - -autoconnect(module_a(), module_b()) -``` - -Connections are linked based on `(property_name, object_type)`. In this case `('image', Image)` will be connected between the two modules, but `begin_explore` will not be linked to `start_explore`. - -## Topic names - -By default, the name of the property is used to generate the topic name. So for `image`, the topic will be `/image`. - -The property name is used only if it's unique. If two modules have the same property name with different types, then both get a random topic such as `/SGVsbG8sIFdvcmxkI`. - -If you don't like the name you can always override it like in the next section. - -## Which transport is used? - -By default `LCMTransport` is used if the object supports `lcm_encode`. If it doesn't `pLCMTransport` is used (meaning "pickled LCM"). - -You can override transports with the `transports` method. It returns a new blueprint in which the override is set. - -```python session=blueprint-ex1 -from dimos.core.transport import pSHMTransport, pLCMTransport - -base_blueprint = autoconnect( - module1(arg1=1), - module2(), -) -expanded_blueprint = autoconnect( - base_blueprint, - module4(), - module5(), -) -base_blueprint = base_blueprint.transports({ - ("image", Image): pSHMTransport( - "/go2/color_image", default_capacity=1920 * 1080 * 3, # 1920x1080 frame x 3 (RGB) x uint8 - ), - ("start_explore", bool): pLCMTransport("/start_explore"), -}) -``` - -Note: `expanded_blueprint` does not get the transport overrides because it's created from the initial value of `base_blueprint`, not the second. - -## Remapping connections - -Sometimes you need to rename a connection to match what other modules expect. You can use `remappings` to rename module connections: - -```python session=blueprint-ex2 -from dimos.core.blueprints import autoconnect -from dimos.core import Module, rpc, Out, In -from dimos.msgs.sensor_msgs import Image - -class ConnectionModule(Module): - color_image: Out[Image] # Outputs on 'color_image' - -class ProcessingModule(Module): - rgb_image: In[Image] # Expects input on 'rgb_image' - -# Without remapping, these wouldn't connect automatically -# With remapping, color_image is renamed to rgb_image -blueprint = ( - autoconnect( - ConnectionModule.blueprint(), - ProcessingModule.blueprint(), - ) - .remappings([ - (ConnectionModule, 'color_image', 'rgb_image'), - ]) -) -``` - -After remapping: -- The `color_image` output from `ConnectionModule` is treated as `rgb_image` -- It automatically connects to any module with an `rgb_image` input of type `Image` -- The topic name becomes `/rgb_image` instead of `/color_image` - -If you want to override the topic, you still have to do it manually: - -```python session=blueprint-ex2 -from dimos.core.transport import LCMTransport -blueprint.remappings([ - (ConnectionModule, 'color_image', 'rgb_image'), -]).transports({ - ("rgb_image", Image): LCMTransport("/custom/rgb/image", Image), -}) -``` - -## Overriding global configuration. - -Each module can optionally take global config as a `cfg` option in `__init__`. E.g.: - -```python session=blueprint-ex3 -from dimos.core import Module, rpc -from dimos.core.global_config import GlobalConfig - -class ModuleA(Module): - - def __init__(self, cfg: GlobalConfig | None = None): - self._global_config: GlobalConfig = cfg - ... -``` - -The config is normally taken from .env or from environment variables. But you can specifically override the values for a specific blueprint: - -```python session=blueprint-ex3 -blueprint = ModuleA.blueprint().global_config(n_dask_workers=8) -``` - -## Calling the methods of other modules - -Imagine you have this code: - -```python session=blueprint-ex3 -from dimos.core import Module, rpc - -class Drone(Module): - - @rpc - def get_time(self) -> str: - ... - -class HelperModule(Module): - def set_alarm_clock(self) -> None: - ... -``` - -And you want to call `ModuleA.get_time` in `ModuleB.request_the_time`. - -To do this, you can request a module reference. - -```python session=blueprint-ex3 -from dimos.core import Module, rpc - -class HelperModule(Module): - drone_module: Drone - - def set_alarm_clock(self) -> None: - print(self.drone_module.get_time_rpc()) -``` - -But what if we want `HelperModule` to work for more than just `Drone`? For that we can use a spec. - -```python session=blueprint-ex3 -from dimos.spec.utils import Spec -from typing import Protocol - -class Drone(Module): - def get_time(self) -> str: - return "1:00 PM" - -class Car(Module): - def get_time(self) -> str: - return "2:00 PM" - -# Your Spec -class AnyModuleWithGetTime(Spec, Protocol): - def get_time(self) -> str: ... - -class ModuleB(Module): - device: AnyModuleWithGetTime - - def request_the_time(self) -> None: - # autoconnect() will automatically find whatever module has a get_time() method - print(self.device.get_time()) -``` - -## Defining skills - -Skills are methods on a `Module` decorated with `@skill`. The agent automatically discovers all skills from launched modules at startup. - -```python session=blueprint-ex4 -from dimos.core import Module, rpc -from dimos.agents.annotation import skill -from dimos.core.global_config import GlobalConfig - -class SomeSkill(Module): - - @skill - def some_skill(self) -> str: - """Description of the skill for the LLM.""" - return "result" -``` - -## Building - -All you have to do to build a blueprint is call: - -```python session=blueprint-ex4 -module_coordinator = SomeSkill.blueprint().build(global_config=GlobalConfig()) -``` - -This returns a `ModuleCoordinator` instance that manages all deployed modules. - -### Running and shutting down - -You can block the thread until it exits with: - -```python session=blueprint-ex4 -module_coordinator.loop() -``` - -This will wait for Ctrl+C and then automatically stop all modules and clean up resources. diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md deleted file mode 100644 index eaad4a9271..0000000000 --- a/docs/usage/configuration.md +++ /dev/null @@ -1,90 +0,0 @@ -# Configuration - -Dimos provides a `Configurable` base class. See [`service/spec.py`](/dimos/protocol/service/spec.py#L22). - -This allows using dataclasses to specify configuration structure and default values per module. - -```python -from dimos.protocol.service import Configurable -from rich import print -from dataclasses import dataclass - -@dataclass -class Config(): - x: int = 3 - hello: str = "world" - -class MyClass(Configurable): - default_config = Config - config: Config - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - -myclass1 = MyClass() -print(myclass1.config) - -# can easily override -myclass2 = MyClass(hello="override") -print(myclass2.config) - -# we will raise an error for unspecified keys -try: - myclass3 = MyClass(something="else") -except TypeError as e: - print(f"Error: {e}") - - -``` - - -``` -Config(x=3, hello='world') -Config(x=3, hello='override') -Error: Config.__init__() got an unexpected keyword argument 'something' -``` - -# Configurable Modules - -[Modules](/docs/usage/modules.md) inherit from `Configurable`, so all of the above applies. Module configs should inherit from `ModuleConfig` ([`core/module.py`](/dimos/core/module.py#L40)), which includes shared configuration for all modules like transport protocols, frame IDs, etc. - -```python -from dataclasses import dataclass -from dimos.core import In, Module, Out, rpc, ModuleConfig -from rich import print - -@dataclass -class Config(ModuleConfig): - frame_id: str = "world" - publish_interval: float = 0 - voxel_size: float = 0.05 - device: str = "CUDA:0" - -class MyModule(Module): - default_config = Config - config: Config - - def __init__(self, **kwargs) -> None: - super().__init__(**kwargs) - print(self.config) - - -myModule = MyModule(frame_id="frame_id_override", device="CPU") - -# In production, use dimos.deploy() instead: -# myModule = dimos.deploy(MyModule, frame_id="frame_id_override") - - -``` - - -``` -Config( - rpc_transport=, - tf_transport=, - frame_id_prefix=None, - frame_id='frame_id_override', - publish_interval=0, - voxel_size=0.05, - device='CPU' -) -``` diff --git a/docs/usage/data_streams/README.md b/docs/usage/data_streams/README.md deleted file mode 100644 index dc2ce6c91d..0000000000 --- a/docs/usage/data_streams/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Sensor Streams - -Dimos uses reactive streams (RxPY) to handle sensor data. This approach naturally fits robotics where multiple sensors emit data asynchronously at different rates, and downstream processors may be slower than the data sources. - -## Guides - -| Guide | Description | -|----------------------------------------------|---------------------------------------------------------------| -| [ReactiveX Fundamentals](reactivex.md) | Observables, subscriptions, and disposables | -| [Advanced Streams](advanced_streams.md) | Backpressure, parallel subscribers, synchronous getters | -| [Quality-Based Filtering](quality_filter.md) | Select highest quality frames when downsampling streams | -| [Temporal Alignment](temporal_alignment.md) | Match messages from multiple sensors by timestamp | -| [Storage & Replay](storage_replay.md) | Record sensor streams to disk and replay with original timing | - -## Quick Example - -```python -from reactivex import operators as ops -from dimos.utils.reactive import backpressure -from dimos.types.timestamped import align_timestamped -from dimos.msgs.sensor_msgs.Image import sharpness_barrier - -# Camera at 30fps, lidar at 10Hz -camera_stream = camera.observable() -lidar_stream = lidar.observable() - -# Pipeline: filter blurry frames -> align with lidar -> handle slow consumers -processed = ( - camera_stream.pipe( - sharpness_barrier(10.0), # Keep sharpest frame per 100ms window (10Hz) - ) -) - -aligned = align_timestamped( - backpressure(processed), # Camera as primary - lidar_stream, # Lidar as secondary - match_tolerance=0.1, -) - -aligned.subscribe(lambda pair: process_frame_with_pointcloud(*pair)) -``` diff --git a/docs/usage/data_streams/advanced_streams.md b/docs/usage/data_streams/advanced_streams.md deleted file mode 100644 index 187d432af2..0000000000 --- a/docs/usage/data_streams/advanced_streams.md +++ /dev/null @@ -1,295 +0,0 @@ -# Advanced Stream Handling - -> **Prerequisite:** Read [ReactiveX Fundamentals](reactivex.md) first for Observable basics. - -## Backpressure and Parallel Subscribers to Hardware - -In robotics, we deal with hardware that produces data at its own pace - a camera outputs 30fps whether you're ready or not. We can't tell the camera to slow down. And we often have multiple consumers: one module wants every frame for recording, another runs slow ML inference and only needs the latest frame. - -**The problem:** A fast producer can overwhelm a slow consumer, causing memory buildup or dropped frames. We might have multiple subscribers to the same hardware that operate at different speeds. - - -
Pikchr - -```pikchr fold output=assets/backpressure.svg -color = white -fill = none - -Fast: box "Camera" "60 fps" rad 5px fit wid 130% ht 130% -arrow right 0.4in -Queue: box "queue" rad 5px fit wid 170% ht 170% -arrow right 0.4in -Slow: box "ML Model" "2 fps" rad 5px fit wid 130% ht 130% - -text "items pile up!" at (Queue.x, Queue.y - 0.45in) -``` - -
- - -![output](assets/backpressure.svg) - - -**The solution:** The `backpressure()` wrapper handles this by: - -1. **Sharing the source** - Camera runs once, all subscribers share the stream -2. **Per-subscriber speed** - Fast subscribers get every frame, slow ones get the latest when ready -3. **No blocking** - Slow subscribers never block the source or each other - -```python session=bp -import time -import reactivex as rx -from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler -from dimos.utils.reactive import backpressure - -# We need this scaffolding here. Normally DimOS handles this. -scheduler = ThreadPoolScheduler(max_workers=4) - -# Simulate fast source -source = rx.interval(0.05).pipe(ops.take(20)) -safe = backpressure(source, scheduler=scheduler) - -fast_results = [] -slow_results = [] - -safe.subscribe(lambda x: fast_results.append(x)) - -def slow_handler(x): - time.sleep(0.15) - slow_results.append(x) - -safe.subscribe(slow_handler) - -time.sleep(1.5) -print(f"fast got {len(fast_results)} items: {fast_results[:5]}...") -print(f"slow got {len(slow_results)} items (skipped {len(fast_results) - len(slow_results)})") -scheduler.executor.shutdown(wait=True) -``` - - -``` -fast got 20 items: [0, 1, 2, 3, 4]... -slow got 7 items (skipped 13) -``` - -### How it works - - -
Pikchr - -```pikchr fold output=assets/backpressure_solution.svg -color = white -fill = none -linewid = 0.3in - -Source: box "Camera" "60 fps" rad 5px fit wid 170% ht 170% -arrow -Core: box "backpressure" rad 5px fit wid 170% ht 170% -arrow from Core.e right 0.3in then up 0.35in then right 0.3in -Fast: box "Fast Sub" rad 5px fit wid 170% ht 170% -arrow from Core.e right 0.3in then down 0.35in then right 0.3in -SlowPre: box "LATEST" rad 5px fit wid 170% ht 170% -arrow -Slow: box "Slow Sub" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/backpressure_solution.svg) - -The `LATEST` strategy means: when the slow subscriber finishes processing, it gets whatever the most recent value is, skipping any values that arrived while it was busy. - -### Usage in modules - -Most module streams offer backpressured observables. - -```python session=bp -from dimos.core import Module, In -from dimos.msgs.sensor_msgs import Image - -class MLModel(Module): - color_image: In[Image] - def start(self): - # no reactivex, simple callback - self.color_image.subscribe(...) - # backpressured - self.color_image.observable().subscribe(...) - # non-backpressured - will pile up queue - self.color_image.pure_observable().subscribe(...) - - -``` - -## Getting Values Synchronously - -Sometimes you don't want a stream, you just want to call a function and get the latest value. - -If you are doing this periodically as a part of a processing loop, it is very likely that your code will be much cleaner and safer using actual reactivex pipeline. So bias towards checking our [reactivex quick guide](reactivex.md) and [official docs](https://rxpy.readthedocs.io/) - -(TODO we should actually make this example actually executable) - -```python skip - self.color_image.observable().pipe( - # takes the best image from a stream every 200ms, - # ensuring we are feeding our detector with highest quality frames - quality_barrier(lambda x: x["quality"], target_frequency=0.2), - - # converts Image into Person detections - ops.map(detect_person), - - # converts Detection2D to Twist pointing in the direction of a detection - ops.map(detection2d_to_twist), - - # emits the latest value every 50ms making our control loop run at 20hz - # despite detections running at 200ms - ops.sample(0.05), - ).subscribe(self.twist.publish) # shoots off the Twist out of the module -``` - - -If you'd still like to switch to synchronous fetching, we provide two approaches, `getter_hot()` and `getter_cold()` - -| | `getter_hot()` | `getter_cold()` | -|------------------|--------------------------------|----------------------------------| -| **Subscription** | Stays active in background | Fresh subscription each call | -| **Read speed** | Instant (value already cached) | Slower (waits for value) | -| **Resources** | Keeps connection open | Opens/closes each call | -| **Use when** | Frequent reads, need latest | Occasional reads, save resources | - -
-diagram source - -```pikchr fold output=assets/getter_hot_cold.svg -color = white -fill = none - -H_Title: box "getter_hot()" rad 5px fit wid 170% ht 170% - -Sub: box "subscribe" rad 5px fit wid 170% ht 170% with .n at H_Title.s + (0, -0.5in) -arrow from H_Title.s to Sub.n -arrow right from Sub.e -Cache: box "Cache" rad 5px fit wid 170% ht 170% - -# blocking box around subscribe->cache (one-time setup) -Blk0: box dashed color 0x5c9ff0 with .nw at Sub.nw + (-0.1in, 0.25in) wid (Cache.e.x - Sub.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk0.n + (0, -0.05in) - -arrow right from Cache.e -Getter: box "getter" rad 5px fit wid 170% ht 170% - -arrow from Getter.e right 0.3in then down 0.25in then right 0.2in -G1: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from G1.e -box invis "instant" fit wid 150% - -arrow from Getter.e right 0.3in then down 0.7in then right 0.2in -G2: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from G2.e -box invis "instant" fit wid 150% - -text "always subscribed" italic with .n at Blk0.s + (0, -0.1in) - - -# === getter_cold section === -C_Title: box "getter_cold()" rad 5px fit wid 170% ht 170% with .nw at H_Title.sw + (0, -1.6in) - -arrow down 0.3in from C_Title.s -ColdGetter: box "getter" rad 5px fit wid 170% ht 170% - -# Branch to first call -arrow from ColdGetter.e right 0.3in then down 0.3in then right 0.2in -Cold1: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from Cold1.e -Sub1: box invis "subscribe" fit wid 150% -arrow right 0.4in from Sub1.e -Wait1: box invis "wait" fit wid 150% -arrow right 0.4in from Wait1.e -Val1: box invis "value" fit wid 150% -arrow right 0.4in from Val1.e -Disp1: box invis "dispose " fit wid 150% - -# blocking box around first row -Blk1: box dashed color 0x5c9ff0 with .nw at Cold1.nw + (-0.1in, 0.25in) wid (Disp1.e.x - Cold1.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk1.n + (0, -0.05in) - -# Branch to second call -arrow from ColdGetter.e right 0.3in then down 1.2in then right 0.2in -Cold2: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from Cold2.e -Sub2: box invis "subscribe" fit wid 150% -arrow right 0.4in from Sub2.e -Wait2: box invis "wait" fit wid 150% -arrow right 0.4in from Wait2.e -Val2: box invis "value" fit wid 150% -arrow right 0.4in from Val2.e -Disp2: box invis "dispose " fit wid 150% - -# blocking box around second row -Blk2: box dashed color 0x5c9ff0 with .nw at Cold2.nw + (-0.1in, 0.25in) wid (Disp2.e.x - Cold2.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk2.n + (0, -0.05in) -``` - -
- - -![output](assets/getter_hot_cold.svg) - - -**Prefer `getter_cold()`** when you can afford to wait and warmup isn't expensive. It's simpler (no cleanup needed) and doesn't hold resources. Only use `getter_hot()` when you need instant reads or the source is expensive to start. - -### `getter_hot()` - Background subscription, instant reads - -Subscribes immediately and keeps updating in the background. Each call returns the cached latest value instantly. - -```python session=sync -import time -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import getter_hot - -source = rx.interval(0.1).pipe(ops.take(10)) - -get_val = getter_hot(source, timeout=5.0) # blocks until first message, with 5s timeout -# alternatively not to block (but get_val() might return None) -# get_val = getter_hot(source, nonblocking=True) - -print("first call:", get_val()) # instant - value already there -time.sleep(0.35) -print("after 350ms:", get_val()) # instant - returns cached latest -time.sleep(0.35) -print("after 700ms:", get_val()) - -get_val.dispose() # Don't forget to clean up! -``` - - -``` -first call: 0 -after 350ms: 3 -after 700ms: 6 -``` - -### `getter_cold()` - Fresh subscription each call - -Each call creates a new subscription, waits for one value, and cleans up. Slower but doesn't hold resources: - -```python session=sync -from dimos.utils.reactive import getter_cold - -source = rx.of(0, 1, 2, 3, 4) -get_val = getter_cold(source, timeout=5.0) - -# Each call creates fresh subscription, gets first value -print("call 1:", get_val()) # subscribes, gets 0, disposes -print("call 2:", get_val()) # subscribes again, gets 0, disposes -print("call 3:", get_val()) # subscribes again, gets 0, disposes -``` - - -``` -call 1: 0 -call 2: 0 -call 3: 0 -``` diff --git a/docs/usage/data_streams/assets/alignment_flow.svg b/docs/usage/data_streams/assets/alignment_flow.svg deleted file mode 100644 index 72aeb337f3..0000000000 --- a/docs/usage/data_streams/assets/alignment_flow.svg +++ /dev/null @@ -1,22 +0,0 @@ - - -Primary -arrives - - - -Check -secondaries - - - -Emit -match -all found - - - -Buffer -primary -waiting... - diff --git a/docs/usage/data_streams/assets/alignment_overview.svg b/docs/usage/data_streams/assets/alignment_overview.svg deleted file mode 100644 index 8abada6d02..0000000000 --- a/docs/usage/data_streams/assets/alignment_overview.svg +++ /dev/null @@ -1,18 +0,0 @@ - - -Camera -30 fps - - - -align_timestamped - -Lidar -10 Hz - - - - - -(image, pointcloud) - diff --git a/docs/usage/data_streams/assets/alignment_timeline.png b/docs/usage/data_streams/assets/alignment_timeline.png deleted file mode 100644 index 235ddd7be0..0000000000 --- a/docs/usage/data_streams/assets/alignment_timeline.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cfea5a6aac40182b25decb9ddaeb387ed97a7708e2c51a48f47453c8df7adf57 -size 16136 diff --git a/docs/usage/data_streams/assets/alignment_timeline2.png b/docs/usage/data_streams/assets/alignment_timeline2.png deleted file mode 100644 index 2bf8ec5eef..0000000000 --- a/docs/usage/data_streams/assets/alignment_timeline2.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22b64923637d05f8f40c9f7c0f0597ee894dc4f31a0f10674aeb809101b54765 -size 23471 diff --git a/docs/usage/data_streams/assets/alignment_timeline3.png b/docs/usage/data_streams/assets/alignment_timeline3.png deleted file mode 100644 index 61ddc3b54b..0000000000 --- a/docs/usage/data_streams/assets/alignment_timeline3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8e9589dcd5308f511a2ec7d41bd36978204ccfe1441907bd139029b0489d605 -size 9969 diff --git a/docs/usage/data_streams/assets/backpressure.svg b/docs/usage/data_streams/assets/backpressure.svg deleted file mode 100644 index b3d69af6fb..0000000000 --- a/docs/usage/data_streams/assets/backpressure.svg +++ /dev/null @@ -1,15 +0,0 @@ - - -Camera -60 fps - - - -queue - - - -ML Model -2 fps -items pile up! - diff --git a/docs/usage/data_streams/assets/backpressure_solution.svg b/docs/usage/data_streams/assets/backpressure_solution.svg deleted file mode 100644 index 454a8f460b..0000000000 --- a/docs/usage/data_streams/assets/backpressure_solution.svg +++ /dev/null @@ -1,21 +0,0 @@ - - -Camera -60 fps - - - -backpressure - - - -Fast Sub - - - -LATEST - - - -Slow Sub - diff --git a/docs/usage/data_streams/assets/frame_mosaic.jpg b/docs/usage/data_streams/assets/frame_mosaic.jpg deleted file mode 100644 index 5c3fbf8350..0000000000 --- a/docs/usage/data_streams/assets/frame_mosaic.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e83934e1179651fbca6c9b62cceb7425d1b2f0e8da18a63d4d95bcb4e6ac33ca -size 88206 diff --git a/docs/usage/data_streams/assets/frame_mosaic2.jpg b/docs/usage/data_streams/assets/frame_mosaic2.jpg deleted file mode 100644 index 5e3032acf2..0000000000 --- a/docs/usage/data_streams/assets/frame_mosaic2.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d73f683e92fda39bac9d1bb840f1fc375c821b4099714829e81f3e739f4d602 -size 91036 diff --git a/docs/usage/data_streams/assets/getter_hot_cold.svg b/docs/usage/data_streams/assets/getter_hot_cold.svg deleted file mode 100644 index d2f336459c..0000000000 --- a/docs/usage/data_streams/assets/getter_hot_cold.svg +++ /dev/null @@ -1,71 +0,0 @@ - - -getter_hot() - -subscribe - - - - - -Cache - -blocking - - - -getter - - -call() - - -instant - - -call() - - -instant -always subscribed - -getter_cold() - - - -getter - - -call() - - -subscribe - - -wait - - -value - - -dispose   - -blocking - - -call() - - -subscribe - - -wait - - -value - - -dispose   - -blocking - diff --git a/docs/usage/data_streams/assets/observable_flow.svg b/docs/usage/data_streams/assets/observable_flow.svg deleted file mode 100644 index d7e0e021d6..0000000000 --- a/docs/usage/data_streams/assets/observable_flow.svg +++ /dev/null @@ -1,16 +0,0 @@ - - -observable - - - -.pipe(ops) - - - -.subscribe() - - - -callback - diff --git a/docs/usage/data_streams/assets/sharpness_graph.svg b/docs/usage/data_streams/assets/sharpness_graph.svg deleted file mode 100644 index 3d61d12d7c..0000000000 --- a/docs/usage/data_streams/assets/sharpness_graph.svg +++ /dev/null @@ -1,1414 +0,0 @@ - - - - - - - - 1980-01-01T00:00:00+00:00 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/usage/data_streams/assets/sharpness_graph2.svg b/docs/usage/data_streams/assets/sharpness_graph2.svg deleted file mode 100644 index 37c1032de0..0000000000 --- a/docs/usage/data_streams/assets/sharpness_graph2.svg +++ /dev/null @@ -1,1429 +0,0 @@ - - - - - - - - 1980-01-01T00:00:00+00:00 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/usage/data_streams/quality_filter.md b/docs/usage/data_streams/quality_filter.md deleted file mode 100644 index db21da9c54..0000000000 --- a/docs/usage/data_streams/quality_filter.md +++ /dev/null @@ -1,316 +0,0 @@ -# Quality-Based Stream Filtering - -When processing sensor streams, you often want to reduce frequency while keeping the best quality data. For discrete data like images that can't be averaged or merged, instead of blindly dropping frames, `quality_barrier` selects the highest quality item within each time window. - -## The Problem - -A camera outputs 30fps, but your ML model only needs 2fps. Simple approaches: - -- **`sample(0.5)`** - Takes whatever frame happens to land on the interval tick -- **`throttle_first(0.5)`** - Takes the first frame, ignores the rest - -Both ignore quality. You might get a blurry frame when a sharp one was available. - -## The Solution: `quality_barrier` - -```python session=qb -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import quality_barrier - -# Simulated sensor data with quality scores -data = [ - {"id": 1, "quality": 0.3}, - {"id": 2, "quality": 0.9}, # best in first window - {"id": 3, "quality": 0.5}, - {"id": 4, "quality": 0.2}, - {"id": 5, "quality": 0.8}, # best in second window - {"id": 6, "quality": 0.4}, -] - -source = rx.of(*data) - -# Select best quality item per window (2 items per second = 0.5s windows) -result = source.pipe( - quality_barrier(lambda x: x["quality"], target_frequency=2.0), - ops.to_list(), -).run() - -print("Selected:", [r["id"] for r in result]) -print("Qualities:", [r["quality"] for r in result]) -``` - - -``` -Selected: [2] -Qualities: [0.9] -``` - -## Image Sharpness Filtering - -For camera streams, we provide `sharpness_barrier` which uses the image's sharpness score. - -Let's use real camera data from the Unitree Go2 robot to demonstrate. We use the [Sensor Storage & Replay](/docs/usage/sensor_streams/storage_replay.md) toolkit, which provides access to recorded robot data: - -```python session=qb -from dimos.utils.testing import TimedSensorReplay -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier - -# Load recorded Go2 camera frames -video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") - -# Use stream() with seek to skip blank frames, speed=10x to collect faster -input_frames = video_replay.stream(seek=5.0, duration=1.4, speed=10.0).pipe( - ops.to_list() -).run() - -def show_frames(frames): - for i, frame in enumerate(frames[:10]): - print(f" Frame {i}: {frame.sharpness:.3f}") - -print(f"Loaded {len(input_frames)} frames from Go2 camera") -print(f"Frame resolution: {input_frames[0].width}x{input_frames[0].height}") -print("Sharpness scores:") -show_frames(input_frames) -``` - - -``` -Loaded 20 frames from Go2 camera -Frame resolution: 1280x720 -Sharpness scores: - Frame 0: 0.351 - Frame 1: 0.227 - Frame 2: 0.223 - Frame 3: 0.267 - Frame 4: 0.295 - Frame 5: 0.307 - Frame 6: 0.328 - Frame 7: 0.348 - Frame 8: 0.346 - Frame 9: 0.322 -``` - -Using `sharpness_barrier` to select the sharpest frames: - -```python session=qb -# Create a stream from the recorded frames - -sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( - sharpness_barrier(2.0), - ops.to_list() -).run() - -print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") -show_frames(sharp_frames) -``` - - -``` -Output: 3 frame(s) (selected sharpest per window) - Frame 0: 0.351 - Frame 1: 0.352 - Frame 2: 0.360 -``` - -
-Visualization helpers - -```python session=qb fold no-result -import matplotlib -import matplotlib.pyplot as plt -import math - -def plot_mosaic(frames, selected, path, cols=5): - matplotlib.use('Agg') - rows = math.ceil(len(frames) / cols) - aspect = frames[0].width / frames[0].height - fig_w, fig_h = 12, 12 * rows / (cols * aspect) - - fig, axes = plt.subplots(rows, cols, figsize=(fig_w, fig_h)) - fig.patch.set_facecolor('black') - for i, ax in enumerate(axes.flat): - if i < len(frames): - ax.imshow(frames[i].data) - for spine in ax.spines.values(): - spine.set_color('lime' if frames[i] in selected else 'black') - spine.set_linewidth(4 if frames[i] in selected else 0) - ax.set_xticks([]); ax.set_yticks([]) - else: - ax.axis('off') - plt.subplots_adjust(wspace=0.02, hspace=0.02, left=0, right=1, top=1, bottom=0) - plt.savefig(path, facecolor='black', dpi=100, bbox_inches='tight', pad_inches=0) - plt.close() - -def plot_sharpness(frames, selected, path): - matplotlib.use('svg') - plt.style.use('dark_background') - sharpness = [f.sharpness for f in frames] - selected_idx = [i for i, f in enumerate(frames) if f in selected] - - plt.figure(figsize=(10, 3)) - plt.plot(sharpness, 'o-', label='All frames', color='#b5e4f4', alpha=0.7) - for i, idx in enumerate(selected_idx): - plt.axvline(x=idx, color='lime', linestyle='--', label='Selected' if i == 0 else None) - plt.xlabel('Frame'); plt.ylabel('Sharpness') - plt.xticks(range(len(sharpness))) - plt.legend(); plt.grid(alpha=0.3); plt.tight_layout() - plt.savefig(path, transparent=True) - plt.close() -``` - -
- -Visualizing which frames were selected (green border = selected as sharpest in window): - -```python session=qb output=assets/frame_mosaic.jpg -plot_mosaic(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/frame_mosaic.jpg) - -```python session=qb output=assets/sharpness_graph.svg -plot_sharpness(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/sharpness_graph.svg) - -Let's request a higher frequency. - -```python session=qb -sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( - sharpness_barrier(4.0), - ops.to_list() -).run() - -print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") -show_frames(sharp_frames) -``` - - -``` -Output: 6 frame(s) (selected sharpest per window) - Frame 0: 0.351 - Frame 1: 0.348 - Frame 2: 0.346 - Frame 3: 0.352 - Frame 4: 0.360 - Frame 5: 0.329 -``` - -```python session=qb output=assets/frame_mosaic2.jpg -plot_mosaic(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/frame_mosaic2.jpg) - - -```python session=qb output=assets/sharpness_graph2.svg -plot_sharpness(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/sharpness_graph2.svg) - -As we can see the system is trying to strike a balance between requested frequency and quality that's available - -### Usage in Camera Module - -Here's how it's used in the actual camera module: - -```python skip -from dimos.core.module import Module - -class CameraModule(Module): - frequency: float = 2.0 # Target output frequency - @rpc - def start(self) -> None: - stream = self.hardware.image_stream() - - if self.config.frequency > 0: - stream = stream.pipe(sharpness_barrier(self.config.frequency)) - - self._disposables.add( - stream.subscribe(self.color_image.publish), - ) - -``` - -### How Sharpness is Calculated - -The sharpness score (0.0 to 1.0) is computed using Sobel edge detection: - -from [`Image.py`](/dimos/msgs/sensor_msgs/Image.py) - -```python session=qb -import cv2 - -# Get a frame and show the calculation -img = input_frames[10] -gray = img.to_grayscale() - -# Sobel gradients - use .data to get the underlying numpy array -sx = cv2.Sobel(gray.data, cv2.CV_32F, 1, 0, ksize=5) -sy = cv2.Sobel(gray.data, cv2.CV_32F, 0, 1, ksize=5) -magnitude = cv2.magnitude(sx, sy) - -print(f"Mean gradient magnitude: {magnitude.mean():.2f}") -print(f"Normalized sharpness: {img.sharpness:.3f}") -``` - - -``` -Mean gradient magnitude: 230.00 -Normalized sharpness: 0.332 -``` - -## Custom Quality Functions - -You can use `quality_barrier` with any quality metric: - -```python session=qb -# Example: select by "confidence" field -detections = [ - {"name": "cat", "confidence": 0.7}, - {"name": "dog", "confidence": 0.95}, # best - {"name": "bird", "confidence": 0.6}, -] - -result = rx.of(*detections).pipe( - quality_barrier(lambda d: d["confidence"], target_frequency=2.0), - ops.to_list(), -).run() - -print(f"Selected: {result[0]['name']} (conf: {result[0]['confidence']})") -``` - - -``` -Selected: dog (conf: 0.95) -``` - -## API Reference - -### `quality_barrier(quality_func, target_frequency)` - -RxPY pipe operator that selects the highest quality item within each time window. - -| Parameter | Type | Description | -|--------------------|------------------------|------------------------------------------------------| -| `quality_func` | `Callable[[T], float]` | Function that returns a quality score for each item | -| `target_frequency` | `float` | Output frequency in Hz (e.g., 2.0 for 2 items/second)| - -**Returns:** A pipe operator for use with `.pipe()` - -### `sharpness_barrier(target_frequency)` - -Convenience wrapper for images that uses `image.sharpness` as the quality function. - -| Parameter | Type | Description | -|--------------------|---------|--------------------------| -| `target_frequency` | `float` | Output frequency in Hz | - -**Returns:** A pipe operator for use with `.pipe()` diff --git a/docs/usage/data_streams/reactivex.md b/docs/usage/data_streams/reactivex.md deleted file mode 100644 index 45873b471b..0000000000 --- a/docs/usage/data_streams/reactivex.md +++ /dev/null @@ -1,494 +0,0 @@ -# ReactiveX (RxPY) Quick Reference - -RxPY provides composable asynchronous data streams. This is a practical guide focused on common patterns in this codebase. - -## Quick Start: Using an Observable - -Given a function that returns an `Observable`, here's how to use it: - -```python session=rx -import reactivex as rx -from reactivex import operators as ops - -# Create an observable that emits 0,1,2,3,4 -source = rx.of(0, 1, 2, 3, 4) - -# Subscribe and print each value -received = [] -source.subscribe(lambda x: received.append(x)) -print("received:", received) -``` - - -``` -received: [0, 1, 2, 3, 4] -``` - -## The `.pipe()` Pattern - -Chain operators using `.pipe()`: - -```python session=rx -# Transform values: multiply by 2, then filter > 4 -result = [] - -# We build another observable. It's passive until `subscribe` is called. -observable = source.pipe( - ops.map(lambda x: x * 2), - ops.filter(lambda x: x > 4), -) - -observable.subscribe(lambda x: result.append(x)) - -print("transformed:", result) -``` - - -``` -transformed: [6, 8] -``` - -## Common Operators - -### Transform: `map` - -```python session=rx -rx.of(1, 2, 3).pipe( - ops.map(lambda x: f"item_{x}") -).subscribe(print) -``` - - -``` -item_1 -item_2 -item_3 - -``` - -### Filter: `filter` - -```python session=rx -rx.of(1, 2, 3, 4, 5).pipe( - ops.filter(lambda x: x % 2 == 0) -).subscribe(print) -``` - - -``` -2 -4 - -``` - -### Limit emissions: `take` - -```python session=rx -rx.of(1, 2, 3, 4, 5).pipe( - ops.take(3) -).subscribe(print) -``` - - -``` -1 -2 -3 - -``` - -### Flatten nested observables: `flat_map` - -```python session=rx -# For each input, emit multiple values -rx.of(1, 2).pipe( - ops.flat_map(lambda x: rx.of(x, x * 10, x * 100)) -).subscribe(print) -``` - - -``` -1 -10 -100 -2 -20 -200 - -``` - -## Rate Limiting - -### `sample(interval)` - Emit latest value every N seconds - -Takes the most recent value at each interval. Good for continuous streams where you want the freshest data. - -```python session=rx -# Use blocking .run() to collect results properly -results = rx.interval(0.05).pipe( - ops.take(10), - ops.sample(0.2), - ops.to_list(), -).run() -print("sample() got:", results) -``` - - -``` -sample() got: [2, 6, 9] -``` - -### `throttle_first(interval)` - Emit first, then block for N seconds - -Takes the first value then ignores subsequent values for the interval. Good for user input debouncing. - -```python session=rx -results = rx.interval(0.05).pipe( - ops.take(10), - ops.throttle_first(0.15), - ops.to_list(), -).run() -print("throttle_first() got:", results) -``` - - -``` -throttle_first() got: [0, 3, 6, 9] -``` - -### Difference Between `sample` and `throttle_first` - -```python session=rx -# sample: takes LATEST value at each interval tick -# throttle_first: takes FIRST value then blocks - -# With fast emissions (0,1,2,3,4,5,6,7,8,9) every 50ms: -# sample(0.2s) -> gets value at 200ms, 400ms marks -> [2, 6, 9] -# throttle_first(0.15s) -> gets 0, blocks, then 3, blocks, then 6... -> [0,3,6,9] -print("sample: latest value at each tick") -print("throttle_first: first value, then block") -``` - - -``` -sample: latest value at each tick -throttle_first: first value, then block -``` - - -## What is an Observable? - -An Observable is like a list, but instead of holding all values at once, it produces values over time. - -| | List | Iterator | Observable | -|-------------|-----------------------|-----------------------|------------------| -| **Values** | All exist now | Generated on demand | Arrive over time | -| **Control** | You pull (`for x in`) | You pull (`next()`) | Pushed to you | -| **Size** | Finite | Can be infinite | Can be infinite | -| **Async** | No | Yes (with asyncio) | Yes | -| **Cancel** | N/A | Stop calling `next()` | `.dispose()` | - -The key difference from iterators: with an Observable, **you don't control when values arrive**. A camera produces frames at 30fps whether you're ready or not. An iterator waits for you to call `next()`. - -**Observables are lazy.** An Observable is just a description of work to be done - it sits there doing nothing until you call `.subscribe()`. That's when it "wakes up" and starts producing values. - -This means you can build complex pipelines, pass them around, and nothing happens until someone subscribes. - -**The three things an Observable can tell you:** - -1. **"Here's a value"** (`on_next`) - A new value arrived -2. **"Something went wrong"** (`on_error`) - An error occurred, stream stops -3. **"I'm done"** (`on_completed`) - No more values coming - -**The basic pattern:** - -``` -observable.subscribe(what_to_do_with_each_value) -``` - -That's it. You create or receive an Observable, then subscribe to start receiving values. - -When you subscribe, data flows through a pipeline: - -
-diagram source - -```pikchr fold output=assets/observable_flow.svg -color = white -fill = none - -Obs: box "observable" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Pipe: box ".pipe(ops)" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Sub: box ".subscribe()" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Handler: box "callback" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/observable_flow.svg) - - -**Key property: Observables are lazy.** Nothing happens until you call `.subscribe()`. This means you can build up complex pipelines without any work being done, then start the flow when ready. - -Here's the full subscribe signature with all three callbacks: - -```python session=rx -rx.of(1, 2, 3).subscribe( - on_next=lambda x: print(f"value: {x}"), - on_error=lambda e: print(f"error: {e}"), - on_completed=lambda: print("done") -) -``` - - -``` -value: 1 -value: 2 -value: 3 -done - -``` - -## Disposables: Cancelling Subscriptions - -When you subscribe, you get back a `Disposable`. This is your "cancel button": - -```python session=rx -import reactivex as rx - -source = rx.interval(0.1) # emits 0, 1, 2, ... every 100ms forever -subscription = source.subscribe(lambda x: print(x)) - -# Later, when you're done: -subscription.dispose() # Stop receiving values, clean up resources -print("disposed") -``` - - -``` -disposed -``` - -**Why does this matter?** - -- Observables can be infinite (sensor feeds, websockets, timers) -- Without disposing, you leak memory and keep processing values forever -- Disposing also cleans up any resources the Observable opened (connections, file handles, etc.) - -**Rule of thumb:** Whenever you subscribe, save the disposable because you have to unsubscribe at some point by calling `disposable.dispose()`. - -**In dimos modules:** Every `Module` has a `self._disposables` (a `CompositeDisposable`) that automatically disposes everything when the module closes: - -```python session=rx -import time -from dimos.core import Module - -class MyModule(Module): - def start(self): - source = rx.interval(0.05) - self._disposables.add(source.subscribe(lambda x: print(f"got {x}"))) - -module = MyModule() -module.start() -time.sleep(0.25) - -# unsubscribes disposables -module.stop() -``` - - -``` -got 0 -got 1 -got 2 -got 3 -got 4 -``` - -## Creating Observables - -There are two common callback patterns in APIs. Use the appropriate helper: - -| Pattern | Example | Helper | -|---------|---------|--------| -| Register/unregister with same callback | `sensor.register(cb)` / `sensor.unregister(cb)` | `callback_to_observable` | -| Subscribe returns unsub function | `unsub = pubsub.subscribe(cb)` | `to_observable` | - -### From register/unregister APIs - -Use `callback_to_observable` when the API has separate register and unregister functions that take the same callback reference: - -```python session=create -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import callback_to_observable - -class MockSensor: - def __init__(self): - self._callbacks = [] - def register(self, cb): - self._callbacks.append(cb) - def unregister(self, cb): - self._callbacks.remove(cb) - def emit(self, value): - for cb in self._callbacks: - cb(value) - -sensor = MockSensor() - -obs = callback_to_observable( - start=sensor.register, - stop=sensor.unregister -) - -received = [] -sub = obs.subscribe(lambda x: received.append(x)) - -sensor.emit("reading_1") -sensor.emit("reading_2") -print("received:", received) - -sub.dispose() -print("callbacks after dispose:", len(sensor._callbacks)) -``` - - -``` -received: ['reading_1', 'reading_2'] -callbacks after dispose: 0 -``` - -### From subscribe-returns-unsub APIs - -Use `to_observable` when the subscribe function returns an unsubscribe callable: - -```python session=create -from dimos.utils.reactive import to_observable - -class MockPubSub: - def __init__(self): - self._callbacks = [] - def subscribe(self, cb): - self._callbacks.append(cb) - return lambda: self._callbacks.remove(cb) # returns unsub function - def publish(self, value): - for cb in self._callbacks: - cb(value) - -pubsub = MockPubSub() - -obs = to_observable(pubsub.subscribe) - -received = [] -sub = obs.subscribe(lambda x: received.append(x)) - -pubsub.publish("msg_1") -pubsub.publish("msg_2") -print("received:", received) - -sub.dispose() -print("callbacks after dispose:", len(pubsub._callbacks)) -``` - - -``` -received: ['msg_1', 'msg_2'] -callbacks after dispose: 0 -``` - -### From scratch with `rx.create` - -```python session=create -from reactivex.disposable import Disposable - -def custom_subscribe(observer, scheduler=None): - observer.on_next("first") - observer.on_next("second") - observer.on_completed() - return Disposable(lambda: print("cleaned up")) - -obs = rx.create(custom_subscribe) - -results = [] -obs.subscribe( - on_next=lambda x: results.append(x), - on_completed=lambda: results.append("DONE") -) -print("results:", results) -``` - - -``` -cleaned up -results: ['first', 'second', 'DONE'] -``` - -## CompositeDisposable - -As we know we can always dispose subscriptions when done to prevent leaks: - -```python session=dispose -import time -import reactivex as rx -from reactivex import operators as ops - -source = rx.interval(0.1).pipe(ops.take(100)) -received = [] - -subscription = source.subscribe(lambda x: received.append(x)) -time.sleep(0.25) -subscription.dispose() -time.sleep(0.2) - -print(f"received {len(received)} items before dispose") -``` - - -``` -received 2 items before dispose -``` - -For multiple subscriptions, use `CompositeDisposable`: - -```python session=dispose -from reactivex.disposable import CompositeDisposable - -disposables = CompositeDisposable() - -s1 = rx.of(1,2,3).subscribe(lambda x: None) -s2 = rx.of(4,5,6).subscribe(lambda x: None) - -disposables.add(s1) -disposables.add(s2) - -print("subscriptions:", len(disposables)) -disposables.dispose() -print("after dispose:", disposables.is_disposed) -``` - - -``` -subscriptions: 2 -after dispose: True -``` - -## Reference - -| Operator | Purpose | Example | -|-----------------------|------------------------------------------|---------------------------------------| -| `map(fn)` | Transform each value | `ops.map(lambda x: x * 2)` | -| `filter(pred)` | Keep values matching predicate | `ops.filter(lambda x: x > 0)` | -| `take(n)` | Take first n values | `ops.take(10)` | -| `first()` | Take first value only | `ops.first()` | -| `sample(sec)` | Emit latest every interval | `ops.sample(0.5)` | -| `throttle_first(sec)` | Emit first, block for interval | `ops.throttle_first(0.5)` | -| `flat_map(fn)` | Map + flatten nested observables | `ops.flat_map(lambda x: rx.of(x, x))` | -| `observe_on(sched)` | Switch scheduler | `ops.observe_on(pool_scheduler)` | -| `replay(n)` | Cache last n values for late subscribers | `ops.replay(buffer_size=1)` | -| `timeout(sec)` | Error if no value within timeout | `ops.timeout(5.0)` | - -See [RxPY documentation](https://rxpy.readthedocs.io/) for complete operator reference. diff --git a/docs/usage/data_streams/storage_replay.md b/docs/usage/data_streams/storage_replay.md deleted file mode 100644 index c5cbe306a8..0000000000 --- a/docs/usage/data_streams/storage_replay.md +++ /dev/null @@ -1,231 +0,0 @@ -# Sensor Storage and Replay - -Record sensor streams to disk and replay them with original timing. Useful for testing, debugging, and creating reproducible datasets. - -## Quick Start - -### Recording - -```python skip -from dimos.utils.testing.replay import TimedSensorStorage - -# Create storage (directory in data folder) -storage = TimedSensorStorage("my_recording") - -# Save frames from a stream -camera_stream.subscribe(storage.save_one) - -# Or save manually -storage.save(frame1, frame2, frame3) -``` - -### Replaying - -```python skip -from dimos.utils.testing.replay import TimedSensorReplay - -# Load recording -replay = TimedSensorReplay("my_recording") - -# Iterate at original speed -for frame in replay.iterate_realtime(): - process(frame) - -# Or as an Observable stream -replay.stream(speed=1.0).subscribe(process) -``` - -## TimedSensorStorage - -Stores sensor data with timestamps as pickle files. Each frame is saved as `000.pickle`, `001.pickle`, etc. - -```python skip -from dimos.utils.testing.replay import TimedSensorStorage - -storage = TimedSensorStorage("lidar_capture") - -# Save individual frames -storage.save_one(lidar_msg) # Returns frame count - -# Save multiple frames -storage.save(frame1, frame2, frame3) - -# Subscribe to a stream -lidar_stream.subscribe(storage.save_one) - -# Or pipe through (emits frame count) -lidar_stream.pipe( - ops.flat_map(storage.save_stream) -).subscribe() -``` - -**Storage location:** Files are saved to the data directory under the given name. The directory must not already contain pickle files (prevents accidental overwrites). - -**What gets stored:** By default, if a frame has a `.raw_msg` attribute, that's pickled instead of the full object. You can customize with the `autocast` parameter: - -```python skip -# Custom serialization -storage = TimedSensorStorage( - "custom_capture", - autocast=lambda frame: frame.to_dict() -) -``` - -## TimedSensorReplay - -Replays stored sensor data with timestamp-aware iteration and seeking. - -### Basic Iteration - -```python skip -from dimos.utils.testing.replay import TimedSensorReplay - -replay = TimedSensorReplay("lidar_capture") - -# Iterate all frames (ignores timing) -for frame in replay.iterate(): - process(frame) - -# Iterate with timestamps -for ts, frame in replay.iterate_ts(): - print(f"Frame at {ts}: {frame}") - -# Iterate with relative timestamps (from start) -for relative_ts, frame in replay.iterate_duration(): - print(f"At {relative_ts:.2f}s: {frame}") -``` - -### Realtime Playback - -```python skip -# Play at original speed (blocks between frames) -for frame in replay.iterate_realtime(): - process(frame) - -# Play at 2x speed -for frame in replay.iterate_realtime(speed=2.0): - process(frame) - -# Play at half speed -for frame in replay.iterate_realtime(speed=0.5): - process(frame) -``` - -### Seeking and Slicing - -```python skip -# Start 10 seconds into the recording -for ts, frame in replay.iterate_ts(seek=10.0): - process(frame) - -# Play only 5 seconds starting at 10s -for ts, frame in replay.iterate_ts(seek=10.0, duration=5.0): - process(frame) - -# Loop forever -for frame in replay.iterate(loop=True): - process(frame) -``` - -### Finding Specific Frames - -```python skip -# Find frame closest to absolute timestamp -frame = replay.find_closest(1704067200.0) - -# Find frame closest to relative time (30s from start) -frame = replay.find_closest_seek(30.0) - -# With tolerance (returns None if no match within 0.1s) -frame = replay.find_closest(timestamp, tolerance=0.1) -``` - -### Observable Stream - -The `.stream()` method returns an Observable that emits frames with original timing: - -```python skip -# Stream at original speed -replay.stream(speed=1.0).subscribe(process) - -# Stream at 2x with seeking -replay.stream( - speed=2.0, - seek=10.0, # Start 10s in - duration=30.0, # Play for 30s - loop=True # Loop forever -).subscribe(process) -``` - -## Usage: Stub Connections for Testing - -A common pattern is creating replay-based connection stubs for testing without hardware. From [`robot/unitree/go2/connection.py`](/dimos/robot/unitree/go2/connection.py#L83): - -This is a bit primitive. We'd like to write a higher-order API for recording full module I/O for any module, but this is a work in progress at the moment. - - -```python skip -class ReplayConnection(UnitreeWebRTCConnection): - dir_name = "unitree_go2_bigoffice" - - def __init__(self, **kwargs) -> None: - get_data(self.dir_name) - self.replay_config = { - "loop": kwargs.get("loop"), - "seek": kwargs.get("seek"), - "duration": kwargs.get("duration"), - } - - def lidar_stream(self): - lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") - return lidar_store.stream(**self.replay_config) - - def video_stream(self): - video_store = TimedSensorReplay(f"{self.dir_name}/video") - return video_store.stream(**self.replay_config) -``` - -This allows running the full perception pipeline against recorded data: - -```python skip -# Use replay connection instead of real hardware -connection = ReplayConnection(loop=True, seek=5.0) -robot = GO2Connection(connection=connection) -``` - -## Data Format - -Each pickle file contains a tuple `(timestamp, data)`: - -- **timestamp**: Unix timestamp (float) when the frame was captured -- **data**: The sensor data (or result of `autocast` if provided) - -Files are numbered sequentially: `000.pickle`, `001.pickle`, etc. - -Recordings are stored in the `data/` directory. See [Data Loading](/docs/development/large_file_management.md) for how data storage works, including Git LFS handling for large datasets. - -## API Reference - -### TimedSensorStorage - -| Method | Description | -|------------------------------|------------------------------------------| -| `save_one(frame)` | Save a single frame, returns frame count | -| `save(*frames)` | Save multiple frames | -| `save_stream(observable)` | Pipe an observable through storage | -| `consume_stream(observable)` | Subscribe and save without returning | - -### TimedSensorReplay - -| Method | Description | -|--------------------------------------------------|---------------------------------------| -| `iterate(loop=False)` | Iterate frames (no timing) | -| `iterate_ts(seek, duration, loop)` | Iterate with absolute timestamps | -| `iterate_duration(...)` | Iterate with relative timestamps | -| `iterate_realtime(speed, ...)` | Iterate with blocking to match timing | -| `stream(speed, seek, duration, loop)` | Observable with original timing | -| `find_closest(timestamp, tolerance)` | Find frame by absolute timestamp | -| `find_closest_seek(relative_seconds, tolerance)` | Find frame by relative time | -| `first()` | Get first frame | -| `first_timestamp()` | Get first timestamp | -| `load(name)` | Load specific frame by name/index | diff --git a/docs/usage/data_streams/temporal_alignment.md b/docs/usage/data_streams/temporal_alignment.md deleted file mode 100644 index 66230c9d54..0000000000 --- a/docs/usage/data_streams/temporal_alignment.md +++ /dev/null @@ -1,313 +0,0 @@ -# Temporal Message Alignment - -Robots have multiple sensors emitting data at different rates and latencies. A camera might run at 30fps, while lidar scans at 10Hz, and each has different processing delays. For perception tasks like projecting 2D detections into 3D pointclouds, we need to match data from these streams by timestamp. - -`align_timestamped` solves this by buffering messages and matching them within a time tolerance. - -
Pikchr - -```pikchr fold output=assets/alignment_overview.svg -color = white -fill = none - -Cam: box "Camera" "30 fps" rad 5px fit wid 170% ht 170% -arrow from Cam.e right 0.4in then down 0.35in then right 0.4in -Align: box "align_timestamped" rad 5px fit wid 170% ht 170% - -Lidar: box "Lidar" "10 Hz" rad 5px fit wid 170% ht 170% with .s at (Cam.s.x, Cam.s.y - 0.7in) -arrow from Lidar.e right 0.4in then up 0.35in then right 0.4in - -arrow from Align.e right 0.4in -Out: box "(image, pointcloud)" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/alignment_overview.svg) - - -## Basic Usage - -Below we set up replay of real camera and lidar data from the Unitree Go2 robot. You can check it if you're interested. - -
-Stream Setup - -You can read more about [sensor storage here](storage_replay.md) and [LFS data storage here](/docs/development/large_file_management.md). - -```python session=align no-result -from reactivex import Subject -from dimos.utils.testing import TimedSensorReplay -from dimos.types.timestamped import Timestamped, align_timestamped -from reactivex import operators as ops -import reactivex as rx - -# Load recorded Go2 sensor streams -video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") -lidar_replay = TimedSensorReplay("unitree_go2_bigoffice/lidar") - -# This is a bit tricky. We find the first video frame timestamp, then add 2 seconds to it. -seek_ts = video_replay.first_timestamp() + 2 - -# Lists to collect items as they flow through streams -video_frames = [] -lidar_scans = [] - -# We are using from_timestamp=... and not seek=... because seek seeks through recording -# timestamps, from_timestamp matches actual message timestamp. -# It's possible for sensor data to come in late, but with correct capture time timestamps -video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: video_frames.append(x)) -) - -lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: lidar_scans.append(x)) -) - -``` - - -
- -Streams would normally come from an actual robot into your module via `In` inputs. [`detection/module3D.py`](/dimos/perception/detection/module3D.py#L11) is a good example of this. - -Assume we have them. Let's align them. - -```python session=align -# Align video (primary) with lidar (secondary) -# match_tolerance: max time difference for a match (seconds) -# buffer_size: how long to keep messages waiting for matches (seconds) -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.025, # 25ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") - -# Show a matched pair -if aligned_pairs: - img, pc = aligned_pairs[0] - dt = abs(img.ts - pc.ts) - print(f"\nFirst matched pair: Δ{dt*1000:.1f}ms") -``` - - -``` -Video: 29 frames, Lidar: 15 scans -Aligned pairs: 11 out of 29 video frames - -First matched pair: Δ11.3ms -``` - -
-Visualization helper - -```python session=align fold no-result -import matplotlib -import matplotlib.pyplot as plt - -def plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, path): - """Single timeline: video above axis, lidar below, green lines for matches.""" - matplotlib.use('Agg') - plt.style.use('dark_background') - - # Get base timestamp for relative times (frames have .ts attribute) - base_ts = video_frames[0].ts - video_ts = [f.ts - base_ts for f in video_frames] - lidar_ts = [s.ts - base_ts for s in lidar_scans] - - # Find matched timestamps - matched_video_ts = set(img.ts for img, _ in aligned_pairs) - matched_lidar_ts = set(pc.ts for _, pc in aligned_pairs) - - fig, ax = plt.subplots(figsize=(12, 2.5)) - - # Video markers above axis (y=0.3) - circles, cyan when matched - for frame in video_frames: - rel_ts = frame.ts - base_ts - matched = frame.ts in matched_video_ts - ax.plot(rel_ts, 0.3, 'o', color='cyan' if matched else '#688', markersize=8) - - # Lidar markers below axis (y=-0.3) - squares, orange when matched - for scan in lidar_scans: - rel_ts = scan.ts - base_ts - matched = scan.ts in matched_lidar_ts - ax.plot(rel_ts, -0.3, 's', color='orange' if matched else '#a86', markersize=8) - - # Green lines connecting matched pairs - for img, pc in aligned_pairs: - img_rel = img.ts - base_ts - pc_rel = pc.ts - base_ts - ax.plot([img_rel, pc_rel], [0.3, -0.3], '-', color='lime', alpha=0.6, linewidth=1) - - # Axis styling - ax.axhline(y=0, color='white', linewidth=0.5, alpha=0.3) - ax.set_xlim(-0.1, max(video_ts + lidar_ts) + 0.1) - ax.set_ylim(-0.6, 0.6) - ax.set_xlabel('Time (s)') - ax.set_yticks([0.3, -0.3]) - ax.set_yticklabels(['Video', 'Lidar']) - ax.set_title(f'{len(aligned_pairs)} matched from {len(video_frames)} video + {len(lidar_scans)} lidar') - plt.tight_layout() - plt.savefig(path, transparent=True) - plt.close() -``` - -
- -```python session=align output=assets/alignment_timeline.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline.png) - -If we loosen up our match tolerance, we might get multiple pairs matching the same lidar frame. - -```python session=align -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.05, # 50ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") -``` - - -``` -Video: 58 frames, Lidar: 30 scans -Aligned pairs: 23 out of 58 video frames -``` - - -```python session=align output=assets/alignment_timeline2.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline2.png) - -## Combine Frame Alignment with a Quality Filter - -More on [quality filtering here](quality_filter.md). - -```python session=align -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier - -# Lists to collect items as they flow through streams -video_frames = [] -lidar_scans = [] - -video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - sharpness_barrier(3.0), - ops.do_action(lambda x: video_frames.append(x)) -) - -lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: lidar_scans.append(x)) -) - -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.025, # 25ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") - -``` - - -``` -Video: 6 frames, Lidar: 15 scans -Aligned pairs: 1 out of 6 video frames -``` - -```python session=align output=assets/alignment_timeline3.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline3.png) - -We are very picky but data is high quality. Best frame, with closest lidar match in this window. - -## How It Works - -The primary stream (first argument) drives emissions. When a primary message arrives: - -1. **Immediate match**: If matching secondaries already exist in buffers, emit immediately -2. **Deferred match**: If secondaries are missing, buffer the primary and wait - -When secondary messages arrive: -1. Add to buffer for future primary matches -2. Check buffered primaries - if this completes a match, emit - -
-diagram source - -```pikchr fold output=assets/alignment_flow.svg -color = white -fill = none -linewid = 0.35in - -Primary: box "Primary" "arrives" rad 5px fit wid 170% ht 170% -arrow -Check: box "Check" "secondaries" rad 5px fit wid 170% ht 170% - -arrow from Check.e right 0.35in then up 0.4in then right 0.35in -Emit: box "Emit" "match" rad 5px fit wid 170% ht 170% -text "all found" at (Emit.w.x - 0.4in, Emit.w.y + 0.15in) - -arrow from Check.e right 0.35in then down 0.4in then right 0.35in -Buffer: box "Buffer" "primary" rad 5px fit wid 170% ht 170% -text "waiting..." at (Buffer.w.x - 0.4in, Buffer.w.y - 0.15in) -``` - -
- - -![output](assets/alignment_flow.svg) - -## Parameters - -| Parameter | Type | Default | Description | -|--------------------------|--------------------|----------|-------------------------------------------------| -| `primary_observable` | `Observable[T]` | required | Primary stream that drives output timing | -| `*secondary_observables` | `Observable[S]...` | required | One or more secondary streams to align | -| `match_tolerance` | `float` | 0.1 | Max time difference for a match (seconds) | -| `buffer_size` | `float` | 1.0 | How long to buffer unmatched messages (seconds) | - - - -## Usage in Modules - -Every module `In` port exposes an `.observable()` method that returns a backpressured stream of incoming messages. This makes it easy to align inputs from multiple sensors. - -From [`detection/module3D.py`](/dimos/perception/detection/module3D.py), projecting 2D detections into 3D pointclouds: - -```python skip -class Detection3DModule(Detection2DModule): - color_image: In[Image] - pointcloud: In[PointCloud2] - - def start(self): - # Align 2D detections with pointcloud data - self.detection_stream_3d = align_timestamped( - backpressure(self.detection_stream_2d()), - self.pointcloud.observable(), - match_tolerance=0.25, - buffer_size=20.0, - ).pipe(ops.map(detection2d_to_3d)) -``` - -The 2D detection stream (camera + ML model) is the primary, matched with raw pointcloud data from lidar. The longer `buffer_size=20.0` accounts for variable ML inference times. diff --git a/docs/usage/lcm.md b/docs/usage/lcm.md deleted file mode 100644 index 99437a2458..0000000000 --- a/docs/usage/lcm.md +++ /dev/null @@ -1,180 +0,0 @@ -# LCM Messages - -DimOS uses [LCM (Lightweight Communications and Marshalling)](https://github.com/lcm-proj/lcm) for inter-process communication on a local machine (similar to how ROS uses DDS). LCM is a simple [UDP multicast](https://lcm-proj.github.io/lcm/content/udp-multicast-protocol.html#lcm-udp-multicast-protocol-description) pubsub protocol with a straightforward [message definition language](https://lcm-proj.github.io/lcm/content/lcm-type-ref.html#lcm-type-specification-language). - -The LCM project provides pubsub clients and code generators for many languages. For us the power of LCM is its message definition format, multi-language classes that encode themselves to a compact binary format. This means LCM messages can be sent over any transport (WebSocket, SSH, shared memory, etc.) between differnt programming languages. - -Our messages are ported from ROS (they are structurally compatible in order to facilitate easy communication to ROS if needed) -Repo that hosts our message definitions and autogenerators is at [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm/) - -our LCM implementation significantly [outperforms ROS for local communication](/docs/usage/transports.md#benchmarks) - -## Supported languages - -Apart from python, we have examples of LCM integrations for: -- [**C++**](/examples/language-interop/cpp/README.md) -- [**TypeScript**](/examples/language-interop/ts/README.md) -- [**Lua**](/examples/language-interop/lua/README.md) - -In our [/examples/language-interop/](/examples/language-interop/) dir - -Types generated (but no examples yet) for: -[**C#**](https://github.com/dimensionalOS/dimos-lcm/tree/main/generated/csharp) and [**Java**](https://github.com/dimensionalOS/dimos-lcm/tree/main/generated/java) - -### Native Modules - -Given LCM is so portable, we can easily run dimos [Modules](/docs/usage/modules.md) written in [third party languages](/docs/usage/native_modules.md) - -## dimos-lcm Package - -The `dimos-lcm` package provides base message types that mirror [ROS message definitions](https://docs.ros.org/en/melodic/api/sensor_msgs/html/index.html): - -```python session=lcm_demo ansi=false -from dimos_lcm.geometry_msgs import Vector3 as LCMVector3 -from dimos_lcm.sensor_msgs.PointCloud2 import PointCloud2 as LCMPointCloud2 - -# LCM messages can encode to binary -msg = LCMVector3() -msg.x, msg.y, msg.z = 1.0, 2.0, 3.0 - -binary = msg.lcm_encode() -print(f"Encoded to {len(binary)} bytes: {binary.hex()}") - -# And decode back -decoded = LCMVector3.lcm_decode(binary) -print(f"Decoded: x={decoded.x}, y={decoded.y}, z={decoded.z}") -``` - - -``` -Encoded to 24 bytes: 000000000000f03f00000000000000400000000000000840 -Decoded: x=1.0, y=2.0, z=3.0 -``` - -## Dimos Message Overlays - -Dimos subclasses the base LCM types to add Python-friendly features while preserving binary compatibility. For example, `dimos.msgs.geometry_msgs.Vector3` extends the LCM base with: - -- Multiple constructor overloads (from tuples, numpy arrays, etc.) -- Math operations (`+`, `-`, `*`, `/`, dot product, cross product) -- Conversions to numpy, quaternions, etc. - -```python session=lcm_demo ansi=false -from dimos.msgs.geometry_msgs import Vector3 - -# Rich constructors -v1 = Vector3(1, 2, 3) -v2 = Vector3([4, 5, 6]) -v3 = Vector3(v1) # copy - -# Math operations -print(f"v1 + v2 = {(v1 + v2).to_tuple()}") -print(f"v1 dot v2 = {v1.dot(v2)}") -print(f"v1 x v2 = {v1.cross(v2).to_tuple()}") -print(f"|v1| = {v1.length():.3f}") - -# Still encodes to LCM binary -binary = v1.lcm_encode() -print(f"LCM encoded: {len(binary)} bytes") -``` - - -``` -v1 + v2 = (5.0, 7.0, 9.0) -v1 dot v2 = 32.0 -v1 x v2 = (-3.0, 6.0, -3.0) -|v1| = 3.742 -LCM encoded: 24 bytes -``` - -## PointCloud2 with Open3D - -A more complex example is `PointCloud2`, which wraps Open3D point clouds while maintaining LCM binary compatibility: - -```python session=lcm_demo ansi=false -import numpy as np -from dimos.msgs.sensor_msgs import PointCloud2 - -# Create from numpy -points = np.random.rand(100, 3).astype(np.float32) -pc = PointCloud2.from_numpy(points, frame_id="camera") - -print(f"PointCloud: {len(pc)} points, frame={pc.frame_id}") -print(f"Center: {pc.center}") - -# Access as Open3D (for visualization, processing) -o3d_cloud = pc.pointcloud -print(f"Open3D type: {type(o3d_cloud).__name__}") - -# Encode to LCM binary (for transport) -binary = pc.lcm_encode() -print(f"LCM encoded: {len(binary)} bytes") - -# Decode back -pc2 = PointCloud2.lcm_decode(binary) -print(f"Decoded: {len(pc2)} points") -``` - - -``` -PointCloud: 100 points, frame=camera -Center: ↗ Vector (Vector([0.49166839, 0.50896413, 0.48393918])) -Open3D type: PointCloud -LCM encoded: 1716 bytes -Decoded: 100 points -``` - -## Transport Independence - -Since LCM messages encode to bytes, you can use them over any transport: - -```python session=lcm_demo ansi=false -from dimos.msgs.geometry_msgs import Vector3 -from dimos.protocol.pubsub.memory import Memory -from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory - -# Same message works with any transport -msg = Vector3(1, 2, 3) - -# In-memory (same process) -memory = Memory() -received = [] -memory.subscribe("velocity", lambda m, t: received.append(m)) -memory.publish("velocity", msg) -print(f"Memory transport: received {received[0]}") - -# The LCM binary can also be sent raw over any byte-oriented channel -binary = msg.lcm_encode() -# send over WebSocket, Redis, TCP, file, etc. -decoded = Vector3.lcm_decode(binary) -print(f"Raw binary transport: decoded {decoded}") -``` - - -``` -Memory transport: received ↗ Vector (Vector([1. 2. 3.])) -Raw binary transport: decoded ↗ Vector (Vector([1. 2. 3.])) -``` - -## Available Message Types - -Dimos provides overlays for common message types: - -| Package | Messages | -|---------|----------| -| `geometry_msgs` | `Vector3`, `Quaternion`, `Pose`, `Twist`, `Transform` | -| `sensor_msgs` | `Image`, `PointCloud2`, `CameraInfo`, `LaserScan` | -| `nav_msgs` | `Odometry`, `Path`, `OccupancyGrid` | -| `vision_msgs` | `Detection2D`, `Detection3D`, `BoundingBox2D` | - -Base LCM types (without Dimos extensions) are available in `dimos_lcm.*`. - -## Creating Custom Message Types - -To create a new message type: - -1. Define the LCM message in `.lcm` format (or use existing `dimos_lcm` base) -2. Create a Python overlay that subclasses the LCM type -3. Add `lcm_encode()` and `lcm_decode()` methods if custom serialization is needed - -See [`PointCloud2.py`](/dimos/msgs/sensor_msgs/PointCloud2.py) and [`Vector3.py`](/dimos/msgs/geometry_msgs/Vector3.py) for examples. diff --git a/docs/usage/modules.md b/docs/usage/modules.md deleted file mode 100644 index e2f91b58ee..0000000000 --- a/docs/usage/modules.md +++ /dev/null @@ -1,179 +0,0 @@ - -# DimOS Modules - -Modules are subsystems on a robot that operate autonomously and communicate with other subsystems using standardized messages. - -Some examples of modules are: - -- Webcam (outputs image) -- Navigation (inputs a map and a target, outputs a path) -- Detection (takes an image and a vision model like YOLO, outputs a stream of detections) - -Below is an example of a structure for controlling a robot. Black blocks represent modules, and colored lines are connections and message types. It's okay if this doesn't make sense now. It will by the end of this document. - -```python output=assets/go2_nav.svg -from dimos.core.introspection import to_svg -from dimos.robot.unitree_webrtc.unitree_go2_blueprints import nav -to_svg(nav, "assets/go2_nav.svg") -``` - - -![output](assets/go2_nav.svg) - -## Camera Module - -Let's learn how to build stuff like the above, starting with a simple camera module. - -```python session=camera_module_demo output=assets/camera_module.svg -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.core.introspection import to_svg -to_svg(CameraModule.module_info(), "assets/camera_module.svg") -``` - - -![output](assets/camera_module.svg) - -We can also print Module I/O quickly to the console via the `.io()` call. We will do this from now on. - -```python session=camera_module_demo ansi=false -print(CameraModule.io()) -``` - - -``` -┌┴─────────────┐ -│ CameraModule │ -└┬─────────────┘ - ├─ color_image: Image - ├─ camera_info: CameraInfo - │ - ├─ RPC start() - ├─ RPC stop() - │ - ├─ Skill take_a_picture -``` - -We can see that the camera module outputs two streams: - -- `color_image` with [sensor_msgs.Image](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/Image.html) type -- `camera_info` with [sensor_msgs.CameraInfo](https://docs.ros.org/en/melodic/api/sensor_msgs/html/msg/CameraInfo.html) type - -It offers two RPC calls: `start()` and `stop()` (lifecycle methods). - -It also exposes an agentic [skill](/docs/usage/blueprints.md#defining-skills) called `take_a_picture` (more on skills in the Blueprints guide). - -We can start this module and explore the output of its streams in real time (this will use your webcam). - -```python session=camera_module_demo ansi=false -import time - -camera = CameraModule() -camera.start() -# Now this module runs in our main loop in a thread. We can observe its outputs. - -print(camera.color_image) - -camera.color_image.subscribe(print) -time.sleep(0.5) -camera.stop() -``` - - -``` -Out color_image[Image] @ CameraModule -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:16) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:16) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2025-12-31 15:54:17) -``` - - -## Connecting modules - -Let's load a standard 2D detector module and hook it up to a camera. - -```python ansi=false session=detection_module -from dimos.perception.detection.module2D import Detection2DModule, Config -print(Detection2DModule.io()) -``` - - -``` - ├─ image: Image -┌┴──────────────────┐ -│ Detection2DModule │ -└┬──────────────────┘ - ├─ detections: Detection2DArray - ├─ annotations: ImageAnnotations - ├─ detected_image_0: Image - ├─ detected_image_1: Image - ├─ detected_image_2: Image - │ - ├─ RPC set_transport(stream_name: str, transport: Transport) -> bool - ├─ RPC start() -> None - ├─ RPC stop() -> None -``` - - - -Looks like the detector just needs an image input and outputs some sort of detection and annotation messages. Let's connect it to a camera. - -```python ansi=false -import time -from dimos.perception.detection.module2D import Detection2DModule, Config -from dimos.hardware.sensors.camera.module import CameraModule - -camera = CameraModule() -detector = Detection2DModule() - -detector.image.connect(camera.color_image) - -camera.start() -detector.start() - -detector.detections.subscribe(print) -time.sleep(3) -detector.stop() -camera.stop() -``` - - -``` -Detection(Person(1)) -Detection(Person(1)) -Detection(Person(1)) -Detection(Person(1)) -``` - -## Distributed Execution - -As we build module structures, we'll quickly want to utilize all cores on the machine (which Python doesn't allow as a single process) and potentially distribute modules across machines or even the internet. - -For this, we use `dimos.core` and DimOS transport protocols. - -Defining message exchange protocols and message types also gives us the ability to write models in faster languages. - -## Blueprints - -A blueprint is a predefined structure of interconnected modules. You can include blueprints or modules in new blueprints. - -A basic Unitree Go2 blueprint looks like what we saw before. - -```python session=blueprints output=assets/go2_agentic.svg -from dimos.core.introspection import to_svg -from dimos.robot.unitree_webrtc.unitree_go2_blueprints import agentic - -to_svg(agentic, "assets/go2_agentic.svg") -``` - - -![output](assets/go2_agentic.svg) - - -To see more information on how to use Blueprints, see [Blueprints](/docs/usage/blueprints.md). diff --git a/docs/usage/native_modules.md b/docs/usage/native_modules.md deleted file mode 100644 index 5a9362839f..0000000000 --- a/docs/usage/native_modules.md +++ /dev/null @@ -1,282 +0,0 @@ -# Native Modules - -Prerequisite for this is to understand dimos [Modules](/docs/usage/modules.md) and [Blueprints](/docs/usage/blueprints.md). - -Native modules let you wrap **any executable** as a first-class DimOS module, given it speaks LCM. - -Python will handle blueprint wiring, lifecycle, and logging. Native binary handles the actual computation, publishing and subscribing directly on LCM. - -Python module **never touches the pubsub data**. It just passes configuration and LCM topic to use via CLI args to your executable. - -On how to speak LCM with the rest of dimos, you can read our [LCM intro](/docs/usage/lcm.md) - -## Defining a native module - -Python side native module is just a definition of a **config** dataclass and **module** class specifying pubsub I/O. - -Both the config dataclass and pubsub topics get converted to CLI args passed down to your executable once the module is started. - -```python no-result session=nativemodule -from dataclasses import dataclass -from dimos.core import Out, LCMTransport -from dimos.core.native_module import NativeModule, NativeModuleConfig -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.msgs.sensor_msgs.Imu import Imu -import time - -@dataclass(kw_only=True) -class MyLidarConfig(NativeModuleConfig): - executable: str = "./build/my_lidar" - host_ip: str = "192.168.1.5" - frequency: float = 10.0 - -class MyLidar(NativeModule): - default_config = MyLidarConfig - pointcloud: Out[PointCloud2] - imu: Out[Imu] - - -``` - -That's it. `MyLidar` is a full DimOS module. You can use it with `autoconnect`, blueprints, transport overrides, and specs. Once this module is started, your `./build/my_lidar` will get called with specific CLI args. - - -## How it works - -When `start()` is called, NativeModule: - -1. **Builds the executable** if it doesn't exist and `build_command` is set. -2. **Collects topics** from blueprint-assigned transports on each declared port. -3. **Builds the command line**: ` -- ... -- ...` -4. **Launches the subprocess** with `Popen`, piping stdout/stderr. -5. **Starts a watchdog** thread that calls `stop()` if the process crashes. - -For the example above, the launched command would look like: - -```sh -./build/my_lidar \ - --pointcloud '/pointcloud#sensor_msgs.PointCloud2' \ - --imu '/imu#sensor_msgs.Imu' \ - --host_ip 192.168.1.5 \ - --frequency 10.0 -``` - -```python ansi=false session=nativemodule skip -mylidar = MyLidar() -mylidar.pointcloud.transport = LCMTransport("/lidar", PointCloud2) -mylidar.imu.transport = LCMTransport("/imu", Imu) -mylidar.start() -``` - - -``` -2026-02-14T11:22:12.123963Z [info ] Starting native process [dimos/core/native_module.py] cmd='./build/my_lidar --pointcloud /lidar#sensor_msgs.PointCloud2 --imu /imu#sensor_msgs.Imu --host_ip 192.168.1.5 --frequency 10.0' cwd=/home/lesh/coding/dimos/docs/usage/build -``` - -Topic strings use the format `/#`, which is the LCM channel name that Python `LCMTransport` subscribers use. The native binary publishes on these exact channels. - -When `stop()` is called, the process receives SIGTERM. If it doesn't exit within `shutdown_timeout` seconds (default 10), it gets SIGKILL. - -## Config - -`NativeModuleConfig` extends `ModuleConfig` with subprocess fields: - -| Field | Type | Default | Description | -|--------------------|------------------|---------------|-------------------------------------------------------------| -| `executable` | `str` | *(required)* | Path to the native binary (relative to `cwd` if set) | -| `build_command` | `str \| None` | `None` | Shell command to run if executable is missing (auto-build) | -| `cwd` | `str \| None` | `None` | Working directory for build and runtime. Relative paths are resolved against the Python file defining the module | -| `extra_args` | `list[str]` | `[]` | Additional CLI arguments appended after auto-generated ones | -| `extra_env` | `dict[str, str]` | `{}` | Extra environment variables for the subprocess | -| `shutdown_timeout` | `float` | `10.0` | Seconds to wait for SIGTERM before SIGKILL | -| `log_format` | `LogFormat` | `TEXT` | How to parse subprocess output (`TEXT` or `JSON`) | -| `cli_exclude` | `frozenset[str]` | `frozenset()` | Config fields to skip when generating CLI args | - -### Auto CLI arg generation - -Any field you add to your config subclass automatically becomes a `--name value` CLI arg. Fields from `NativeModuleConfig` itself (like `executable`, `extra_args`, `cwd`) are **not** passed — they're for Python-side orchestration only. - -```python skip - -class LogFormat(enum.Enum): - TEXT = "text" - JSON = "json" - -@dataclass(kw_only=True) -class MyConfig(NativeModuleConfig): - executable: str = "./build/my_module" # relative or absolute path to your executable - host_ip: str = "192.168.1.5" # becomes --host_ip 192.168.1.5 - frequency: float = 10.0 # becomes --frequency 10.0 - enable_imu: bool = True # becomes --enable_imu true - filters: list[str] = field(default_factory=lambda: ["a", "b"]) # becomes --filters a,b -``` - -- `None` values are skipped. -- Booleans are lowercased (`true`/`false`). -- Lists are comma-joined. - -### Excluding fields - -If a config field shouldn't be a CLI arg, add it to `cli_exclude`: - -```python skip -@dataclass(kw_only=True) -class FastLio2Config(NativeModuleConfig): - executable: str = "./build/fastlio2" - config: str = "mid360.yaml" # human-friendly name - config_path: str | None = None # resolved absolute path - cli_exclude: frozenset[str] = frozenset({"config"}) # only config_path is passed - - def __post_init__(self) -> None: - if self.config_path is None: - self.config_path = str(Path(self.config).resolve()) -``` - -## Using with blueprints - -Native modules work with `autoconnect` exactly like Python modules: - -```python skip -from dimos.core.blueprints import autoconnect - -class PointCloudConsumer(Module): - pointcloud: In[PointCloud2] - imu: In[Imu] - -autoconnect( - MyLidar.blueprint(host_ip="192.168.1.10"), - PointCloudConsumer.blueprint(), -).build().loop() -``` - -`autoconnect` matches ports by `(name, type)`, assigns LCM topics, and passes them to the native binary as CLI args. You can override transports as usual: - -```python skip -blueprint = autoconnect( - MyLidar.blueprint(), - PointCloudConsumer.blueprint(), -).transports({ - ("pointcloud", PointCloud2): LCMTransport("/my/custom/lidar", PointCloud2), -}) -``` - -## Logging - -NativeModule pipes subprocess stdout and stderr through structlog: - -- **stdout** is logged at `info` level. -- **stderr** is logged at `warning` level. - -### JSON log format - -If your native binary outputs structured JSON lines, set `log_format=LogFormat.JSON`: - -```python skip -@dataclass(kw_only=True) -class MyConfig(NativeModuleConfig): - executable: str = "./build/my_module" - log_format: LogFormat = LogFormat.JSON -``` - -The module will parse each line as JSON and feed the key-value pairs into structlog. The `event` key becomes the log message: - -```json -{"event": "sensor initialized", "device": "/dev/ttyUSB0", "baud": 115200} -``` - -Malformed lines fall back to plain text logging. - -## Writing the C++ side - -A header-only helper is provided at [`dimos/hardware/sensors/lidar/common/dimos_native_module.hpp`](/dimos/hardware/sensors/lidar/common/dimos_native_module.hpp): - -```cpp -#include "dimos_native_module.hpp" -#include "sensor_msgs/PointCloud2.hpp" - -int main(int argc, char** argv) { - dimos::NativeModule mod(argc, argv); - - // Get the LCM channel for a declared port - std::string pc_topic = mod.topic("pointcloud"); - - // Get config values - float freq = mod.arg_float("frequency", 10.0); - std::string ip = mod.arg("host_ip", "192.168.1.5"); - - // Set up LCM publisher and publish on pc_topic... -} -``` - -The helper provides: - -| Method | Description | -|---------------------------|----------------------------------------------------------------| -| `topic(port)` | Get the full LCM channel string (`/topic#msg_type`) for a port | -| `arg(key, default)` | Get a string config value | -| `arg_float(key, default)` | Get a float config value | -| `arg_int(key, default)` | Get an int config value | -| `has(key)` | Check if a port/arg was provided | - -It also includes `make_header()` and `time_from_seconds()` for building ROS-compatible stamped messages. - -## Examples - -For language interop examples (subscribing to DimOS topics from C++, TypeScript, Lua), see [/examples/language-interop/](/examples/language-interop/README.md). - -### Livox Mid-360 Module - -The Livox Mid-360 LiDAR driver is a complete example at [`dimos/hardware/sensors/lidar/livox/module.py`](/dimos/hardware/sensors/lidar/livox/module.py): - -```python skip -from dimos.core import Out -from dimos.core.native_module import NativeModule, NativeModuleConfig -from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 -from dimos.msgs.sensor_msgs.Imu import Imu -from dimos.spec import perception - -@dataclass(kw_only=True) -class Mid360Config(NativeModuleConfig): - cwd: str | None = "cpp" - executable: str = "result/bin/mid360_native" - build_command: str | None = "nix build .#mid360_native" - host_ip: str = "192.168.1.5" - lidar_ip: str = "192.168.1.155" - frequency: float = 10.0 - enable_imu: bool = True - frame_id: str = "lidar_link" - # ... SDK port configuration - -class Mid360(NativeModule, perception.Lidar, perception.IMU): - default_config = Mid360Config - lidar: Out[PointCloud2] - imu: Out[Imu] -``` - -Usage: - -```python skip -from dimos.hardware.sensors.lidar.livox.module import Mid360 - -autoconnect( - Mid360.blueprint(host_ip="192.168.1.5"), - SomeConsumer.blueprint(), -) -``` - -## Auto Building - -If `build_command` is set in the module config, and the executable doesn't exist when `start()` is called, NativeModule runs the build command automatically. -Build output is piped through structlog (stdout at `info`, stderr at `warning`). - -```python skip -@dataclass(kw_only=True) -class MyLidarConfig(NativeModuleConfig): - cwd: str | None = "cpp" - executable: str = "result/bin/my_lidar" - build_command: str | None = "nix build .#my_lidar" -``` - -`cwd` is used for both the build command and the runtime subprocess. Relative paths are resolved against the directory of the Python file that defines the module - -If the executable already exists, the build step is skipped entirely. diff --git a/docs/usage/sensor_streams/README.md b/docs/usage/sensor_streams/README.md deleted file mode 100644 index dc2ce6c91d..0000000000 --- a/docs/usage/sensor_streams/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Sensor Streams - -Dimos uses reactive streams (RxPY) to handle sensor data. This approach naturally fits robotics where multiple sensors emit data asynchronously at different rates, and downstream processors may be slower than the data sources. - -## Guides - -| Guide | Description | -|----------------------------------------------|---------------------------------------------------------------| -| [ReactiveX Fundamentals](reactivex.md) | Observables, subscriptions, and disposables | -| [Advanced Streams](advanced_streams.md) | Backpressure, parallel subscribers, synchronous getters | -| [Quality-Based Filtering](quality_filter.md) | Select highest quality frames when downsampling streams | -| [Temporal Alignment](temporal_alignment.md) | Match messages from multiple sensors by timestamp | -| [Storage & Replay](storage_replay.md) | Record sensor streams to disk and replay with original timing | - -## Quick Example - -```python -from reactivex import operators as ops -from dimos.utils.reactive import backpressure -from dimos.types.timestamped import align_timestamped -from dimos.msgs.sensor_msgs.Image import sharpness_barrier - -# Camera at 30fps, lidar at 10Hz -camera_stream = camera.observable() -lidar_stream = lidar.observable() - -# Pipeline: filter blurry frames -> align with lidar -> handle slow consumers -processed = ( - camera_stream.pipe( - sharpness_barrier(10.0), # Keep sharpest frame per 100ms window (10Hz) - ) -) - -aligned = align_timestamped( - backpressure(processed), # Camera as primary - lidar_stream, # Lidar as secondary - match_tolerance=0.1, -) - -aligned.subscribe(lambda pair: process_frame_with_pointcloud(*pair)) -``` diff --git a/docs/usage/sensor_streams/advanced_streams.md b/docs/usage/sensor_streams/advanced_streams.md deleted file mode 100644 index 187d432af2..0000000000 --- a/docs/usage/sensor_streams/advanced_streams.md +++ /dev/null @@ -1,295 +0,0 @@ -# Advanced Stream Handling - -> **Prerequisite:** Read [ReactiveX Fundamentals](reactivex.md) first for Observable basics. - -## Backpressure and Parallel Subscribers to Hardware - -In robotics, we deal with hardware that produces data at its own pace - a camera outputs 30fps whether you're ready or not. We can't tell the camera to slow down. And we often have multiple consumers: one module wants every frame for recording, another runs slow ML inference and only needs the latest frame. - -**The problem:** A fast producer can overwhelm a slow consumer, causing memory buildup or dropped frames. We might have multiple subscribers to the same hardware that operate at different speeds. - - -
Pikchr - -```pikchr fold output=assets/backpressure.svg -color = white -fill = none - -Fast: box "Camera" "60 fps" rad 5px fit wid 130% ht 130% -arrow right 0.4in -Queue: box "queue" rad 5px fit wid 170% ht 170% -arrow right 0.4in -Slow: box "ML Model" "2 fps" rad 5px fit wid 130% ht 130% - -text "items pile up!" at (Queue.x, Queue.y - 0.45in) -``` - -
- - -![output](assets/backpressure.svg) - - -**The solution:** The `backpressure()` wrapper handles this by: - -1. **Sharing the source** - Camera runs once, all subscribers share the stream -2. **Per-subscriber speed** - Fast subscribers get every frame, slow ones get the latest when ready -3. **No blocking** - Slow subscribers never block the source or each other - -```python session=bp -import time -import reactivex as rx -from reactivex import operators as ops -from reactivex.scheduler import ThreadPoolScheduler -from dimos.utils.reactive import backpressure - -# We need this scaffolding here. Normally DimOS handles this. -scheduler = ThreadPoolScheduler(max_workers=4) - -# Simulate fast source -source = rx.interval(0.05).pipe(ops.take(20)) -safe = backpressure(source, scheduler=scheduler) - -fast_results = [] -slow_results = [] - -safe.subscribe(lambda x: fast_results.append(x)) - -def slow_handler(x): - time.sleep(0.15) - slow_results.append(x) - -safe.subscribe(slow_handler) - -time.sleep(1.5) -print(f"fast got {len(fast_results)} items: {fast_results[:5]}...") -print(f"slow got {len(slow_results)} items (skipped {len(fast_results) - len(slow_results)})") -scheduler.executor.shutdown(wait=True) -``` - - -``` -fast got 20 items: [0, 1, 2, 3, 4]... -slow got 7 items (skipped 13) -``` - -### How it works - - -
Pikchr - -```pikchr fold output=assets/backpressure_solution.svg -color = white -fill = none -linewid = 0.3in - -Source: box "Camera" "60 fps" rad 5px fit wid 170% ht 170% -arrow -Core: box "backpressure" rad 5px fit wid 170% ht 170% -arrow from Core.e right 0.3in then up 0.35in then right 0.3in -Fast: box "Fast Sub" rad 5px fit wid 170% ht 170% -arrow from Core.e right 0.3in then down 0.35in then right 0.3in -SlowPre: box "LATEST" rad 5px fit wid 170% ht 170% -arrow -Slow: box "Slow Sub" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/backpressure_solution.svg) - -The `LATEST` strategy means: when the slow subscriber finishes processing, it gets whatever the most recent value is, skipping any values that arrived while it was busy. - -### Usage in modules - -Most module streams offer backpressured observables. - -```python session=bp -from dimos.core import Module, In -from dimos.msgs.sensor_msgs import Image - -class MLModel(Module): - color_image: In[Image] - def start(self): - # no reactivex, simple callback - self.color_image.subscribe(...) - # backpressured - self.color_image.observable().subscribe(...) - # non-backpressured - will pile up queue - self.color_image.pure_observable().subscribe(...) - - -``` - -## Getting Values Synchronously - -Sometimes you don't want a stream, you just want to call a function and get the latest value. - -If you are doing this periodically as a part of a processing loop, it is very likely that your code will be much cleaner and safer using actual reactivex pipeline. So bias towards checking our [reactivex quick guide](reactivex.md) and [official docs](https://rxpy.readthedocs.io/) - -(TODO we should actually make this example actually executable) - -```python skip - self.color_image.observable().pipe( - # takes the best image from a stream every 200ms, - # ensuring we are feeding our detector with highest quality frames - quality_barrier(lambda x: x["quality"], target_frequency=0.2), - - # converts Image into Person detections - ops.map(detect_person), - - # converts Detection2D to Twist pointing in the direction of a detection - ops.map(detection2d_to_twist), - - # emits the latest value every 50ms making our control loop run at 20hz - # despite detections running at 200ms - ops.sample(0.05), - ).subscribe(self.twist.publish) # shoots off the Twist out of the module -``` - - -If you'd still like to switch to synchronous fetching, we provide two approaches, `getter_hot()` and `getter_cold()` - -| | `getter_hot()` | `getter_cold()` | -|------------------|--------------------------------|----------------------------------| -| **Subscription** | Stays active in background | Fresh subscription each call | -| **Read speed** | Instant (value already cached) | Slower (waits for value) | -| **Resources** | Keeps connection open | Opens/closes each call | -| **Use when** | Frequent reads, need latest | Occasional reads, save resources | - -
-diagram source - -```pikchr fold output=assets/getter_hot_cold.svg -color = white -fill = none - -H_Title: box "getter_hot()" rad 5px fit wid 170% ht 170% - -Sub: box "subscribe" rad 5px fit wid 170% ht 170% with .n at H_Title.s + (0, -0.5in) -arrow from H_Title.s to Sub.n -arrow right from Sub.e -Cache: box "Cache" rad 5px fit wid 170% ht 170% - -# blocking box around subscribe->cache (one-time setup) -Blk0: box dashed color 0x5c9ff0 with .nw at Sub.nw + (-0.1in, 0.25in) wid (Cache.e.x - Sub.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk0.n + (0, -0.05in) - -arrow right from Cache.e -Getter: box "getter" rad 5px fit wid 170% ht 170% - -arrow from Getter.e right 0.3in then down 0.25in then right 0.2in -G1: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from G1.e -box invis "instant" fit wid 150% - -arrow from Getter.e right 0.3in then down 0.7in then right 0.2in -G2: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from G2.e -box invis "instant" fit wid 150% - -text "always subscribed" italic with .n at Blk0.s + (0, -0.1in) - - -# === getter_cold section === -C_Title: box "getter_cold()" rad 5px fit wid 170% ht 170% with .nw at H_Title.sw + (0, -1.6in) - -arrow down 0.3in from C_Title.s -ColdGetter: box "getter" rad 5px fit wid 170% ht 170% - -# Branch to first call -arrow from ColdGetter.e right 0.3in then down 0.3in then right 0.2in -Cold1: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from Cold1.e -Sub1: box invis "subscribe" fit wid 150% -arrow right 0.4in from Sub1.e -Wait1: box invis "wait" fit wid 150% -arrow right 0.4in from Wait1.e -Val1: box invis "value" fit wid 150% -arrow right 0.4in from Val1.e -Disp1: box invis "dispose " fit wid 150% - -# blocking box around first row -Blk1: box dashed color 0x5c9ff0 with .nw at Cold1.nw + (-0.1in, 0.25in) wid (Disp1.e.x - Cold1.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk1.n + (0, -0.05in) - -# Branch to second call -arrow from ColdGetter.e right 0.3in then down 1.2in then right 0.2in -Cold2: box invis "call()" color 0x8cbdf2 fit wid 150% -arrow right 0.4in from Cold2.e -Sub2: box invis "subscribe" fit wid 150% -arrow right 0.4in from Sub2.e -Wait2: box invis "wait" fit wid 150% -arrow right 0.4in from Wait2.e -Val2: box invis "value" fit wid 150% -arrow right 0.4in from Val2.e -Disp2: box invis "dispose " fit wid 150% - -# blocking box around second row -Blk2: box dashed color 0x5c9ff0 with .nw at Cold2.nw + (-0.1in, 0.25in) wid (Disp2.e.x - Cold2.w.x + 0.2in) ht 0.7in rad 5px -text "blocking" italic with .n at Blk2.n + (0, -0.05in) -``` - -
- - -![output](assets/getter_hot_cold.svg) - - -**Prefer `getter_cold()`** when you can afford to wait and warmup isn't expensive. It's simpler (no cleanup needed) and doesn't hold resources. Only use `getter_hot()` when you need instant reads or the source is expensive to start. - -### `getter_hot()` - Background subscription, instant reads - -Subscribes immediately and keeps updating in the background. Each call returns the cached latest value instantly. - -```python session=sync -import time -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import getter_hot - -source = rx.interval(0.1).pipe(ops.take(10)) - -get_val = getter_hot(source, timeout=5.0) # blocks until first message, with 5s timeout -# alternatively not to block (but get_val() might return None) -# get_val = getter_hot(source, nonblocking=True) - -print("first call:", get_val()) # instant - value already there -time.sleep(0.35) -print("after 350ms:", get_val()) # instant - returns cached latest -time.sleep(0.35) -print("after 700ms:", get_val()) - -get_val.dispose() # Don't forget to clean up! -``` - - -``` -first call: 0 -after 350ms: 3 -after 700ms: 6 -``` - -### `getter_cold()` - Fresh subscription each call - -Each call creates a new subscription, waits for one value, and cleans up. Slower but doesn't hold resources: - -```python session=sync -from dimos.utils.reactive import getter_cold - -source = rx.of(0, 1, 2, 3, 4) -get_val = getter_cold(source, timeout=5.0) - -# Each call creates fresh subscription, gets first value -print("call 1:", get_val()) # subscribes, gets 0, disposes -print("call 2:", get_val()) # subscribes again, gets 0, disposes -print("call 3:", get_val()) # subscribes again, gets 0, disposes -``` - - -``` -call 1: 0 -call 2: 0 -call 3: 0 -``` diff --git a/docs/usage/sensor_streams/assets/alignment_flow.svg b/docs/usage/sensor_streams/assets/alignment_flow.svg deleted file mode 100644 index 72aeb337f3..0000000000 --- a/docs/usage/sensor_streams/assets/alignment_flow.svg +++ /dev/null @@ -1,22 +0,0 @@ - - -Primary -arrives - - - -Check -secondaries - - - -Emit -match -all found - - - -Buffer -primary -waiting... - diff --git a/docs/usage/sensor_streams/assets/alignment_overview.svg b/docs/usage/sensor_streams/assets/alignment_overview.svg deleted file mode 100644 index 8abada6d02..0000000000 --- a/docs/usage/sensor_streams/assets/alignment_overview.svg +++ /dev/null @@ -1,18 +0,0 @@ - - -Camera -30 fps - - - -align_timestamped - -Lidar -10 Hz - - - - - -(image, pointcloud) - diff --git a/docs/usage/sensor_streams/assets/alignment_timeline.png b/docs/usage/sensor_streams/assets/alignment_timeline.png deleted file mode 100644 index 235ddd7be0..0000000000 --- a/docs/usage/sensor_streams/assets/alignment_timeline.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cfea5a6aac40182b25decb9ddaeb387ed97a7708e2c51a48f47453c8df7adf57 -size 16136 diff --git a/docs/usage/sensor_streams/assets/alignment_timeline2.png b/docs/usage/sensor_streams/assets/alignment_timeline2.png deleted file mode 100644 index 2bf8ec5eef..0000000000 --- a/docs/usage/sensor_streams/assets/alignment_timeline2.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:22b64923637d05f8f40c9f7c0f0597ee894dc4f31a0f10674aeb809101b54765 -size 23471 diff --git a/docs/usage/sensor_streams/assets/alignment_timeline3.png b/docs/usage/sensor_streams/assets/alignment_timeline3.png deleted file mode 100644 index 61ddc3b54b..0000000000 --- a/docs/usage/sensor_streams/assets/alignment_timeline3.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b8e9589dcd5308f511a2ec7d41bd36978204ccfe1441907bd139029b0489d605 -size 9969 diff --git a/docs/usage/sensor_streams/assets/backpressure.svg b/docs/usage/sensor_streams/assets/backpressure.svg deleted file mode 100644 index b3d69af6fb..0000000000 --- a/docs/usage/sensor_streams/assets/backpressure.svg +++ /dev/null @@ -1,15 +0,0 @@ - - -Camera -60 fps - - - -queue - - - -ML Model -2 fps -items pile up! - diff --git a/docs/usage/sensor_streams/assets/backpressure_solution.svg b/docs/usage/sensor_streams/assets/backpressure_solution.svg deleted file mode 100644 index 454a8f460b..0000000000 --- a/docs/usage/sensor_streams/assets/backpressure_solution.svg +++ /dev/null @@ -1,21 +0,0 @@ - - -Camera -60 fps - - - -backpressure - - - -Fast Sub - - - -LATEST - - - -Slow Sub - diff --git a/docs/usage/sensor_streams/assets/frame_mosaic.jpg b/docs/usage/sensor_streams/assets/frame_mosaic.jpg deleted file mode 100644 index 5c3fbf8350..0000000000 --- a/docs/usage/sensor_streams/assets/frame_mosaic.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e83934e1179651fbca6c9b62cceb7425d1b2f0e8da18a63d4d95bcb4e6ac33ca -size 88206 diff --git a/docs/usage/sensor_streams/assets/frame_mosaic2.jpg b/docs/usage/sensor_streams/assets/frame_mosaic2.jpg deleted file mode 100644 index 5e3032acf2..0000000000 --- a/docs/usage/sensor_streams/assets/frame_mosaic2.jpg +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d73f683e92fda39bac9d1bb840f1fc375c821b4099714829e81f3e739f4d602 -size 91036 diff --git a/docs/usage/sensor_streams/assets/getter_hot_cold.svg b/docs/usage/sensor_streams/assets/getter_hot_cold.svg deleted file mode 100644 index d2f336459c..0000000000 --- a/docs/usage/sensor_streams/assets/getter_hot_cold.svg +++ /dev/null @@ -1,71 +0,0 @@ - - -getter_hot() - -subscribe - - - - - -Cache - -blocking - - - -getter - - -call() - - -instant - - -call() - - -instant -always subscribed - -getter_cold() - - - -getter - - -call() - - -subscribe - - -wait - - -value - - -dispose   - -blocking - - -call() - - -subscribe - - -wait - - -value - - -dispose   - -blocking - diff --git a/docs/usage/sensor_streams/assets/observable_flow.svg b/docs/usage/sensor_streams/assets/observable_flow.svg deleted file mode 100644 index d7e0e021d6..0000000000 --- a/docs/usage/sensor_streams/assets/observable_flow.svg +++ /dev/null @@ -1,16 +0,0 @@ - - -observable - - - -.pipe(ops) - - - -.subscribe() - - - -callback - diff --git a/docs/usage/sensor_streams/assets/sharpness_graph.svg b/docs/usage/sensor_streams/assets/sharpness_graph.svg deleted file mode 100644 index 3d61d12d7c..0000000000 --- a/docs/usage/sensor_streams/assets/sharpness_graph.svg +++ /dev/null @@ -1,1414 +0,0 @@ - - - - - - - - 1980-01-01T00:00:00+00:00 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/usage/sensor_streams/assets/sharpness_graph2.svg b/docs/usage/sensor_streams/assets/sharpness_graph2.svg deleted file mode 100644 index 37c1032de0..0000000000 --- a/docs/usage/sensor_streams/assets/sharpness_graph2.svg +++ /dev/null @@ -1,1429 +0,0 @@ - - - - - - - - 1980-01-01T00:00:00+00:00 - image/svg+xml - - - Matplotlib v3.10.8, https://matplotlib.org/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/usage/sensor_streams/quality_filter.md b/docs/usage/sensor_streams/quality_filter.md deleted file mode 100644 index db21da9c54..0000000000 --- a/docs/usage/sensor_streams/quality_filter.md +++ /dev/null @@ -1,316 +0,0 @@ -# Quality-Based Stream Filtering - -When processing sensor streams, you often want to reduce frequency while keeping the best quality data. For discrete data like images that can't be averaged or merged, instead of blindly dropping frames, `quality_barrier` selects the highest quality item within each time window. - -## The Problem - -A camera outputs 30fps, but your ML model only needs 2fps. Simple approaches: - -- **`sample(0.5)`** - Takes whatever frame happens to land on the interval tick -- **`throttle_first(0.5)`** - Takes the first frame, ignores the rest - -Both ignore quality. You might get a blurry frame when a sharp one was available. - -## The Solution: `quality_barrier` - -```python session=qb -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import quality_barrier - -# Simulated sensor data with quality scores -data = [ - {"id": 1, "quality": 0.3}, - {"id": 2, "quality": 0.9}, # best in first window - {"id": 3, "quality": 0.5}, - {"id": 4, "quality": 0.2}, - {"id": 5, "quality": 0.8}, # best in second window - {"id": 6, "quality": 0.4}, -] - -source = rx.of(*data) - -# Select best quality item per window (2 items per second = 0.5s windows) -result = source.pipe( - quality_barrier(lambda x: x["quality"], target_frequency=2.0), - ops.to_list(), -).run() - -print("Selected:", [r["id"] for r in result]) -print("Qualities:", [r["quality"] for r in result]) -``` - - -``` -Selected: [2] -Qualities: [0.9] -``` - -## Image Sharpness Filtering - -For camera streams, we provide `sharpness_barrier` which uses the image's sharpness score. - -Let's use real camera data from the Unitree Go2 robot to demonstrate. We use the [Sensor Storage & Replay](/docs/usage/sensor_streams/storage_replay.md) toolkit, which provides access to recorded robot data: - -```python session=qb -from dimos.utils.testing import TimedSensorReplay -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier - -# Load recorded Go2 camera frames -video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") - -# Use stream() with seek to skip blank frames, speed=10x to collect faster -input_frames = video_replay.stream(seek=5.0, duration=1.4, speed=10.0).pipe( - ops.to_list() -).run() - -def show_frames(frames): - for i, frame in enumerate(frames[:10]): - print(f" Frame {i}: {frame.sharpness:.3f}") - -print(f"Loaded {len(input_frames)} frames from Go2 camera") -print(f"Frame resolution: {input_frames[0].width}x{input_frames[0].height}") -print("Sharpness scores:") -show_frames(input_frames) -``` - - -``` -Loaded 20 frames from Go2 camera -Frame resolution: 1280x720 -Sharpness scores: - Frame 0: 0.351 - Frame 1: 0.227 - Frame 2: 0.223 - Frame 3: 0.267 - Frame 4: 0.295 - Frame 5: 0.307 - Frame 6: 0.328 - Frame 7: 0.348 - Frame 8: 0.346 - Frame 9: 0.322 -``` - -Using `sharpness_barrier` to select the sharpest frames: - -```python session=qb -# Create a stream from the recorded frames - -sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( - sharpness_barrier(2.0), - ops.to_list() -).run() - -print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") -show_frames(sharp_frames) -``` - - -``` -Output: 3 frame(s) (selected sharpest per window) - Frame 0: 0.351 - Frame 1: 0.352 - Frame 2: 0.360 -``` - -
-Visualization helpers - -```python session=qb fold no-result -import matplotlib -import matplotlib.pyplot as plt -import math - -def plot_mosaic(frames, selected, path, cols=5): - matplotlib.use('Agg') - rows = math.ceil(len(frames) / cols) - aspect = frames[0].width / frames[0].height - fig_w, fig_h = 12, 12 * rows / (cols * aspect) - - fig, axes = plt.subplots(rows, cols, figsize=(fig_w, fig_h)) - fig.patch.set_facecolor('black') - for i, ax in enumerate(axes.flat): - if i < len(frames): - ax.imshow(frames[i].data) - for spine in ax.spines.values(): - spine.set_color('lime' if frames[i] in selected else 'black') - spine.set_linewidth(4 if frames[i] in selected else 0) - ax.set_xticks([]); ax.set_yticks([]) - else: - ax.axis('off') - plt.subplots_adjust(wspace=0.02, hspace=0.02, left=0, right=1, top=1, bottom=0) - plt.savefig(path, facecolor='black', dpi=100, bbox_inches='tight', pad_inches=0) - plt.close() - -def plot_sharpness(frames, selected, path): - matplotlib.use('svg') - plt.style.use('dark_background') - sharpness = [f.sharpness for f in frames] - selected_idx = [i for i, f in enumerate(frames) if f in selected] - - plt.figure(figsize=(10, 3)) - plt.plot(sharpness, 'o-', label='All frames', color='#b5e4f4', alpha=0.7) - for i, idx in enumerate(selected_idx): - plt.axvline(x=idx, color='lime', linestyle='--', label='Selected' if i == 0 else None) - plt.xlabel('Frame'); plt.ylabel('Sharpness') - plt.xticks(range(len(sharpness))) - plt.legend(); plt.grid(alpha=0.3); plt.tight_layout() - plt.savefig(path, transparent=True) - plt.close() -``` - -
- -Visualizing which frames were selected (green border = selected as sharpest in window): - -```python session=qb output=assets/frame_mosaic.jpg -plot_mosaic(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/frame_mosaic.jpg) - -```python session=qb output=assets/sharpness_graph.svg -plot_sharpness(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/sharpness_graph.svg) - -Let's request a higher frequency. - -```python session=qb -sharp_frames = video_replay.stream(seek=5.0, duration=1.5, speed=1.0).pipe( - sharpness_barrier(4.0), - ops.to_list() -).run() - -print(f"Output: {len(sharp_frames)} frame(s) (selected sharpest per window)") -show_frames(sharp_frames) -``` - - -``` -Output: 6 frame(s) (selected sharpest per window) - Frame 0: 0.351 - Frame 1: 0.348 - Frame 2: 0.346 - Frame 3: 0.352 - Frame 4: 0.360 - Frame 5: 0.329 -``` - -```python session=qb output=assets/frame_mosaic2.jpg -plot_mosaic(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/frame_mosaic2.jpg) - - -```python session=qb output=assets/sharpness_graph2.svg -plot_sharpness(input_frames, sharp_frames, '{output}') -``` - - -![output](assets/sharpness_graph2.svg) - -As we can see the system is trying to strike a balance between requested frequency and quality that's available - -### Usage in Camera Module - -Here's how it's used in the actual camera module: - -```python skip -from dimos.core.module import Module - -class CameraModule(Module): - frequency: float = 2.0 # Target output frequency - @rpc - def start(self) -> None: - stream = self.hardware.image_stream() - - if self.config.frequency > 0: - stream = stream.pipe(sharpness_barrier(self.config.frequency)) - - self._disposables.add( - stream.subscribe(self.color_image.publish), - ) - -``` - -### How Sharpness is Calculated - -The sharpness score (0.0 to 1.0) is computed using Sobel edge detection: - -from [`Image.py`](/dimos/msgs/sensor_msgs/Image.py) - -```python session=qb -import cv2 - -# Get a frame and show the calculation -img = input_frames[10] -gray = img.to_grayscale() - -# Sobel gradients - use .data to get the underlying numpy array -sx = cv2.Sobel(gray.data, cv2.CV_32F, 1, 0, ksize=5) -sy = cv2.Sobel(gray.data, cv2.CV_32F, 0, 1, ksize=5) -magnitude = cv2.magnitude(sx, sy) - -print(f"Mean gradient magnitude: {magnitude.mean():.2f}") -print(f"Normalized sharpness: {img.sharpness:.3f}") -``` - - -``` -Mean gradient magnitude: 230.00 -Normalized sharpness: 0.332 -``` - -## Custom Quality Functions - -You can use `quality_barrier` with any quality metric: - -```python session=qb -# Example: select by "confidence" field -detections = [ - {"name": "cat", "confidence": 0.7}, - {"name": "dog", "confidence": 0.95}, # best - {"name": "bird", "confidence": 0.6}, -] - -result = rx.of(*detections).pipe( - quality_barrier(lambda d: d["confidence"], target_frequency=2.0), - ops.to_list(), -).run() - -print(f"Selected: {result[0]['name']} (conf: {result[0]['confidence']})") -``` - - -``` -Selected: dog (conf: 0.95) -``` - -## API Reference - -### `quality_barrier(quality_func, target_frequency)` - -RxPY pipe operator that selects the highest quality item within each time window. - -| Parameter | Type | Description | -|--------------------|------------------------|------------------------------------------------------| -| `quality_func` | `Callable[[T], float]` | Function that returns a quality score for each item | -| `target_frequency` | `float` | Output frequency in Hz (e.g., 2.0 for 2 items/second)| - -**Returns:** A pipe operator for use with `.pipe()` - -### `sharpness_barrier(target_frequency)` - -Convenience wrapper for images that uses `image.sharpness` as the quality function. - -| Parameter | Type | Description | -|--------------------|---------|--------------------------| -| `target_frequency` | `float` | Output frequency in Hz | - -**Returns:** A pipe operator for use with `.pipe()` diff --git a/docs/usage/sensor_streams/reactivex.md b/docs/usage/sensor_streams/reactivex.md deleted file mode 100644 index 45873b471b..0000000000 --- a/docs/usage/sensor_streams/reactivex.md +++ /dev/null @@ -1,494 +0,0 @@ -# ReactiveX (RxPY) Quick Reference - -RxPY provides composable asynchronous data streams. This is a practical guide focused on common patterns in this codebase. - -## Quick Start: Using an Observable - -Given a function that returns an `Observable`, here's how to use it: - -```python session=rx -import reactivex as rx -from reactivex import operators as ops - -# Create an observable that emits 0,1,2,3,4 -source = rx.of(0, 1, 2, 3, 4) - -# Subscribe and print each value -received = [] -source.subscribe(lambda x: received.append(x)) -print("received:", received) -``` - - -``` -received: [0, 1, 2, 3, 4] -``` - -## The `.pipe()` Pattern - -Chain operators using `.pipe()`: - -```python session=rx -# Transform values: multiply by 2, then filter > 4 -result = [] - -# We build another observable. It's passive until `subscribe` is called. -observable = source.pipe( - ops.map(lambda x: x * 2), - ops.filter(lambda x: x > 4), -) - -observable.subscribe(lambda x: result.append(x)) - -print("transformed:", result) -``` - - -``` -transformed: [6, 8] -``` - -## Common Operators - -### Transform: `map` - -```python session=rx -rx.of(1, 2, 3).pipe( - ops.map(lambda x: f"item_{x}") -).subscribe(print) -``` - - -``` -item_1 -item_2 -item_3 - -``` - -### Filter: `filter` - -```python session=rx -rx.of(1, 2, 3, 4, 5).pipe( - ops.filter(lambda x: x % 2 == 0) -).subscribe(print) -``` - - -``` -2 -4 - -``` - -### Limit emissions: `take` - -```python session=rx -rx.of(1, 2, 3, 4, 5).pipe( - ops.take(3) -).subscribe(print) -``` - - -``` -1 -2 -3 - -``` - -### Flatten nested observables: `flat_map` - -```python session=rx -# For each input, emit multiple values -rx.of(1, 2).pipe( - ops.flat_map(lambda x: rx.of(x, x * 10, x * 100)) -).subscribe(print) -``` - - -``` -1 -10 -100 -2 -20 -200 - -``` - -## Rate Limiting - -### `sample(interval)` - Emit latest value every N seconds - -Takes the most recent value at each interval. Good for continuous streams where you want the freshest data. - -```python session=rx -# Use blocking .run() to collect results properly -results = rx.interval(0.05).pipe( - ops.take(10), - ops.sample(0.2), - ops.to_list(), -).run() -print("sample() got:", results) -``` - - -``` -sample() got: [2, 6, 9] -``` - -### `throttle_first(interval)` - Emit first, then block for N seconds - -Takes the first value then ignores subsequent values for the interval. Good for user input debouncing. - -```python session=rx -results = rx.interval(0.05).pipe( - ops.take(10), - ops.throttle_first(0.15), - ops.to_list(), -).run() -print("throttle_first() got:", results) -``` - - -``` -throttle_first() got: [0, 3, 6, 9] -``` - -### Difference Between `sample` and `throttle_first` - -```python session=rx -# sample: takes LATEST value at each interval tick -# throttle_first: takes FIRST value then blocks - -# With fast emissions (0,1,2,3,4,5,6,7,8,9) every 50ms: -# sample(0.2s) -> gets value at 200ms, 400ms marks -> [2, 6, 9] -# throttle_first(0.15s) -> gets 0, blocks, then 3, blocks, then 6... -> [0,3,6,9] -print("sample: latest value at each tick") -print("throttle_first: first value, then block") -``` - - -``` -sample: latest value at each tick -throttle_first: first value, then block -``` - - -## What is an Observable? - -An Observable is like a list, but instead of holding all values at once, it produces values over time. - -| | List | Iterator | Observable | -|-------------|-----------------------|-----------------------|------------------| -| **Values** | All exist now | Generated on demand | Arrive over time | -| **Control** | You pull (`for x in`) | You pull (`next()`) | Pushed to you | -| **Size** | Finite | Can be infinite | Can be infinite | -| **Async** | No | Yes (with asyncio) | Yes | -| **Cancel** | N/A | Stop calling `next()` | `.dispose()` | - -The key difference from iterators: with an Observable, **you don't control when values arrive**. A camera produces frames at 30fps whether you're ready or not. An iterator waits for you to call `next()`. - -**Observables are lazy.** An Observable is just a description of work to be done - it sits there doing nothing until you call `.subscribe()`. That's when it "wakes up" and starts producing values. - -This means you can build complex pipelines, pass them around, and nothing happens until someone subscribes. - -**The three things an Observable can tell you:** - -1. **"Here's a value"** (`on_next`) - A new value arrived -2. **"Something went wrong"** (`on_error`) - An error occurred, stream stops -3. **"I'm done"** (`on_completed`) - No more values coming - -**The basic pattern:** - -``` -observable.subscribe(what_to_do_with_each_value) -``` - -That's it. You create or receive an Observable, then subscribe to start receiving values. - -When you subscribe, data flows through a pipeline: - -
-diagram source - -```pikchr fold output=assets/observable_flow.svg -color = white -fill = none - -Obs: box "observable" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Pipe: box ".pipe(ops)" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Sub: box ".subscribe()" rad 5px fit wid 170% ht 170% -arrow right 0.3in -Handler: box "callback" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/observable_flow.svg) - - -**Key property: Observables are lazy.** Nothing happens until you call `.subscribe()`. This means you can build up complex pipelines without any work being done, then start the flow when ready. - -Here's the full subscribe signature with all three callbacks: - -```python session=rx -rx.of(1, 2, 3).subscribe( - on_next=lambda x: print(f"value: {x}"), - on_error=lambda e: print(f"error: {e}"), - on_completed=lambda: print("done") -) -``` - - -``` -value: 1 -value: 2 -value: 3 -done - -``` - -## Disposables: Cancelling Subscriptions - -When you subscribe, you get back a `Disposable`. This is your "cancel button": - -```python session=rx -import reactivex as rx - -source = rx.interval(0.1) # emits 0, 1, 2, ... every 100ms forever -subscription = source.subscribe(lambda x: print(x)) - -# Later, when you're done: -subscription.dispose() # Stop receiving values, clean up resources -print("disposed") -``` - - -``` -disposed -``` - -**Why does this matter?** - -- Observables can be infinite (sensor feeds, websockets, timers) -- Without disposing, you leak memory and keep processing values forever -- Disposing also cleans up any resources the Observable opened (connections, file handles, etc.) - -**Rule of thumb:** Whenever you subscribe, save the disposable because you have to unsubscribe at some point by calling `disposable.dispose()`. - -**In dimos modules:** Every `Module` has a `self._disposables` (a `CompositeDisposable`) that automatically disposes everything when the module closes: - -```python session=rx -import time -from dimos.core import Module - -class MyModule(Module): - def start(self): - source = rx.interval(0.05) - self._disposables.add(source.subscribe(lambda x: print(f"got {x}"))) - -module = MyModule() -module.start() -time.sleep(0.25) - -# unsubscribes disposables -module.stop() -``` - - -``` -got 0 -got 1 -got 2 -got 3 -got 4 -``` - -## Creating Observables - -There are two common callback patterns in APIs. Use the appropriate helper: - -| Pattern | Example | Helper | -|---------|---------|--------| -| Register/unregister with same callback | `sensor.register(cb)` / `sensor.unregister(cb)` | `callback_to_observable` | -| Subscribe returns unsub function | `unsub = pubsub.subscribe(cb)` | `to_observable` | - -### From register/unregister APIs - -Use `callback_to_observable` when the API has separate register and unregister functions that take the same callback reference: - -```python session=create -import reactivex as rx -from reactivex import operators as ops -from dimos.utils.reactive import callback_to_observable - -class MockSensor: - def __init__(self): - self._callbacks = [] - def register(self, cb): - self._callbacks.append(cb) - def unregister(self, cb): - self._callbacks.remove(cb) - def emit(self, value): - for cb in self._callbacks: - cb(value) - -sensor = MockSensor() - -obs = callback_to_observable( - start=sensor.register, - stop=sensor.unregister -) - -received = [] -sub = obs.subscribe(lambda x: received.append(x)) - -sensor.emit("reading_1") -sensor.emit("reading_2") -print("received:", received) - -sub.dispose() -print("callbacks after dispose:", len(sensor._callbacks)) -``` - - -``` -received: ['reading_1', 'reading_2'] -callbacks after dispose: 0 -``` - -### From subscribe-returns-unsub APIs - -Use `to_observable` when the subscribe function returns an unsubscribe callable: - -```python session=create -from dimos.utils.reactive import to_observable - -class MockPubSub: - def __init__(self): - self._callbacks = [] - def subscribe(self, cb): - self._callbacks.append(cb) - return lambda: self._callbacks.remove(cb) # returns unsub function - def publish(self, value): - for cb in self._callbacks: - cb(value) - -pubsub = MockPubSub() - -obs = to_observable(pubsub.subscribe) - -received = [] -sub = obs.subscribe(lambda x: received.append(x)) - -pubsub.publish("msg_1") -pubsub.publish("msg_2") -print("received:", received) - -sub.dispose() -print("callbacks after dispose:", len(pubsub._callbacks)) -``` - - -``` -received: ['msg_1', 'msg_2'] -callbacks after dispose: 0 -``` - -### From scratch with `rx.create` - -```python session=create -from reactivex.disposable import Disposable - -def custom_subscribe(observer, scheduler=None): - observer.on_next("first") - observer.on_next("second") - observer.on_completed() - return Disposable(lambda: print("cleaned up")) - -obs = rx.create(custom_subscribe) - -results = [] -obs.subscribe( - on_next=lambda x: results.append(x), - on_completed=lambda: results.append("DONE") -) -print("results:", results) -``` - - -``` -cleaned up -results: ['first', 'second', 'DONE'] -``` - -## CompositeDisposable - -As we know we can always dispose subscriptions when done to prevent leaks: - -```python session=dispose -import time -import reactivex as rx -from reactivex import operators as ops - -source = rx.interval(0.1).pipe(ops.take(100)) -received = [] - -subscription = source.subscribe(lambda x: received.append(x)) -time.sleep(0.25) -subscription.dispose() -time.sleep(0.2) - -print(f"received {len(received)} items before dispose") -``` - - -``` -received 2 items before dispose -``` - -For multiple subscriptions, use `CompositeDisposable`: - -```python session=dispose -from reactivex.disposable import CompositeDisposable - -disposables = CompositeDisposable() - -s1 = rx.of(1,2,3).subscribe(lambda x: None) -s2 = rx.of(4,5,6).subscribe(lambda x: None) - -disposables.add(s1) -disposables.add(s2) - -print("subscriptions:", len(disposables)) -disposables.dispose() -print("after dispose:", disposables.is_disposed) -``` - - -``` -subscriptions: 2 -after dispose: True -``` - -## Reference - -| Operator | Purpose | Example | -|-----------------------|------------------------------------------|---------------------------------------| -| `map(fn)` | Transform each value | `ops.map(lambda x: x * 2)` | -| `filter(pred)` | Keep values matching predicate | `ops.filter(lambda x: x > 0)` | -| `take(n)` | Take first n values | `ops.take(10)` | -| `first()` | Take first value only | `ops.first()` | -| `sample(sec)` | Emit latest every interval | `ops.sample(0.5)` | -| `throttle_first(sec)` | Emit first, block for interval | `ops.throttle_first(0.5)` | -| `flat_map(fn)` | Map + flatten nested observables | `ops.flat_map(lambda x: rx.of(x, x))` | -| `observe_on(sched)` | Switch scheduler | `ops.observe_on(pool_scheduler)` | -| `replay(n)` | Cache last n values for late subscribers | `ops.replay(buffer_size=1)` | -| `timeout(sec)` | Error if no value within timeout | `ops.timeout(5.0)` | - -See [RxPY documentation](https://rxpy.readthedocs.io/) for complete operator reference. diff --git a/docs/usage/sensor_streams/storage_replay.md b/docs/usage/sensor_streams/storage_replay.md deleted file mode 100644 index c5cbe306a8..0000000000 --- a/docs/usage/sensor_streams/storage_replay.md +++ /dev/null @@ -1,231 +0,0 @@ -# Sensor Storage and Replay - -Record sensor streams to disk and replay them with original timing. Useful for testing, debugging, and creating reproducible datasets. - -## Quick Start - -### Recording - -```python skip -from dimos.utils.testing.replay import TimedSensorStorage - -# Create storage (directory in data folder) -storage = TimedSensorStorage("my_recording") - -# Save frames from a stream -camera_stream.subscribe(storage.save_one) - -# Or save manually -storage.save(frame1, frame2, frame3) -``` - -### Replaying - -```python skip -from dimos.utils.testing.replay import TimedSensorReplay - -# Load recording -replay = TimedSensorReplay("my_recording") - -# Iterate at original speed -for frame in replay.iterate_realtime(): - process(frame) - -# Or as an Observable stream -replay.stream(speed=1.0).subscribe(process) -``` - -## TimedSensorStorage - -Stores sensor data with timestamps as pickle files. Each frame is saved as `000.pickle`, `001.pickle`, etc. - -```python skip -from dimos.utils.testing.replay import TimedSensorStorage - -storage = TimedSensorStorage("lidar_capture") - -# Save individual frames -storage.save_one(lidar_msg) # Returns frame count - -# Save multiple frames -storage.save(frame1, frame2, frame3) - -# Subscribe to a stream -lidar_stream.subscribe(storage.save_one) - -# Or pipe through (emits frame count) -lidar_stream.pipe( - ops.flat_map(storage.save_stream) -).subscribe() -``` - -**Storage location:** Files are saved to the data directory under the given name. The directory must not already contain pickle files (prevents accidental overwrites). - -**What gets stored:** By default, if a frame has a `.raw_msg` attribute, that's pickled instead of the full object. You can customize with the `autocast` parameter: - -```python skip -# Custom serialization -storage = TimedSensorStorage( - "custom_capture", - autocast=lambda frame: frame.to_dict() -) -``` - -## TimedSensorReplay - -Replays stored sensor data with timestamp-aware iteration and seeking. - -### Basic Iteration - -```python skip -from dimos.utils.testing.replay import TimedSensorReplay - -replay = TimedSensorReplay("lidar_capture") - -# Iterate all frames (ignores timing) -for frame in replay.iterate(): - process(frame) - -# Iterate with timestamps -for ts, frame in replay.iterate_ts(): - print(f"Frame at {ts}: {frame}") - -# Iterate with relative timestamps (from start) -for relative_ts, frame in replay.iterate_duration(): - print(f"At {relative_ts:.2f}s: {frame}") -``` - -### Realtime Playback - -```python skip -# Play at original speed (blocks between frames) -for frame in replay.iterate_realtime(): - process(frame) - -# Play at 2x speed -for frame in replay.iterate_realtime(speed=2.0): - process(frame) - -# Play at half speed -for frame in replay.iterate_realtime(speed=0.5): - process(frame) -``` - -### Seeking and Slicing - -```python skip -# Start 10 seconds into the recording -for ts, frame in replay.iterate_ts(seek=10.0): - process(frame) - -# Play only 5 seconds starting at 10s -for ts, frame in replay.iterate_ts(seek=10.0, duration=5.0): - process(frame) - -# Loop forever -for frame in replay.iterate(loop=True): - process(frame) -``` - -### Finding Specific Frames - -```python skip -# Find frame closest to absolute timestamp -frame = replay.find_closest(1704067200.0) - -# Find frame closest to relative time (30s from start) -frame = replay.find_closest_seek(30.0) - -# With tolerance (returns None if no match within 0.1s) -frame = replay.find_closest(timestamp, tolerance=0.1) -``` - -### Observable Stream - -The `.stream()` method returns an Observable that emits frames with original timing: - -```python skip -# Stream at original speed -replay.stream(speed=1.0).subscribe(process) - -# Stream at 2x with seeking -replay.stream( - speed=2.0, - seek=10.0, # Start 10s in - duration=30.0, # Play for 30s - loop=True # Loop forever -).subscribe(process) -``` - -## Usage: Stub Connections for Testing - -A common pattern is creating replay-based connection stubs for testing without hardware. From [`robot/unitree/go2/connection.py`](/dimos/robot/unitree/go2/connection.py#L83): - -This is a bit primitive. We'd like to write a higher-order API for recording full module I/O for any module, but this is a work in progress at the moment. - - -```python skip -class ReplayConnection(UnitreeWebRTCConnection): - dir_name = "unitree_go2_bigoffice" - - def __init__(self, **kwargs) -> None: - get_data(self.dir_name) - self.replay_config = { - "loop": kwargs.get("loop"), - "seek": kwargs.get("seek"), - "duration": kwargs.get("duration"), - } - - def lidar_stream(self): - lidar_store = TimedSensorReplay(f"{self.dir_name}/lidar") - return lidar_store.stream(**self.replay_config) - - def video_stream(self): - video_store = TimedSensorReplay(f"{self.dir_name}/video") - return video_store.stream(**self.replay_config) -``` - -This allows running the full perception pipeline against recorded data: - -```python skip -# Use replay connection instead of real hardware -connection = ReplayConnection(loop=True, seek=5.0) -robot = GO2Connection(connection=connection) -``` - -## Data Format - -Each pickle file contains a tuple `(timestamp, data)`: - -- **timestamp**: Unix timestamp (float) when the frame was captured -- **data**: The sensor data (or result of `autocast` if provided) - -Files are numbered sequentially: `000.pickle`, `001.pickle`, etc. - -Recordings are stored in the `data/` directory. See [Data Loading](/docs/development/large_file_management.md) for how data storage works, including Git LFS handling for large datasets. - -## API Reference - -### TimedSensorStorage - -| Method | Description | -|------------------------------|------------------------------------------| -| `save_one(frame)` | Save a single frame, returns frame count | -| `save(*frames)` | Save multiple frames | -| `save_stream(observable)` | Pipe an observable through storage | -| `consume_stream(observable)` | Subscribe and save without returning | - -### TimedSensorReplay - -| Method | Description | -|--------------------------------------------------|---------------------------------------| -| `iterate(loop=False)` | Iterate frames (no timing) | -| `iterate_ts(seek, duration, loop)` | Iterate with absolute timestamps | -| `iterate_duration(...)` | Iterate with relative timestamps | -| `iterate_realtime(speed, ...)` | Iterate with blocking to match timing | -| `stream(speed, seek, duration, loop)` | Observable with original timing | -| `find_closest(timestamp, tolerance)` | Find frame by absolute timestamp | -| `find_closest_seek(relative_seconds, tolerance)` | Find frame by relative time | -| `first()` | Get first frame | -| `first_timestamp()` | Get first timestamp | -| `load(name)` | Load specific frame by name/index | diff --git a/docs/usage/sensor_streams/temporal_alignment.md b/docs/usage/sensor_streams/temporal_alignment.md deleted file mode 100644 index 66230c9d54..0000000000 --- a/docs/usage/sensor_streams/temporal_alignment.md +++ /dev/null @@ -1,313 +0,0 @@ -# Temporal Message Alignment - -Robots have multiple sensors emitting data at different rates and latencies. A camera might run at 30fps, while lidar scans at 10Hz, and each has different processing delays. For perception tasks like projecting 2D detections into 3D pointclouds, we need to match data from these streams by timestamp. - -`align_timestamped` solves this by buffering messages and matching them within a time tolerance. - -
Pikchr - -```pikchr fold output=assets/alignment_overview.svg -color = white -fill = none - -Cam: box "Camera" "30 fps" rad 5px fit wid 170% ht 170% -arrow from Cam.e right 0.4in then down 0.35in then right 0.4in -Align: box "align_timestamped" rad 5px fit wid 170% ht 170% - -Lidar: box "Lidar" "10 Hz" rad 5px fit wid 170% ht 170% with .s at (Cam.s.x, Cam.s.y - 0.7in) -arrow from Lidar.e right 0.4in then up 0.35in then right 0.4in - -arrow from Align.e right 0.4in -Out: box "(image, pointcloud)" rad 5px fit wid 170% ht 170% -``` - -
- - -![output](assets/alignment_overview.svg) - - -## Basic Usage - -Below we set up replay of real camera and lidar data from the Unitree Go2 robot. You can check it if you're interested. - -
-Stream Setup - -You can read more about [sensor storage here](storage_replay.md) and [LFS data storage here](/docs/development/large_file_management.md). - -```python session=align no-result -from reactivex import Subject -from dimos.utils.testing import TimedSensorReplay -from dimos.types.timestamped import Timestamped, align_timestamped -from reactivex import operators as ops -import reactivex as rx - -# Load recorded Go2 sensor streams -video_replay = TimedSensorReplay("unitree_go2_bigoffice/video") -lidar_replay = TimedSensorReplay("unitree_go2_bigoffice/lidar") - -# This is a bit tricky. We find the first video frame timestamp, then add 2 seconds to it. -seek_ts = video_replay.first_timestamp() + 2 - -# Lists to collect items as they flow through streams -video_frames = [] -lidar_scans = [] - -# We are using from_timestamp=... and not seek=... because seek seeks through recording -# timestamps, from_timestamp matches actual message timestamp. -# It's possible for sensor data to come in late, but with correct capture time timestamps -video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: video_frames.append(x)) -) - -lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: lidar_scans.append(x)) -) - -``` - - -
- -Streams would normally come from an actual robot into your module via `In` inputs. [`detection/module3D.py`](/dimos/perception/detection/module3D.py#L11) is a good example of this. - -Assume we have them. Let's align them. - -```python session=align -# Align video (primary) with lidar (secondary) -# match_tolerance: max time difference for a match (seconds) -# buffer_size: how long to keep messages waiting for matches (seconds) -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.025, # 25ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") - -# Show a matched pair -if aligned_pairs: - img, pc = aligned_pairs[0] - dt = abs(img.ts - pc.ts) - print(f"\nFirst matched pair: Δ{dt*1000:.1f}ms") -``` - - -``` -Video: 29 frames, Lidar: 15 scans -Aligned pairs: 11 out of 29 video frames - -First matched pair: Δ11.3ms -``` - -
-Visualization helper - -```python session=align fold no-result -import matplotlib -import matplotlib.pyplot as plt - -def plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, path): - """Single timeline: video above axis, lidar below, green lines for matches.""" - matplotlib.use('Agg') - plt.style.use('dark_background') - - # Get base timestamp for relative times (frames have .ts attribute) - base_ts = video_frames[0].ts - video_ts = [f.ts - base_ts for f in video_frames] - lidar_ts = [s.ts - base_ts for s in lidar_scans] - - # Find matched timestamps - matched_video_ts = set(img.ts for img, _ in aligned_pairs) - matched_lidar_ts = set(pc.ts for _, pc in aligned_pairs) - - fig, ax = plt.subplots(figsize=(12, 2.5)) - - # Video markers above axis (y=0.3) - circles, cyan when matched - for frame in video_frames: - rel_ts = frame.ts - base_ts - matched = frame.ts in matched_video_ts - ax.plot(rel_ts, 0.3, 'o', color='cyan' if matched else '#688', markersize=8) - - # Lidar markers below axis (y=-0.3) - squares, orange when matched - for scan in lidar_scans: - rel_ts = scan.ts - base_ts - matched = scan.ts in matched_lidar_ts - ax.plot(rel_ts, -0.3, 's', color='orange' if matched else '#a86', markersize=8) - - # Green lines connecting matched pairs - for img, pc in aligned_pairs: - img_rel = img.ts - base_ts - pc_rel = pc.ts - base_ts - ax.plot([img_rel, pc_rel], [0.3, -0.3], '-', color='lime', alpha=0.6, linewidth=1) - - # Axis styling - ax.axhline(y=0, color='white', linewidth=0.5, alpha=0.3) - ax.set_xlim(-0.1, max(video_ts + lidar_ts) + 0.1) - ax.set_ylim(-0.6, 0.6) - ax.set_xlabel('Time (s)') - ax.set_yticks([0.3, -0.3]) - ax.set_yticklabels(['Video', 'Lidar']) - ax.set_title(f'{len(aligned_pairs)} matched from {len(video_frames)} video + {len(lidar_scans)} lidar') - plt.tight_layout() - plt.savefig(path, transparent=True) - plt.close() -``` - -
- -```python session=align output=assets/alignment_timeline.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline.png) - -If we loosen up our match tolerance, we might get multiple pairs matching the same lidar frame. - -```python session=align -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.05, # 50ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") -``` - - -``` -Video: 58 frames, Lidar: 30 scans -Aligned pairs: 23 out of 58 video frames -``` - - -```python session=align output=assets/alignment_timeline2.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline2.png) - -## Combine Frame Alignment with a Quality Filter - -More on [quality filtering here](quality_filter.md). - -```python session=align -from dimos.msgs.sensor_msgs.Image import Image, sharpness_barrier - -# Lists to collect items as they flow through streams -video_frames = [] -lidar_scans = [] - -video_stream = video_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - sharpness_barrier(3.0), - ops.do_action(lambda x: video_frames.append(x)) -) - -lidar_stream = lidar_replay.stream(from_timestamp=seek_ts, duration=2.0).pipe( - ops.do_action(lambda x: lidar_scans.append(x)) -) - -aligned_pairs = align_timestamped( - video_stream, - lidar_stream, - match_tolerance=0.025, # 25ms tolerance - buffer_size=5.0, # how long to wait for match -).pipe(ops.to_list()).run() - -print(f"Video: {len(video_frames)} frames, Lidar: {len(lidar_scans)} scans") -print(f"Aligned pairs: {len(aligned_pairs)} out of {len(video_frames)} video frames") - -``` - - -``` -Video: 6 frames, Lidar: 15 scans -Aligned pairs: 1 out of 6 video frames -``` - -```python session=align output=assets/alignment_timeline3.png -plot_alignment_timeline(video_frames, lidar_scans, aligned_pairs, '{output}') -``` - - -![output](assets/alignment_timeline3.png) - -We are very picky but data is high quality. Best frame, with closest lidar match in this window. - -## How It Works - -The primary stream (first argument) drives emissions. When a primary message arrives: - -1. **Immediate match**: If matching secondaries already exist in buffers, emit immediately -2. **Deferred match**: If secondaries are missing, buffer the primary and wait - -When secondary messages arrive: -1. Add to buffer for future primary matches -2. Check buffered primaries - if this completes a match, emit - -
-diagram source - -```pikchr fold output=assets/alignment_flow.svg -color = white -fill = none -linewid = 0.35in - -Primary: box "Primary" "arrives" rad 5px fit wid 170% ht 170% -arrow -Check: box "Check" "secondaries" rad 5px fit wid 170% ht 170% - -arrow from Check.e right 0.35in then up 0.4in then right 0.35in -Emit: box "Emit" "match" rad 5px fit wid 170% ht 170% -text "all found" at (Emit.w.x - 0.4in, Emit.w.y + 0.15in) - -arrow from Check.e right 0.35in then down 0.4in then right 0.35in -Buffer: box "Buffer" "primary" rad 5px fit wid 170% ht 170% -text "waiting..." at (Buffer.w.x - 0.4in, Buffer.w.y - 0.15in) -``` - -
- - -![output](assets/alignment_flow.svg) - -## Parameters - -| Parameter | Type | Default | Description | -|--------------------------|--------------------|----------|-------------------------------------------------| -| `primary_observable` | `Observable[T]` | required | Primary stream that drives output timing | -| `*secondary_observables` | `Observable[S]...` | required | One or more secondary streams to align | -| `match_tolerance` | `float` | 0.1 | Max time difference for a match (seconds) | -| `buffer_size` | `float` | 1.0 | How long to buffer unmatched messages (seconds) | - - - -## Usage in Modules - -Every module `In` port exposes an `.observable()` method that returns a backpressured stream of incoming messages. This makes it easy to align inputs from multiple sensors. - -From [`detection/module3D.py`](/dimos/perception/detection/module3D.py), projecting 2D detections into 3D pointclouds: - -```python skip -class Detection3DModule(Detection2DModule): - color_image: In[Image] - pointcloud: In[PointCloud2] - - def start(self): - # Align 2D detections with pointcloud data - self.detection_stream_3d = align_timestamped( - backpressure(self.detection_stream_2d()), - self.pointcloud.observable(), - match_tolerance=0.25, - buffer_size=20.0, - ).pipe(ops.map(detection2d_to_3d)) -``` - -The 2D detection stream (camera + ML model) is the primary, matched with raw pointcloud data from lidar. The longer `buffer_size=20.0` accounts for variable ML inference times. diff --git a/docs/usage/transforms.md b/docs/usage/transforms.md deleted file mode 100644 index 2435839feb..0000000000 --- a/docs/usage/transforms.md +++ /dev/null @@ -1,469 +0,0 @@ -# Transforms - -## The Problem: Everything Measures from Its Own Perspective - -Imagine your robot has an RGB-D camera—a camera that captures both color images and depth (distance to each pixel). These are common in robotics: Intel RealSense, Microsoft Kinect, and similar sensors. - -The camera spots a coffee mug at pixel (320, 240), and the depth sensor says it's 1.2 meters away. You want the robot arm to pick it up—but the arm doesn't understand pixels or camera-relative distances. It needs coordinates in its own workspace: "move to position (0.8, 0.3, 0.1) meters from my base." - -To convert camera measurements to arm coordinates, you need to know: -- The camera's intrinsic parameters (focal length, sensor size) to convert pixels to a 3D direction -- The depth value to get the full 3D position relative to the camera -- Where the camera is mounted relative to the arm, and at what angle - -This chain of conversions—(pixels + depth) → 3D point in camera frame → robot coordinates—is what **transforms** handle. - -
-diagram source - -```pikchr fold output=assets/transforms_tree.svg -color = white -fill = none - -# Root (left side) -W: box "world" rad 5px fit wid 170% ht 170% -arrow right 0.4in -RB: box "robot_base" rad 5px fit wid 170% ht 170% - -# Camera branch (top) -arrow from RB.e right 0.3in then up 0.4in then right 0.3in -CL: box "camera_link" rad 5px fit wid 170% ht 170% -arrow right 0.4in -CO: box "camera_optical" rad 5px fit wid 170% ht 170% -text "mug here" small italic at (CO.s.x, CO.s.y - 0.25in) - -# Arm branch (bottom) -arrow from RB.e right 0.3in then down 0.4in then right 0.3in -AB: box "arm_base" rad 5px fit wid 170% ht 170% -arrow right 0.4in -GR: box "gripper" rad 5px fit wid 170% ht 170% -text "target here" small italic at (GR.s.x, GR.s.y - 0.25in) -``` - -
- - -![output](assets/transforms_tree.svg) - - -Each arrow in this tree is a transform. To get the mug's position in gripper coordinates, you chain transforms through their common parent: camera → robot_base → arm → gripper. - -## What's a Coordinate Frame? - -A **coordinate frame** is simply a point of view—an origin point and a set of axes (X, Y, Z) from which you measure positions and orientations. - -Think of it like giving directions: -- **GPS** says you're at 37.7749° N, 122.4194° W -- The **coffee shop floor plan** says "table 5 is 3 meters from the entrance" -- Your **friend** says "I'm two tables to your left" - -These all describe positions in the same physical space, but from different reference points. Each is a coordinate frame. - -In a robot: -- The **camera** measures in pixels, or in meters relative to its lens -- The **LIDAR** measures distances from its own mounting point -- The **robot arm** thinks in terms of its base or end-effector position -- The **world** has a fixed coordinate system everything lives in - -Each sensor, joint, and reference point has its own frame. - -## The Transform Class - -The `Transform` class at [`geometry_msgs/Transform.py`](/dimos/msgs/geometry_msgs/Transform.py#L21) represents a spatial transformation with: - -- `frame_id` - The parent frame name -- `child_frame_id` - The child frame name -- `translation` - A `Vector3` (x, y, z) offset -- `rotation` - A `Quaternion` (x, y, z, w) orientation -- `ts` - Timestamp for temporal lookups - -```python -from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion - -# Camera 0.5m forward and 0.3m up from base, no rotation -camera_transform = Transform( - translation=Vector3(0.5, 0.0, 0.3), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), # Identity rotation - frame_id="base_link", - child_frame_id="camera_link", -) -print(camera_transform) -``` - - -``` -base_link -> camera_link - Translation: → Vector Vector([0.5 0. 0.3]) - Rotation: Quaternion(0.000000, 0.000000, 0.000000, 1.000000) -``` - - -### Transform Operations - -Transforms can be composed and inverted: - -```python -from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion - -# Create two transforms -t1 = Transform( - translation=Vector3(1.0, 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", -) -t2 = Transform( - translation=Vector3(0.0, 0.5, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="camera_link", - child_frame_id="end_effector", -) - -# Compose: base_link -> camera -> end_effector -t3 = t1 + t2 -print(f"Composed: {t3.frame_id} -> {t3.child_frame_id}") -print(f"Translation: ({t3.translation.x}, {t3.translation.y}, {t3.translation.z})") - -# Inverse: if t goes A -> B, -t goes B -> A -t_inverse = -t1 -print(f"Inverse: {t_inverse.frame_id} -> {t_inverse.child_frame_id}") -``` - - -``` -Composed: base_link -> end_effector -Translation: (1.0, 0.5, 0.0) -Inverse: camera_link -> base_link -``` - - -### Converting to Matrix Form - -For integration with libraries like NumPy or OpenCV: - -```python -from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion - -t = Transform( - translation=Vector3(1.0, 2.0, 3.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), -) -matrix = t.to_matrix() -print("4x4 transformation matrix:") -print(matrix) -``` - - -``` -4x4 transformation matrix: -[[1. 0. 0. 1.] - [0. 1. 0. 2.] - [0. 0. 1. 3.] - [0. 0. 0. 1.]] -``` - - - -## Frame IDs in Modules - -Modules in DimOS automatically get a `frame_id` property. This is controlled by two config options in [`core/module.py`](/dimos/core/module.py#L78): - -- `frame_id` - The base frame name (defaults to the class name) -- `frame_id_prefix` - Optional prefix for namespacing - -```python -from dimos.core import Module, ModuleConfig -from dataclasses import dataclass - -@dataclass -class MyModuleConfig(ModuleConfig): - frame_id: str = "sensor_link" - frame_id_prefix: str | None = None - -class MySensorModule(Module[MyModuleConfig]): - default_config = MyModuleConfig - -# With default config: -sensor = MySensorModule() -print(f"Default frame_id: {sensor.frame_id}") - -# With prefix (useful for multi-robot scenarios): -sensor2 = MySensorModule(frame_id_prefix="robot1") -print(f"With prefix: {sensor2.frame_id}") -``` - - -``` -Default frame_id: sensor_link -With prefix: robot1/sensor_link -``` - - -## The TF Service - -Every module has access to `self.tf`, a transform service that: - -- **Publishes** transforms to the system -- **Looks up** transforms between any two frames -- **Buffers** historical transforms for temporal queries - -The TF service is implemented in [`tf.py`](/dimos/protocol/tf/tf.py) and is lazily initialized on first access. - -### Multi-Module Transform Example - -This example demonstrates how multiple modules publish and receive transforms. Three modules work together: - -1. **RobotBaseModule** - Publishes `world -> base_link` (robot's position in the world) -2. **CameraModule** - Publishes `base_link -> camera_link` (camera mounting position) and `camera_link -> camera_optical` (optical frame convention) -3. **PerceptionModule** - Looks up transforms between any frames - -```python ansi=false -import time -import reactivex as rx -from reactivex import operators as ops -from dimos.core import Module, rpc, start -from dimos.msgs.geometry_msgs import Quaternion, Transform, Vector3 - -class RobotBaseModule(Module): - """Publishes the robot's position in the world frame at 10Hz.""" - def __init__(self, **kwargs: object) -> None: - super().__init__(**kwargs) - - @rpc - def start(self) -> None: - super().start() - - def publish_pose(_): - robot_pose = Transform( - translation=Vector3(2.5, 3.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="world", - child_frame_id="base_link", - ts=time.time(), - ) - self.tf.publish(robot_pose) - - self._disposables.add( - rx.interval(0.1).subscribe(publish_pose) - ) - -class CameraModule(Module): - """Publishes camera transforms at 10Hz.""" - @rpc - def start(self) -> None: - super().start() - - def publish_transforms(_): - camera_mount = Transform( - translation=Vector3(1.0, 0.0, 0.3), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=time.time(), - ) - optical_frame = Transform( - translation=Vector3(0.0, 0.0, 0.0), - rotation=Quaternion(-0.5, 0.5, -0.5, 0.5), - frame_id="camera_link", - child_frame_id="camera_optical", - ts=time.time(), - ) - self.tf.publish(camera_mount, optical_frame) - - self._disposables.add( - rx.interval(0.1).subscribe(publish_transforms) - ) - - -class PerceptionModule(Module): - """Receives transforms and performs lookups.""" - - def start(self) -> None: - # This is just to init the transforms system. - # Touching the property for the first time enables the system for this module. - # Transform lookups normally happen in fast loops in IRL modules. - _ = self.tf - - @rpc - def lookup(self) -> None: - - # Will pretty-print information on transforms in the buffer - print(self.tf) - - direct = self.tf.get("world", "base_link") - print(f"Direct: robot is at ({direct.translation.x}, {direct.translation.y})m in world\n") - - # Chained lookup - automatically composes world -> base -> camera -> optical - chained = self.tf.get("world", "camera_optical") - print(f"Chained: {chained}\n") - - # Inverse lookup - automatically inverts direction - inverse = self.tf.get("camera_optical", "world") - print(f"Inverse: {inverse}\n") - - print("Transform tree:") - print(self.tf.graph()) - - -if __name__ == "__main__": - dimos = start(3) - - # Deploy and start modules - robot = dimos.deploy(RobotBaseModule) - camera = dimos.deploy(CameraModule) - perception = dimos.deploy(PerceptionModule) - - robot.start() - camera.start() - perception.start() - - time.sleep(1.0) - - perception.lookup() - - dimos.stop() - -``` - - -``` -Initialized dimos local cluster with 3 workers, memory limit: auto -2025-12-29T12:47:01.433394Z [info ] Deployed module. [dimos/core/__init__.py] module=RobotBaseModule worker_id=1 -2025-12-29T12:47:01.603269Z [info ] Deployed module. [dimos/core/__init__.py] module=CameraModule worker_id=0 -2025-12-29T12:47:01.698970Z [info ] Deployed module. [dimos/core/__init__.py] module=PerceptionModule worker_id=2 -LCMTF(3 buffers): - TBuffer(world -> base_link, 10 msgs, 0.90s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) - TBuffer(base_link -> camera_link, 9 msgs, 0.80s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) - TBuffer(camera_link -> camera_optical, 9 msgs, 0.80s [2025-12-29 20:47:01 - 2025-12-29 20:47:02]) -Direct: robot is at (2.5, 3.0)m in world - -Chained: world -> camera_optical - Translation: → Vector Vector([3.5 3. 0.3]) - Rotation: Quaternion(-0.500000, 0.500000, -0.500000, 0.500000) - -Inverse: camera_optical -> world - Translation: → Vector Vector([ 3. 0.3 -3.5]) - Rotation: Quaternion(0.500000, -0.500000, 0.500000, 0.500000) - -Transform tree: -┌─────┐ -│world│ -└┬────┘ -┌▽────────┐ -│base_link│ -└┬────────┘ -┌▽──────────┐ -│camera_link│ -└┬──────────┘ -┌▽─────────────┐ -│camera_optical│ -└──────────────┘ -``` - - -You can also run `foxglove-studio-bridge` in the next terminal (binary provided by DimOS and should be in your Python env) and `foxglove-studio` to view these transforms in 3D. (TODO we need to update this for rerun) - -![transforms](assets/transforms.png) - -Key points: - -- **Automatic broadcasting**: `self.tf.publish()` broadcasts via LCM to all modules -- **Chained lookups**: TF finds paths through the tree automatically -- **Inverse lookups**: Request transforms in either direction -- **Temporal buffering**: Transforms are timestamped and buffered (default 10s) for sensor fusion - -The transform tree from the example above, showing which module publishes each transform: - -
-diagram source - -```pikchr fold output=assets/transforms_modules.svg -color = white -fill = none - -# Frame boxes -W: box "world" rad 5px fit wid 170% ht 170% -A1: arrow right 0.4in -BL: box "base_link" rad 5px fit wid 170% ht 170% -A2: arrow right 0.4in -CL: box "camera_link" rad 5px fit wid 170% ht 170% -A3: arrow right 0.4in -CO: box "camera_optical" rad 5px fit wid 170% ht 170% - -# RobotBaseModule box - encompasses world->base_link -box width (BL.e.x - W.w.x + 0.15in) height 0.7in \ - at ((W.w.x + BL.e.x)/2, W.y - 0.05in) \ - rad 10px color 0x6699cc fill none -text "RobotBaseModule" italic at ((W.x + BL.x)/2, W.n.y + 0.25in) - -# CameraModule box - encompasses camera_link->camera_optical (starts after base_link) -box width (CO.e.x - BL.e.x + 0.1in) height 0.7in \ - at ((BL.e.x + CO.e.x)/2, CL.y + 0.05in) \ - rad 10px color 0xcc9966 fill none -text "CameraModule" italic at ((CL.x + CO.x)/2, CL.s.y - 0.25in) -``` - - -
- - -![output](assets/transforms_modules.svg) - - -# Internals - -## Transform Buffer - -`self.tf` on module is a transform buffer. This is a standalone class that maintains a temporal buffer of transforms (default 10 seconds) allowing queries at past timestamps, you can use it directly: - -```python -from dimos.protocol.tf import TF -from dimos.msgs.geometry_msgs import Transform, Vector3, Quaternion -import time - -tf = TF(autostart=False) - -# Simulate transforms at different times -for i in range(5): - t = Transform( - translation=Vector3(float(i), 0.0, 0.0), - rotation=Quaternion(0.0, 0.0, 0.0, 1.0), - frame_id="base_link", - child_frame_id="camera_link", - ts=time.time() + i * 0.1, - ) - tf.receive_transform(t) - -# Query the latest transform -result = tf.get("base_link", "camera_link") -print(f"Latest transform: x={result.translation.x}") -print(f"Buffer has {len(tf.buffers)} transform pair(s)") -print(tf) -``` - - -``` -Latest transform: x=4.0 -Buffer has 1 transform pair(s) -LCMTF(1 buffers): - TBuffer(base_link -> camera_link, 5 msgs, 0.40s [2025-12-29 18:19:18 - 2025-12-29 18:19:18]) -``` - - -This is essential for sensor fusion where you need to know where the camera was when an image was captured, not where it is now. - - -## Further Reading - -For a visual introduction to transforms and coordinate frames: -- [Coordinate Transforms (YouTube)](https://www.youtube.com/watch?v=NGPn9nvLPmg) - -For the mathematical foundations, the ROS documentation provides detailed background: - -- [ROS tf2 Concepts](http://wiki.ros.org/tf2) -- [ROS REP 103 - Standard Units and Coordinate Conventions](https://www.ros.org/reps/rep-0103.html) -- [ROS REP 105 - Coordinate Frames for Mobile Platforms](https://www.ros.org/reps/rep-0105.html) - -See also: -- [Modules](/docs/usage/modules.md) for understanding the module system -- [Configuration](/docs/usage/configuration.md) for module configuration patterns diff --git a/docs/usage/transports.md b/docs/usage/transports.md deleted file mode 100644 index 4c80776531..0000000000 --- a/docs/usage/transports.md +++ /dev/null @@ -1,437 +0,0 @@ -# Transports - -Transports connect **module streams** across **process boundaries** and/or **networks**. - -* **Module**: a running component (e.g., camera, mapping, nav). -* **Stream**: a unidirectional flow of messages owned by a module (one broadcaster → many receivers). -* **Topic**: the name/identifier used by a transport or pubsub backend. -* **Message**: payload carried on a stream (often `dimos.msgs.*`, but can be bytes / images / pointclouds / etc.). - -Each edge in the graph is a **transported stream** (potentially different protocols). Each node is a **module**: - -![go2_nav](assets/go2_nav.svg) - -## What the transport layer guarantees (and what it doesn’t) - -Modules **don’t** know or care *how* data moves. They just: - -* emit messages (broadcast) -* subscribe to messages (receive) - -A transport is responsible for the mechanics of delivery (IPC, sockets, Redis, ROS 2, etc.). - -**Important:** delivery semantics depend on the backend: - -* Some are **best-effort** (e.g., UDP multicast / LCM): loss can happen. -* Some can be **reliable** (e.g., TCP-backed, Redis, some DDS configs) but may add latency/backpressure. - -So: treat the API as uniform, but pick a backend whose semantics match the task. - ---- - -## Benchmarks - -Quick view on performance of our pubsub backends: - -```sh skip -python -m pytest -svm tool -k "not bytes" dimos/protocol/pubsub/benchmark/test_benchmark.py -``` - -![Benchmark results](assets/pubsub_benchmark.png) - ---- - -## Abstraction layers - -
Pikchr - -```pikchr output=assets/abstraction_layers.svg fold -color = white -fill = none -linewid = 0.5in -boxwid = 1.0in -boxht = 0.4in - -# Boxes with labels -B: box "Blueprints" rad 10px -arrow -M: box "Modules" rad 5px -arrow -T: box "Transports" rad 5px -arrow -P: box "PubSub" rad 5px - -# Descriptions below -text "robot configs" at B.s + (0.1, -0.2in) -text "camera, nav" at M.s + (0, -0.2in) -text "LCM, SHM, ROS" at T.s + (0, -0.2in) -text "pub/sub API" at P.s + (0, -0.2in) -``` - -
- - -![output](assets/abstraction_layers.svg) - -We’ll go through these layers top-down. - ---- - -## Using transports with blueprints - -See [Blueprints](blueprints.md) for the blueprint API. - -From [`unitree/go2/blueprints/__init__.py`](/dimos/robot/unitree/go2/blueprints/__init__.py). - -Example: rebind a few streams from the default `LCMTransport` to `ROSTransport` (defined at [`transport.py`](/dimos/core/transport.py#L226)) so you can visualize in **rviz2**. - -```python skip -nav = autoconnect( - basic, - voxel_mapper(voxel_size=0.1), - cost_mapper(), - replanning_a_star_planner(), - wavefront_frontier_explorer(), -).global_config(n_dask_workers=6, robot_model="unitree_go2") - -ros = nav.transports( - { - ("lidar", PointCloud2): ROSTransport("lidar", PointCloud2), - ("global_map", PointCloud2): ROSTransport("global_map", PointCloud2), - ("odom", PoseStamped): ROSTransport("odom", PoseStamped), - ("color_image", Image): ROSTransport("color_image", Image), - } -) -``` - ---- - -## Using transports with modules - -Each **stream** on a module can use a different transport. Set `.transport` on the stream **before starting** modules. - -```python ansi=false -import time - -from dimos.core import In, Module, start -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.msgs.sensor_msgs import Image - - -class ImageListener(Module): - image: In[Image] - - def start(self): - super().start() - self.image.subscribe(lambda img: print(f"Received: {img.shape}")) - - -if __name__ == "__main__": - # Start local cluster and deploy modules to separate processes - dimos = start(2) - - camera = dimos.deploy(CameraModule, frequency=2.0) - listener = dimos.deploy(ImageListener) - - # Choose a transport for the stream (example: LCM typed channel) - camera.color_image.transport = LCMTransport("/camera/rgb", Image) - - # Connect listener input to camera output - listener.image.connect(camera.color_image) - - camera.start() - listener.start() - - time.sleep(2) - dimos.stop() -``` - - - -``` -Initialized dimos local cluster with 2 workers, memory limit: auto -2026-01-24T13:17:50.190559Z [info ] Deploying module. [dimos/core/__init__.py] module=CameraModule -2026-01-24T13:17:50.218466Z [info ] Deployed module. [dimos/core/__init__.py] module=CameraModule worker_id=1 -2026-01-24T13:17:50.229474Z [info ] Deploying module. [dimos/core/__init__.py] module=ImageListener -2026-01-24T13:17:50.250199Z [info ] Deployed module. [dimos/core/__init__.py] module=ImageListener worker_id=0 -Received: (480, 640, 3) -Received: (480, 640, 3) -Received: (480, 640, 3) -``` - -See [Modules](modules.md) for more on module architecture. - ---- - -## Inspecting LCM traffic (CLI) - -`lcmspy` shows topic frequency/bandwidth stats: - -![lcmspy](assets/lcmspy.png) - -`dimos topic echo /topic` listens on typed channels like `/topic#pkg.Msg` and decodes automatically: - -```sh skip -Listening on /camera/rgb (inferring from typed LCM channels like '/camera/rgb#pkg.Msg')... (Ctrl+C to stop) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2026-01-24 20:28:59) -``` - ---- - -## Implementing a transport - -At the stream layer, a transport is implemented by subclassing `Transport` (see [`core/stream.py`](/dimos/core/stream.py#L83)) and implementing: - -* `broadcast(...)` -* `subscribe(...)` - -Your `Transport.__init__` args can be anything meaningful for your backend: - -* `(ip, port)` -* a shared-memory segment name -* a filesystem path -* a Redis channel - -Encoding is an implementation detail, but we encourage using LCM-compatible message types when possible. - -### Encoding helpers - -Many of our message types provide `lcm_encode` / `lcm_decode` for compact, language-agnostic binary encoding (often faster than pickle). For details, see [LCM](/docs/usage/lcm.md). - ---- - -## PubSub transports - -Even though transport can be anything (TCP connection, unix socket) for now all our transport backends implement the `PubSub` interface. - -* `publish(topic, message)` -* `subscribe(topic, callback) -> unsubscribe` - -```python -from dimos.protocol.pubsub.spec import PubSub -import inspect - -print(inspect.getsource(PubSub.publish)) -print(inspect.getsource(PubSub.subscribe)) -``` - - -```python - @abstractmethod - def publish(self, topic: TopicT, message: MsgT) -> None: - """Publish a message to a topic.""" - ... - - @abstractmethod - def subscribe( - self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] - ) -> Callable[[], None]: - """Subscribe to a topic with a callback. returns unsubscribe function""" - ... -``` - -Topic/message types are flexible: bytes, JSON, or our ROS-compatible [LCM](/docs/usage/lcm.md) types. We also have pickle-based transports for arbitrary Python objects. - -### LCM (UDP multicast) - -LCM is UDP multicast. It’s very fast on a robot LAN, but it’s **best-effort** (packets can drop). -For local emission it autoconfigures system in a way in which it's more robust and faster then other more common protocols like ROS, DDS - -```python -from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.msgs.geometry_msgs import Vector3 - -lcm = LCM(autoconf=True) -lcm.start() - -received = [] -topic = Topic("/robot/velocity", Vector3) - -lcm.subscribe(topic, lambda msg, t: received.append(msg)) -lcm.publish(topic, Vector3(1.0, 0.0, 0.5)) - -import time -time.sleep(0.1) - -print(f"Received velocity: x={received[0].x}, y={received[0].y}, z={received[0].z}") -lcm.stop() -``` - - -``` -Received velocity: x=1.0, y=0.0, z=0.5 -``` - -### Shared memory (IPC) - -Shared memory is highest performance, but only works on the **same machine**. - -```python -from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory - -shm = PickleSharedMemory(prefer="cpu") -shm.start() - -received = [] -shm.subscribe("test/topic", lambda msg, topic: received.append(msg)) -shm.publish("test/topic", {"data": [1, 2, 3]}) - -import time -time.sleep(0.1) - -print(f"Received: {received}") -shm.stop() -``` - - -``` -Received: [{'data': [1, 2, 3]}] -``` - -### DDS Transport - -For network communication, DDS uses the Data Distribution Service (DDS) protocol: - -```python session=dds_demo ansi=false -from dataclasses import dataclass -from cyclonedds.idl import IdlStruct - -from dimos.protocol.pubsub.impl.ddspubsub import DDS, Topic - -@dataclass -class SensorReading(IdlStruct): - value: float - -dds = DDS() -dds.start() - -received = [] -sensor_topic = Topic(name="sensors/temperature", data_type=SensorReading) - -dds.subscribe(sensor_topic, lambda msg, t: received.append(msg)) -dds.publish(sensor_topic, SensorReading(value=22.5)) - -import time -time.sleep(0.1) - -print(f"Received: {received}") -dds.stop() -``` - - -``` -Received: [SensorReading(value=22.5)] -``` - ---- - -## A minimal transport: `Memory` - -The simplest toy backend is `Memory` (single process). Start from there when implementing a new pubsub backend. - -```python -from dimos.protocol.pubsub.memory import Memory - -bus = Memory() -received = [] - -unsubscribe = bus.subscribe("sensor/data", lambda msg, topic: received.append(msg)) - -bus.publish("sensor/data", {"temperature": 22.5}) -bus.publish("sensor/data", {"temperature": 23.0}) - -print(f"Received {len(received)} messages:") -for msg in received: - print(f" {msg}") - -unsubscribe() -``` - - -``` -Received 2 messages: - {'temperature': 22.5} - {'temperature': 23.0} -``` - -See [`memory.py`](/dimos/protocol/pubsub/impl/memory.py) for the complete source. - ---- - -## Encode/decode mixins - -Transports often need to serialize messages before sending and deserialize after receiving. - -`PubSubEncoderMixin` at [`pubsub/spec.py`](/dimos/protocol/pubsub/spec.py#L95) provides a clean way to add encoding/decoding to any pubsub implementation. - -### Available mixins - -| Mixin | Encoding | Use case | -|----------------------|-----------------|------------------------------------| -| `PickleEncoderMixin` | Python pickle | Any Python object, Python-only | -| `LCMEncoderMixin` | LCM binary | Cross-language (C/C++/Python/Go/…) | -| `JpegEncoderMixin` | JPEG compressed | Image data, reduces bandwidth | - -`LCMEncoderMixin` is especially useful: you can use LCM message definitions with *any* transport (not just UDP multicast). See [LCM](/docs/usage/lcm.md) for details. - -### Creating a custom mixin - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.spec import PubSubEncoderMixin -import json - -class JsonEncoderMixin(PubSubEncoderMixin[str, dict, bytes]): - def encode(self, msg: dict, topic: str) -> bytes: - return json.dumps(msg).encode("utf-8") - - def decode(self, msg: bytes, topic: str) -> dict: - return json.loads(msg.decode("utf-8")) -``` - -Combine with a pubsub implementation via multiple inheritance: - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.memory import Memory - -class MyJsonPubSub(JsonEncoderMixin, Memory): - pass -``` - -Swap serialization by changing the mixin: - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.spec import PickleEncoderMixin - -class MyPicklePubSub(PickleEncoderMixin, Memory): - pass -``` - ---- - -## Testing and benchmarks - -### Spec tests - -See [`pubsub/test_spec.py`](/dimos/protocol/pubsub/test_spec.py) for the grid tests your new backend should pass. - -### Benchmarks - -Add your backend to benchmarks to compare in context: - -```sh skip -python -m pytest -svm tool -k "not bytes" dimos/protocol/pubsub/benchmark/test_benchmark.py -``` - ---- - -# Available transports - -| Transport | Use case | Cross-process | Network | Notes | -|----------------|-------------------------------------|---------------|---------|--------------------------------------| -| `Memory` | Testing only, single process | No | No | Minimal reference impl | -| `SharedMemory` | Multi-process on same machine | Yes | No | Highest throughput (IPC) | -| `LCM` | Robot LAN broadcast (UDP multicast) | Yes | Yes | Best-effort; can drop packets on LAN | -| `Redis` | Network pubsub via Redis server | Yes | Yes | Central broker; adds hop | -| `ROS` | ROS 2 topic communication | Yes | Yes | Integrates with RViz/ROS tools | -| `DDS` | Cyclone DDS without ROS (WIP) | Yes | Yes | WIP | diff --git a/docs/usage/transports/dds.md b/docs/usage/transports/dds.md deleted file mode 100644 index 1aec0bafe5..0000000000 --- a/docs/usage/transports/dds.md +++ /dev/null @@ -1,26 +0,0 @@ -# Installing DDS Transport Libs on Ubuntu - -The `dds` extra provides DDS (Data Distribution Service) transport support via [Eclipse Cyclone DDS](https://cyclonedds.io/docs/cyclonedds-python/latest/). This requires installing system libraries before the Python package can be built. - -```bash -# Install the CycloneDDS development library -sudo apt install cyclonedds-dev - -# Create a compatibility directory structure -# (required because Ubuntu's multiarch layout doesn't match the expected CMake layout) -sudo mkdir -p /opt/cyclonedds/{lib,bin,include} -sudo ln -sf /usr/lib/x86_64-linux-gnu/libddsc.so* /opt/cyclonedds/lib/ -sudo ln -sf /usr/lib/x86_64-linux-gnu/libcycloneddsidl.so* /opt/cyclonedds/lib/ -sudo ln -sf /usr/bin/idlc /opt/cyclonedds/bin/ -sudo ln -sf /usr/bin/ddsperf /opt/cyclonedds/bin/ -sudo ln -sf /usr/include/dds /opt/cyclonedds/include/ - -# Install with the dds extra -CYCLONEDDS_HOME=/opt/cyclonedds uv pip install -e '.[dds]' -``` - -To install all extras including DDS: - -```bash -CYCLONEDDS_HOME=/opt/cyclonedds uv sync --extra dds -``` diff --git a/docs/usage/transports/index.md b/docs/usage/transports/index.md deleted file mode 100644 index 748cf03aa1..0000000000 --- a/docs/usage/transports/index.md +++ /dev/null @@ -1,437 +0,0 @@ -# Transports - -Transports connect **module streams** across **process boundaries** and/or **networks**. - -* **Module**: a running component (e.g., camera, mapping, nav). -* **Stream**: a unidirectional flow of messages owned by a module (one broadcaster → many receivers). -* **Topic**: the name/identifier used by a transport or pubsub backend. -* **Message**: payload carried on a stream (often `dimos.msgs.*`, but can be bytes / images / pointclouds / etc.). - -Each edge in the graph is a **transported stream** (potentially different protocols). Each node is a **module**: - -![go2_nav](../assets/go2_nav.svg) - -## What the transport layer guarantees (and what it doesn’t) - -Modules **don’t** know or care *how* data moves. They just: - -* emit messages (broadcast) -* subscribe to messages (receive) - -A transport is responsible for the mechanics of delivery (IPC, sockets, Redis, ROS 2, etc.). - -**Important:** delivery semantics depend on the backend: - -* Some are **best-effort** (e.g., UDP multicast / LCM): loss can happen. -* Some can be **reliable** (e.g., TCP-backed, Redis, some DDS configs) but may add latency/backpressure. - -So: treat the API as uniform, but pick a backend whose semantics match the task. - ---- - -## Benchmarks - -Quick view on performance of our pubsub backends: - -```sh skip -python -m pytest -svm tool -k "not bytes" dimos/protocol/pubsub/benchmark/test_benchmark.py -``` - -![Benchmark results](../assets/pubsub_benchmark.png) - ---- - -## Abstraction layers - -
Pikchr - -```pikchr output=../assets/abstraction_layers.svg fold -color = white -fill = none -linewid = 0.5in -boxwid = 1.0in -boxht = 0.4in - -# Boxes with labels -B: box "Blueprints" rad 10px -arrow -M: box "Modules" rad 5px -arrow -T: box "Transports" rad 5px -arrow -P: box "PubSub" rad 5px - -# Descriptions below -text "robot configs" at B.s + (0.1, -0.2in) -text "camera, nav" at M.s + (0, -0.2in) -text "LCM, SHM, ROS" at T.s + (0, -0.2in) -text "pub/sub API" at P.s + (0, -0.2in) -``` - -
- - -![output](../assets/abstraction_layers.svg) - -We’ll go through these layers top-down. - ---- - -## Using transports with blueprints - -See [Blueprints](blueprints.md) for the blueprint API. - -From [`unitree/go2/blueprints/__init__.py`](/dimos/robot/unitree/go2/blueprints/__init__.py). - -Example: rebind a few streams from the default `LCMTransport` to `ROSTransport` (defined at [`transport.py`](/dimos/core/transport.py#L226)) so you can visualize in **rviz2**. - -```python skip -nav = autoconnect( - basic, - voxel_mapper(voxel_size=0.1), - cost_mapper(), - replanning_a_star_planner(), - wavefront_frontier_explorer(), -).global_config(n_dask_workers=6, robot_model="unitree_go2") - -ros = nav.transports( - { - ("lidar", PointCloud2): ROSTransport("lidar", PointCloud2), - ("global_map", PointCloud2): ROSTransport("global_map", PointCloud2), - ("odom", PoseStamped): ROSTransport("odom", PoseStamped), - ("color_image", Image): ROSTransport("color_image", Image), - } -) -``` - ---- - -## Using transports with modules - -Each **stream** on a module can use a different transport. Set `.transport` on the stream **before starting** modules. - -```python ansi=false -import time - -from dimos.core import In, Module, start -from dimos.core.transport import LCMTransport -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.msgs.sensor_msgs import Image - - -class ImageListener(Module): - image: In[Image] - - def start(self): - super().start() - self.image.subscribe(lambda img: print(f"Received: {img.shape}")) - - -if __name__ == "__main__": - # Start local cluster and deploy modules to separate processes - dimos = start(2) - - camera = dimos.deploy(CameraModule, frequency=2.0) - listener = dimos.deploy(ImageListener) - - # Choose a transport for the stream (example: LCM typed channel) - camera.color_image.transport = LCMTransport("/camera/rgb", Image) - - # Connect listener input to camera output - listener.image.connect(camera.color_image) - - camera.start() - listener.start() - - time.sleep(2) - dimos.stop() -``` - - - -``` -Initialized dimos local cluster with 2 workers, memory limit: auto -2026-01-24T13:17:50.190559Z [info ] Deploying module. [dimos/core/__init__.py] module=CameraModule -2026-01-24T13:17:50.218466Z [info ] Deployed module. [dimos/core/__init__.py] module=CameraModule worker_id=1 -2026-01-24T13:17:50.229474Z [info ] Deploying module. [dimos/core/__init__.py] module=ImageListener -2026-01-24T13:17:50.250199Z [info ] Deployed module. [dimos/core/__init__.py] module=ImageListener worker_id=0 -Received: (480, 640, 3) -Received: (480, 640, 3) -Received: (480, 640, 3) -``` - -See [Modules](modules.md) for more on module architecture. - ---- - -## Inspecting LCM traffic (CLI) - -`lcmspy` shows topic frequency/bandwidth stats: - -![lcmspy](../assets/lcmspy.png) - -`dimos topic echo /topic` listens on typed channels like `/topic#pkg.Msg` and decodes automatically: - -```sh skip -Listening on /camera/rgb (inferring from typed LCM channels like '/camera/rgb#pkg.Msg')... (Ctrl+C to stop) -Image(shape=(480, 640, 3), format=RGB, dtype=uint8, dev=cpu, ts=2026-01-24 20:28:59) -``` - ---- - -## Implementing a transport - -At the stream layer, a transport is implemented by subclassing `Transport` (see [`core/stream.py`](/dimos/core/stream.py#L83)) and implementing: - -* `broadcast(...)` -* `subscribe(...)` - -Your `Transport.__init__` args can be anything meaningful for your backend: - -* `(ip, port)` -* a shared-memory segment name -* a filesystem path -* a Redis channel - -Encoding is an implementation detail, but we encourage using LCM-compatible message types when possible. - -### Encoding helpers - -Many of our message types provide `lcm_encode` / `lcm_decode` for compact, language-agnostic binary encoding (often faster than pickle). For details, see [LCM](/docs/usage/lcm.md). - ---- - -## PubSub transports - -Even though transport can be anything (TCP connection, unix socket) for now all our transport backends implement the `PubSub` interface. - -* `publish(topic, message)` -* `subscribe(topic, callback) -> unsubscribe` - -```python -from dimos.protocol.pubsub.spec import PubSub -import inspect - -print(inspect.getsource(PubSub.publish)) -print(inspect.getsource(PubSub.subscribe)) -``` - - -```python - @abstractmethod - def publish(self, topic: TopicT, message: MsgT) -> None: - """Publish a message to a topic.""" - ... - - @abstractmethod - def subscribe( - self, topic: TopicT, callback: Callable[[MsgT, TopicT], None] - ) -> Callable[[], None]: - """Subscribe to a topic with a callback. returns unsubscribe function""" - ... -``` - -Topic/message types are flexible: bytes, JSON, or our ROS-compatible [LCM](/docs/usage/lcm.md) types. We also have pickle-based transports for arbitrary Python objects. - -### LCM (UDP multicast) - -LCM is UDP multicast. It’s very fast on a robot LAN, but it’s **best-effort** (packets can drop). -For local emission it autoconfigures system in a way in which it's more robust and faster then other more common protocols like ROS, DDS - -```python -from dimos.protocol.pubsub.lcmpubsub import LCM, Topic -from dimos.msgs.geometry_msgs import Vector3 - -lcm = LCM(autoconf=True) -lcm.start() - -received = [] -topic = Topic("/robot/velocity", Vector3) - -lcm.subscribe(topic, lambda msg, t: received.append(msg)) -lcm.publish(topic, Vector3(1.0, 0.0, 0.5)) - -import time -time.sleep(0.1) - -print(f"Received velocity: x={received[0].x}, y={received[0].y}, z={received[0].z}") -lcm.stop() -``` - - -``` -Received velocity: x=1.0, y=0.0, z=0.5 -``` - -### Shared memory (IPC) - -Shared memory is highest performance, but only works on the **same machine**. - -```python -from dimos.protocol.pubsub.shmpubsub import PickleSharedMemory - -shm = PickleSharedMemory(prefer="cpu") -shm.start() - -received = [] -shm.subscribe("test/topic", lambda msg, topic: received.append(msg)) -shm.publish("test/topic", {"data": [1, 2, 3]}) - -import time -time.sleep(0.1) - -print(f"Received: {received}") -shm.stop() -``` - - -``` -Received: [{'data': [1, 2, 3]}] -``` - -### DDS Transport - -For network communication, DDS uses the Data Distribution Service (DDS) protocol: - -```python session=dds_demo ansi=false -from dataclasses import dataclass -from cyclonedds.idl import IdlStruct - -from dimos.protocol.pubsub.impl.ddspubsub import DDS, Topic - -@dataclass -class SensorReading(IdlStruct): - value: float - -dds = DDS() -dds.start() - -received = [] -sensor_topic = Topic(name="sensors/temperature", data_type=SensorReading) - -dds.subscribe(sensor_topic, lambda msg, t: received.append(msg)) -dds.publish(sensor_topic, SensorReading(value=22.5)) - -import time -time.sleep(0.1) - -print(f"Received: {received}") -dds.stop() -``` - - -``` -Received: [SensorReading(value=22.5)] -``` - ---- - -## A minimal transport: `Memory` - -The simplest toy backend is `Memory` (single process). Start from there when implementing a new pubsub backend. - -```python -from dimos.protocol.pubsub.memory import Memory - -bus = Memory() -received = [] - -unsubscribe = bus.subscribe("sensor/data", lambda msg, topic: received.append(msg)) - -bus.publish("sensor/data", {"temperature": 22.5}) -bus.publish("sensor/data", {"temperature": 23.0}) - -print(f"Received {len(received)} messages:") -for msg in received: - print(f" {msg}") - -unsubscribe() -``` - - -``` -Received 2 messages: - {'temperature': 22.5} - {'temperature': 23.0} -``` - -See [`memory.py`](/dimos/protocol/pubsub/impl/memory.py) for the complete source. - ---- - -## Encode/decode mixins - -Transports often need to serialize messages before sending and deserialize after receiving. - -`PubSubEncoderMixin` at [`pubsub/spec.py`](/dimos/protocol/pubsub/spec.py#L95) provides a clean way to add encoding/decoding to any pubsub implementation. - -### Available mixins - -| Mixin | Encoding | Use case | -|----------------------|-----------------|------------------------------------| -| `PickleEncoderMixin` | Python pickle | Any Python object, Python-only | -| `LCMEncoderMixin` | LCM binary | Cross-language (C/C++/Python/Go/…) | -| `JpegEncoderMixin` | JPEG compressed | Image data, reduces bandwidth | - -`LCMEncoderMixin` is especially useful: you can use LCM message definitions with *any* transport (not just UDP multicast). See [LCM](/docs/usage/lcm.md) for details. - -### Creating a custom mixin - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.spec import PubSubEncoderMixin -import json - -class JsonEncoderMixin(PubSubEncoderMixin[str, dict, bytes]): - def encode(self, msg: dict, topic: str) -> bytes: - return json.dumps(msg).encode("utf-8") - - def decode(self, msg: bytes, topic: str) -> dict: - return json.loads(msg.decode("utf-8")) -``` - -Combine with a pubsub implementation via multiple inheritance: - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.memory import Memory - -class MyJsonPubSub(JsonEncoderMixin, Memory): - pass -``` - -Swap serialization by changing the mixin: - -```python session=jsonencoder no-result -from dimos.protocol.pubsub.spec import PickleEncoderMixin - -class MyPicklePubSub(PickleEncoderMixin, Memory): - pass -``` - ---- - -## Testing and benchmarks - -### Spec tests - -See [`pubsub/test_spec.py`](/dimos/protocol/pubsub/test_spec.py) for the grid tests your new backend should pass. - -### Benchmarks - -Add your backend to benchmarks to compare in context: - -```sh skip -python -m pytest -svm tool -k "not bytes" dimos/protocol/pubsub/benchmark/test_benchmark.py -``` - ---- - -# Available transports - -| Transport | Use case | Cross-process | Network | Notes | -|----------------|-------------------------------------|---------------|---------|--------------------------------------| -| `Memory` | Testing only, single process | No | No | Minimal reference impl | -| `SharedMemory` | Multi-process on same machine | Yes | No | Highest throughput (IPC) | -| `LCM` | Robot LAN broadcast (UDP multicast) | Yes | Yes | Best-effort; can drop packets on LAN | -| `Redis` | Network pubsub via Redis server | Yes | Yes | Central broker; adds hop | -| `ROS` | ROS 2 topic communication | Yes | Yes | Integrates with RViz/ROS tools | -| `DDS` | Cyclone DDS without ROS (WIP) | Yes | Yes | WIP | diff --git a/docs/usage/visualization.md b/docs/usage/visualization.md deleted file mode 100644 index 56d0f006f0..0000000000 --- a/docs/usage/visualization.md +++ /dev/null @@ -1,115 +0,0 @@ -# Viewer Backends - -Dimos supports three visualization backends: Rerun (web or native) and Foxglove. - -## Quick Start - -Choose your viewer backend via the CLI (preferred): - -```bash -# Rerun web viewer (default) - Full vis dashboard and teleop panel in browser -dimos run unitree-go2 - -# Explicitly select the viewer backend: -dimos --viewer-backend rerun run unitree-go2 -dimos --viewer-backend rerun-web run unitree-go2 -dimos --viewer-backend foxglove run unitree-go2 -``` - -Alternative (environment variable): - -```bash -# Rerun web viewer - Full dashboard in browser -VIEWER_BACKEND=rerun-web dimos run unitree-go2 - -# Rerun native viewer - native Rerun window + teleop panel at http://localhost:7779 -VIEWER_BACKEND=rerun dimos run unitree-go2 - -# Foxglove - Use Foxglove Studio instead of Rerun -VIEWER_BACKEND=foxglove dimos run unitree-go2 -``` - -## Viewer Modes Explained - -### Rerun Web (`rerun-web`) - -**What you get:** -- Full dashboard at http://localhost:7779 -- Rerun 3D viewer + command center sidebar in one page -- Works in browser, no display required (headless-friendly) - ---- - -### Rerun Native (`rerun`) - -**What you get:** -- Native Rerun application (separate window opens automatically) -- Command center at http://localhost:7779 -- Better performance with larger maps/higher resolution - ---- - -### Foxglove (`foxglove`) - -**What you get:** -- Foxglove bridge on ws://localhost:8765 -- No Rerun (saves resources) -- Better performance with larger maps/higher resolution -- Open layout: `assets/foxglove_dashboards/old/foxglove_unitree_lcm_dashboard.json` - ---- - -## Rendering with Custom Blueprints - -To enable rerun within your own blueprint simply include `RerunBridgeModule`: - -```python -from dimos.visualization.rerun.bridge import RerunBridgeModule -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.protocol.pubsub.impl.lcmpubsub import LCM - -camera_demo = autoconnect( - CameraModule.blueprint(), - RerunBridgeModule.blueprint( - viewer_mode="native", # native (desktop), web (browser), none (headless) - ), -) - -if __name__ == "__main__": - camera_demo.build().loop() -``` - -Every LCM stream, such as `color_image` (output by CameraModule), that uses a data type (like `Image`) that has a `.to_rerun` method will get rendered (`rr.log`) using the LCM topic as the rerun entity path. In other words: to render something, simply log it to a stream and it will automatically be available in rerun. - -## Performance Tuning - -### Symptom: Slow Map Updates - -If you notice: -- Robot appears to "walk across empty space" -- Costmap updates lag behind the robot -- Visualization stutters or freezes - -This happens on lower-end hardware (NUC, older laptops) with large maps. - -### Increase Voxel Size - -Edit [`dimos/robot/unitree/go2/blueprints/__init__.py`](/dimos/robot/unitree/go2/blueprints/__init__.py) line 82: - -```python -# Before (high detail, slower on large maps) -voxel_mapper(voxel_size=0.05), # 5cm voxels - -# After (lower detail, 8x faster) -voxel_mapper(voxel_size=0.1), # 10cm voxels -``` - -**Trade-off:** -- Larger voxels = fewer voxels = faster updates -- But slightly less detail in the map - ---- - -## How to use Rerun on `dev` (and the TF/entity nuances) - -Rerun on `dev` is **module-driven**: modules decide what to log, and `Blueprint.build()` sets up the shared viewer + default layout. diff --git a/examples/camera_grayscale.py b/examples/camera_grayscale.py deleted file mode 100644 index 0d21a0449f..0000000000 --- a/examples/camera_grayscale.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dimos.core import In, Module, Out, autoconnect, rpc -from dimos.hardware.sensors.camera.module import CameraModule -from dimos.msgs.sensor_msgs.Image import Image -from dimos.visualization.rerun.bridge import RerunBridgeModule - - -class Grayscale(Module): - color_image: In[Image] - gray_image: Out[Image] - - @rpc - def start(self): - self.color_image.subscribe(self._publish_grayscale) - - def _publish_grayscale(self, image: Image): - self.gray_image.publish(image.to_grayscale()) - - -if __name__ == "__main__": - autoconnect( - CameraModule.blueprint(), - Grayscale.blueprint(), - RerunBridgeModule.blueprint(), - ).build().loop() diff --git a/examples/language-interop/README.md b/examples/language-interop/README.md deleted file mode 100644 index 52ae561ddb..0000000000 --- a/examples/language-interop/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Language Interop Examples - -Demonstrates controlling a dimos robot from non-Python languages. - -## Usage - -1. Start the robot (in another terminal): - ```bash - cd ../simplerobot - python simplerobot.py - ``` - -2. Run any language example: - - [TypeScript](ts/) - CLI and browser-based web UI - - [C++](cpp/) - - [Lua](lua/) - -3. (Optional) Monitor traffic with `lcmspy` - -![lcmspy](assets/lcmspy.png) diff --git a/examples/language-interop/assets/lcmspy.png b/examples/language-interop/assets/lcmspy.png deleted file mode 100644 index dc6a824f69..0000000000 --- a/examples/language-interop/assets/lcmspy.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:87d3af9d9105d048c3e55faff52981c15cc1bfd8168c58a3d8c8f603aa8b7769 -size 5110 diff --git a/examples/language-interop/cpp/.gitignore b/examples/language-interop/cpp/.gitignore deleted file mode 100644 index 567609b123..0000000000 --- a/examples/language-interop/cpp/.gitignore +++ /dev/null @@ -1 +0,0 @@ -build/ diff --git a/examples/language-interop/cpp/CMakeLists.txt b/examples/language-interop/cpp/CMakeLists.txt deleted file mode 100644 index a0b8481cef..0000000000 --- a/examples/language-interop/cpp/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(robot_control) - -set(CMAKE_CXX_STANDARD 17) - -include(FetchContent) - -# Fetch dimos-lcm for message headers -FetchContent_Declare(dimos_lcm - GIT_REPOSITORY https://github.com/dimensionalOS/dimos-lcm.git - GIT_TAG main - GIT_SHALLOW TRUE -) -FetchContent_MakeAvailable(dimos_lcm) - -# Find LCM via pkg-config -find_package(PkgConfig REQUIRED) -pkg_check_modules(LCM REQUIRED lcm) - -add_executable(robot_control main.cpp) - -target_include_directories(robot_control PRIVATE - ${dimos_lcm_SOURCE_DIR}/generated/cpp_lcm_msgs - ${LCM_INCLUDE_DIRS} -) - -target_link_libraries(robot_control PRIVATE ${LCM_LIBRARIES}) -target_link_directories(robot_control PRIVATE ${LCM_LIBRARY_DIRS}) diff --git a/examples/language-interop/cpp/README.md b/examples/language-interop/cpp/README.md deleted file mode 100644 index ea11cd4505..0000000000 --- a/examples/language-interop/cpp/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# C++ Robot Control Example - -Subscribes to `/odom` and publishes velocity commands to `/cmd_vel`. - -## Build - -```bash -mkdir build && cd build -cmake .. -make -./robot_control -``` - -## Dependencies - -- [lcm](https://lcm-proj.github.io/) - install via package manager -- Message headers fetched automatically from [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm) diff --git a/examples/language-interop/cpp/main.cpp b/examples/language-interop/cpp/main.cpp deleted file mode 100644 index fef2607365..0000000000 --- a/examples/language-interop/cpp/main.cpp +++ /dev/null @@ -1,68 +0,0 @@ -// C++ robot control example -// Subscribes to robot pose and publishes twist commands - -#include -#include -#include -#include -#include -#include - -#include "geometry_msgs/PoseStamped.hpp" -#include "geometry_msgs/Twist.hpp" - -class RobotController { -public: - RobotController() : lcm_(), running_(true) {} - - void onPose(const lcm::ReceiveBuffer*, const std::string&, - const geometry_msgs::PoseStamped* msg) { - const auto& pos = msg->pose.position; - const auto& ori = msg->pose.orientation; - printf("[pose] x=%.2f y=%.2f z=%.2f | qw=%.2f\n", - pos.x, pos.y, pos.z, ori.w); - } - - void run() { - lcm_.subscribe("/odom#geometry_msgs.PoseStamped", &RobotController::onPose, this); - - printf("Robot control started\n"); - printf("Subscribing to /odom, publishing to /cmd_vel\n"); - printf("Press Ctrl+C to stop.\n\n"); - - // Publisher thread - std::thread pub_thread([this]() { - double t = 0; - while (running_) { - geometry_msgs::Twist twist; - twist.linear.x = 0.5; - twist.linear.y = 0; - twist.linear.z = 0; - twist.angular.x = 0; - twist.angular.y = 0; - twist.angular.z = std::sin(t) * 0.3; - - lcm_.publish("/cmd_vel#geometry_msgs.Twist", &twist); - printf("[twist] linear=%.2f angular=%.2f\n", twist.linear.x, twist.angular.z); - t += 0.1; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - }); - - // Handle incoming messages - while (lcm_.handle() == 0) {} - - running_ = false; - pub_thread.join(); - } - -private: - lcm::LCM lcm_; - std::atomic running_; -}; - -int main() { - RobotController controller; - controller.run(); - return 0; -} diff --git a/examples/language-interop/lua/.gitignore b/examples/language-interop/lua/.gitignore deleted file mode 100644 index f87dd2d125..0000000000 --- a/examples/language-interop/lua/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -lcm/ -dimos-lcm/ -msgs/ diff --git a/examples/language-interop/lua/README.md b/examples/language-interop/lua/README.md deleted file mode 100644 index b804194e55..0000000000 --- a/examples/language-interop/lua/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Lua Robot Control Example - -Subscribes to robot odometry and publishes twist commands using LCM. - -## Prerequisites - -- Lua 5.4 -- LuaSocket (`sudo luarocks install luasocket`) -- System dependencies: `glib`, `cmake` - -## Setup - -```bash -./setup.sh -``` - -This will: -1. Clone and build official [LCM](https://github.com/lcm-proj/lcm) Lua bindings -2. Clone [dimos-lcm](https://github.com/dimensionalOS/dimos-lcm) message definitions - -## Run - -```bash -lua main.lua -``` - -## Output - -``` -Robot control started -Subscribing to /odom, publishing to /cmd_vel -Press Ctrl+C to stop. - -[pose] x=15.29 y=9.62 z=0.00 | qw=0.57 -[twist] linear=0.50 angular=0.00 -[pose] x=15.28 y=9.63 z=0.00 | qw=0.57 -... -``` diff --git a/examples/language-interop/lua/main.lua b/examples/language-interop/lua/main.lua deleted file mode 100644 index f6d2dccca1..0000000000 --- a/examples/language-interop/lua/main.lua +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env lua - --- Lua robot control example --- Subscribes to robot pose and publishes twist commands - --- Add local msgs folder to path -local script_dir = arg[0]:match("(.*/)") or "./" -package.path = script_dir .. "msgs/?.lua;" .. package.path -package.path = script_dir .. "msgs/?/init.lua;" .. package.path - -local lcm = require("lcm") -local PoseStamped = require("geometry_msgs.PoseStamped") -local Twist = require("geometry_msgs.Twist") -local Vector3 = require("geometry_msgs.Vector3") - -local lc = lcm.lcm.new() - -print("Robot control started") -print("Subscribing to /odom, publishing to /cmd_vel") -print("Press Ctrl+C to stop.\n") - --- Subscribe to pose -lc:subscribe("/odom#geometry_msgs.PoseStamped", function(channel, msg) - msg = PoseStamped.decode(msg) - local pos = msg.pose.position - local ori = msg.pose.orientation - print(string.format("[pose] x=%.2f y=%.2f z=%.2f | qw=%.2f", - pos.x, pos.y, pos.z, ori.w)) -end) - --- Publisher loop -local t = 0 -local socket = require("socket") -local last_pub = socket.gettime() - -while true do - -- Handle incoming messages - lc:handle() - - -- Publish at ~10 Hz - local now = socket.gettime() - if now - last_pub >= 0.1 then - local twist = Twist:new() - twist.linear = Vector3:new() - twist.linear.x = 0.5 - twist.linear.y = 0 - twist.linear.z = 0 - twist.angular = Vector3:new() - twist.angular.x = 0 - twist.angular.y = 0 - twist.angular.z = math.sin(t) * 0.3 - - lc:publish("/cmd_vel#geometry_msgs.Twist", twist:encode()) - print(string.format("[twist] linear=%.2f angular=%.2f", twist.linear.x, twist.angular.z)) - - t = t + 0.1 - last_pub = now - end -end diff --git a/examples/language-interop/lua/setup.sh b/examples/language-interop/lua/setup.sh deleted file mode 100755 index 682d676183..0000000000 --- a/examples/language-interop/lua/setup.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash -# Setup script for LCM Lua bindings -# Clones official LCM repo and builds Lua bindings -# -# Tested on: Arch Linux, Ubuntu, macOS (with Homebrew) - -set -e - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -LCM_DIR="$SCRIPT_DIR/lcm" - -echo "=== LCM Lua Setup ===" - -# Detect Lua version -if command -v lua &>/dev/null; then - LUA_VERSION=$(lua -v 2>&1 | grep -oE '[0-9]+\.[0-9]+' | head -1) - echo "Detected Lua version: $LUA_VERSION" -else - echo "Error: lua not found in PATH" - exit 1 -fi - -# Detect Lua paths using pkg-config if available -if command -v pkg-config &>/dev/null && pkg-config --exists "lua$LUA_VERSION" 2>/dev/null; then - LUA_INCLUDE_DIR=$(pkg-config --variable=includedir "lua$LUA_VERSION") - LUA_LIBRARY=$(pkg-config --libs "lua$LUA_VERSION" | grep -oE '/[^ ]+\.so' | head -1 || echo "") -elif command -v pkg-config &>/dev/null && pkg-config --exists lua 2>/dev/null; then - LUA_INCLUDE_DIR=$(pkg-config --variable=includedir lua) - LUA_LIBRARY=$(pkg-config --libs lua | grep -oE '/[^ ]+\.so' | head -1 || echo "") -fi - -# Platform-specific defaults -if [[ "$OSTYPE" == "darwin"* ]]; then - # macOS with Homebrew - LUA_INCLUDE_DIR="${LUA_INCLUDE_DIR:-$(brew --prefix lua 2>/dev/null)/include/lua}" - LUA_LIBRARY="${LUA_LIBRARY:-$(brew --prefix lua 2>/dev/null)/lib/liblua.dylib}" - LUA_CPATH_BASE="${LUA_CPATH_BASE:-/usr/local/lib/lua}" -else - # Linux defaults - LUA_INCLUDE_DIR="${LUA_INCLUDE_DIR:-/usr/include}" - LUA_LIBRARY="${LUA_LIBRARY:-/usr/lib/liblua.so}" - LUA_CPATH_BASE="${LUA_CPATH_BASE:-/usr/local/lib/lua}" -fi - -echo "Lua include: $LUA_INCLUDE_DIR" -echo "Lua library: $LUA_LIBRARY" - -# Clone LCM if not present -if [ ! -d "$LCM_DIR" ]; then - echo "Cloning LCM..." - git clone --depth 1 https://github.com/lcm-proj/lcm.git "$LCM_DIR" -else - echo "LCM already cloned" -fi - -# Build Lua bindings using cmake -echo "Building LCM Lua bindings..." -cd "$LCM_DIR" -mkdir -p build && cd build - -# Configure with Lua support -cmake .. \ - -DLCM_ENABLE_LUA=ON \ - -DLCM_ENABLE_PYTHON=OFF \ - -DLCM_ENABLE_JAVA=OFF \ - -DLCM_ENABLE_TESTS=OFF \ - -DLCM_ENABLE_EXAMPLES=OFF \ - -DLUA_INCLUDE_DIR="$LUA_INCLUDE_DIR" \ - -DLUA_LIBRARY="$LUA_LIBRARY" - -# Build just the lua target -make lcm-lua -j$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4) - -# Install the lua module -LUA_CPATH_DIR="$LUA_CPATH_BASE/$LUA_VERSION" -echo "Installing lcm.so to $LUA_CPATH_DIR" -sudo mkdir -p "$LUA_CPATH_DIR" -sudo cp lcm-lua/lcm.so "$LUA_CPATH_DIR/" - -# Get dimos-lcm message definitions -DIMOS_LCM_DIR="$SCRIPT_DIR/dimos-lcm" -MSGS_DST="$SCRIPT_DIR/msgs" - -echo "Getting message definitions..." -if [ -d "$DIMOS_LCM_DIR" ]; then - echo "Updating dimos-lcm..." - cd "$DIMOS_LCM_DIR" && git pull -else - echo "Cloning dimos-lcm..." - git clone --depth 1 https://github.com/dimensionalOS/dimos-lcm.git "$DIMOS_LCM_DIR" -fi - -# Link/copy messages -rm -rf "$MSGS_DST" -cp -r "$DIMOS_LCM_DIR/generated/lua_lcm_msgs" "$MSGS_DST" -echo "Messages installed to $MSGS_DST" - -echo "" -echo "=== Setup complete ===" -echo "Run: lua main.lua" diff --git a/examples/language-interop/ts/README.md b/examples/language-interop/ts/README.md deleted file mode 100644 index 373d7df3be..0000000000 --- a/examples/language-interop/ts/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# TypeScript Robot Control Examples - -Subscribes to `/odom` and publishes velocity commands to `/cmd_vel`. - -## CLI Example - -```bash -deno task start -``` - -## Web Example - -Browser-based control with WebSocket bridge: - -```bash -cd web -deno run --allow-net --allow-read --unstable-net server.ts -``` - -Open http://localhost:8080 in your browser. - -Features: -- Real-time pose display -- Arrow keys / WASD for control -- Click buttons to send twist commands - -The browser imports `@dimos/msgs` via [esm.sh](https://esm.sh) and encodes/decodes LCM packets directly - the server just forwards raw binary between WebSocket and UDP multicast. - -## Dependencies - -Main documentation for TS interop: - -- [@dimos/lcm](https://jsr.io/@dimos/lcm) -- [@dimos/msgs](https://jsr.io/@dimos/msgs) diff --git a/examples/language-interop/ts/deno.json b/examples/language-interop/ts/deno.json deleted file mode 100644 index b64363a64a..0000000000 --- a/examples/language-interop/ts/deno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "imports": { - "@dimos/lcm": "jsr:@dimos/lcm", - "@dimos/msgs": "jsr:@dimos/msgs" - }, - "tasks": { - "start": "deno run --allow-net --unstable-net main.ts" - } -} diff --git a/examples/language-interop/ts/deno.lock b/examples/language-interop/ts/deno.lock deleted file mode 100644 index 9529ebdb34..0000000000 --- a/examples/language-interop/ts/deno.lock +++ /dev/null @@ -1,21 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@dimos/lcm@*": "0.2.0", - "jsr:@dimos/msgs@*": "0.1.4" - }, - "jsr": { - "@dimos/lcm@0.2.0": { - "integrity": "03399f5e4800f28a0c294981e0210d784232fc65a57707de19052ad805bd5fea" - }, - "@dimos/msgs@0.1.4": { - "integrity": "564bc30b4bc41a562c296c257a15055283ca0cbd66d0627991ede5295832d0c4" - } - }, - "workspace": { - "dependencies": [ - "jsr:@dimos/lcm@*", - "jsr:@dimos/msgs@*" - ] - } -} diff --git a/examples/language-interop/ts/main.ts b/examples/language-interop/ts/main.ts deleted file mode 100644 index 9026304f2c..0000000000 --- a/examples/language-interop/ts/main.ts +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --unstable-net - -// TypeScript robot control example -// Subscribes to robot odometry and publishes twist commands - -import { LCM } from "@dimos/lcm"; -import { geometry_msgs } from "@dimos/msgs"; - -const lcm = new LCM(); -await lcm.start(); - -console.log("Robot control started"); -console.log("Subscribing to /odom, publishing to /cmd_vel"); -console.log("Press Ctrl+C to stop.\n"); - -// Subscribe to pose - prints robot position -lcm.subscribe("/odom", geometry_msgs.PoseStamped, (msg) => { - const pos = msg.data.pose.position; - const ori = msg.data.pose.orientation; - console.log( - `[pose] x=${pos.x.toFixed(2)} y=${pos.y.toFixed(2)} z=${pos.z.toFixed(2)} | qw=${ori.w.toFixed(2)}` - ); -}); - -// Publish twist commands at 10 Hz - simple forward motion -let t = 0; -const interval = setInterval(async () => { - if (!lcm.isRunning()) { - clearInterval(interval); - return; - } - - const twist = new geometry_msgs.Twist({ - linear: new geometry_msgs.Vector3({ x: 0.5, y: 0, z: 0 }), - angular: new geometry_msgs.Vector3({ x: 0, y: 0, z: Math.sin(t) * 0.3 }), - }); - - await lcm.publish("/cmd_vel", twist); - console.log(`[twist] linear=${twist.linear.x.toFixed(2)} angular=${twist.angular.z.toFixed(2)}`); - t += 0.1; -}, 100); - -await lcm.run(); diff --git a/examples/language-interop/ts/web/index.html b/examples/language-interop/ts/web/index.html deleted file mode 100644 index 1ce89604b6..0000000000 --- a/examples/language-interop/ts/web/index.html +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - Robot Control - - - -

Robot Control

-
Connecting...
- -
-
-

Pose

-
X--
-
Y--
-
Z--
-
Qw--
-
- -
-

Controls

-
-
- -
- - - -
- -
-
-
- -
-

Log

-
-
- - - - diff --git a/examples/language-interop/ts/web/server.ts b/examples/language-interop/ts/web/server.ts deleted file mode 100644 index 5b8ec035be..0000000000 --- a/examples/language-interop/ts/web/server.ts +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env -S deno run --allow-net --allow-read --unstable-net - -// LCM to WebSocket Bridge for Robot Control -// Forwards robot pose to browser, receives twist commands from browser - -import { LCM } from "jsr:@dimos/lcm"; -import { decodePacket, geometry_msgs } from "jsr:@dimos/msgs"; - -const PORT = 8080; -const clients = new Set(); - -Deno.serve({ port: PORT }, async (req) => { - const url = new URL(req.url); - - if (req.headers.get("upgrade") === "websocket") { - const { socket, response } = Deno.upgradeWebSocket(req); - socket.onopen = () => { console.log("Client connected"); clients.add(socket); }; - socket.onclose = () => { console.log("Client disconnected"); clients.delete(socket); }; - socket.onerror = () => clients.delete(socket); - - // Forward binary LCM packets from browser directly to UDP - socket.binaryType = "arraybuffer"; - socket.onmessage = async (event) => { - if (event.data instanceof ArrayBuffer) { - const packet = new Uint8Array(event.data); - try { - // we don't need to decode, just showing we can - const { channel, data } = decodePacket(packet); - console.log(`[ws->lcm] ${channel}`, data); - await lcm.publishPacket(packet); - } catch (e) { - console.error("Forward error:", e); - } - } - }; - - return response; - } - - if (url.pathname === "/" || url.pathname === "/index.html") { - const html = await Deno.readTextFile(new URL("./index.html", import.meta.url)); - return new Response(html, { headers: { "content-type": "text/html" } }); - } - - return new Response("Not found", { status: 404 }); -}); - -console.log(`Server: http://localhost:${PORT}`); - -const lcm = new LCM(); -await lcm.start(); - -// Subscribe to pose and just log to show how server can decode messages for itself -lcm.subscribe("/odom", geometry_msgs.PoseStamped, (msg) => { - const pos = msg.data.pose.position; - const ori = msg.data.pose.orientation; - console.log(`[pose] x=${pos.x.toFixed(2)} y=${pos.y.toFixed(2)} z=${pos.z.toFixed(2)}`); -}); - -// Forward all raw packets to browser (we are decoding LCM directly in the browser) -lcm.subscribePacket((packet) => { - for (const client of clients) { - if (client.readyState === WebSocket.OPEN) { - client.send(packet); - } - } -}); - -await lcm.run(); diff --git a/examples/rpc_calls.py b/examples/rpc_calls.py deleted file mode 100644 index c39ff8d4e8..0000000000 --- a/examples/rpc_calls.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from typing import Protocol - -from dimos.core import Module, rpc -from dimos.core.blueprints import autoconnect -from dimos.spec.utils import Spec - - -# this would be defined in some other file (this could be imported from a library) -class Calculator(Module): - @rpc - def compute1(self, a: int, b: int) -> int: - return a + b - - @rpc - def compute2(self, a: float, b: float) -> float: - return a + b - - -# what your module needs/expects -class ComputeSpec(Spec, Protocol): - @rpc - def compute1(self, a: int, b: int) -> int: ... - - @rpc - def compute2(self, a: float, b: float) -> float: ... - - -class Client(Module): - # this says: "hey dimos, give me access to a module that has a compute1 and compute2 method" - calc: ComputeSpec - - @rpc - def start(self) -> None: - print("compute1:", self.calc.compute1(2, 3)) - print("compute2:", self.calc.compute2(1.5, 2.5)) - - -if __name__ == "__main__": - autoconnect( - Calculator.blueprint(), - Client.blueprint(), - ).build().loop() diff --git a/examples/simplerobot/README.md b/examples/simplerobot/README.md deleted file mode 100644 index 8c7b6dfbc7..0000000000 --- a/examples/simplerobot/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# SimpleRobot - -A minimal virtual robot for testing and development. It implements some of the same LCM interface as real robots, making it ideal for testing third-party integrations (see `examples/language-interop/`) or experimeting with dimos Module patterns - -## Interface - -| Topic | Type | Direction | Description | -|------------|---------------|-----------|-----------------------------------------| -| `/cmd_vel` | `Twist` | Subscribe | Velocity commands (linear.x, angular.z) | -| `/odom` | `PoseStamped` | Publish | Current pose at 30Hz | - -Physical robots typically publish multiple poses in a relationship as `TransformStamped` in a TF tree, while SimpleRobot publishes `PoseStamped` directly for simplicity. - -For details on this check [Transforms](/docs/usage/transforms.md) - -## Usage - -```bash -# With pygame visualization -python examples/simplerobot/simplerobot.py - -# Headless mode -python examples/simplerobot/simplerobot.py --headless - -# Run self-test demo -python examples/simplerobot/simplerobot.py --headless --selftest -``` - -Use `lcmspy` in another terminal to inspect messages. Press `q` or `Esc` to quit visualization. - -## Sending Commands - -From any language with LCM bindings, publish `Twist` messages to `/cmd_vel`: - -```python -from dimos.core import LCMTransport -from dimos.msgs.geometry_msgs import Twist - -transport = LCMTransport("/cmd_vel", Twist) -transport.publish(Twist(linear=(0.5, 0, 0), angular=(0, 0, 0.3))) -``` - -See `examples/language-interop/` for C++, TypeScript, and Lua examples. - -## Physics - -SimpleRobot uses a 2D unicycle model: -- `linear.x` drives forward/backward -- `angular.z` rotates left/right -- Commands timeout after 0.5s (robot stops if no new commands) diff --git a/examples/simplerobot/simplerobot.py b/examples/simplerobot/simplerobot.py deleted file mode 100644 index b959fa7d6f..0000000000 --- a/examples/simplerobot/simplerobot.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Copyright 2026 Dimensional Inc. -# SPDX-License-Identifier: Apache-2.0 - -""" -Simple virtual robot demonstrating a dimos Module with In/Out ports. - -Subscribes to Twist commands and publishes PoseStamped. -""" - -from dataclasses import dataclass -import math -import time -from typing import Any - -import reactivex as rx - -from dimos.core import In, Module, ModuleConfig, Out, rpc -from dimos.msgs.geometry_msgs import Pose, PoseStamped, Quaternion, Twist, Vector3 - - -def apply_twist(pose: Pose, twist: Twist, dt: float) -> Pose: - """Apply a velocity command to a pose (unicycle model).""" - yaw = pose.yaw + twist.angular.z * dt - return Pose( - position=( - pose.x + twist.linear.x * math.cos(yaw) * dt, - pose.y + twist.linear.x * math.sin(yaw) * dt, - pose.z, - ), - orientation=Quaternion.from_euler(Vector3(0, 0, yaw)), - ) - - -@dataclass -class SimpleRobotConfig(ModuleConfig): - frame_id: str = "world" - update_rate: float = 30.0 - cmd_timeout: float = 0.5 - - -class SimpleRobot(Module[SimpleRobotConfig]): - """A 2D robot that integrates velocity commands into pose.""" - - cmd_vel: In[Twist] - pose: Out[PoseStamped] - default_config = SimpleRobotConfig - - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._pose = Pose() - self._vel = Twist() - self._vel_time = 0.0 - - @rpc - def start(self) -> None: - self._disposables.add(self.cmd_vel.observable().subscribe(self._on_twist)) - self._disposables.add( - rx.interval(1.0 / self.config.update_rate).subscribe(lambda _: self._update()) - ) - self._disposables.add( - rx.interval(1.0).subscribe(lambda _: print(f"\033[34m{self._pose}\033[0m")) - ) - - def _on_twist(self, twist: Twist) -> None: - self._vel = twist - self._vel_time = time.time() - print(f"\033[32m{twist}\033[0m") - - def _update(self) -> None: - now = time.time() - dt = 1.0 / self.config.update_rate - - vel = self._vel if now - self._vel_time < self.config.cmd_timeout else Twist() - - self._pose = apply_twist(self._pose, vel, dt) - - self.pose.publish( - PoseStamped( - ts=now, - frame_id=self.config.frame_id, - position=self._pose.position, - orientation=self._pose.orientation, - ) - ) - - -if __name__ == "__main__": - import argparse - - from dimos.core import LCMTransport - - parser = argparse.ArgumentParser(description="Simple virtual robot") - parser.add_argument("--headless", action="store_true") - parser.add_argument("--selftest", action="store_true", help="Run demo movements") - args = parser.parse_args() - - # If running in a dimos cluster we'd call - # - # from dimos.core import start - # dimos = start() - # robot = dimos.deploy(SimpleRobot) - # - # but this is a standalone example - # and we don't mind running in the main thread - - robot = SimpleRobot() - robot.pose.transport = LCMTransport("/odom", PoseStamped) - robot.cmd_vel.transport = LCMTransport("/cmd_vel", Twist) - robot.start() - - if not args.headless: - from vis import start_visualization - - start_visualization(robot) - - print("Robot running.") - print(" Publishing: /odom (PoseStamped)") - print(" Subscribing: /cmd_vel (Twist)") - print(" Run 'lcmspy' in another terminal to see LCM messages") - print(" Check /examples/language-interop for sending commands from LUA, C++, TS etc.") - print(" Ctrl+C to exit") - - try: - if args.selftest: - time.sleep(1) - print("Forward...") - for _ in range(8): - robot._on_twist(Twist(linear=(1.0, 0, 0))) - time.sleep(0.25) - print("Turn...") - for _ in range(12): - robot._on_twist(Twist(linear=(0.5, 0, 0), angular=(0, 0, 0.5))) - time.sleep(0.25) - print("Stop") - robot._on_twist(Twist()) - time.sleep(1) - else: - while True: - time.sleep(1) - except KeyboardInterrupt: - print("\nStopping...") - finally: - robot.stop() diff --git a/examples/simplerobot/vis.py b/examples/simplerobot/vis.py deleted file mode 100644 index 951a6be1b9..0000000000 --- a/examples/simplerobot/vis.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright 2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Pygame visualization for SimpleRobot.""" - -import math -import threading - - -def run_visualization(robot, window_size=(800, 800), meters_per_pixel=0.02): - """Run pygame visualization for a robot. Call from a thread.""" - import pygame - - pygame.init() - screen = pygame.display.set_mode(window_size) - pygame.display.set_caption("Simple Robot") - clock = pygame.time.Clock() - font = pygame.font.Font(None, 24) - - BG = (30, 30, 40) - GRID = (50, 50, 60) - ROBOT = (100, 200, 255) - ARROW = (255, 150, 100) - TEXT = (200, 200, 200) - - w, h = window_size - cx, cy = w // 2, h // 2 - running = True - - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - if event.type == pygame.KEYDOWN and event.key in (pygame.K_ESCAPE, pygame.K_q): - running = False - - pose, vel = robot._pose, robot._vel - - screen.fill(BG) - - # Grid (1m spacing) - grid_spacing = int(1.0 / meters_per_pixel) - for x in range(0, w, grid_spacing): - pygame.draw.line(screen, GRID, (x, 0), (x, h)) - for y in range(0, h, grid_spacing): - pygame.draw.line(screen, GRID, (0, y), (w, y)) - - # Robot position in screen coords - rx = cx + int(pose.x / meters_per_pixel) - ry = cy - int(pose.y / meters_per_pixel) - - # Robot body - pygame.draw.circle(screen, ROBOT, (rx, ry), 20) - - # Direction arrow - ax = rx + int(45 * math.cos(pose.yaw)) - ay = ry - int(45 * math.sin(pose.yaw)) - pygame.draw.line(screen, ARROW, (rx, ry), (ax, ay), 3) - for sign in [-1, 1]: - hx = ax - int(10 * math.cos(pose.yaw + sign * 0.5)) - hy = ay + int(10 * math.sin(pose.yaw + sign * 0.5)) - pygame.draw.line(screen, ARROW, (ax, ay), (hx, hy), 3) - - # Info text - info = [ - f"Position: ({pose.x:.2f}, {pose.y:.2f}) m", - f"Heading: {math.degrees(pose.yaw):.1f}°", - f"Velocity: {vel.linear.x:.2f} m/s", - f"Angular: {math.degrees(vel.angular.z):.1f}°/s", - ] - for i, text in enumerate(info): - screen.blit(font.render(text, True, TEXT), (10, 10 + i * 25)) - - pygame.display.flip() - clock.tick(60) - - pygame.quit() - - -def start_visualization(robot, **kwargs): - """Start visualization in a background thread.""" - thread = threading.Thread(target=run_visualization, args=(robot,), kwargs=kwargs, daemon=True) - thread.start() - return thread diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 402f251030..0000000000 --- a/flake.lock +++ /dev/null @@ -1,177 +0,0 @@ -{ - "nodes": { - "diagon": { - "locked": { - "lastModified": 1763299369, - "narHash": "sha256-z/q22EqZfF79vZQh6K/yCmt8iqDvUSkIVTH+Omhv1VE=", - "owner": "petertrotman", - "repo": "nixpkgs", - "rev": "dff059e25eee7aa958c606aeb6b5879ae1c674f0", - "type": "github" - }, - "original": { - "owner": "petertrotman", - "ref": "Diagon", - "repo": "nixpkgs", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "home-manager": { - "inputs": { - "nixpkgs": [ - "xome", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1753983724, - "narHash": "sha256-2vlAOJv4lBrE+P1uOGhZ1symyjXTRdn/mz0tZ6faQcg=", - "owner": "nix-community", - "repo": "home-manager", - "rev": "7035020a507ed616e2b20c61491ae3eaa8e5462c", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "home-manager", - "type": "github" - } - }, - "lib": { - "inputs": { - "flakeUtils": [ - "flake-utils" - ], - "libSource": "libSource" - }, - "locked": { - "lastModified": 1764022662, - "narHash": "sha256-vS3EeyELqCskh88JkUW/ce8A8b3m+iRPLPd4kDRTqPY=", - "owner": "jeff-hykin", - "repo": "quick-nix-toolkits", - "rev": "de1cc174579ecc7b655de5ba9618548d1b72306c", - "type": "github" - }, - "original": { - "owner": "jeff-hykin", - "repo": "quick-nix-toolkits", - "type": "github" - } - }, - "libSource": { - "locked": { - "lastModified": 1766884708, - "narHash": "sha256-x8nyRwtD0HMeYtX60xuIuZJbwwoI7/UKAdCiATnQNz0=", - "owner": "nix-community", - "repo": "nixpkgs.lib", - "rev": "15177f81ad356040b4460a676838154cbf7f6213", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "nixpkgs.lib", - "type": "github" - } - }, - "libSource_2": { - "locked": { - "lastModified": 1753579242, - "narHash": "sha256-zvaMGVn14/Zz8hnp4VWT9xVnhc8vuL3TStRqwk22biA=", - "owner": "divnix", - "repo": "nixpkgs.lib", - "rev": "0f36c44e01a6129be94e3ade315a5883f0228a6e", - "type": "github" - }, - "original": { - "owner": "divnix", - "repo": "nixpkgs.lib", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1748929857, - "narHash": "sha256-lcZQ8RhsmhsK8u7LIFsJhsLh/pzR9yZ8yqpTzyGdj+Q=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c2a03962b8e24e669fb37b7df10e7c79531ff1a4", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "diagon": "diagon", - "flake-utils": "flake-utils", - "lib": "lib", - "nixpkgs": "nixpkgs", - "xome": "xome" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "xome": { - "inputs": { - "flake-utils": [ - "flake-utils" - ], - "home-manager": "home-manager", - "libSource": "libSource_2", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1765466883, - "narHash": "sha256-c4YxXoS6U9BFcxP4TWZirwycaxT2oFyPMeyVp5vrME8=", - "owner": "jeff-hykin", - "repo": "xome", - "rev": "1f3507c4985e05177bd1a5b57d2862e30bb5da9b", - "type": "github" - }, - "original": { - "owner": "jeff-hykin", - "repo": "xome", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 68dbf0ee8c..0000000000 --- a/flake.nix +++ /dev/null @@ -1,315 +0,0 @@ -{ - description = "Project dev environment as Nix shell + DockerTools layered image"; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - flake-utils.url = "github:numtide/flake-utils"; - lib.url = "github:jeff-hykin/quick-nix-toolkits"; - lib.inputs.flakeUtils.follows = "flake-utils"; - xome.url = "github:jeff-hykin/xome"; - xome.inputs.nixpkgs.follows = "nixpkgs"; - xome.inputs.flake-utils.follows = "flake-utils"; - diagon.url = "github:petertrotman/nixpkgs/Diagon"; - }; - - outputs = { self, nixpkgs, flake-utils, lib, xome, diagon, ... }: - flake-utils.lib.eachDefaultSystem (system: - let - pkgs = import nixpkgs { inherit system; }; - - # ------------------------------------------------------------ - # 1. Shared package list (tool-chain + project deps) - # ------------------------------------------------------------ - # we "flag" each package with what we need it for (e.g. LD_LIBRARY_PATH, nativeBuildInputs vs buildInputs, etc) - aggregation = lib.aggregator [ - ### Core shell & utils - { vals.pkg=pkgs.bashInteractive; flags={}; } - { vals.pkg=pkgs.coreutils; flags={}; } - { vals.pkg=pkgs.gh; flags={}; } - { vals.pkg=pkgs.stdenv.cc.cc.lib; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.stdenv.cc; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.gfortran.cc.lib; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.cctools; flags={}; onlyIf=pkgs.stdenv.isDarwin; } # for pip install opencv-python - { vals.pkg=pkgs.pcre2; flags={ ldLibraryGroup=pkgs.stdenv.isDarwin; packageConfGroup=pkgs.stdenv.isDarwin; }; } - { vals.pkg=pkgs.libsysprof-capture; flags.packageConfGroup=true; onlyIf=pkgs.stdenv.isDarwin; } - { vals.pkg=pkgs.xcbuild; flags={}; onlyIf=pkgs.stdenv.isDarwin; } - { vals.pkg=pkgs.git-lfs; flags={}; } - { vals.pkg=pkgs.gnugrep; flags={}; } - { vals.pkg=pkgs.gnused; flags={}; } - { vals.pkg=pkgs.iproute2; flags={}; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.pkg-config; flags={}; } - { vals.pkg=pkgs.git; flags={}; } - { vals.pkg=pkgs.opensshWithKerberos;flags={}; } - { vals.pkg=pkgs.unixtools.ifconfig; flags={}; } - { vals.pkg=pkgs.unixtools.netstat; flags={}; } - - # when pip packages call cc with -I/usr/include, that causes problems on some machines, this swaps that out for the nix cc headers - # this is only necessary for pip packages from venv, pip packages from nixpkgs.python312Packages.* already have "-I/usr/include" patched with the nix equivalent - { - vals.pkg=(pkgs.writeShellScriptBin - "cc-no-usr-include" - '' - #!${pkgs.bash}/bin/bash - set -euo pipefail - - real_cc="${pkgs.stdenv.cc}/bin/gcc" - - args=() - for a in "$@"; do - case "$a" in - -I/usr/include|-I/usr/local/include) - # drop these - ;; - *) - args+=("$a") - ;; - esac - done - - exec "$real_cc" "''${args[@]}" - '' - ); - flags={}; - } - - ### Python + static analysis - { vals.pkg=pkgs.python312; flags={}; vals.pythonMinorVersion="12";} - { vals.pkg=pkgs.python312Packages.pip; flags={}; } - { vals.pkg=pkgs.python312Packages.setuptools; flags={}; } - { vals.pkg=pkgs.python312Packages.virtualenv; flags={}; } - { vals.pkg=pkgs.pre-commit; flags={}; } - - ### Runtime deps - { vals.pkg=pkgs.portaudio; flags={ldLibraryGroup=true; packageConfGroup=true;}; } - { vals.pkg=pkgs.ffmpeg_6; flags={}; } - { vals.pkg=pkgs.ffmpeg_6.dev; flags={}; } - - ### Graphics / X11 stack - { vals.pkg=pkgs.libGL; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.libGLU; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.mesa; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.glfw; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libX11; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXi; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXext; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXrandr; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXinerama; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXcursor; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXfixes; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXrender; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXdamage; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXcomposite; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libxcb; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXScrnSaver; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.xorg.libXxf86vm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.udev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.SDL2; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.SDL2.dev; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.zlib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - - ### GTK / OpenCV helpers - { vals.pkg=pkgs.glib; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.gtk3; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.gdk-pixbuf; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.gobject-introspection; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - - ### GStreamer - { vals.pkg=pkgs.gst_all_1.gstreamer; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } - { vals.pkg=pkgs.gst_all_1.gst-plugins-base; flags.ldLibraryGroup=true; flags.giTypelibGroup=true; } - { vals.pkg=pkgs.gst_all_1.gst-plugins-good; flags={}; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.gst_all_1.gst-plugins-bad; flags={}; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.gst_all_1.gst-plugins-ugly; flags={}; onlyIf=pkgs.stdenv.isLinux; } - { vals.pkg=pkgs.python312Packages.gst-python; flags={}; onlyIf=pkgs.stdenv.isLinux; } - - ### Open3D & build-time - { vals.pkg=pkgs.eigen; flags={}; } - { vals.pkg=pkgs.cmake; flags={}; } - { vals.pkg=pkgs.ninja; flags={}; } - { vals.pkg=pkgs.jsoncpp; flags={}; } - { vals.pkg=pkgs.libjpeg; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.libjpeg_turbo; flags.ldLibraryGroup=true; } - { vals.pkg=pkgs.libpng; flags={}; } - { vals.pkg=pkgs.libidn2; flags.ldLibraryGroup=true; } - - ### Docs generators - { vals.pkg=pkgs.pikchr; flags={}; } - { vals.pkg=pkgs.graphviz; flags={}; } - { vals.pkg=pkgs.imagemagick; flags={}; } - { vals.pkg=diagon.legacyPackages.${system}.diagon; flags={}; } - - ### LCM (Lightweight Communications and Marshalling) - { vals.pkg=pkgs.lcm; flags.ldLibraryGroup=true; onlyIf=pkgs.stdenv.isLinux; } - # lcm works on darwin, but only after two fixes (1. pkg-config, 2. fsync) - { - onlyIf=pkgs.stdenv.isDarwin; - flags.ldLibraryGroup=true; - flags.manualPythonPackages=true; - vals.pkg=pkgs.lcm.overrideAttrs (old: - let - # 1. fix pkg-config on darwin - pkgConfPackages = aggregation.getAll { hasAllFlags=[ "packageConfGroup" ]; attrPath=[ "pkg" ]; }; - packageConfPackagesString = (aggregation.getAll { - hasAllFlags=[ "packageConfGroup" ]; - attrPath=[ "pkg" ]; - strAppend="/lib/pkgconfig"; - strJoin=":"; - }); - in - { - buildInputs = (old.buildInputs or []) ++ pkgConfPackages; - nativeBuildInputs = (old.nativeBuildInputs or []) ++ [ pkgs.pkg-config pkgs.python312 ]; - # 1. fix pkg-config on darwin - env.PKG_CONFIG_PATH = packageConfPackagesString; - # 2. Fix fsync on darwin - patches = [ - (pkgs.writeText "lcm-darwin-fsync.patch" "--- ./lcm-logger/lcm_logger.c 2025-11-14 09:46:01.000000000 -0600\n+++ ./lcm-logger/lcm_logger.c 2025-11-14 09:47:05.000000000 -0600\n@@ -428,9 +428,13 @@\n if (needs_flushed) {\n fflush(logger->log->f);\n #ifndef WIN32\n+#ifdef __APPLE__\n+ fsync(fileno(logger->log->f));\n+#else\n // Perform a full fsync operation after flush\n fdatasync(fileno(logger->log->f));\n #endif\n+#endif\n logger->last_fflush_time = log_event->timestamp;\n }\n") - ]; - } - ); - } - { vals.pkg=pkgs.cyclonedds; flags.ldLibraryGroup=true; flags.packageConfGroup=true; } - ]; - - # ------------------------------------------------------------ - # 2. group / aggregate our packages - # ------------------------------------------------------------ - devPackages = aggregation.getAll { attrPath=[ "pkg" ]; }; - ldLibraryPackages = aggregation.getAll { hasAllFlags=[ "ldLibraryGroup" ]; attrPath=[ "pkg" ]; }; - giTypelibPackagesString = aggregation.getAll { - hasAllFlags=[ "giTypelibGroup" ]; - attrPath=[ "pkg" ]; - strAppend="/lib/girepository-1.0"; - strJoin=":"; - }; - packageConfPackagesString = (aggregation.getAll { - hasAllFlags=[ "packageConfGroup" ]; - attrPath=[ "pkg" ]; - strAppend="/lib/pkgconfig"; - strJoin=":"; - }); - manualPythonPackages = (aggregation.getAll { - hasAllFlags=[ "manualPythonPackages" ]; - attrPath=[ "pkg" ]; - strAppend="/lib/python3.${aggregation.mergedVals.pythonMinorVersion}/site-packages"; - strJoin=":"; - }); - - # ------------------------------------------------------------ - # 3. Host interactive shell → `nix develop` - # ------------------------------------------------------------ - shellHook = '' - shopt -s nullglob 2>/dev/null || setopt +o nomatch 2>/dev/null || true # allow globs to be empty without throwing an error - if [ "$OSTYPE" = "linux-gnu" ]; then - export CC="cc-no-usr-include" # basically patching for nix - # Create nvidia-only lib symlinks to avoid glibc conflicts - NVIDIA_LIBS_DIR="/tmp/nix-nvidia-libs" - mkdir -p "$NVIDIA_LIBS_DIR" - for lib in /usr/lib/libcuda.so* /usr/lib/libnvidia*.so* /usr/lib/x86_64-linux-gnu/libnvidia*.so*; do - [ -e "$lib" ] && ln -sf "$lib" "$NVIDIA_LIBS_DIR/" 2>/dev/null - done - fi - export LD_LIBRARY_PATH="$NVIDIA_LIBS_DIR:${pkgs.lib.makeLibraryPath ldLibraryPackages}:$LD_LIBRARY_PATH" - export LIBRARY_PATH="$LD_LIBRARY_PATH" # fixes python find_library for pyaudio - export DISPLAY=:0 - export GI_TYPELIB_PATH="${giTypelibPackagesString}:$GI_TYPELIB_PATH" - export PKG_CONFIG_PATH=${lib.escapeShellArg packageConfPackagesString} - export PYTHONPATH="$PYTHONPATH:"${lib.escapeShellArg manualPythonPackages} - export CYCLONEDDS_HOME="${pkgs.cyclonedds}" - export CMAKE_PREFIX_PATH="${pkgs.cyclonedds}:$CMAKE_PREFIX_PATH" - # CC, CFLAGS, and LDFLAGS are bascially all for `pip install pyaudio` - export CFLAGS="$(pkg-config --cflags portaudio-2.0) $CFLAGS" - export LDFLAGS="-L$(pkg-config --variable=libdir portaudio-2.0) $LDFLAGS" - - # without this alias, the pytest uses the non-venv python and fails - alias pytest="python -m pytest" - - PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD") - if [ -f "$PROJECT_ROOT/env/bin/activate" ]; then - . "$PROJECT_ROOT/env/bin/activate" - fi - - [ -f "$PROJECT_ROOT/motd" ] && cat "$PROJECT_ROOT/motd" - [ -f "$PROJECT_ROOT/.pre-commit-config.yaml" ] && [ ! -f "$PROJECT_ROOT/.git/hooks/pre-commit" ] && pre-commit install --install-hooks - ''; - devShells = { - # basic shell (blends with your current environment) - default = pkgs.mkShell { - buildInputs = devPackages; - shellHook = shellHook; - }; - # strict shell (creates a fake home, only select exteral commands (e.g. sudo) from your system are available) - isolated = (xome.simpleMakeHomeFor { - inherit pkgs; - pure = true; - commandPassthrough = [ "sudo" "nvim" "code" "sysctl" "sw_vers" "git" "vim" "emacs" "openssl" "ssh" "osascript" "otool" "hidutil" "logger" "codesign" ]; # e.g. use external nvim instead of nix's - # commonly needed for MacOS: [ "osascript" "otool" "hidutil" "logger" "codesign" ] - homeSubpathPassthrough = [ "cache/nix/" ]; # share nix cache between projects - homeModule = { - # for home-manager examples, see: - # https://deepwiki.com/nix-community/home-manager/5-configuration-examples - # all home-manager options: - # https://nix-community.github.io/home-manager/options.xhtml - home.homeDirectory = "/tmp/virtual_homes/dimos"; - home.stateVersion = "25.11"; - home.packages = devPackages; - - programs = { - home-manager = { - enable = true; - }; - zsh = { - enable = true; - enableCompletion = true; - autosuggestion.enable = true; - syntaxHighlighting.enable = true; - shellAliases.ll = "ls -la"; - history.size = 100000; - # this is kinda like .zshrc - initContent = '' - # most people expect comments in their shell to to work - setopt interactivecomments - # fix emoji prompt offset issues (this shouldn't lock people into English b/c LANG can be non-english) - export LC_CTYPE=en_US.UTF-8 - ${shellHook} - ''; - }; - starship = { - enable = true; - enableZshIntegration = true; - settings = { - character = { - success_symbol = "[▣](bold green)"; - error_symbol = "[▣](bold red)"; - }; - }; - }; - }; - }; - }).default; - }; - - # ------------------------------------------------------------ - # 4. Closure copied into the OCI image rootfs - # ------------------------------------------------------------ - imageRoot = pkgs.buildEnv { - name = "dimos-image-root"; - paths = devPackages; - pathsToLink = [ "/bin" ]; - }; - - in { - ## Local dev shell - devShells = devShells; - - ## Layered docker image with DockerTools - packages.devcontainer = pkgs.dockerTools.buildLayeredImage { - name = "dimensionalos/dimos-dev"; - tag = "latest"; - contents = [ imageRoot ]; - config = { - WorkingDir = "/workspace"; - Cmd = [ "bash" ]; - }; - }; - }); -} diff --git a/onnx/metric3d_vit_small.onnx b/onnx/metric3d_vit_small.onnx deleted file mode 100644 index bfddd41628..0000000000 --- a/onnx/metric3d_vit_small.onnx +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14805174265dd721ac3b396bd5ee7190c708cec41150ed298267f6c3126bc060 -size 151333865 diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 9dea7e1921..0000000000 --- a/pyproject.toml +++ /dev/null @@ -1,422 +0,0 @@ -[build-system] -requires = ["setuptools>=70", "wheel", "pybind11>=2.12"] -build-backend = "setuptools.build_meta" - -[tool.setuptools] -include-package-data = false - -[tool.setuptools.packages.find] -where = ["."] -include = ["dimos*"] -exclude = ["dimos.web.websocket_vis.node_modules*"] - -[tool.setuptools.package-data] -"dimos" = ["*.html", "*.css", "*.js", "*.json", "*.txt", "*.yaml", "*.yml"] -"dimos.utils.cli" = ["*.tcss"] -"dimos.robot.unitree.go2" = ["*.urdf"] -"dimos.robot.unitree_webrtc.params" = ["*.yaml", "*.yml"] -"dimos.web.templates" = ["*"] -"dimos.rxpy_backpressure" = ["*.txt"] - -[project] -name = "dimos" -authors = [ - {name = "Dimensional Team", email = "build@dimensionalOS.com"}, -] -version = "0.0.10.post1" -description = "Powering agentive generalist robotics" -requires-python = ">=3.10" -readme = "README.md" - -dependencies = [ - # Transport Protocols - "dimos-lcm", - "PyTurboJPEG==1.8.2", - # Core - "numpy>=1.26.4", - "scipy>=1.15.1", - "pin>=3.3.0", # Pinocchio IK library - "reactivex", - "asyncio==3.4.3", - "sortedcontainers==2.4.0", - "pydantic", - "python-dotenv", - "annotation-protocol>=1.4.0", - "lazy_loader", - - # Multiprocess - "dask[complete]==2025.5.1", - "plum-dispatch==2.5.7", - # Logging - "structlog>=25.5.0,<26", - "colorlog==6.9.0", - # Core Msgs - "opencv-python", - "open3d-unofficial-arm; platform_system == 'Linux' and platform_machine == 'aarch64'", - "open3d>=0.18.0; platform_system != 'Linux' or platform_machine != 'aarch64'", - # CLI - "pydantic-settings>=2.11.0,<3", - "textual==3.7.1", - "terminaltexteffects==0.12.2", - "typer>=0.19.2,<1", - "plotext==5.3.2", - # Used for calculating the occupancy map. - "numba>=0.60.0", # First version supporting Python 3.12 - "llvmlite>=0.42.0", # Required by numba 0.60+ - # TODO: rerun shouldn't be required but rn its in core (there is NO WAY to use dimos without rerun rn) - # remove this once rerun is optional in core - "rerun-sdk>=0.20.0", - "toolz>=1.1.0", -] - - -[project.scripts] -lcmspy = "dimos.utils.cli.lcmspy.run_lcmspy:main" -foxglove-bridge = "dimos.utils.cli.foxglove_bridge.run_foxglove_bridge:main" -agentspy = "dimos.utils.cli.agentspy.agentspy:main" -humancli = "dimos.utils.cli.human.humanclianim:main" -dimos = "dimos.robot.cli.dimos:main" -rerun-bridge = "dimos.visualization.rerun.bridge:app" -doclinks = "dimos.utils.docs.doclinks:main" - -[project.optional-dependencies] -misc = [ - # Core requirements - "cerebras-cloud-sdk", - "yapf==0.40.2", - "typeguard", - "empy==3.3.4", - "catkin_pkg", - "lark", - "tiktoken>=0.8.0", - "python-multipart==0.0.20", - "tensorzero==2025.7.5", - - # Developer Specific - "ipykernel", - - # Vector Embedding - "sentence_transformers", - - # Perception Dependencies - "scikit-learn", - "timm>=1.0.15", - "edgetam-dimos", - "opencv-contrib-python==4.10.0.84", - - # embedding models - "open_clip_torch==3.2.0", - "torchreid==0.2.5", - "gdown==5.2.0", - "tensorboard==2.20.0", - - # Mapping - "googlemaps>=4.10.0", - - # Inference - "onnx", - "einops==0.8.1", - - # Hardware SDKs - "xarm-python-sdk>=1.17.0", -] - -visualization = [ - "rerun-sdk>=0.20.0", -] - -agents = [ - "langchain==1.2.3", - "langchain-chroma>=1,<2", - "langchain-core==1.2.3", - "langchain-openai>=1,<2", - "langchain-text-splitters>=1,<2", - "langchain-huggingface>=1,<2", - "langchain-ollama>=1,<2", - "bitsandbytes>=0.48.2,<1.0; sys_platform == 'linux'", - "ollama>=0.6.0", - "anthropic>=0.19.0", - - # Audio - "openai", - "openai-whisper", - "sounddevice", - - # MCP Server - "mcp>=1.0.0", -] - -web = [ - "fastapi>=0.115.6", - "sse-starlette>=2.2.1", - "uvicorn>=0.34.0", - "ffmpeg-python", - "soundfile", -] - -perception = [ - "ultralytics>=8.3.70", - "filterpy>=1.4.5", - "Pillow", - "lap>=0.5.12", - "transformers[torch]==4.49.0", - "moondream", - "omegaconf>=2.3.0", - "hydra-core>=1.3.0", -] - -unitree = [ - "dimos[base]", - "unitree-webrtc-connect-leshy>=2.0.7" -] - -manipulation = [ - # Planning (Drake) - "drake==1.45.0; sys_platform == 'darwin' and platform_machine != 'aarch64'", - "drake>=1.40.0; sys_platform != 'darwin' and platform_machine != 'aarch64'", - - # Hardware SDKs - "piper-sdk", - "xarm-python-sdk>=1.17.0", - - # Visualization (Optional) - "kaleido>=0.2.1", - "plotly>=5.9.0", - "xacro", - - # Other - "matplotlib>=3.7.1", - "pyyaml>=6.0", -] - - -cpu = [ - # CPU inference backends - "onnxruntime", - "ctransformers==0.2.27", -] - -cuda = [ - "cupy-cuda12x==13.6.0; platform_machine == 'x86_64'", - "nvidia-nvimgcodec-cu12[all]; platform_machine == 'x86_64'", - "onnxruntime-gpu>=1.17.1; platform_machine == 'x86_64'", # Only versions supporting both cuda11 and cuda12 - "ctransformers[cuda]==0.2.27", - "xformers>=0.0.20; platform_machine == 'x86_64'", -] - -dev = [ - "ruff==0.14.3", - "mypy==1.19.0", - "pre_commit==4.2.0", - "pytest==8.3.5", - "pytest-asyncio==0.26.0", - "pytest-mock==3.15.0", - "pytest-env==1.1.5", - "pytest-timeout==2.4.0", - "coverage>=7.0", # Required for numba compatibility (coverage.types) - "requests-mock==1.12.1", - "terminaltexteffects==0.12.2", - "watchdog>=3.0.0", - - # docs - "md-babel-py==1.1.1", - - # LSP - "python-lsp-server[all]==1.14.0", - "python-lsp-ruff==2.3.0", - - # Types - "lxml-stubs>=0.5.1,<1", - "pandas-stubs>=2.3.2.250926,<3", - "types-PySocks>=1.7.1.20251001,<2", - "types-PyYAML>=6.0.12.20250915,<7", - "types-colorama>=0.4.15.20250801,<1", - "types-defusedxml>=0.7.0.20250822,<1", - "types-gevent>=25.4.0.20250915,<26", - "types-greenlet>=3.2.0.20250915,<4", - "types-jmespath>=1.0.2.20250809,<2", - "types-jsonschema>=4.25.1.20251009,<5", - "types-networkx>=3.5.0.20251001,<4", - "types-protobuf>=6.32.1.20250918,<7", - "types-psutil>=7.0.0.20251001,<8", - "types-pytz>=2025.2.0.20250809,<2026", - "types-simplejson>=3.20.0.20250822,<4", - "types-tabulate>=0.9.0.20241207,<1", - "types-tensorflow>=2.18.0.20251008,<3", - "types-tqdm>=4.67.0.20250809,<5", - "types-psycopg2>=2.9.21.20251012", - - # Tools - "py-spy", -] - -psql = [ - "psycopg2-binary>=2.9.11" -] - -sim = [ - # Simulation - "mujoco>=3.3.4", - "playground>=0.0.5", - "pygame>=2.6.1", -] - -# NOTE: jetson-jp6-cuda126 extra is disabled due to 404 errors from wheel URLs -# The pypi.jetson-ai-lab.io URLs are currently unavailable. Update with working URLs when available. -# jetson-jp6-cuda126 = [ -# # Jetson Jetpack 6.2 with CUDA 12.6 specific wheels (aarch64 Linux only) -# "torch @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../torch-2.8.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", -# "torchvision @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../torchvision-0.23.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", -# "onnxruntime-gpu @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../onnxruntime_gpu-1.23.0-cp310-cp310-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", -# "xformers @ https://pypi.jetson-ai-lab.io/jp6/cu126/+f/.../xformers-0.0.33-cp39-abi3-linux_aarch64.whl ; platform_machine == 'aarch64' and sys_platform == 'linux'", -# ] - -drone = [ - "pymavlink" -] - -dds = [ - "dimos[dev]", - "cyclonedds>=0.10.5", -] - -# Minimal dependencies for Docker modules that communicate with the DimOS host -docker = [ - "dimos-lcm", - "numpy>=1.26.4", - "scipy>=1.15.1", - "reactivex", - "dask[distributed]==2025.5.1", - "plum-dispatch==2.5.7", - "structlog>=25.5.0,<26", - "pydantic", - "pydantic-settings>=2.11.0,<3", - "typer>=0.19.2,<1", - "opencv-python-headless", - "lcm", - "sortedcontainers", - "PyTurboJPEG", - "rerun-sdk", - "open3d-unofficial-arm; platform_system == 'Linux' and platform_machine == 'aarch64'", - "open3d>=0.18.0; platform_system != 'Linux' or platform_machine != 'aarch64'", -] - -base = [ - "dimos[agents,web,perception,visualization,sim]", -] - -[tool.ruff] -line-length = 100 -exclude = [ - ".git", - ".pytest_cache", - ".ruff_cache", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "build", - "dist", - "node_modules", - "site-packages", - "venv", - "libs", - "external", - "src" -] - -[tool.ruff.lint] -extend-select = ["E", "W", "F", "B", "UP", "N", "I", "C90", "A", "RUF", "TCH"] -# TODO: All of these should be fixed, but it's easier commit autofixes first -ignore = ["A001", "A002", "B008", "B017", "B019", "B024", "B026", "B904", "C901", "E402", "E501", "E721", "E722", "E741", "F811", "F821", "F821", "F821", "N801", "N802", "N803", "N806", "N817", "N999", "RUF003", "RUF009", "RUF012", "RUF034", "RUF043", "RUF059", "UP007"] - -[tool.ruff.lint.per-file-ignores] -"dimos/models/Detic/*" = ["ALL"] - -[tool.ruff.lint.isort] -known-first-party = ["dimos"] -combine-as-imports = true -force-sort-within-sections = true - -[tool.mypy] -python_version = "3.12" -incremental = true -strict = true -warn_unused_ignores = false -exclude = "^dimos/models/Detic(/|$)|^dimos/rxpy_backpressure(/|$)|.*/test_.|.*/conftest.py*" - -[[tool.mypy.overrides]] -module = [ - "annotation_protocol", - "cyclonedds", - "cyclonedds.*", - "dimos_lcm.*", - "etils", - "geometry_msgs.*", - "lazy_loader", - "mujoco", - "mujoco_playground.*", - "nav_msgs.*", - "open_clip", - "pinocchio", - "piper_sdk.*", - "plotext", - "pydrake", - "pydrake.*", - "plum.*", - "pycuda", - "pycuda.*", - "pyzed", - "pyzed.*", - "rclpy.*", - "sam2.*", - "sensor_msgs.*", - "std_msgs.*", - "tf2_msgs.*", - "torchreid", - "ultralytics.*", - "unitree_webrtc_connect.*", - "xarm.*", -] -ignore_missing_imports = true - -[[tool.mypy.overrides]] -module = ["dimos.rxpy_backpressure", "dimos.rxpy_backpressure.*"] -follow_imports = "skip" - -[[tool.mypy.overrides]] -module = ["pydrake", "pydrake.*"] -follow_imports = "skip" - -[tool.pytest.ini_options] -testpaths = ["dimos"] -markers = [ - "heavy: resource heavy test", - "tool: dev tooling", - "ros: depend on ros", - "lcm: tests that run actual LCM bus (can't execute in CI)", - "module: tests that need to run directly as modules", - "gpu: tests that require GPU", - "cuda: tests which require CUDA (specifically CUDA not just GPU acceleration)", - "e2e: end to end tests", - "integration: slower integration tests", - "neverending: they don't finish", - "mujoco: tests which open mujoco", -] -env = [ - "GOOGLE_MAPS_API_KEY=AIzafake_google_key", - "PYTHONWARNINGS=ignore:cupyx.jit.rawkernel is experimental:FutureWarning", -] -addopts = "-v -s -p no:warnings -ra --color=yes -m 'not (vis or exclude or tool or lcm or ros or heavy or gpu or module or e2e or integration or neverending or mujoco)'" -asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" - -[tool.largefiles] -max_size_kb = 50 -ignore = [ - "uv.lock", - "*/package-lock.json", - "dimos/dashboard/dimos.rbl", - "dimos/web/dimos_interface/themes.json", - "dimos/manipulation/manipulation_module.py", -] diff --git a/setup.py b/setup.py deleted file mode 100644 index 50ac0a37ab..0000000000 --- a/setup.py +++ /dev/null @@ -1,84 +0,0 @@ -# Copyright 2025-2026 Dimensional Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -from pathlib import Path -import struct -import sys - -from pybind11.setup_helpers import Pybind11Extension, build_ext -from setuptools import find_packages, setup - - -def python_is_macos_universal_binary(executable: str | None = None) -> bool: - """ - Returns True if the given executable is a macOS universal (fat) binary. - """ - FAT_MAGIC = 0xCAFEBABE # big-endian fat - FAT_CIGAM = 0xBEBAFECA # little-endian fat - FAT_MAGIC_64 = 0xCAFEBABF # big-endian fat 64 - FAT_CIGAM_64 = 0xBFBAFECA # little-endian fat 64 - - if executable is None: - executable = sys.executable - - path = Path(executable) - if not path.exists(): - return False - - try: - with path.open("rb") as f: - header = f.read(4) - if len(header) < 4: - return False - - magic = struct.unpack(">I", header)[0] - return magic in { - FAT_MAGIC, - FAT_CIGAM, - FAT_MAGIC_64, - FAT_CIGAM_64, - } - except OSError: - return False - - -extra_compile_args = [ - "-O3", # Maximum optimization - "-ffast-math", # Fast floating point -] -# when the python exe is a universal binary, this option fails because the compiler -# call tries to build a matching (e.g. universal) binary, clang doesn't support this option for universal binaries -# if the user is using an arm64 specific binary (ex: nix build) then the optimization exists and is useful -if not python_is_macos_universal_binary(): - extra_compile_args.append("-march=native") - -# C++ extensions -ext_modules = [ - Pybind11Extension( - "dimos.navigation.replanning_a_star.min_cost_astar_ext", - [os.path.join("dimos", "navigation", "replanning_a_star", "min_cost_astar_cpp.cpp")], - extra_compile_args=extra_compile_args, - define_macros=[ - ("NDEBUG", "1"), - ], - ), -] - -setup( - packages=find_packages(), - package_dir={"": "."}, - ext_modules=ext_modules, - cmdclass={"build_ext": build_ext}, -) diff --git a/uv.lock b/uv.lock deleted file mode 100644 index d971dcfeaa..0000000000 --- a/uv.lock +++ /dev/null @@ -1,11069 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] - -[[package]] -name = "absl-py" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, -] - -[[package]] -name = "accelerate" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "safetensors" }, - { name = "torch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, -] - -[[package]] -name = "addict" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, -] - -[[package]] -name = "aiofiles" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, -] - -[[package]] -name = "aioice" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "dnspython" }, - { name = "ifaddr" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/04/df7286233f468e19e9bedff023b6b246182f0b2ccb04ceeb69b2994021c6/aioice-0.10.2.tar.gz", hash = "sha256:bf236c6829ee33c8e540535d31cd5a066b531cb56de2be94c46be76d68b1a806", size = 44307, upload-time = "2025-11-28T15:56:48.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e3/0d23b1f930c17d371ce1ec36ee529f22fd19ebc2a07fe3418e3d1d884ce2/aioice-0.10.2-py3-none-any.whl", hash = "sha256:14911c15ab12d096dd14d372ebb4aecbb7420b52c9b76fdfcf54375dec17fcbf", size = 24875, upload-time = "2025-11-28T15:56:47.847Z" }, -] - -[[package]] -name = "aiortc" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aioice" }, - { name = "av" }, - { name = "cryptography" }, - { name = "google-crc32c" }, - { name = "pyee" }, - { name = "pylibsrtp" }, - { name = "pyopenssl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/9c/4e027bfe0195de0442da301e2389329496745d40ae44d2d7c4571c4290ce/aiortc-1.14.0.tar.gz", hash = "sha256:adc8a67ace10a085721e588e06a00358ed8eaf5f6b62f0a95358ff45628dd762", size = 1180864, upload-time = "2025-10-13T21:40:37.905Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/ab/31646a49209568cde3b97eeade0d28bb78b400e6645c56422c101df68932/aiortc-1.14.0-py3-none-any.whl", hash = "sha256:4b244d7e482f4e1f67e685b3468269628eca1ec91fa5b329ab517738cfca086e", size = 93183, upload-time = "2025-10-13T21:40:36.59Z" }, -] - -[[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.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "annotation-protocol" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/fd/612c96531b1c1d1c06e5d79547faea3f805785d67481b350f3f6a9cf6dc5/annotation_protocol-1.4.0.tar.gz", hash = "sha256:15d846a4984339bab6cbf80a44623219b8cb06b4f4fee0f22c31a255d16900f8", size = 8470, upload-time = "2026-01-19T08:48:27.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8b/71a5e1392dd3aca7ffeef0c3b10ea9b0e62959b5f39889702a06e11eda96/annotation_protocol-1.4.0-py3-none-any.whl", hash = "sha256:6fc66f1506f015db16fdd50fad18520cbb126a7902b27257c9fa521eb5efec60", size = 7834, upload-time = "2026-01-19T08:48:25.848Z" }, -] - -[[package]] -name = "anthropic" -version = "0.79.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/15/b1/91aea3f8fd180d01d133d931a167a78a3737b3fd39ccef2ae8d6619c24fd/anthropic-0.79.0.tar.gz", hash = "sha256:8707aafb3b1176ed6c13e2b1c9fb3efddce90d17aee5d8b83a86c70dcdcca871", size = 509825, upload-time = "2026-02-07T18:06:18.388Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b2/cc0b8e874a18d7da50b0fda8c99e4ac123f23bf47b471827c5f6f3e4a767/anthropic-0.79.0-py3-none-any.whl", hash = "sha256:04cbd473b6bbda4ca2e41dd670fe2f829a911530f01697d0a1e37321eb75f3cf", size = 405918, upload-time = "2026-02-07T18:06:20.246Z" }, -] - -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, -] - -[[package]] -name = "appnope" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, -] - -[[package]] -name = "astroid" -version = "4.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/63/0adf26577da5eff6eb7a177876c1cfa213856be9926a000f65c4add9692b/astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0", size = 406358, upload-time = "2026-02-07T23:35:07.509Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/cf/1c5f42b110e57bc5502eb80dbc3b03d256926062519224835ef08134f1f9/astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753", size = 276445, upload-time = "2026-02-07T23:35:05.344Z" }, -] - -[[package]] -name = "asttokens" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, -] - -[[package]] -name = "asyncio" -version = "3.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411, upload-time = "2015-03-10T14:11:26.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767, upload-time = "2015-03-10T14:05:10.959Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "autopep8" -version = "2.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycodestyle" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e0/8a/9be661f5400867a09706e29f5ab99a59987fd3a4c337757365e7491fa90b/autopep8-2.0.4.tar.gz", hash = "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c", size = 116472, upload-time = "2023-08-26T13:49:59.375Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/f2/e63c9f9c485cd90df8e4e7ae90fa3be2469c9641888558c7b45fa98a76f8/autopep8-2.0.4-py2.py3-none-any.whl", hash = "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", size = 45340, upload-time = "2023-08-26T13:49:56.111Z" }, -] - -[[package]] -name = "av" -version = "16.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/51/2217a9249409d2e88e16e3f16f7c0def9fd3e7ffc4238b2ec211f9935bdb/av-16.1.0-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:2395748b0c34fe3a150a1721e4f3d4487b939520991b13e7b36f8926b3b12295", size = 26942590, upload-time = "2026-01-09T20:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/bf/cd/a7070f4febc76a327c38808e01e2ff6b94531fe0b321af54ea3915165338/av-16.1.0-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:72d7ac832710a158eeb7a93242370aa024a7646516291c562ee7f14a7ea881fd", size = 21507910, upload-time = "2026-01-09T20:18:02.309Z" }, - { url = "https://files.pythonhosted.org/packages/ae/30/ec812418cd9b297f0238fe20eb0747d8a8b68d82c5f73c56fe519a274143/av-16.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6cbac833092e66b6b0ac4d81ab077970b8ca874951e9c3974d41d922aaa653ed", size = 38738309, upload-time = "2026-01-09T20:18:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b8/6c5795bf1f05f45c5261f8bce6154e0e5e86b158a6676650ddd77c28805e/av-16.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:eb990672d97c18f99c02f31c8d5750236f770ffe354b5a52c5f4d16c5e65f619", size = 40293006, upload-time = "2026-01-09T20:18:07.238Z" }, - { url = "https://files.pythonhosted.org/packages/a7/44/5e183bcb9333fc3372ee6e683be8b0c9b515a506894b2d32ff465430c074/av-16.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05ad70933ac3b8ef896a820ea64b33b6cca91a5fac5259cb9ba7fa010435be15", size = 40123516, upload-time = "2026-01-09T20:18:09.955Z" }, - { url = "https://files.pythonhosted.org/packages/12/1d/b5346d582a3c3d958b4d26a2cc63ce607233582d956121eb20d2bbe55c2e/av-16.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d831a1062a3c47520bf99de6ec682bd1d64a40dfa958e5457bb613c5270e7ce3", size = 41463289, upload-time = "2026-01-09T20:18:12.459Z" }, - { url = "https://files.pythonhosted.org/packages/fa/31/acc946c0545f72b8d0d74584cb2a0ade9b7dfe2190af3ef9aa52a2e3c0b1/av-16.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:358ab910fef3c5a806c55176f2b27e5663b33c4d0a692dafeb049c6ed71f8aff", size = 31754959, upload-time = "2026-01-09T20:18:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/48/d0/b71b65d1b36520dcb8291a2307d98b7fc12329a45614a303ff92ada4d723/av-16.1.0-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:e88ad64ee9d2b9c4c5d891f16c22ae78e725188b8926eb88187538d9dd0b232f", size = 26927747, upload-time = "2026-01-09T20:18:16.976Z" }, - { url = "https://files.pythonhosted.org/packages/2f/79/720a5a6ccdee06eafa211b945b0a450e3a0b8fc3d12922f0f3c454d870d2/av-16.1.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:cb296073fa6935724de72593800ba86ae49ed48af03960a4aee34f8a611f442b", size = 21492232, upload-time = "2026-01-09T20:18:19.266Z" }, - { url = "https://files.pythonhosted.org/packages/8e/4f/a1ba8d922f2f6d1a3d52419463ef26dd6c4d43ee364164a71b424b5ae204/av-16.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:720edd4d25aa73723c1532bb0597806d7b9af5ee34fc02358782c358cfe2f879", size = 39291737, upload-time = "2026-01-09T20:18:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/1a/31/fc62b9fe8738d2693e18d99f040b219e26e8df894c10d065f27c6b4f07e3/av-16.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c7f2bc703d0df260a1fdf4de4253c7f5500ca9fc57772ea241b0cb241bcf972e", size = 40846822, upload-time = "2026-01-09T20:18:24.275Z" }, - { url = "https://files.pythonhosted.org/packages/53/10/ab446583dbce730000e8e6beec6ec3c2753e628c7f78f334a35cad0317f4/av-16.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d69c393809babada7d54964d56099e4b30a3e1f8b5736ca5e27bd7be0e0f3c83", size = 40675604, upload-time = "2026-01-09T20:18:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/31/d7/1003be685277005f6d63fd9e64904ee222fe1f7a0ea70af313468bb597db/av-16.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:441892be28582356d53f282873c5a951592daaf71642c7f20165e3ddcb0b4c63", size = 42015955, upload-time = "2026-01-09T20:18:29.461Z" }, - { url = "https://files.pythonhosted.org/packages/2f/4a/fa2a38ee9306bf4579f556f94ecbc757520652eb91294d2a99c7cf7623b9/av-16.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:273a3e32de64819e4a1cd96341824299fe06f70c46f2288b5dc4173944f0fd62", size = 31750339, upload-time = "2026-01-09T20:18:32.249Z" }, - { url = "https://files.pythonhosted.org/packages/9c/84/2535f55edcd426cebec02eb37b811b1b0c163f26b8d3f53b059e2ec32665/av-16.1.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:640f57b93f927fba8689f6966c956737ee95388a91bd0b8c8b5e0481f73513d6", size = 26945785, upload-time = "2026-01-09T20:18:34.486Z" }, - { url = "https://files.pythonhosted.org/packages/b6/17/ffb940c9e490bf42e86db4db1ff426ee1559cd355a69609ec1efe4d3a9eb/av-16.1.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:ae3fb658eec00852ebd7412fdc141f17f3ddce8afee2d2e1cf366263ad2a3b35", size = 21481147, upload-time = "2026-01-09T20:18:36.716Z" }, - { url = "https://files.pythonhosted.org/packages/15/c1/e0d58003d2d83c3921887d5c8c9b8f5f7de9b58dc2194356a2656a45cfdc/av-16.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ee558d9c02a142eebcbe55578a6d817fedfde42ff5676275504e16d07a7f86", size = 39517197, upload-time = "2026-01-11T09:57:31.937Z" }, - { url = "https://files.pythonhosted.org/packages/32/77/787797b43475d1b90626af76f80bfb0c12cfec5e11eafcfc4151b8c80218/av-16.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7ae547f6d5fa31763f73900d43901e8c5fa6367bb9a9840978d57b5a7ae14ed2", size = 41174337, upload-time = "2026-01-11T09:57:35.792Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ac/d90df7f1e3b97fc5554cf45076df5045f1e0a6adf13899e10121229b826c/av-16.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8cf065f9d438e1921dc31fc7aa045790b58aee71736897866420d80b5450f62a", size = 40817720, upload-time = "2026-01-11T09:57:39.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/6f/13c3a35f9dbcebafd03fe0c4cbd075d71ac8968ec849a3cfce406c35a9d2/av-16.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a345877a9d3cc0f08e2bc4ec163ee83176864b92587afb9d08dff50f37a9a829", size = 42267396, upload-time = "2026-01-11T09:57:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b9/275df9607f7fb44317ccb1d4be74827185c0d410f52b6e2cd770fe209118/av-16.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:f49243b1d27c91cd8c66fdba90a674e344eb8eb917264f36117bf2b6879118fd", size = 31752045, upload-time = "2026-01-11T09:57:45.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/2a/63797a4dde34283dd8054219fcb29294ba1c25d68ba8c8c8a6ae53c62c45/av-16.1.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:ce2a1b3d8bf619f6c47a9f28cfa7518ff75ddd516c234a4ee351037b05e6a587", size = 26916715, upload-time = "2026-01-11T09:57:47.682Z" }, - { url = "https://files.pythonhosted.org/packages/d2/c4/0b49cf730d0ae8cda925402f18ae814aef351f5772d14da72dd87ff66448/av-16.1.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:408dbe6a2573ca58a855eb8cd854112b33ea598651902c36709f5f84c991ed8e", size = 21452167, upload-time = "2026-01-11T09:57:50.606Z" }, - { url = "https://files.pythonhosted.org/packages/51/23/408806503e8d5d840975aad5699b153aaa21eb6de41ade75248a79b7a37f/av-16.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:57f657f86652a160a8a01887aaab82282f9e629abf94c780bbdbb01595d6f0f7", size = 39215659, upload-time = "2026-01-11T09:57:53.757Z" }, - { url = "https://files.pythonhosted.org/packages/c4/19/a8528d5bba592b3903f44c28dab9cc653c95fcf7393f382d2751a1d1523e/av-16.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:adbad2b355c2ee4552cac59762809d791bda90586d134a33c6f13727fb86cb3a", size = 40874970, upload-time = "2026-01-11T09:57:56.802Z" }, - { url = "https://files.pythonhosted.org/packages/e8/24/2dbcdf0e929ad56b7df078e514e7bd4ca0d45cba798aff3c8caac097d2f7/av-16.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f42e1a68ec2aebd21f7eb6895be69efa6aa27eec1670536876399725bbda4b99", size = 40530345, upload-time = "2026-01-11T09:58:00.421Z" }, - { url = "https://files.pythonhosted.org/packages/54/27/ae91b41207f34e99602d1c72ab6ffd9c51d7c67e3fbcd4e3a6c0e54f882c/av-16.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58fe47aeaef0f100c40ec8a5de9abbd37f118d3ca03829a1009cf288e9aef67c", size = 41972163, upload-time = "2026-01-11T09:58:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7a/22158fb923b2a9a00dfab0e96ef2e8a1763a94dd89e666a5858412383d46/av-16.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:565093ebc93b2f4b76782589564869dadfa83af5b852edebedd8fee746457d06", size = 31729230, upload-time = "2026-01-11T09:58:07.254Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f1/878f8687d801d6c4565d57ebec08449c46f75126ebca8e0fed6986599627/av-16.1.0-cp313-cp313t-macosx_11_0_x86_64.whl", hash = "sha256:574081a24edb98343fd9f473e21ae155bf61443d4ec9d7708987fa597d6b04b2", size = 27008769, upload-time = "2026-01-11T09:58:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/30/f1/bd4ce8c8b5cbf1d43e27048e436cbc9de628d48ede088a1d0a993768eb86/av-16.1.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:9ab00ea29c25ebf2ea1d1e928d7babb3532d562481c5d96c0829212b70756ad0", size = 21590588, upload-time = "2026-01-11T09:58:12.629Z" }, - { url = "https://files.pythonhosted.org/packages/1d/dd/c81f6f9209201ff0b5d5bed6da6c6e641eef52d8fbc930d738c3f4f6f75d/av-16.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a84a91188c1071f238a9523fd42dbe567fb2e2607b22b779851b2ce0eac1b560", size = 40638029, upload-time = "2026-01-11T09:58:15.399Z" }, - { url = "https://files.pythonhosted.org/packages/15/4d/07edff82b78d0459a6e807e01cd280d3180ce832efc1543de80d77676722/av-16.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c2cd0de4dd022a7225ff224fde8e7971496d700be41c50adaaa26c07bb50bf97", size = 41970776, upload-time = "2026-01-11T09:58:19.075Z" }, - { url = "https://files.pythonhosted.org/packages/da/9d/1f48b354b82fa135d388477cd1b11b81bdd4384bd6a42a60808e2ec2d66b/av-16.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0816143530624a5a93bc5494f8c6eeaf77549b9366709c2ac8566c1e9bff6df5", size = 41764751, upload-time = "2026-01-11T09:58:22.788Z" }, - { url = "https://files.pythonhosted.org/packages/2f/c7/a509801e98db35ec552dd79da7bdbcff7104044bfeb4c7d196c1ce121593/av-16.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e3a28053af29644696d0c007e897d19b1197585834660a54773e12a40b16974c", size = 43034355, upload-time = "2026-01-11T09:58:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/36/8b/e5f530d9e8f640da5f5c5f681a424c65f9dd171c871cd255d8a861785a6e/av-16.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e3e67144a202b95ed299d165232533989390a9ea3119d37eccec697dc6dbb0c", size = 31947047, upload-time = "2026-01-11T09:58:31.867Z" }, - { url = "https://files.pythonhosted.org/packages/df/18/8812221108c27d19f7e5f486a82c827923061edf55f906824ee0fcaadf50/av-16.1.0-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:39a634d8e5a87e78ea80772774bfd20c0721f0d633837ff185f36c9d14ffede4", size = 26916179, upload-time = "2026-01-11T09:58:36.506Z" }, - { url = "https://files.pythonhosted.org/packages/38/ef/49d128a9ddce42a2766fe2b6595bd9c49e067ad8937a560f7838a541464e/av-16.1.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0ba32fb9e9300948a7fa9f8a3fc686e6f7f77599a665c71eb2118fdfd2c743f9", size = 21460168, upload-time = "2026-01-11T09:58:39.231Z" }, - { url = "https://files.pythonhosted.org/packages/e6/a9/b310d390844656fa74eeb8c2750e98030877c75b97551a23a77d3f982741/av-16.1.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ca04d17815182d34ce3edc53cbda78a4f36e956c0fd73e3bab249872a831c4d7", size = 39210194, upload-time = "2026-01-11T09:58:42.138Z" }, - { url = "https://files.pythonhosted.org/packages/0c/7b/e65aae179929d0f173af6e474ad1489b5b5ad4c968a62c42758d619e54cf/av-16.1.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ee0e8de2e124a9ef53c955fe2add6ee7c56cc8fd83318265549e44057db77142", size = 40811675, upload-time = "2026-01-11T09:58:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/54/3f/5d7edefd26b6a5187d6fac0f5065ee286109934f3dea607ef05e53f05b31/av-16.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:22bf77a2f658827043a1e184b479c3bf25c4c43ab32353677df2d119f080e28f", size = 40543942, upload-time = "2026-01-11T09:58:49.759Z" }, - { url = "https://files.pythonhosted.org/packages/1b/24/f8b17897b67be0900a211142f5646a99d896168f54d57c81f3e018853796/av-16.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2dd419d262e6a71cab206d80bbf28e0a10d0f227b671cdf5e854c028faa2d043", size = 41924336, upload-time = "2026-01-11T09:58:53.344Z" }, - { url = "https://files.pythonhosted.org/packages/1c/cf/d32bc6bbbcf60b65f6510c54690ed3ae1c4ca5d9fafbce835b6056858686/av-16.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:53585986fd431cd436f290fba662cfb44d9494fbc2949a183de00acc5b33fa88", size = 31735077, upload-time = "2026-01-11T09:58:56.684Z" }, - { url = "https://files.pythonhosted.org/packages/53/f4/9b63dc70af8636399bd933e9df4f3025a0294609510239782c1b746fc796/av-16.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:76f5ed8495cf41e1209a5775d3699dc63fdc1740b94a095e2485f13586593205", size = 27014423, upload-time = "2026-01-11T09:58:59.703Z" }, - { url = "https://files.pythonhosted.org/packages/d1/da/787a07a0d6ed35a0888d7e5cfb8c2ffa202f38b7ad2c657299fac08eb046/av-16.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8d55397190f12a1a3ae7538be58c356cceb2bf50df1b33523817587748ce89e5", size = 21595536, upload-time = "2026-01-11T09:59:02.508Z" }, - { url = "https://files.pythonhosted.org/packages/d8/f4/9a7d8651a611be6e7e3ab7b30bb43779899c8cac5f7293b9fb634c44a3f3/av-16.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:9d51d9037437218261b4bbf9df78a95e216f83d7774fbfe8d289230b5b2e28e2", size = 40642490, upload-time = "2026-01-11T09:59:05.842Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e4/eb79bc538a94b4ff93cd4237d00939cba797579f3272490dd0144c165a21/av-16.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:0ce07a89c15644407f49d942111ca046e323bbab0a9078ff43ee57c9b4a50dad", size = 41976905, upload-time = "2026-01-11T09:59:09.169Z" }, - { url = "https://files.pythonhosted.org/packages/5e/f5/f6db0dd86b70167a4d55ee0d9d9640983c570d25504f2bde42599f38241e/av-16.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cac0c074892ea97113b53556ff41c99562db7b9f09f098adac1f08318c2acad5", size = 41770481, upload-time = "2026-01-11T09:59:12.74Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/33651d658e45e16ab7671ea5fcf3d20980ea7983234f4d8d0c63c65581a5/av-16.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7dec3dcbc35a187ce450f65a2e0dda820d5a9e6553eea8344a1459af11c98649", size = 43036824, upload-time = "2026-01-11T09:59:16.507Z" }, - { url = "https://files.pythonhosted.org/packages/83/41/7f13361db54d7e02f11552575c0384dadaf0918138f4eaa82ea03a9f9580/av-16.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6f90dc082ff2068ddbe77618400b44d698d25d9c4edac57459e250c16b33d700", size = 31948164, upload-time = "2026-01-11T09:59:19.501Z" }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - -[[package]] -name = "bcrypt" -version = "5.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" }, - { url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" }, - { url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" }, - { url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" }, - { url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" }, - { url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" }, - { url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" }, - { url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" }, - { url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" }, - { url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" }, - { url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" }, - { url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" }, - { url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" }, - { url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" }, - { url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" }, - { url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" }, - { url = "https://files.pythonhosted.org/packages/aa/e7/d7dba133e02abcda3b52087a7eea8c0d4f64d3e593b4fffc10c31b7061f3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", size = 269753, upload-time = "2025-09-25T19:49:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/33/fc/5b145673c4b8d01018307b5c2c1fc87a6f5a436f0ad56607aee389de8ee3/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", size = 289587, upload-time = "2025-09-25T19:49:35.144Z" }, - { url = "https://files.pythonhosted.org/packages/27/d7/1ff22703ec6d4f90e62f1a5654b8867ef96bafb8e8102c2288333e1a6ca6/bcrypt-5.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", size = 272178, upload-time = "2025-09-25T19:49:36.793Z" }, - { url = "https://files.pythonhosted.org/packages/c8/88/815b6d558a1e4d40ece04a2f84865b0fef233513bd85fd0e40c294272d62/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", size = 269295, upload-time = "2025-09-25T19:49:38.164Z" }, - { url = "https://files.pythonhosted.org/packages/51/8c/e0db387c79ab4931fc89827d37608c31cc57b6edc08ccd2386139028dc0d/bcrypt-5.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", size = 271700, upload-time = "2025-09-25T19:49:39.917Z" }, - { url = "https://files.pythonhosted.org/packages/06/83/1570edddd150f572dbe9fc00f6203a89fc7d4226821f67328a85c330f239/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", size = 334034, upload-time = "2025-09-25T19:49:41.227Z" }, - { url = "https://files.pythonhosted.org/packages/c9/f2/ea64e51a65e56ae7a8a4ec236c2bfbdd4b23008abd50ac33fbb2d1d15424/bcrypt-5.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", size = 352766, upload-time = "2025-09-25T19:49:43.08Z" }, - { url = "https://files.pythonhosted.org/packages/d7/d4/1a388d21ee66876f27d1a1f41287897d0c0f1712ef97d395d708ba93004c/bcrypt-5.0.0-cp314-cp314t-win32.whl", hash = "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", size = 152449, upload-time = "2025-09-25T19:49:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/3f/61/3291c2243ae0229e5bca5d19f4032cecad5dfb05a2557169d3a69dc0ba91/bcrypt-5.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", size = 149310, upload-time = "2025-09-25T19:49:46.162Z" }, - { url = "https://files.pythonhosted.org/packages/3e/89/4b01c52ae0c1a681d4021e5dd3e45b111a8fb47254a274fa9a378d8d834b/bcrypt-5.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", size = 143761, upload-time = "2025-09-25T19:49:47.345Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/6237f151fbfe295fe3e074ecc6d44228faa1e842a81f6d34a02937ee1736/bcrypt-5.0.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b", size = 494553, upload-time = "2025-09-25T19:49:49.006Z" }, - { url = "https://files.pythonhosted.org/packages/45/b6/4c1205dde5e464ea3bd88e8742e19f899c16fa8916fb8510a851fae985b5/bcrypt-5.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", size = 275009, upload-time = "2025-09-25T19:49:50.581Z" }, - { url = "https://files.pythonhosted.org/packages/3b/71/427945e6ead72ccffe77894b2655b695ccf14ae1866cd977e185d606dd2f/bcrypt-5.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", size = 278029, upload-time = "2025-09-25T19:49:52.533Z" }, - { url = "https://files.pythonhosted.org/packages/17/72/c344825e3b83c5389a369c8a8e58ffe1480b8a699f46c127c34580c4666b/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", size = 275907, upload-time = "2025-09-25T19:49:54.709Z" }, - { url = "https://files.pythonhosted.org/packages/0b/7e/d4e47d2df1641a36d1212e5c0514f5291e1a956a7749f1e595c07a972038/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", size = 296500, upload-time = "2025-09-25T19:49:56.013Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c3/0ae57a68be2039287ec28bc463b82e4b8dc23f9d12c0be331f4782e19108/bcrypt-5.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", size = 278412, upload-time = "2025-09-25T19:49:57.356Z" }, - { url = "https://files.pythonhosted.org/packages/45/2b/77424511adb11e6a99e3a00dcc7745034bee89036ad7d7e255a7e47be7d8/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", size = 275486, upload-time = "2025-09-25T19:49:59.116Z" }, - { url = "https://files.pythonhosted.org/packages/43/0a/405c753f6158e0f3f14b00b462d8bca31296f7ecfc8fc8bc7919c0c7d73a/bcrypt-5.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", size = 277940, upload-time = "2025-09-25T19:50:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/62/83/b3efc285d4aadc1fa83db385ec64dcfa1707e890eb42f03b127d66ac1b7b/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", size = 310776, upload-time = "2025-09-25T19:50:02.393Z" }, - { url = "https://files.pythonhosted.org/packages/95/7d/47ee337dacecde6d234890fe929936cb03ebc4c3a7460854bbd9c97780b8/bcrypt-5.0.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", size = 312922, upload-time = "2025-09-25T19:50:04.232Z" }, - { url = "https://files.pythonhosted.org/packages/d6/3a/43d494dfb728f55f4e1cf8fd435d50c16a2d75493225b54c8d06122523c6/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", size = 341367, upload-time = "2025-09-25T19:50:05.559Z" }, - { url = "https://files.pythonhosted.org/packages/55/ab/a0727a4547e383e2e22a630e0f908113db37904f58719dc48d4622139b5c/bcrypt-5.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", size = 359187, upload-time = "2025-09-25T19:50:06.916Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bb/461f352fdca663524b4643d8b09e8435b4990f17fbf4fea6bc2a90aa0cc7/bcrypt-5.0.0-cp38-abi3-win32.whl", hash = "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", size = 153752, upload-time = "2025-09-25T19:50:08.515Z" }, - { url = "https://files.pythonhosted.org/packages/41/aa/4190e60921927b7056820291f56fc57d00d04757c8b316b2d3c0d1d6da2c/bcrypt-5.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", size = 150881, upload-time = "2025-09-25T19:50:09.742Z" }, - { url = "https://files.pythonhosted.org/packages/54/12/cd77221719d0b39ac0b55dbd39358db1cd1246e0282e104366ebbfb8266a/bcrypt-5.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", size = 144931, upload-time = "2025-09-25T19:50:11.016Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/2af136406e1c3839aea9ecadc2f6be2bcd1eff255bd451dd39bcf302c47a/bcrypt-5.0.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", size = 495313, upload-time = "2025-09-25T19:50:12.309Z" }, - { url = "https://files.pythonhosted.org/packages/ac/ee/2f4985dbad090ace5ad1f7dd8ff94477fe089b5fab2040bd784a3d5f187b/bcrypt-5.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", size = 275290, upload-time = "2025-09-25T19:50:13.673Z" }, - { url = "https://files.pythonhosted.org/packages/e4/6e/b77ade812672d15cf50842e167eead80ac3514f3beacac8902915417f8b7/bcrypt-5.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", size = 278253, upload-time = "2025-09-25T19:50:15.089Z" }, - { url = "https://files.pythonhosted.org/packages/36/c4/ed00ed32f1040f7990dac7115f82273e3c03da1e1a1587a778d8cea496d8/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", size = 276084, upload-time = "2025-09-25T19:50:16.699Z" }, - { url = "https://files.pythonhosted.org/packages/e7/c4/fa6e16145e145e87f1fa351bbd54b429354fd72145cd3d4e0c5157cf4c70/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", size = 297185, upload-time = "2025-09-25T19:50:18.525Z" }, - { url = "https://files.pythonhosted.org/packages/24/b4/11f8a31d8b67cca3371e046db49baa7c0594d71eb40ac8121e2fc0888db0/bcrypt-5.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", size = 278656, upload-time = "2025-09-25T19:50:19.809Z" }, - { url = "https://files.pythonhosted.org/packages/ac/31/79f11865f8078e192847d2cb526e3fa27c200933c982c5b2869720fa5fce/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", size = 275662, upload-time = "2025-09-25T19:50:21.567Z" }, - { url = "https://files.pythonhosted.org/packages/d4/8d/5e43d9584b3b3591a6f9b68f755a4da879a59712981ef5ad2a0ac1379f7a/bcrypt-5.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", size = 278240, upload-time = "2025-09-25T19:50:23.305Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/44590e3fc158620f680a978aafe8f87a4c4320da81ed11552f0323aa9a57/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", size = 311152, upload-time = "2025-09-25T19:50:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/5f/85/e4fbfc46f14f47b0d20493669a625da5827d07e8a88ee460af6cd9768b44/bcrypt-5.0.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", size = 313284, upload-time = "2025-09-25T19:50:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/25/ae/479f81d3f4594456a01ea2f05b132a519eff9ab5768a70430fa1132384b1/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", size = 341643, upload-time = "2025-09-25T19:50:28.02Z" }, - { url = "https://files.pythonhosted.org/packages/df/d2/36a086dee1473b14276cd6ea7f61aef3b2648710b5d7f1c9e032c29b859f/bcrypt-5.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", size = 359698, upload-time = "2025-09-25T19:50:31.347Z" }, - { url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" }, - { url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" }, - { url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" }, - { url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" }, -] - -[[package]] -name = "beartype" -version = "0.22.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, -] - -[[package]] -name = "bidict" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/6e/026678aa5a830e07cd9498a05d3e7e650a4f56a42f267a53d22bcda1bdc9/bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71", size = 29093, upload-time = "2024-02-18T19:09:05.748Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl", hash = "sha256:5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5", size = 32764, upload-time = "2024-02-18T19:09:04.156Z" }, -] - -[[package]] -name = "bitsandbytes" -version = "0.49.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'win32'" }, - { name = "packaging", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, - { name = "torch", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/dd/5820e09213a3f7c0ee5aff20fce8b362ce935f9dd9958827274de4eaeec6/bitsandbytes-0.49.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:acd4730a0db3762d286707f4a3bc1d013d21dd5f0e441900da57ec4198578d4e", size = 31065659, upload-time = "2026-01-08T14:31:28.676Z" }, - { url = "https://files.pythonhosted.org/packages/1d/4f/02d3cb62a1b0b5a1ca7ff03dce3606be1bf3ead4744f47eb762dbf471069/bitsandbytes-0.49.1-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:e7940bf32457dc2e553685285b2a86e82f5ec10b2ae39776c408714f9ae6983c", size = 59054193, upload-time = "2026-01-08T14:31:31.743Z" }, -] - -[[package]] -name = "black" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, - { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, - { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, - { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - -[[package]] -name = "bokeh" -version = "3.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jinja2" }, - { name = "narwhals" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "pyyaml" }, - { name = "tornado", marker = "sys_platform != 'emscripten'" }, - { name = "xyzservices" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e4/31/7ee0c4dfd0255631b0624ce01be178704f91f763f02a1879368eb109befd/bokeh-3.8.2.tar.gz", hash = "sha256:8e7dcacc21d53905581b54328ad2705954f72f2997f99fc332c1de8da53aa3cc", size = 6529251, upload-time = "2026-01-06T00:20:06.568Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/a8/877f306720bc114c612579c5af36bcb359026b83d051226945499b306b1a/bokeh-3.8.2-py3-none-any.whl", hash = "sha256:5e2c0d84f75acb25d60efb9e4d2f434a791c4639b47d685534194c4e07bd0111", size = 7207131, upload-time = "2026-01-06T00:20:04.917Z" }, -] - -[[package]] -name = "brax" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "etils" }, - { name = "flask" }, - { name = "flask-cors" }, - { name = "flax", version = "0.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "flax", version = "0.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxopt" }, - { name = "jinja2" }, - { name = "ml-collections" }, - { name = "mujoco" }, - { name = "mujoco-mjx" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "optax" }, - { name = "orbax-checkpoint" }, - { name = "pillow" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tensorboardx" }, - { name = "trimesh" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/8f/480ec7af5570dd8e8f03e226eea3f26e11c1053d3fdc319c4d5fbd6af248/brax-0.14.1.tar.gz", hash = "sha256:e2641b2a0ac151da4bb2bae69443a8e8080a0a85907431ec49b42ce72e3097df", size = 206577, upload-time = "2026-02-12T23:21:51.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/8f/ff354be75b3b0142e3a890cb8312b46fc5853b85e87432a146803f654935/brax-0.14.1-py3-none-any.whl", hash = "sha256:2cd82259a9857f3280d422c1c5103725429904295d22685b4f60c27996933ca9", size = 351008, upload-time = "2026-02-12T23:21:49.99Z" }, -] - -[[package]] -name = "build" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, - { name = "packaging" }, - { name = "pyproject-hooks" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, -] - -[[package]] -name = "catkin-pkg" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "packaging" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/7a/dcd7ba56dc82d88b3059a6770828388fc2e136ca4c5d79003f9febf33087/catkin_pkg-1.1.0.tar.gz", hash = "sha256:df1cb6879a3a772e770a100a6613ce8fc508b4855e5b2790106ddad4a8beb43c", size = 65547, upload-time = "2025-09-10T17:34:36.911Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/1b/50316bd6f95c50686b35799abebb6168d90ee18b7c03e3065f587f010f7c/catkin_pkg-1.1.0-py3-none-any.whl", hash = "sha256:7f5486b4f5681b5f043316ce10fc638c8d0ba8127146e797c85f4024e4356027", size = 76369, upload-time = "2025-09-10T17:34:35.639Z" }, -] - -[[package]] -name = "cattrs" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, -] - -[[package]] -name = "cerebras-cloud-sdk" -version = "1.67.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx", extra = ["http2"] }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/12/c201f07582068141e88f9a523ab02fdc97de58f2f7c0df775c6c52b9d8dd/cerebras_cloud_sdk-1.67.0.tar.gz", hash = "sha256:3aed6f86c6c7a83ee9d4cfb08a2acea089cebf2af5b8aed116ef79995a4f4813", size = 131536, upload-time = "2026-01-29T23:31:27.306Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/36a364f3d1bab4073454b75e7c91dc7ec6879b960063d1a9c929f1c7ea71/cerebras_cloud_sdk-1.67.0-py3-none-any.whl", hash = "sha256:658b79ca2e9c16f75cc6b4e5d523ee014c9e54a88bd39f88905c28ecb33daae1", size = 97807, upload-time = "2026-01-29T23:31:25.77Z" }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, - { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, - { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, - { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, - { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, - { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, - { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, - { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, - { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, - { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, - { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, - { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, - { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, - { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, - { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, - { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, - { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, - { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "chex" -version = "0.1.90" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "absl-py", marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "toolz", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/77/70/53c7d404ce9e2a94009aea7f77ef6e392f6740e071c62683a506647c520f/chex-0.1.90.tar.gz", hash = "sha256:d3c375aeb6154b08f1cccd2bee4ed83659ee2198a6acf1160d2fe2e4a6c87b5c", size = 92363, upload-time = "2025-07-23T19:50:47.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/3d/46bb04776c465cea2dd8aa2d4b61ab610b707f798f47838ef7e6105b025c/chex-0.1.90-py3-none-any.whl", hash = "sha256:fce3de82588f72d4796e545e574a433aa29229cbdcf792555e41bead24b704ae", size = 101047, upload-time = "2025-07-23T19:50:46.603Z" }, -] - -[[package]] -name = "chex" -version = "0.1.91" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "absl-py", marker = "python_full_version >= '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "toolz", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/7d/812f01e7b2ddf28a0caa8dde56bd951a2c8f691c9bbfce38d469458d1502/chex-0.1.91.tar.gz", hash = "sha256:65367a521415ada905b8c0222b0a41a68337fcadf79a1fb6fc992dbd95dd9f76", size = 90302, upload-time = "2025-09-01T21:49:32.834Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/0c/96102c01dd02ae740d4afc3644d5c7d7fc51d3feefd67300a2aa1ddbf7cb/chex-0.1.91-py3-none-any.whl", hash = "sha256:6fc4cbfc22301c08d4a7ef706045668410100962eba8ba6af03fa07f4e5dcf9b", size = 100965, upload-time = "2025-09-01T21:49:31.141Z" }, -] - -[[package]] -name = "choreographer" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "logistro" }, - { name = "simplejson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/47/64a035c6f764450ea9f902cbeba14c8c70316c2641125510066d8f912bfa/choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f", size = 43399, upload-time = "2025-11-09T23:04:44.749Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/9f/d73dfb85d7a5b1a56a99adc50f2074029468168c970ff5daeade4ad819e4/choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e", size = 49338, upload-time = "2025-11-09T23:04:43.154Z" }, -] - -[[package]] -name = "chromadb" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bcrypt" }, - { name = "build" }, - { name = "grpcio" }, - { name = "httpx" }, - { name = "importlib-resources" }, - { name = "jsonschema" }, - { name = "kubernetes" }, - { name = "mmh3" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "onnxruntime" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-sdk" }, - { name = "orjson" }, - { name = "overrides" }, - { name = "posthog" }, - { name = "pybase64" }, - { name = "pydantic" }, - { name = "pypika" }, - { name = "pyyaml" }, - { name = "rich" }, - { name = "tenacity" }, - { name = "tokenizers" }, - { name = "tqdm" }, - { name = "typer" }, - { name = "typing-extensions" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/10/a9/88d14ec43948ba164c45a2b8a80df26f68b69d963b4fbdf6e777c7ee6ab9/chromadb-1.5.0.tar.gz", hash = "sha256:357c5516ede08305db65f078d1dd4e001b8ecca80a13fd0db0b45bc473554ecb", size = 2343898, upload-time = "2026-02-09T08:46:05.077Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/22b8c965551ce41646d6d0c2b30ce6868b5471e04611d30180823226f273/chromadb-1.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4dc035ed075ddf80dfcdcd6bbedf6cd7c81052132333f03e6a71cdeac5ea0899", size = 20609722, upload-time = "2026-02-09T08:46:00.376Z" }, - { url = "https://files.pythonhosted.org/packages/13/75/b1354faa6e55ff1cfc916884da1b78629e689a3ddf57871000a62644e583/chromadb-1.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:3ae46c642c0bf3b86319b3883456ce8bb4a097a1d0552e7ce8cd4836a0cd1f22", size = 19850671, upload-time = "2026-02-09T08:45:57.065Z" }, - { url = "https://files.pythonhosted.org/packages/ba/6e/c9a9be7b3ca3fbcb59561464fe713637a475e39fc72e2dd7c60b2f360480/chromadb-1.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20dbfcd178cb93159891e3a0ff085659b8b3e4cbeef3dae311091c325791f4cc", size = 20498323, upload-time = "2026-02-09T08:45:49.941Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2d/d9faa17c38f49212ed66ed8f7923ee327a9d5a218dd9b7565f28f538bfa7/chromadb-1.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f5258d5b578c48b7c78effb6b582050ee13b1ac2e9eade4c83cd66de1a78c33", size = 21402789, upload-time = "2026-02-09T08:45:54.005Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7c/791d03e23ebcfaff35db5b1e6e7eb5c572046d2a562932305de63d0898fc/chromadb-1.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:8298cde5ffe448ca5a9794450c8b9700393e824ef8951be425ba2691330e78e6", size = 21724723, upload-time = "2026-02-09T08:46:07.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[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 = "cmeel" -version = "0.59.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/85/58/2448af92b3761a1b321014a653f79d322026681728f96ebe9f419ae0d6b8/cmeel-0.59.0.tar.gz", hash = "sha256:d9871f96ad0499c1cf8671e69622c805265a6be4383a1abfd18f20b4a33e3e3a", size = 14890, upload-time = "2026-01-19T11:48:25.431Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/c7/f7a2ea2e88cba4828c9b5bba5b8448ad6e6cbd652d782cc97bb14a54e6a6/cmeel-0.59.0-py3-none-any.whl", hash = "sha256:04a24b960e602484306721ce148610ddda4cbc83b8c5f27ef915366a86901e06", size = 20991, upload-time = "2026-01-19T11:48:24.259Z" }, -] - -[[package]] -name = "cmeel-assimp" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-zlib" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/24/960751adf9ae9725d1fc9642919b6f5a7ab54df2321f04b54d25f658e5f7/cmeel_assimp-6.0.2-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:f5bebbb5f9aba6825421f07bd41a02297c51589e26bfa171a8f1f442fd1614cd", size = 9970438, upload-time = "2025-10-15T20:19:18.168Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2a/0ada16dae4638b0b9f31688ed3756f903f2bd390e45c1bbc6ca815b43b38/cmeel_assimp-6.0.2-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ca055b64aff80ada91bfca21282484b60aff507609c06957af128322d74d7a8", size = 9164044, upload-time = "2025-10-15T20:19:20.835Z" }, - { url = "https://files.pythonhosted.org/packages/dd/f2/f343a56b4627fbd31fda09055f112f8ec78fd5bd7184be5c5a9b39fba1ec/cmeel_assimp-6.0.2-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d8de7cba2bb47b9d59dfe6e5c62e6ca327835690c574f4891dee91d8f522eb21", size = 13672921, upload-time = "2025-10-15T20:19:23.465Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d1/bd37525a1c1835a5e089e27e2b2160ff5d1a214b3ce6fdab97751cfe6772/cmeel_assimp-6.0.2-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:0cfe56553cfa3dfbcd71d420a154e1e3ac34bbd3341bb7c6c8730eca047866e9", size = 14807515, upload-time = "2025-10-15T20:19:25.996Z" }, - { url = "https://files.pythonhosted.org/packages/2a/14/c09ec9e0cd6343d9bb5f394350d4346a532ad29254d26764b9f3765c717a/cmeel_assimp-6.0.2-0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3683ad2ae72bc4b0f1fe4397bb3c65c20123a159820d5289a7100e6ba27ac55a", size = 14085316, upload-time = "2025-10-15T20:19:27.995Z" }, - { url = "https://files.pythonhosted.org/packages/98/24/dee37d3e1a8eb5256412b538bd3fb68827a878d8cc89172cbf5fcc463f37/cmeel_assimp-6.0.2-0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9c8a7071d0f3b5ab3f613caf9e1faa969b4a081d1c4565bbfa88f2208734317c", size = 15086416, upload-time = "2025-10-15T20:19:30.472Z" }, -] - -[[package]] -name = "cmeel-boost" -version = "1.89.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/7c/4d9bbc00d4f9286a48d38ffdcf030fe50c99fe00d3601303270740f22424/cmeel_boost-1.89.0.tar.gz", hash = "sha256:e28d4aa61f4b8dbcb6cb83e732e1076fe4f5a3a0d338d73d1c0821944b37a332", size = 4158, upload-time = "2025-08-22T22:29:36.37Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/6b/36e51770bfa546bb182eacc0a9c88cfb9817aa2914305cfd8d31ff7d5ae5/cmeel_boost-1.89.0-0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:509729c9a3549753df5b219773fef76b1be90e052089e40a6193d1fea4861f80", size = 30666822, upload-time = "2025-08-22T22:28:19.58Z" }, - { url = "https://files.pythonhosted.org/packages/40/35/89b58a680189f511d7543a89a7e647943d0058e81639badde448b8deaa23/cmeel_boost-1.89.0-0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad20fdf62d41eb74c3cba4c0f8d32d49f40bc2bf0b1fb508d97f63f911f7cece", size = 30565392, upload-time = "2025-08-22T22:28:23.378Z" }, - { url = "https://files.pythonhosted.org/packages/45/10/f59c72182391176fc18f76cab503df57bf10234d5b13856dd1f44237679c/cmeel_boost-1.89.0-0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:82aaaed1bb6703664f87bc1ec666703ef917ff67baa61491c8796e13853bbd91", size = 35676696, upload-time = "2025-08-22T22:28:27.258Z" }, - { url = "https://files.pythonhosted.org/packages/79/85/7f61c694bf55a239a3821c65767e5d104adaf0faa890c9c63d8ed4dc44b1/cmeel_boost-1.89.0-0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:78fd13be11d7c570e67400f5e734cf00787fe268cee7fd6bdde466ffd13e12be", size = 36013432, upload-time = "2025-08-22T22:28:32.225Z" }, - { url = "https://files.pythonhosted.org/packages/01/63/3069c1f50ebf7054226b9788cdf7cd950ac5823aa24dea3906e2f3e68262/cmeel_boost-1.89.0-0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a7b8a4ef8ccf9e9efb04b0b0b44edf3019d68ed986332fcc4e6918ef87b1b4", size = 30666832, upload-time = "2025-08-22T22:28:36.161Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ad/0354d4d2b2635b6cdd766b2eb1cceb595b1e1d3d103317267c5ab3821161/cmeel_boost-1.89.0-0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2c4c70dee56fd9caf40459267dcdcbdea891e5ad2e7c431ff07719ea55ba2872", size = 30565408, upload-time = "2025-08-22T22:28:39.979Z" }, - { url = "https://files.pythonhosted.org/packages/4e/37/f09082881d49d86b9e18fd42a5862a96746cb90be72b56107aa9ce02b38a/cmeel_boost-1.89.0-0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:ab744d059a78df3df17b8d412b19c0d13f94b5c8b044e32a7f8be293f7c168df", size = 35675964, upload-time = "2025-08-22T22:28:43.241Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/03861623b94c94dfbdcf41131bdf77eb7b863525a138cdc44a04323519f7/cmeel_boost-1.89.0-0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2a42ba5886762f273a118dab1399934e217b4429bff0a0c51e8bf3479edd37d2", size = 36013073, upload-time = "2025-08-22T22:28:47.944Z" }, - { url = "https://files.pythonhosted.org/packages/97/59/15b74f016279299ec3aef3d36fbe20a5838a122b528b0f7be07f66b1d423/cmeel_boost-1.89.0-0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7b65289d73476e8a28665faa3bfd9c3567053787945395e0ae14796e9ddc6e3f", size = 30669752, upload-time = "2025-08-22T22:28:51.965Z" }, - { url = "https://files.pythonhosted.org/packages/0d/65/d5dec74ed4a64a95fe694c5580b862a4da44abc2925e0660a3d7e6ea8ecf/cmeel_boost-1.89.0-0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f628b40a6c73be41fd76b8be9ae4ca3ad6010fb496e991a1dee2bb30cdcfbc", size = 30566986, upload-time = "2025-08-22T22:28:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/04/9f/b32b6ab06fb5840f19df4d01cd1a836cda54bb53d1d05b54dfb18ff68d1b/cmeel_boost-1.89.0-0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7ef14b1e2904059e70260ffeb6e295a9df94d4092c0c623d5b06092d45ccb427", size = 35677140, upload-time = "2025-08-22T22:28:58.848Z" }, - { url = "https://files.pythonhosted.org/packages/80/f7/08849f3ca2254daff09d9f0cbc9ad34e5d9552f8eb902f650294944ccc16/cmeel_boost-1.89.0-0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:624ff4401f054fd777f89ae81fc1bf2e16a44ccdff0f2a19ecd3ae74a42c8ba9", size = 36017953, upload-time = "2025-08-22T22:29:02.823Z" }, - { url = "https://files.pythonhosted.org/packages/6d/71/270925d5a51ae348db3b3cd6e45956b7a54bdacd90fa3aa4e4e9cc27ec85/cmeel_boost-1.89.0-0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:5fc5ec4e11086e5766c57c91a03021b3c1d7a2ea82d8fcf8873af0c027eab563", size = 30669798, upload-time = "2025-08-22T22:29:08.619Z" }, - { url = "https://files.pythonhosted.org/packages/07/26/2161982a3b3ab46658a8d3eda4525b9164218b3ff3a8f49a0cfe199666dc/cmeel_boost-1.89.0-0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:00603ac77c1875bae2a6d265dabec46436240d7c2e91d0736e615b58611fc8d3", size = 30567033, upload-time = "2025-08-22T22:29:12.349Z" }, - { url = "https://files.pythonhosted.org/packages/03/c6/42f60dbd47f20214ee554d08bf0ff73f4758f37aaad0bcaa7fcc5848aef7/cmeel_boost-1.89.0-0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b109d58fb3c352e496d3abad409c42547886c58ce362fd488bdddc94752f9f25", size = 35677367, upload-time = "2025-08-22T22:29:16.179Z" }, - { url = "https://files.pythonhosted.org/packages/1c/73/ad4e7ca9ef30b077160d78a8c00c7ebca03dc00cbb7c65296383376f1d20/cmeel_boost-1.89.0-0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:73f308ed3d4ba94beb4bd271d3b1f8c0afd9fe050d24e080a392eb6599109004", size = 36018012, upload-time = "2025-08-22T22:29:19.711Z" }, - { url = "https://files.pythonhosted.org/packages/82/39/4ff562a699082dfc93a521418655653fe6223a61480cc58aab4333093864/cmeel_boost-1.89.0-1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b7f0c174c7a9216cf0221b36bb0bac81e62f9cc1c25a6082474b1dedee9c62e", size = 30658750, upload-time = "2025-10-15T23:59:22.997Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e6/e468de28bcb1140d8a61d45f6eb88c8cd79b1fa3ccdd538f58204dbae681/cmeel_boost-1.89.0-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e76274a984c3f69bd57bbb8923e8acd68b1e65fe7d73df69393e2a5bca601094", size = 30551561, upload-time = "2025-10-15T23:59:26.451Z" }, - { url = "https://files.pythonhosted.org/packages/a8/d8/129cd530587cbcf032120f80176d67f06592e9dc62dddd7dde5f8ce80edc/cmeel_boost-1.89.0-1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:849308a26b07c268f4b2b68a2a8730ffa82e3dc8569bb016088c542c89117580", size = 35676698, upload-time = "2025-10-15T23:59:29.852Z" }, - { url = "https://files.pythonhosted.org/packages/74/96/3b6a784577ff6e69a32b65c970d27ad1a8705645c7cdef181fa475fafd0d/cmeel_boost-1.89.0-1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2d35dd5a8bc06f75d3c0c1d2db38e700693bf87c7f40a7cbd52af29a51df64c0", size = 36013428, upload-time = "2025-10-15T23:59:33.402Z" }, - { url = "https://files.pythonhosted.org/packages/24/fb/2b34be1e74e2b32fe7915cd7b61f85a68583738a8ef0601669aafc444592/cmeel_boost-1.89.0-1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f5c10faf11f1c679a8f756a1b7332dbd7d3d31251e0a36bcdd1b04102632ab57", size = 30658753, upload-time = "2025-10-15T23:59:36.577Z" }, - { url = "https://files.pythonhosted.org/packages/9a/0a/8af1c2b3f36b21b39052e70c4ef3dcaed981956cda08d0cb45e85890e111/cmeel_boost-1.89.0-1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f06143f546983c190957ffc9bdca970661c2d20576d4b4b290f7541bb60923c", size = 30551569, upload-time = "2025-10-15T23:59:39.674Z" }, - { url = "https://files.pythonhosted.org/packages/96/12/6ac1d6d292e5f135c4187fb0e5dd4f18c4fd20aa52ae7434f48f547958bb/cmeel_boost-1.89.0-1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:b6d8b6e3f3de8373b5b174d47b8cd95a013a17d3d226837752b725b27c26e567", size = 35675961, upload-time = "2025-10-15T23:59:43.064Z" }, - { url = "https://files.pythonhosted.org/packages/5a/db/f36899392766281352ac3c6935ebf8fb4465cef00cc44a221a18c1e3894c/cmeel_boost-1.89.0-1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:7f714d604c9aabe5e8e65324d2051a34c683be260a957ca31f17a16f36575ef9", size = 36013075, upload-time = "2025-10-15T23:59:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/cf/40/e033edcc740a559d417698348e0c39f99acc22e76863db3344399c735fb3/cmeel_boost-1.89.0-1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d1b1bcb47c946c70bf9d06ee6a1d48d32993a929d69fd615ea0b7bdef14e7a33", size = 30661931, upload-time = "2025-10-15T23:59:49.829Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2d/1b3f417d82aaedc84d51cd625b44f7a7e832fcf2a2b23e3712dacd3642b8/cmeel_boost-1.89.0-1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e2aaec12844c197fcbfb88cd2e2318592308b4c7393cff581582a7ff50a48d7", size = 30553328, upload-time = "2025-10-15T23:59:53.174Z" }, - { url = "https://files.pythonhosted.org/packages/8c/fe/b8cfa242d9a3f4a77c1646976c27ce3085f96918266d72dc1072452a73e0/cmeel_boost-1.89.0-1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:727392d2e98654f08775cd9aca11292fa9006ce6579b6a5180edadf40558ff78", size = 35677139, upload-time = "2025-10-15T23:59:56.774Z" }, - { url = "https://files.pythonhosted.org/packages/0f/60/a7d1d88a4af68db913707385d5ba3abf2419fa03dacff443cc8ddf128a22/cmeel_boost-1.89.0-1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5602a74998ed6d89466a78e6f684418846c0a90b114431f80efd5327a6aa6564", size = 36017957, upload-time = "2025-10-15T23:59:59.761Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d0/a496896682c670626df92a409e623fb37b4f4592f35f6d1e9b39840fd188/cmeel_boost-1.89.0-1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:cab231c4c7c9919a8b14548079aa92903d55a677f6c1c4377704a3928ea671a3", size = 30661948, upload-time = "2025-10-16T00:00:03.158Z" }, - { url = "https://files.pythonhosted.org/packages/77/75/105c192e54ad7c28f11eb8df316be5e1b4c5c8de69d9de2948c50170630f/cmeel_boost-1.89.0-1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:210bd45616ae335cea14f5f61b8b1285e72f45429b2e0f6049ccd8239d71b966", size = 30553320, upload-time = "2025-10-16T00:00:06.641Z" }, - { url = "https://files.pythonhosted.org/packages/ab/11/7d6b28639a9c3fb4c4da36b8f99b35c5c74089f6b7d97a6fdee097da2c07/cmeel_boost-1.89.0-1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2c7840865b31f087a6d011d6f14f5633bcd4792692129f1f001c5e1f196100dc", size = 35677361, upload-time = "2025-10-16T00:00:10.173Z" }, - { url = "https://files.pythonhosted.org/packages/95/ad/ee74e0505a4d9271a886f65fed6c3128e88f97d570f81ae3756eb93eb427/cmeel_boost-1.89.0-1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4fc5d368240d1055c63d22514b33f8746a453dcd65a25437ad1ac778140885ee", size = 36018012, upload-time = "2025-10-16T00:00:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/68/e6/98b28e0eb55948c44b81bba866327bad3f7506adaf1bbb5de549189be0be/cmeel_boost-1.89.0-1-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:8b8193d5a07fd5157ac121dbe6d07fedea5200ccac436df891143a74bee6fbc9", size = 30664665, upload-time = "2025-10-16T00:00:16.667Z" }, - { url = "https://files.pythonhosted.org/packages/b7/7d/a1afced6a647497c2d7db79ade56edd530f447d12131d5bd200b9c4a21eb/cmeel_boost-1.89.0-1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a57c710fcbc433475d84a57922d350f76545c077c31cf8ae5629f1ad2711bad", size = 30553850, upload-time = "2025-10-16T00:00:20.162Z" }, - { url = "https://files.pythonhosted.org/packages/41/3a/395e727e0256473fccf58d9e2badad005491ff90a4de79dabc2e70fd1274/cmeel_boost-1.89.0-1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:60b890495c38e3ce98d2bb68b03da005c3f0fc5e28965848f2ff6b1bac5d57e2", size = 35683786, upload-time = "2025-10-16T00:00:23.786Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f8/6341b1b5c2a5edf724a59ce97594b73388e282a13daf2e1dd068340dba90/cmeel_boost-1.89.0-1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ced81a026b2c4a6b38ae15ddd9059fc63a0e5d41d4abd0927da52f24d9aefc94", size = 36024423, upload-time = "2025-10-16T00:00:27.291Z" }, -] - -[[package]] -name = "cmeel-console-bridge" -version = "1.0.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/13/2e9e9d23db8548aef975564055bdb4fb6da8a397a1e7df8cb61f5afebefb/cmeel_console_bridge-1.0.2.3.tar.gz", hash = "sha256:3b2837da7ab408e9d1a775c83c0a7772356062b3a3672e4ce247f2da71a8ecd9", size = 262061, upload-time = "2025-03-19T18:22:06.845Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/a7/527fa060e5881acb3b0a07bf1d803ccb831cb87739abb62b6bcd14f5aed3/cmeel_console_bridge-1.0.2.3-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:7aa19b2d006073a1fad55d32968c7d0c7136749e06f98405f4f73a71038a5c41", size = 21341, upload-time = "2025-03-19T18:21:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/bb/db/f8643a8766e8909e0dbfcda6191ca92454cf9a3fadd89be417db261601a1/cmeel_console_bridge-1.0.2.3-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c47d8c97cb120feed1c01f30845d16c67e4e8205941e3977951018972b9b8721", size = 21286, upload-time = "2025-03-19T18:21:57.984Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/9c79177152a220ab2e4ffa0140722165035f6a5c2abbed2912352bd7e7b9/cmeel_console_bridge-1.0.2.3-0-py3-none-manylinux_2_17_i686.whl", hash = "sha256:cad9723ac44ab563cd23bf361b604733623d11847c4edf2a2b4ebd1d984ade09", size = 23740, upload-time = "2025-03-19T18:21:59.683Z" }, - { url = "https://files.pythonhosted.org/packages/92/65/5741de6f550fe701d0780546d97b283306676315a3e1f379a6038e8c0ab0/cmeel_console_bridge-1.0.2.3-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:372942e9c44f681bfff377fba25b348801283aa6f3826a00e4195089bda9737a", size = 25762, upload-time = "2025-03-19T18:22:01.055Z" }, - { url = "https://files.pythonhosted.org/packages/50/a5/70e23c5570506bb39b56aa4d0f3a4a414e38082ddb33e86a48b546620121/cmeel_console_bridge-1.0.2.3-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:5bb1115ed38441b2396e732e10ec63d1e68445674f9f5d321f7985eb10e9aeef", size = 24477, upload-time = "2025-03-19T18:22:02.091Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/bfd5a255348902e39243ccc6eba693bce714b891cd3be5603a9bd50c6de5/cmeel_console_bridge-1.0.2.3-0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2b8d084b797f592942208c2040b08e06b82f8832aa6c5e582ba6f1a4a653505b", size = 24970, upload-time = "2025-03-19T18:22:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/b3/02/3ae074e9acb9e150a4d5d97f341c2064573cd5fe9e5af20ab58bf8c0020a/cmeel_console_bridge-1.0.2.3-0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:fb6753a9864217d969c4965389d66a476ac978136c03eadf1063b1619c359220", size = 24689, upload-time = "2025-03-19T18:22:04.084Z" }, - { url = "https://files.pythonhosted.org/packages/69/d0/321f74b7d4167a6c59bb7714a6899ba402d9fad611f62573b9d646107320/cmeel_console_bridge-1.0.2.3-0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9d446c0fc541413d8d2ceea3c1cfb9cbfd57938d6659c113121eca6c245caafe", size = 24404, upload-time = "2025-03-19T18:22:05.232Z" }, -] - -[[package]] -name = "cmeel-octomap" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/ab/2fed2dbee13e4b39949591685419f1dbb691295e32a6bbbaf87edc005922/cmeel_octomap-1.10.0.tar.gz", hash = "sha256:bd79d1d17adede534de242e42e13ef0d9f04bdd27daf7d56c57f7c43670c9b05", size = 1694189, upload-time = "2025-01-06T17:57:05.477Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/22/ea67d35df31ec4bb2ed6e594b173c572c72dbd2a87e96906eac67b4af930/cmeel_octomap-1.10.0-4-py3-none-macosx_14_0_arm64.whl", hash = "sha256:c116eb151920d26ee2b2c1f656cd7526862006739817205f11f9366ab0ef6cb4", size = 639956, upload-time = "2025-01-06T17:56:56.826Z" }, - { url = "https://files.pythonhosted.org/packages/b2/da/07725a8c11224881f536ad252e97a3d9801b48e5e776017d5f00fb39b17f/cmeel_octomap-1.10.0-4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:76cc42553f54bae97584aaf0c7bc33753ff287e2738aa2ecac4820121101dd46", size = 1044402, upload-time = "2025-01-06T17:56:58.553Z" }, - { url = "https://files.pythonhosted.org/packages/a2/15/9617b7039afd6d17d3148f6f970d953f5e265d7736f8fdbca09c86e976a0/cmeel_octomap-1.10.0-4-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:5fdb04546fff3accac5f8626c3fc15c3b99e94ab887793565e0b92cedaf96468", size = 1105037, upload-time = "2025-01-06T17:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/51/69/88c1d1eca1abf2387ee8263ac7e12708c8b1b5b70b46a0bd9f43b485165b/cmeel_octomap-1.10.0-4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:84a7376cfced954bb7e3e347afbd02bdc1c83066b995afbdd0fb1e2d9f57ebec", size = 1108359, upload-time = "2025-01-06T17:57:04.085Z" }, - { url = "https://files.pythonhosted.org/packages/f5/59/57b3b38cf7a382855902b9d24266c283c29d977706438e6b7af62df74e2b/cmeel_octomap-1.10.0-5-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:042b4a21b5e5e19ee78a9a7db78e1b06fb8a287c832031788aec0d3fcabbfecd", size = 748832, upload-time = "2025-02-12T11:57:34.252Z" }, - { url = "https://files.pythonhosted.org/packages/55/8b/f5ec7676808a48c0185e216c0da700e34cb13ba233f13a4557a5ec56324a/cmeel_octomap-1.10.0-5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:79c15a0ece5ca3746170088ef2a377dfb3df8326fafde9bdba688852219758b9", size = 706924, upload-time = "2025-02-12T11:57:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/09/50/56de5a4d9f8ca58100146f16f42c4e2fbb49c0957bfe40d3fd2bc910afe4/cmeel_octomap-1.10.0-5-py3-none-manylinux_2_17_i686.whl", hash = "sha256:e2923bf593ebdafed86b6f3890a122c62fbd9cc9f325d60dbecb72b6b60d78fb", size = 1073973, upload-time = "2025-02-12T11:57:37.914Z" }, - { url = "https://files.pythonhosted.org/packages/d3/f5/8dddf5cdd31176288acd85cc8bf0262b7c3de81d5cb2cb33aa6646f44eb1/cmeel_octomap-1.10.0-5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d9e6f9c826905e8de632e9df8cc20e59ce2eb5d1e0b368d8d4abbbc5c0829c1a", size = 1044533, upload-time = "2025-02-12T11:57:39.672Z" }, - { url = "https://files.pythonhosted.org/packages/d6/14/b85bd33bb05c9bb7e87b9ac8401793c12a80a6d594b3ca4bcb5e971a24b7/cmeel_octomap-1.10.0-5-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b0b54fac180dce4f483afe7029c29cc55f6f2b21be8413e8e2275845b0c204d7", size = 1105199, upload-time = "2025-02-12T11:57:41.286Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/fe3360441159974ebdbb4c013a92ad0425d5f8bf414868d5161060e40660/cmeel_octomap-1.10.0-5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c8691e665bab7c12b6f51e6c5fbbb83ee6f91dce9d15d9d0387553950e7fb5ee", size = 1092962, upload-time = "2025-02-12T11:57:43.297Z" }, - { url = "https://files.pythonhosted.org/packages/82/a6/074166544cc0ce3a5d7844f97dfd13d1b3ec7bff6a6e2cfb18d66a671a7f/cmeel_octomap-1.10.0-5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:735c0ad84dacbbcc8c4237f127c57244c236b7d6c7500b5c45a4c225e19daac1", size = 1083321, upload-time = "2025-02-12T11:57:46.121Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a3/b19ea0d30837369091141b248936b0757ee17f58b809007399bad0b398e4/cmeel_octomap-1.10.0-5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f86a83f6bd60de290cd327f0374d525328369e76591e3ab2ad1bc0b183678c4", size = 1109207, upload-time = "2025-02-12T11:57:48.709Z" }, -] - -[[package]] -name = "cmeel-qhull" -version = "8.0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/dd/8d0bcfb18771b2ea02bf85dfbbc587c97b274496fb5419b72134eb69430b/cmeel_qhull-8.0.2.1.tar.gz", hash = "sha256:68e8d41d95f61830f2d460af1e4d760f0dbe4d46413d7c736f0ed701153ebe52", size = 1308055, upload-time = "2023-11-17T14:21:06.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/b4/d72ebd5e9ee711b68ad466e7bd4c0edcb45b0c2c8a358fdcdb64b092666a/cmeel_qhull-8.0.2.1-0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:39f5183a6e026754c3c043239bac005bf1825240d72e1d8fdf090a0f3ea27307", size = 2804225, upload-time = "2023-11-17T14:15:39.958Z" }, - { url = "https://files.pythonhosted.org/packages/29/dc/4bfb8d51a09401cf740e66d10bdb388eacd7c73bae12ef78149cbbc93e83/cmeel_qhull-8.0.2.1-0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:f135c5a4f4c8ed53f061bc86b794aaca2c0c34761c9269c06b71329c9da56f82", size = 2972481, upload-time = "2023-11-17T14:20:58.418Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7c/74b5c781cbfc8e4a9bb73b71659cc595bc0163223fd700b18133dbcf2831/cmeel_qhull-8.0.2.1-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:17f519106df79aed9fc5ec92833d4958d132d23021f02a78a9564cdf83a36c7c", size = 3078962, upload-time = "2023-11-17T14:21:00.183Z" }, - { url = "https://files.pythonhosted.org/packages/b4/16/ef7b6201835ba2492753c9c91b266d047b6664507be42ec858e2b24673b5/cmeel_qhull-8.0.2.1-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c513abafa40e2b8eb7cd3640e3f92d5391fbd6ec0f4182dbf9536934d8a8ea3e", size = 3194917, upload-time = "2023-11-17T14:21:01.879Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ae/200bdf257507e2c95d0656bf02278cd666d49f0a9e2e6d281ea76d7d085c/cmeel_qhull-8.0.2.1-0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:20a69cb34b6250aee1f018412989734c9ddcad6ff66717a7c5516fc19f55d5ff", size = 3290068, upload-time = "2023-11-17T14:21:03.828Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/de3fa6091ef58ab40f02653e777c8943acf7cec486184d6007885123571d/cmeel_qhull-8.0.2.1-1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:b5d47b113c1cb8f519bc813cf015d0d01f8ce5b08912733a24a6018f7caa6e96", size = 2902499, upload-time = "2025-02-12T11:51:16.999Z" }, - { url = "https://files.pythonhosted.org/packages/05/0c/5e5d9a033c683eb272508ccf560c03ac6bf5d397b038fe05f896a2283eaf/cmeel_qhull-8.0.2.1-1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:33a0169f4ee37d093c450195b0ef73d4fe0d9d62abb7899ebe79f778b36e1f36", size = 2773563, upload-time = "2025-02-12T11:51:19.893Z" }, - { url = "https://files.pythonhosted.org/packages/52/9b/00c73069348e60fbbdf6a5a10de046083f7d1ad36844958bbf12163ac688/cmeel_qhull-8.0.2.1-1-py3-none-manylinux_2_17_i686.whl", hash = "sha256:a577e76ac94d128f2966b137ead9f088749513df63749728e2b588f4564b7fdf", size = 3228684, upload-time = "2025-02-12T11:51:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/c0/4a/81b8c88b444935a64d8c83b41e662f696c36dd5937c3ca687113ac4778d0/cmeel_qhull-8.0.2.1-1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:fd0b2d4ce749b102c3cdead4588249befd34f1a660628f6bfc090ce942925aac", size = 3156051, upload-time = "2025-02-12T11:51:24.594Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c1/44874cd8bfc1e3f7cb15678c836c7a1d5537f34f5a727a0207e01f395598/cmeel_qhull-8.0.2.1-1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2371a7c80a14f3e874876359ae3e3094861f081fcdd7a03987c3e880d14e07b9", size = 3262508, upload-time = "2025-02-12T11:51:27.147Z" }, - { url = "https://files.pythonhosted.org/packages/54/0e/425d9ce1f2a831025d39fa5b6479b856bd4d73614c9caa690ac72bbfca04/cmeel_qhull-8.0.2.1-1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:197c14c2006dbeba8f5a5771700a7afea72c1a441aab7cdeaaf10b4ed8c1137d", size = 3172646, upload-time = "2025-02-12T11:51:28.967Z" }, - { url = "https://files.pythonhosted.org/packages/00/c1/e973e287a7d793911b8e6497b17586e601a678f2379ba2c615f72bd76480/cmeel_qhull-8.0.2.1-1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:886d1be24b31842286ae42755af5c312a43a4199632826e4110185ec36dc5c6a", size = 3530837, upload-time = "2025-02-12T11:51:31.651Z" }, - { url = "https://files.pythonhosted.org/packages/fd/65/c6cd54f04b5fcaa4ec52f5b57692c1dcef812ff9ee86545e5607369d365e/cmeel_qhull-8.0.2.1-1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1a49ce7f8492c9a8b49f930e34cce75b5e9b9843b015033dd0a25421441159fc", size = 3301908, upload-time = "2025-02-12T11:51:34.53Z" }, -] - -[[package]] -name = "cmeel-tinyxml2" -version = "10.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/9f/030eca702c485f7a641f975f167fa93164911b3329f005fb0730ff5e793f/cmeel_tinyxml2-10.0.0.tar.gz", hash = "sha256:00252aefc1c94a55b89f25ad08ee79fda2da8d1d94703e051598ddb52a9088fe", size = 645297, upload-time = "2025-02-06T10:29:00.106Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/5d/bc3a932eb7996a0a789979426a9bb8a3948bf57f3f17bab87dddbef62433/cmeel_tinyxml2-10.0.0-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:924499bb1b60b9a17bd001d12a9af88ddbee4ca888638ae684ba7f0f3ce49e87", size = 111913, upload-time = "2025-02-06T10:28:45.723Z" }, - { url = "https://files.pythonhosted.org/packages/92/bf/67d11e123313c034712896e94038291fe506bb099bdb75a136392002ffd0/cmeel_tinyxml2-10.0.0-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:26a1eb30c2a00bfc172e89ed015a18b8efb2b383546252ca8859574aed684686", size = 109487, upload-time = "2025-02-06T10:28:47.546Z" }, - { url = "https://files.pythonhosted.org/packages/ca/48/d8c81ce19b4b278ed0e8f81f93ae8670209bf3a9ac20141b9c386bb40cc7/cmeel_tinyxml2-10.0.0-0-py3-none-manylinux_2_17_i686.whl", hash = "sha256:53d86e02864c712f51f9a9adfcd8b6046b2ed51d44a0c34a8438d93b72b48325", size = 160118, upload-time = "2025-02-06T10:28:49.627Z" }, - { url = "https://files.pythonhosted.org/packages/87/4e/62193e27c9581f8ba7aeaeca7805632a64f2f4a824b1db37ad02ee953e8a/cmeel_tinyxml2-10.0.0-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:74112e2e9473afbf6ee2d25c9942553e9f6a40465e714533db72db48bc7658e1", size = 158477, upload-time = "2025-02-06T10:28:51.667Z" }, - { url = "https://files.pythonhosted.org/packages/14/f9/d0420c39e9ade99beeec61cd3abc68880fe6e14d85e9df292af8fabe65c8/cmeel_tinyxml2-10.0.0-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:ecd6e99caa2a06ac0d4b333b740c20fca526d0ca426f99eb5c0a0039117afdb6", size = 147025, upload-time = "2025-02-06T10:28:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/66/9e/df63147fc162ab487217fa5596778ab7a81a82d9b3ce4236fd3a1e48cecb/cmeel_tinyxml2-10.0.0-0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:30993fffb7032a45d5d3b1e5670cb879dad667a13144cd68c8f4e0371a8a3d2e", size = 150958, upload-time = "2025-02-06T10:28:55.301Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a8/b03567275fd83f5af33ddb61de942689dec72c5b21bec01e6a5b11101aa5/cmeel_tinyxml2-10.0.0-0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8c09ede51784af54211a6225884dc7ddbb02ea1681656d173060c7ad2a5b9a3c", size = 160300, upload-time = "2025-02-06T10:28:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ec/2781635b66c1059ca1243ae0f5a0410e171a5d8b8a71be3e34cb172f9f2d/cmeel_tinyxml2-10.0.0-0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3bd511d6d0758224efdebc23d3ead6e94f0755b04141ebf7d5493377829e8332", size = 149184, upload-time = "2025-02-06T10:28:58.734Z" }, -] - -[[package]] -name = "cmeel-urdfdom" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-console-bridge" }, - { name = "cmeel-tinyxml2" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/09/be81a5e7db56f34b6ccdbe7afe855c95a18c8439e173519e0146e9276a8c/cmeel_urdfdom-4.0.1.tar.gz", hash = "sha256:2e3f41e8483889e195b574acb326a4464cf11a3c0a8724031ac28bcda2223efc", size = 291511, upload-time = "2025-02-12T12:07:09.699Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/d0/20147dd6bb723afc44a58d89ea624df2bad1bed7b898a2df112aaca4a479/cmeel_urdfdom-4.0.1-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:2fe56939c6b47f6ec57021aac154123da47ecdcd79a217f3a5e3c4b705a07dee", size = 300860, upload-time = "2025-02-12T12:06:58.536Z" }, - { url = "https://files.pythonhosted.org/packages/8e/98/f832bca347e2d987c6b0ebb6930caf7b2c402535324aeed466b6aa2c4513/cmeel_urdfdom-4.0.1-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:00a0aba78b68c428b27abeed1db58d73e65319ed966911a0e97b37367442e756", size = 300616, upload-time = "2025-02-12T12:07:00.556Z" }, - { url = "https://files.pythonhosted.org/packages/cf/10/bf5765b6f388037cff166a754a0958ac2fee34ca3c0975ef64d0324e4647/cmeel_urdfdom-4.0.1-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a701a8f9671331f11b18ecf37a6537db546a21e6a0e5d0ff53341fea0693ed7f", size = 385951, upload-time = "2025-02-12T12:07:02.556Z" }, - { url = "https://files.pythonhosted.org/packages/c3/82/cb3f8f587d293a17bdbea15b50cdaa4a1e28e04583eb4cb4821685b89466/cmeel_urdfdom-4.0.1-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:12e39fc388c077d79fc9b3841d3d972a1da90b90de754d3363194c1540e18abf", size = 399619, upload-time = "2025-02-12T12:07:04.388Z" }, - { url = "https://files.pythonhosted.org/packages/24/77/322d7ac92c692d8dfaeda9de2d937087d15e2b564dc457d656e5fde3991d/cmeel_urdfdom-4.0.1-0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4a83925df1d5923c4485c3eb2b80b3a61b14f119ab724fb5bd04cec494690ee", size = 373969, upload-time = "2025-02-12T12:07:06.222Z" }, - { url = "https://files.pythonhosted.org/packages/9f/63/bdc6b55cc8bd99bb9dce6be801b30feffaa1c3841ecb7f4fe4d137424518/cmeel_urdfdom-4.0.1-0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4c4f44270971b3d05c45a4e21b1fb2df7e05a750363ae918f59532bff0bfe0e1", size = 388237, upload-time = "2025-02-12T12:07:08.326Z" }, - { url = "https://files.pythonhosted.org/packages/1d/2d/8463fc23230612daf4da1e31d3229f47708381f3ae4d1500f0f007ac0f92/cmeel_urdfdom-4.0.1-1-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:f7535158f45992eb2ba79e90d9db1bf9adc3846d9c7ed3e7a8c1c4d5343afa37", size = 301006, upload-time = "2025-02-13T11:42:08.8Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d5/c8cdf500e49300d85624cbc3ef804107ddcdc9c541b1d3f726bfb58a9fc1/cmeel_urdfdom-4.0.1-1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fef2a01a00d61d41b3d35dd4958bba973e9025c26eea1d3c9880932f4dba89a5", size = 300758, upload-time = "2025-02-13T11:42:10.449Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b3/2f7bac1544113a7f8e0f6d8b1fab5e75c6a3d27ffbb584b03267251b2165/cmeel_urdfdom-4.0.1-1-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:7a52eb36950ce982014d99a55717ca29985da056e3705f20746f15d3244c1f7a", size = 386043, upload-time = "2025-02-13T11:42:11.923Z" }, - { url = "https://files.pythonhosted.org/packages/86/03/8bdeb36ba6a3e8125d523ecfc010403049e463fe589f9896858d4bdcaf1e/cmeel_urdfdom-4.0.1-1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9f3b9c80b10d7246821ff61c2573f799e3da23d483e6f7367ddcad8a48baf58f", size = 399719, upload-time = "2025-02-13T11:42:14.325Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ed/43f99e7512460294cd8acc5753ba25f8a20bdf28d62e143eaf3ec7a28bb6/cmeel_urdfdom-4.0.1-1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2de69f47e8312cc09157624802d5bdaad6406443f863fb4b9ec62a19b4de3c72", size = 374073, upload-time = "2025-02-13T11:42:17.907Z" }, - { url = "https://files.pythonhosted.org/packages/17/c6/2e9bde6d7c02c1cf203ea896f8ce1afd441412f09b44830f1ee4a96d77de/cmeel_urdfdom-4.0.1-1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7708c1402de450fbeab21f7ca264a9a4676ed4c1cdf8d84d840bc5d057aac920", size = 388337, upload-time = "2025-02-13T11:42:19.657Z" }, -] - -[[package]] -name = "cmeel-zlib" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/74/b458f2fbfb652479c06400937cd67022e50d312033221602a9eca75022bc/cmeel_zlib-1.3.1.tar.gz", hash = "sha256:ebb34c54d1b7921dee5e7cd7003c9203b3297a5ba9d93983f1b7d3bb04976c3a", size = 3051, upload-time = "2025-02-11T12:20:39.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/dd/1bc2bc50c4ea217a993b2c9d3a7dd5959f839bc2b941556326b1ce71b961/cmeel_zlib-1.3.1-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:810779922c64d8074a3d12fcc471b1f62255e4402a1ca5f91f5749cc89214b93", size = 268796, upload-time = "2025-02-11T12:20:26.953Z" }, - { url = "https://files.pythonhosted.org/packages/a1/94/cf7e4554b7e2e4348da3f456be3c495774d1972a8dba384b6558b8f0e66b/cmeel_zlib-1.3.1-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2ccfac8fc80c6ee94ac61a9991f2ac18a5ea3a6cc2e753c221eb7c82729e839d", size = 191024, upload-time = "2025-02-11T12:20:28.737Z" }, - { url = "https://files.pythonhosted.org/packages/a2/cf/92d5a06071326ce3208f6cabc6d07d6c285b415df67e7ea9b87f0b46d44b/cmeel_zlib-1.3.1-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f59862cde12d0dcd51fc8f35c408a51e0f279f9d8d9103d5497fe82572e194e4", size = 286338, upload-time = "2025-02-11T12:20:30.784Z" }, - { url = "https://files.pythonhosted.org/packages/21/10/13b53ce0f693085cbad31be9fceb1b6a2b4e3bae5851c1f114c3e7b3c447/cmeel_zlib-1.3.1-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:7f95b4ed5090fb0fef195f52485f3719dd60213e67a4c07ac4718660bd24da25", size = 282556, upload-time = "2025-02-11T12:20:32.337Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2e/58b295975403b147e5df681e3e3470ba1802feed06a836843f02386d6506/cmeel_zlib-1.3.1-0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2864a55ab1dad1d86749c8410693f3bca6e866cbb5ac16286be686aedb781f6e", size = 287625, upload-time = "2025-02-11T12:20:34.471Z" }, - { url = "https://files.pythonhosted.org/packages/56/f3/4da9d5c5308ef2019ab65a8a9f519ac95004446902d01e859f9ac6b8cdd6/cmeel_zlib-1.3.1-0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e36ac8dccca22ff1f6e4df428ae5597f6288d9e6f85b08c9b767dc63e90fb55", size = 285662, upload-time = "2025-02-11T12:20:37.298Z" }, -] - -[[package]] -name = "coal" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-assimp" }, - { name = "cmeel-boost" }, - { name = "cmeel-octomap" }, - { name = "cmeel-qhull" }, - { name = "eigenpy" }, - { name = "libcoal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/4f/9b1f2cb921827aa877c09f6e727215fb633e4e3671682bd2a6559cd42d09/coal-3.0.2.tar.gz", hash = "sha256:7ca3f961fe72962b543894492efb33ee71bdc1091d93b87dc6988cdf0d4dedca", size = 1463955, upload-time = "2025-10-16T00:52:33.982Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/da/8b4758f8183d6808e542f97b5719b191ceda8f23e5958a1c3324535b9049/coal-3.0.2-1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eab5b68f1e25babd10a5d788bdce2ae61196c3e548c900ff8d060462e60e5194", size = 1612332, upload-time = "2025-10-16T00:51:56.269Z" }, - { url = "https://files.pythonhosted.org/packages/5e/14/21ba9435ce088452f903cc54312e04fd337d00f63f1a5cc90ceb37511dba/coal-3.0.2-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bb21e74d9071f87629026c1177b3c346145630869dc136cc4704899f3dfbf9db", size = 1505686, upload-time = "2025-10-16T00:51:57.948Z" }, - { url = "https://files.pythonhosted.org/packages/51/6c/68c42fe06b1ee8c5962edb4c9cecd9e8a042ebc5f850510d76dcb5beea0b/coal-3.0.2-1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6d97c0137b22a41e03090d044824596f76ccc065407f6fc538af7aedb2995306", size = 2218478, upload-time = "2025-10-16T00:51:59.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/6977e63ca97451b6888f69531d26513b64ce94235aa06ea49b24b0e2bb12/coal-3.0.2-1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:89af2fcc4f74474487e8e42ced4a2222db81ec50d27f0f9482fec9ca6309cad4", size = 2216338, upload-time = "2025-10-16T00:52:01.211Z" }, - { url = "https://files.pythonhosted.org/packages/14/ac/cee49d27d602e49c92b920414fa38d2c8ba0c245bfe840d5f0fc42893eeb/coal-3.0.2-1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1b76ab4101a779482dd25cf90ac66f81d6b90941ef5a559a6227053ff9d65f60", size = 1612334, upload-time = "2025-10-16T00:52:02.942Z" }, - { url = "https://files.pythonhosted.org/packages/a1/86/1f16a0227aa77b6539fe8056f4ac539238e5148aff6d29b86f5cdf1878e1/coal-3.0.2-1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:59ebb247b091dd7e97035d860e5a929ab04d4a3449d1cb30ed0a0c24aad3e705", size = 1505700, upload-time = "2025-10-16T00:52:04.697Z" }, - { url = "https://files.pythonhosted.org/packages/0b/84/e4185042b73f1e6f99fa1a32dd09dede94e8c4f7c2876649b650ffacf4d7/coal-3.0.2-1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e00e0ab0306c6db3ca5cd9ee70287fe8e457dba63439318d108806da67761213", size = 2218114, upload-time = "2025-10-16T00:52:06.739Z" }, - { url = "https://files.pythonhosted.org/packages/bd/bf/a2b18c35608f031d14ada9ff2217c421ba4459f1a87de914322a076798e1/coal-3.0.2-1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d43e3c61bc96068e561a924af0f2190490292e5b8b9af99ce5bb6e417a0b6c3", size = 2215822, upload-time = "2025-10-16T00:52:08.365Z" }, - { url = "https://files.pythonhosted.org/packages/06/09/522c4023c8871c70b32960709fde7f14d91ee4e0b1bbf5058ed7da106784/coal-3.0.2-1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ee24e2118bae43ec5abee45e1d228da3355dde10050db574be6f5b9eb9834bab", size = 1626929, upload-time = "2025-10-16T00:52:10.024Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ad/c5c2de5acf88c87596e1fdf0480e1ff369348b80dbcee63c3c0261b1356e/coal-3.0.2-1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1027e59bf17a0c4264e4fd2a87a1b7415e93fbbda94375e1ab7c001195dc1400", size = 1516707, upload-time = "2025-10-16T00:52:11.31Z" }, - { url = "https://files.pythonhosted.org/packages/9a/37/18811f130072d612ef32933b51fe8e090f93fcb2d55ef5a543ba2d155476/coal-3.0.2-1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:69b028b281fb0417a0dbeaa5a59c916ba5b04e037b717b0861da50f60ee81ad7", size = 2189981, upload-time = "2025-10-16T00:52:12.689Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f3/b895cb74d85b3e39c7f4d41976381f2006f370d15d6e83f5e5c8121b559f/coal-3.0.2-1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4a840b976f445455dde40f68e0e808daee9a3343dacf9a95ba98ea5c1f8c5995", size = 2201654, upload-time = "2025-10-16T00:52:14.329Z" }, - { url = "https://files.pythonhosted.org/packages/fa/be/e45f18c63e0ff84630a3fc00fbd572eb610b4b6cfc0dbdc952d87ba6c784/coal-3.0.2-1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:e9358b17ea61c1041bd9b4498eed0864192be3b15c572a48760107f027ea9ac5", size = 1626929, upload-time = "2025-10-16T00:52:16.147Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/3d49e31d934530458279d3689edd54306b517d8f87fdeb061ddc4abe1f3e/coal-3.0.2-1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d279c77926838cb5d60c4fb96dcd96d5773462c86ace43705a9d872d000650e3", size = 1516710, upload-time = "2025-10-16T00:52:17.718Z" }, - { url = "https://files.pythonhosted.org/packages/7e/f0/53833a83e74cf34592cdf2fd7aecdbc9684997fe5c0b8fd3ddfb22030e4b/coal-3.0.2-1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:df9bb9ea76f6df5dbaabb3b07dd82437e295c13e420f063238bb9fc058059dc3", size = 2189977, upload-time = "2025-10-16T00:52:18.989Z" }, - { url = "https://files.pythonhosted.org/packages/29/f1/96fb0b8e98b8ce873cba5b0e9237d3cb3c0c750974df990f3e9182e2902f/coal-3.0.2-1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d6ab1c6961df4a5064b51bc8c76db05a16b03fcd1977f7d2fffe1c8dc5f4d3c3", size = 2201659, upload-time = "2025-10-16T00:52:20.826Z" }, - { url = "https://files.pythonhosted.org/packages/90/a9/8436d58720bd08d4039f5cef557f524612fb15448419982a7a3145d4c498/coal-3.0.2-1-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:38a10d82120768bd618227c102b958bf3d3d647269e3e5736d947285027a1449", size = 1628018, upload-time = "2025-10-16T00:52:22.838Z" }, - { url = "https://files.pythonhosted.org/packages/8c/c8/c381f70f19c1d16e50e37cc5b8d8d48d5bd0815f148543f4b6de6eb822d9/coal-3.0.2-1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae569844c064863ff0e84c338c4a32f4993a5a1ee3d6d76304369a7e47d2b4a0", size = 1517130, upload-time = "2025-10-16T00:52:24.504Z" }, - { url = "https://files.pythonhosted.org/packages/a7/cd/a99d4c84b6e7ed422c411a9c5b966ea0e5f535dfd641ebaf51cb6ff8c7d4/coal-3.0.2-1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:936161b2bb5096af101b51aaebdf3deeb21876e7d4c42db3cd029a692812e333", size = 2197006, upload-time = "2025-10-16T00:52:25.965Z" }, - { url = "https://files.pythonhosted.org/packages/1e/7f/3f358742302090aa3064b2873084d833e8c67568d655c4c8e013a6d68cdf/coal-3.0.2-1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:41c1e84d3b6050892250287aa750d0f1d791abf0819b0a30a4eeb24f141b6741", size = 2204039, upload-time = "2025-10-16T00:52:27.287Z" }, -] - -[[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 = "colorlog" -version = "6.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, -] - -[[package]] -name = "comm" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" }, -] - -[[package]] -name = "configargparse" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, -] - -[[package]] -name = "contourpy" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, - { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, - { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, - { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, - { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, - { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, - { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, - { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, - { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, - { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, - { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, - { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, - { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, - { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, - { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, - { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, - { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, - { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, - { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, - { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, - { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, - { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, - { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, - { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - -[[package]] -name = "contourpy" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, - { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, - { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, - { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, - { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, - { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, - { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, - { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, - { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, - { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, - { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, - { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, - { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, - { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, - { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, - { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, - { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, - { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, - { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, - { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, - { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, - { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, - { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, - { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, - { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, - { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, - { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, - { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, - { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, - { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, - { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, - { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, - { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, - { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, - { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, - { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, - { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, - { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, - { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, - { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, - { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, - { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, - { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, - { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, - { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, -] - -[[package]] -name = "coverage" -version = "7.13.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/d4/7827d9ffa34d5d4d752eec907022aa417120936282fc488306f5da08c292/coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415", size = 219152, upload-time = "2026-02-09T12:56:11.974Z" }, - { url = "https://files.pythonhosted.org/packages/35/b0/d69df26607c64043292644dbb9dc54b0856fabaa2cbb1eeee3331cc9e280/coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b", size = 219667, upload-time = "2026-02-09T12:56:13.33Z" }, - { url = "https://files.pythonhosted.org/packages/82/a4/c1523f7c9e47b2271dbf8c2a097e7a1f89ef0d66f5840bb59b7e8814157b/coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a", size = 246425, upload-time = "2026-02-09T12:56:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/f8/02/aa7ec01d1a5023c4b680ab7257f9bfde9defe8fdddfe40be096ac19e8177/coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f", size = 248229, upload-time = "2026-02-09T12:56:16.31Z" }, - { url = "https://files.pythonhosted.org/packages/35/98/85aba0aed5126d896162087ef3f0e789a225697245256fc6181b95f47207/coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012", size = 250106, upload-time = "2026-02-09T12:56:18.024Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1db59bd67494bc162e3e4cd5fbc7edba2c7026b22f7c8ef1496d58c2b94c/coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def", size = 252021, upload-time = "2026-02-09T12:56:19.272Z" }, - { url = "https://files.pythonhosted.org/packages/9d/97/72899c59c7066961de6e3daa142d459d47d104956db43e057e034f015c8a/coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256", size = 247114, upload-time = "2026-02-09T12:56:21.051Z" }, - { url = "https://files.pythonhosted.org/packages/39/1f/f1885573b5970235e908da4389176936c8933e86cb316b9620aab1585fa2/coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda", size = 248143, upload-time = "2026-02-09T12:56:22.585Z" }, - { url = "https://files.pythonhosted.org/packages/a8/cf/e80390c5b7480b722fa3e994f8202807799b85bc562aa4f1dde209fbb7be/coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92", size = 246152, upload-time = "2026-02-09T12:56:23.748Z" }, - { url = "https://files.pythonhosted.org/packages/44/bf/f89a8350d85572f95412debb0fb9bb4795b1d5b5232bd652923c759e787b/coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c", size = 249959, upload-time = "2026-02-09T12:56:25.209Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6e/612a02aece8178c818df273e8d1642190c4875402ca2ba74514394b27aba/coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58", size = 246416, upload-time = "2026-02-09T12:56:26.475Z" }, - { url = "https://files.pythonhosted.org/packages/cb/98/b5afc39af67c2fa6786b03c3a7091fc300947387ce8914b096db8a73d67a/coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9", size = 247025, upload-time = "2026-02-09T12:56:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/51/30/2bba8ef0682d5bd210c38fe497e12a06c9f8d663f7025e9f5c2c31ce847d/coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf", size = 221758, upload-time = "2026-02-09T12:56:29.051Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/331f94934cf6c092b8ea59ff868eb587bc8fe0893f02c55bc6c0183a192e/coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95", size = 222693, upload-time = "2026-02-09T12:56:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" }, - { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" }, - { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" }, - { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" }, - { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" }, - { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" }, - { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" }, - { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" }, - { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" }, - { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" }, - { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" }, - { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" }, - { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" }, - { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" }, - { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" }, - { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" }, - { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" }, - { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" }, - { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" }, - { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" }, - { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" }, - { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" }, - { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" }, - { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" }, - { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" }, - { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" }, - { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" }, - { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" }, - { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" }, - { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" }, - { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" }, - { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" }, - { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" }, - { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" }, - { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" }, - { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" }, - { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" }, - { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" }, - { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" }, - { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" }, - { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" }, - { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" }, - { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" }, - { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" }, - { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" }, - { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" }, - { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" }, - { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" }, - { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" }, - { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" }, - { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" }, - { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" }, - { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" }, - { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" }, - { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" }, - { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" }, - { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" }, - { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" }, - { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" }, - { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" }, - { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, -] - -[[package]] -name = "ctransformers" -version = "0.2.27" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "py-cpuinfo" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/5e/6ed7eaf8f54b5b078e2a609e90369c6999e67f915b9c1927c0d686c494f9/ctransformers-0.2.27.tar.gz", hash = "sha256:25653d4be8a5ed4e2d3756544c1e9881bf95404be5371c3ed506a256c28663d5", size = 376065, upload-time = "2023-09-10T15:19:14.99Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/50/0b608e2abee4fc695b4e7ff5f569f5d32faf84a49e322034716fa157d1cf/ctransformers-0.2.27-py3-none-any.whl", hash = "sha256:6a3ba47556471850d95fdbc59299a82ab91c9dc8b40201c5e7e82d71360772d9", size = 9853506, upload-time = "2023-09-10T15:18:58.741Z" }, -] - -[package.optional-dependencies] -cuda = [ - { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-cublas-cu12", version = "12.9.1.4", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32'" }, - { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-cuda-runtime-cu12", version = "12.9.79", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or sys_platform == 'darwin' or sys_platform == 'win32'" }, -] - -[[package]] -name = "cuda-bindings" -version = "12.9.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cuda-pathfinder", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, - { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, - { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, - { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, -] - -[[package]] -name = "cuda-pathfinder" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/5e/db279a3bfbd18d59d0598922a3b3c1454908d0969e8372260afec9736376/cuda_pathfinder-1.3.4-py3-none-any.whl", hash = "sha256:fb983f6e0d43af27ef486e14d5989b5f904ef45cedf40538bfdcbffa6bb01fb2", size = 30878, upload-time = "2026-02-11T18:50:31.008Z" }, -] - -[[package]] -name = "cupy-cuda12x" -version = "13.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastrlock", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/2b/8064d94a6ab6b5c4e643d8535ab6af6cabe5455765540931f0ef60a0bc3b/cupy_cuda12x-13.6.0-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e78409ea72f5ac7d6b6f3d33d99426a94005254fa57e10617f430f9fd7c3a0a1", size = 112238589, upload-time = "2025-08-18T08:24:15.541Z" }, - { url = "https://files.pythonhosted.org/packages/de/7b/bac3ca73e164d2b51c6298620261637c7286e06d373f597b036fc45f5563/cupy_cuda12x-13.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f33c9c975782ef7a42c79b6b4fb3d5b043498f9b947126d792592372b432d393", size = 89874119, upload-time = "2025-08-18T08:24:20.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d9/5c5077243cd92368c3eccecdbf91d76db15db338169042ffd1647533c6b1/cupy_cuda12x-13.6.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:77ba6745a130d880c962e687e4e146ebbb9014f290b0a80dbc4e4634eb5c3b48", size = 113039337, upload-time = "2025-08-18T08:24:31.814Z" }, - { url = "https://files.pythonhosted.org/packages/88/f5/02bea5cdf108e2a66f98e7d107b4c9a6709e5dbfedf663340e5c11719d83/cupy_cuda12x-13.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:a20b7acdc583643a623c8d8e3efbe0db616fbcf5916e9c99eedf73859b6133af", size = 89885526, upload-time = "2025-08-18T08:24:37.258Z" }, - { url = "https://files.pythonhosted.org/packages/e0/95/d7e1295141e7d530674a3cc567e13ed0eb6b81524cb122d797ed996b5bea/cupy_cuda12x-13.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:79b0cacb5e8b190ef409f9e03f06ac8de1b021b0c0dda47674d446f5557e0eb1", size = 112886268, upload-time = "2025-08-18T08:24:49.294Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8c/14555b63fd78cfac7b88af0094cea0a3cb845d243661ec7da69f7b3ea0de/cupy_cuda12x-13.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca06fede7b8b83ca9ad80062544ef2e5bb8d4762d1c4fc3ac8349376de9c8a5e", size = 89785108, upload-time = "2025-08-18T08:24:54.527Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b8/30127bcdac53a25f94ee201bf4802fcd8d012145567d77c54174d6d01c01/cupy_cuda12x-13.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:52d9e7f83d920da7d81ec2e791c2c2c747fdaa1d7b811971b34865ce6371e98a", size = 112654824, upload-time = "2025-08-18T08:25:05.944Z" }, - { url = "https://files.pythonhosted.org/packages/72/36/c9e24acb19f039f814faea880b3704a3661edaa6739456b73b27540663e3/cupy_cuda12x-13.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:297b4268f839de67ef7865c2202d3f5a0fb8d20bd43360bc51b6e60cb4406447", size = 89750580, upload-time = "2025-08-18T08:25:10.972Z" }, -] - -[[package]] -name = "cycler" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, -] - -[[package]] -name = "cyclonedds" -version = "0.10.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "rich-click" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/cf/28eb9c823dfc245c540f5286d71b44aeee2a51021fc85b25bb9562be78cc/cyclonedds-0.10.5.tar.gz", hash = "sha256:63fc4d6fdb2fd35181c40f4e90757149f2def5f570ef19fb71edc4f568755f8a", size = 156919, upload-time = "2024-06-05T18:50:42.999Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/c3/69ba063a51c06ba24fa4fd463157d4cc2bc54ab1a2ab8ebdf88e8f3dde25/cyclonedds-0.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:03644e406d0c1cac45887b378d35054a0033c48f2e29d9aab3bfc1ee6c4b9aa6", size = 864591, upload-time = "2024-06-05T18:50:46.563Z" }, - { url = "https://files.pythonhosted.org/packages/cf/98/08508aff65c87bcef473e23a51506a100fb35bf70450c40eb227a576a018/cyclonedds-0.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a0d9fa8747827dc9bd678d73ed6f12b0ab9853b2cb7ebadbf3d8d89625f0e34", size = 799626, upload-time = "2024-06-05T18:50:48.17Z" }, - { url = "https://files.pythonhosted.org/packages/99/0d/02da52ffd27b92b85b64997cc449106479456648da17aa44a09124e8ebe5/cyclonedds-0.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861d2ffd9513126d6a62ad9f842e85122518a7db1fb0a11d6e4fa86e3cacf61c", size = 6631487, upload-time = "2024-06-05T18:50:50.747Z" }, - { url = "https://files.pythonhosted.org/packages/e4/2b/d8fff5008c2c62882c2ffc185bdb0d4d1c9caf7bc5aaaef77bd9739bdc12/cyclonedds-0.10.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8276b2bc347540e3ca892adf976421dbce4c6d2672934a32409db121a1431b86", size = 6653044, upload-time = "2024-06-05T18:50:52.786Z" }, - { url = "https://files.pythonhosted.org/packages/07/ab/acaa119f552019bdb2b06478553cf712967672f5970be80ecc9b4ca805f4/cyclonedds-0.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:103a681e9490229f12c151a125e00c4db8fdb344c8e12e35ee515cd9d5d1ecd7", size = 1200672, upload-time = "2024-06-05T18:50:54.303Z" }, -] - -[[package]] -name = "dash" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "importlib-metadata" }, - { name = "nest-asyncio" }, - { name = "plotly" }, - { name = "requests" }, - { name = "retrying" }, - { name = "setuptools" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/dd/3aed9bfd81dfd8f44b3a5db0583080ac9470d5e92ee134982bd5c69e286e/dash-4.0.0.tar.gz", hash = "sha256:c5f2bca497af288f552aea3ae208f6a0cca472559003dac84ac21187a1c3a142", size = 6943263, upload-time = "2026-02-03T19:42:27.92Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/8c/dd63d210b28a7589f4bc1e84880525368147425c717d12834ab562f52d14/dash-4.0.0-py3-none-any.whl", hash = "sha256:e36b4b4eae9e1fa4136bf4f1450ed14ef76063bc5da0b10f8ab07bd57a7cb1ab", size = 7247521, upload-time = "2026-02-03T19:42:25.01Z" }, -] - -[[package]] -name = "dask" -version = "2025.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "cloudpickle" }, - { name = "fsspec" }, - { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, - { name = "packaging" }, - { name = "partd" }, - { name = "pyyaml" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/29/05feb8e2531c46d763547c66b7f5deb39b53d99b3be1b4ddddbd1cec6567/dask-2025.5.1.tar.gz", hash = "sha256:979d9536549de0e463f4cab8a8c66c3a2ef55791cd740d07d9bf58fab1d1076a", size = 10969324, upload-time = "2025-05-20T19:54:30.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/30/53b0844a7a4c6b041b111b24ca15cc9b8661a86fe1f6aaeb2d0d7f0fb1f2/dask-2025.5.1-py3-none-any.whl", hash = "sha256:3b85fdaa5f6f989dde49da6008415b1ae996985ebdfb1e40de2c997d9010371d", size = 1474226, upload-time = "2025-05-20T19:54:20.309Z" }, -] - -[package.optional-dependencies] -complete = [ - { name = "bokeh" }, - { name = "distributed" }, - { name = "jinja2" }, - { name = "lz4" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pyarrow" }, -] -distributed = [ - { name = "distributed" }, -] - -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow", marker = "python_full_version >= '3.11'" }, - { name = "typing-inspect", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, -] - -[[package]] -name = "debugpy" -version = "1.8.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/b7/cd8080344452e4874aae67c40d8940e2b4d47b01601a8fd9f44786c757c7/debugpy-1.8.20.tar.gz", hash = "sha256:55bc8701714969f1ab89a6d5f2f3d40c36f91b2cbe2f65d98bf8196f6a6a2c33", size = 1645207, upload-time = "2026-01-29T23:03:28.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/be/8bd693a0b9d53d48c8978fa5d889e06f3b5b03e45fd1ea1e78267b4887cb/debugpy-1.8.20-cp310-cp310-macosx_15_0_x86_64.whl", hash = "sha256:157e96ffb7f80b3ad36d808646198c90acb46fdcfd8bb1999838f0b6f2b59c64", size = 2099192, upload-time = "2026-01-29T23:03:29.707Z" }, - { url = "https://files.pythonhosted.org/packages/77/1b/85326d07432086a06361d493d2743edd0c4fc2ef62162be7f8618441ac37/debugpy-1.8.20-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:c1178ae571aff42e61801a38b007af504ec8e05fde1c5c12e5a7efef21009642", size = 3088568, upload-time = "2026-01-29T23:03:31.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/60/3e08462ee3eccd10998853eb35947c416e446bfe2bc37dbb886b9044586c/debugpy-1.8.20-cp310-cp310-win32.whl", hash = "sha256:c29dd9d656c0fbd77906a6e6a82ae4881514aa3294b94c903ff99303e789b4a2", size = 5284399, upload-time = "2026-01-29T23:03:33.678Z" }, - { url = "https://files.pythonhosted.org/packages/72/43/09d49106e770fe558ced5e80df2e3c2ebee10e576eda155dcc5670473663/debugpy-1.8.20-cp310-cp310-win_amd64.whl", hash = "sha256:3ca85463f63b5dd0aa7aaa933d97cbc47c174896dcae8431695872969f981893", size = 5316388, upload-time = "2026-01-29T23:03:35.095Z" }, - { url = "https://files.pythonhosted.org/packages/51/56/c3baf5cbe4dd77427fd9aef99fcdade259ad128feeb8a786c246adb838e5/debugpy-1.8.20-cp311-cp311-macosx_15_0_universal2.whl", hash = "sha256:eada6042ad88fa1571b74bd5402ee8b86eded7a8f7b827849761700aff171f1b", size = 2208318, upload-time = "2026-01-29T23:03:36.481Z" }, - { url = "https://files.pythonhosted.org/packages/9a/7d/4fa79a57a8e69fe0d9763e98d1110320f9ecd7f1f362572e3aafd7417c9d/debugpy-1.8.20-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:7de0b7dfeedc504421032afba845ae2a7bcc32ddfb07dae2c3ca5442f821c344", size = 3171493, upload-time = "2026-01-29T23:03:37.775Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/1e8f8affe51e12a26f3a8a8a4277d6e60aa89d0a66512f63b1e799d424a4/debugpy-1.8.20-cp311-cp311-win32.whl", hash = "sha256:773e839380cf459caf73cc533ea45ec2737a5cc184cf1b3b796cd4fd98504fec", size = 5209240, upload-time = "2026-01-29T23:03:39.109Z" }, - { url = "https://files.pythonhosted.org/packages/d5/92/1cb532e88560cbee973396254b21bece8c5d7c2ece958a67afa08c9f10dc/debugpy-1.8.20-cp311-cp311-win_amd64.whl", hash = "sha256:1f7650546e0eded1902d0f6af28f787fa1f1dbdbc97ddabaf1cd963a405930cb", size = 5233481, upload-time = "2026-01-29T23:03:40.659Z" }, - { url = "https://files.pythonhosted.org/packages/14/57/7f34f4736bfb6e00f2e4c96351b07805d83c9a7b33d28580ae01374430f7/debugpy-1.8.20-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:4ae3135e2089905a916909ef31922b2d733d756f66d87345b3e5e52b7a55f13d", size = 2550686, upload-time = "2026-01-29T23:03:42.023Z" }, - { url = "https://files.pythonhosted.org/packages/ab/78/b193a3975ca34458f6f0e24aaf5c3e3da72f5401f6054c0dfd004b41726f/debugpy-1.8.20-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:88f47850a4284b88bd2bfee1f26132147d5d504e4e86c22485dfa44b97e19b4b", size = 4310588, upload-time = "2026-01-29T23:03:43.314Z" }, - { url = "https://files.pythonhosted.org/packages/c1/55/f14deb95eaf4f30f07ef4b90a8590fc05d9e04df85ee379712f6fb6736d7/debugpy-1.8.20-cp312-cp312-win32.whl", hash = "sha256:4057ac68f892064e5f98209ab582abfee3b543fb55d2e87610ddc133a954d390", size = 5331372, upload-time = "2026-01-29T23:03:45.526Z" }, - { url = "https://files.pythonhosted.org/packages/a1/39/2bef246368bd42f9bd7cba99844542b74b84dacbdbea0833e610f384fee8/debugpy-1.8.20-cp312-cp312-win_amd64.whl", hash = "sha256:a1a8f851e7cf171330679ef6997e9c579ef6dd33c9098458bd9986a0f4ca52e3", size = 5372835, upload-time = "2026-01-29T23:03:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/15/e2/fc500524cc6f104a9d049abc85a0a8b3f0d14c0a39b9c140511c61e5b40b/debugpy-1.8.20-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:5dff4bb27027821fdfcc9e8f87309a28988231165147c31730128b1c983e282a", size = 2539560, upload-time = "2026-01-29T23:03:48.738Z" }, - { url = "https://files.pythonhosted.org/packages/90/83/fb33dcea789ed6018f8da20c5a9bc9d82adc65c0c990faed43f7c955da46/debugpy-1.8.20-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:84562982dd7cf5ebebfdea667ca20a064e096099997b175fe204e86817f64eaf", size = 4293272, upload-time = "2026-01-29T23:03:50.169Z" }, - { url = "https://files.pythonhosted.org/packages/a6/25/b1e4a01bfb824d79a6af24b99ef291e24189080c93576dfd9b1a2815cd0f/debugpy-1.8.20-cp313-cp313-win32.whl", hash = "sha256:da11dea6447b2cadbf8ce2bec59ecea87cc18d2c574980f643f2d2dfe4862393", size = 5331208, upload-time = "2026-01-29T23:03:51.547Z" }, - { url = "https://files.pythonhosted.org/packages/13/f7/a0b368ce54ffff9e9028c098bd2d28cfc5b54f9f6c186929083d4c60ba58/debugpy-1.8.20-cp313-cp313-win_amd64.whl", hash = "sha256:eb506e45943cab2efb7c6eafdd65b842f3ae779f020c82221f55aca9de135ed7", size = 5372930, upload-time = "2026-01-29T23:03:53.585Z" }, - { url = "https://files.pythonhosted.org/packages/33/2e/f6cb9a8a13f5058f0a20fe09711a7b726232cd5a78c6a7c05b2ec726cff9/debugpy-1.8.20-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:9c74df62fc064cd5e5eaca1353a3ef5a5d50da5eb8058fcef63106f7bebe6173", size = 2538066, upload-time = "2026-01-29T23:03:54.999Z" }, - { url = "https://files.pythonhosted.org/packages/c5/56/6ddca50b53624e1ca3ce1d1e49ff22db46c47ea5fb4c0cc5c9b90a616364/debugpy-1.8.20-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:077a7447589ee9bc1ff0cdf443566d0ecf540ac8aa7333b775ebcb8ce9f4ecad", size = 4269425, upload-time = "2026-01-29T23:03:56.518Z" }, - { url = "https://files.pythonhosted.org/packages/c5/d9/d64199c14a0d4c476df46c82470a3ce45c8d183a6796cfb5e66533b3663c/debugpy-1.8.20-cp314-cp314-win32.whl", hash = "sha256:352036a99dd35053b37b7803f748efc456076f929c6a895556932eaf2d23b07f", size = 5331407, upload-time = "2026-01-29T23:03:58.481Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d9/1f07395b54413432624d61524dfd98c1a7c7827d2abfdb8829ac92638205/debugpy-1.8.20-cp314-cp314-win_amd64.whl", hash = "sha256:a98eec61135465b062846112e5ecf2eebb855305acc1dfbae43b72903b8ab5be", size = 5372521, upload-time = "2026-01-29T23:03:59.864Z" }, - { url = "https://files.pythonhosted.org/packages/e0/c3/7f67dea8ccf8fdcb9c99033bbe3e90b9e7395415843accb81428c441be2d/debugpy-1.8.20-py2.py3-none-any.whl", hash = "sha256:5be9bed9ae3be00665a06acaa48f8329d2b9632f15fd09f6a9a8c8d9907e54d7", size = 5337658, upload-time = "2026-01-29T23:04:17.404Z" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, -] - -[[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 = "dimos" -version = "0.0.10.post1" -source = { editable = "." } -dependencies = [ - { name = "annotation-protocol" }, - { name = "asyncio" }, - { name = "colorlog" }, - { name = "dask", extra = ["complete"] }, - { name = "dimos-lcm" }, - { name = "lazy-loader" }, - { name = "llvmlite" }, - { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "open3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "open3d-unofficial-arm", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "opencv-python" }, - { name = "pin" }, - { name = "plotext" }, - { name = "plum-dispatch" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "pyturbojpeg" }, - { name = "reactivex" }, - { name = "rerun-sdk" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sortedcontainers" }, - { name = "structlog" }, - { name = "terminaltexteffects" }, - { name = "textual" }, - { name = "toolz" }, - { name = "typer" }, -] - -[package.optional-dependencies] -agents = [ - { name = "anthropic" }, - { name = "bitsandbytes", marker = "sys_platform == 'linux'" }, - { name = "langchain" }, - { name = "langchain-chroma" }, - { name = "langchain-core" }, - { name = "langchain-huggingface" }, - { name = "langchain-ollama" }, - { name = "langchain-openai" }, - { name = "langchain-text-splitters" }, - { name = "mcp" }, - { name = "ollama" }, - { name = "openai" }, - { name = "openai-whisper" }, - { name = "sounddevice" }, -] -base = [ - { name = "anthropic" }, - { name = "bitsandbytes", marker = "sys_platform == 'linux'" }, - { name = "fastapi" }, - { name = "ffmpeg-python" }, - { name = "filterpy" }, - { name = "hydra-core" }, - { name = "langchain" }, - { name = "langchain-chroma" }, - { name = "langchain-core" }, - { name = "langchain-huggingface" }, - { name = "langchain-ollama" }, - { name = "langchain-openai" }, - { name = "langchain-text-splitters" }, - { name = "lap" }, - { name = "mcp" }, - { name = "moondream" }, - { name = "mujoco" }, - { name = "ollama" }, - { name = "omegaconf" }, - { name = "openai" }, - { name = "openai-whisper" }, - { name = "pillow" }, - { name = "playground" }, - { name = "pygame" }, - { name = "rerun-sdk" }, - { name = "sounddevice" }, - { name = "soundfile" }, - { name = "sse-starlette" }, - { name = "transformers", extra = ["torch"] }, - { name = "ultralytics" }, - { name = "uvicorn" }, -] -cpu = [ - { name = "ctransformers" }, - { name = "onnxruntime" }, -] -cuda = [ - { name = "ctransformers", extra = ["cuda"] }, - { name = "cupy-cuda12x", marker = "platform_machine == 'x86_64'" }, - { name = "nvidia-nvimgcodec-cu12", extra = ["all"], marker = "platform_machine == 'x86_64'" }, - { name = "onnxruntime-gpu", marker = "platform_machine == 'x86_64'" }, - { name = "xformers", marker = "platform_machine == 'x86_64'" }, -] -dds = [ - { name = "coverage" }, - { name = "cyclonedds" }, - { name = "lxml-stubs" }, - { name = "md-babel-py" }, - { name = "mypy" }, - { name = "pandas-stubs" }, - { name = "pre-commit" }, - { name = "py-spy" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-env" }, - { name = "pytest-mock" }, - { name = "pytest-timeout" }, - { name = "python-lsp-ruff" }, - { name = "python-lsp-server", extra = ["all"] }, - { name = "requests-mock" }, - { name = "ruff" }, - { name = "terminaltexteffects" }, - { name = "types-colorama" }, - { name = "types-defusedxml" }, - { name = "types-gevent" }, - { name = "types-greenlet" }, - { name = "types-jmespath" }, - { name = "types-jsonschema" }, - { name = "types-networkx" }, - { name = "types-protobuf" }, - { name = "types-psutil" }, - { name = "types-psycopg2" }, - { name = "types-pysocks" }, - { name = "types-pytz" }, - { name = "types-pyyaml" }, - { name = "types-simplejson" }, - { name = "types-tabulate" }, - { name = "types-tensorflow" }, - { name = "types-tqdm" }, - { name = "watchdog" }, -] -dev = [ - { name = "coverage" }, - { name = "lxml-stubs" }, - { name = "md-babel-py" }, - { name = "mypy" }, - { name = "pandas-stubs" }, - { name = "pre-commit" }, - { name = "py-spy" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-env" }, - { name = "pytest-mock" }, - { name = "pytest-timeout" }, - { name = "python-lsp-ruff" }, - { name = "python-lsp-server", extra = ["all"] }, - { name = "requests-mock" }, - { name = "ruff" }, - { name = "terminaltexteffects" }, - { name = "types-colorama" }, - { name = "types-defusedxml" }, - { name = "types-gevent" }, - { name = "types-greenlet" }, - { name = "types-jmespath" }, - { name = "types-jsonschema" }, - { name = "types-networkx" }, - { name = "types-protobuf" }, - { name = "types-psutil" }, - { name = "types-psycopg2" }, - { name = "types-pysocks" }, - { name = "types-pytz" }, - { name = "types-pyyaml" }, - { name = "types-simplejson" }, - { name = "types-tabulate" }, - { name = "types-tensorflow" }, - { name = "types-tqdm" }, - { name = "watchdog" }, -] -docker = [ - { name = "dask", extra = ["distributed"] }, - { name = "dimos-lcm" }, - { name = "lcm" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "open3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "open3d-unofficial-arm", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "opencv-python-headless" }, - { name = "plum-dispatch" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyturbojpeg" }, - { name = "reactivex" }, - { name = "rerun-sdk" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sortedcontainers" }, - { name = "structlog" }, - { name = "typer" }, -] -drone = [ - { name = "pymavlink" }, -] -manipulation = [ - { name = "drake", version = "1.45.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'aarch64' and sys_platform == 'darwin'" }, - { name = "drake", version = "1.49.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'aarch64' and sys_platform != 'darwin'" }, - { name = "kaleido" }, - { name = "matplotlib" }, - { name = "piper-sdk" }, - { name = "plotly" }, - { name = "pyyaml" }, - { name = "xacro" }, - { name = "xarm-python-sdk" }, -] -misc = [ - { name = "catkin-pkg" }, - { name = "cerebras-cloud-sdk" }, - { name = "edgetam-dimos" }, - { name = "einops" }, - { name = "empy" }, - { name = "gdown" }, - { name = "googlemaps" }, - { name = "ipykernel" }, - { name = "lark" }, - { name = "onnx" }, - { name = "open-clip-torch" }, - { name = "opencv-contrib-python" }, - { name = "python-multipart" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "sentence-transformers" }, - { name = "tensorboard" }, - { name = "tensorzero" }, - { name = "tiktoken" }, - { name = "timm" }, - { name = "torchreid" }, - { name = "typeguard" }, - { name = "xarm-python-sdk" }, - { name = "yapf" }, -] -perception = [ - { name = "filterpy" }, - { name = "hydra-core" }, - { name = "lap" }, - { name = "moondream" }, - { name = "omegaconf" }, - { name = "pillow" }, - { name = "transformers", extra = ["torch"] }, - { name = "ultralytics" }, -] -psql = [ - { name = "psycopg2-binary" }, -] -sim = [ - { name = "mujoco" }, - { name = "playground" }, - { name = "pygame" }, -] -unitree = [ - { name = "anthropic" }, - { name = "bitsandbytes", marker = "sys_platform == 'linux'" }, - { name = "fastapi" }, - { name = "ffmpeg-python" }, - { name = "filterpy" }, - { name = "hydra-core" }, - { name = "langchain" }, - { name = "langchain-chroma" }, - { name = "langchain-core" }, - { name = "langchain-huggingface" }, - { name = "langchain-ollama" }, - { name = "langchain-openai" }, - { name = "langchain-text-splitters" }, - { name = "lap" }, - { name = "mcp" }, - { name = "moondream" }, - { name = "mujoco" }, - { name = "ollama" }, - { name = "omegaconf" }, - { name = "openai" }, - { name = "openai-whisper" }, - { name = "pillow" }, - { name = "playground" }, - { name = "pygame" }, - { name = "rerun-sdk" }, - { name = "sounddevice" }, - { name = "soundfile" }, - { name = "sse-starlette" }, - { name = "transformers", extra = ["torch"] }, - { name = "ultralytics" }, - { name = "unitree-webrtc-connect-leshy" }, - { name = "uvicorn" }, -] -visualization = [ - { name = "rerun-sdk" }, -] -web = [ - { name = "fastapi" }, - { name = "ffmpeg-python" }, - { name = "soundfile" }, - { name = "sse-starlette" }, - { name = "uvicorn" }, -] - -[package.metadata] -requires-dist = [ - { name = "annotation-protocol", specifier = ">=1.4.0" }, - { name = "anthropic", marker = "extra == 'agents'", specifier = ">=0.19.0" }, - { name = "asyncio", specifier = "==3.4.3" }, - { name = "bitsandbytes", marker = "sys_platform == 'linux' and extra == 'agents'", specifier = ">=0.48.2,<1.0" }, - { name = "catkin-pkg", marker = "extra == 'misc'" }, - { name = "cerebras-cloud-sdk", marker = "extra == 'misc'" }, - { name = "colorlog", specifier = "==6.9.0" }, - { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.0" }, - { name = "ctransformers", marker = "extra == 'cpu'", specifier = "==0.2.27" }, - { name = "ctransformers", extras = ["cuda"], marker = "extra == 'cuda'", specifier = "==0.2.27" }, - { name = "cupy-cuda12x", marker = "platform_machine == 'x86_64' and extra == 'cuda'", specifier = "==13.6.0" }, - { name = "cyclonedds", marker = "extra == 'dds'", specifier = ">=0.10.5" }, - { name = "dask", extras = ["complete"], specifier = "==2025.5.1" }, - { name = "dask", extras = ["distributed"], marker = "extra == 'docker'", specifier = "==2025.5.1" }, - { name = "dimos", extras = ["agents", "web", "perception", "visualization", "sim"], marker = "extra == 'base'" }, - { name = "dimos", extras = ["base"], marker = "extra == 'unitree'" }, - { name = "dimos", extras = ["dev"], marker = "extra == 'dds'" }, - { name = "dimos-lcm" }, - { name = "dimos-lcm", marker = "extra == 'docker'" }, - { name = "drake", marker = "platform_machine != 'aarch64' and sys_platform == 'darwin' and extra == 'manipulation'", specifier = "==1.45.0" }, - { name = "drake", marker = "platform_machine != 'aarch64' and sys_platform != 'darwin' and extra == 'manipulation'", specifier = ">=1.40.0" }, - { name = "edgetam-dimos", marker = "extra == 'misc'" }, - { name = "einops", marker = "extra == 'misc'", specifier = "==0.8.1" }, - { name = "empy", marker = "extra == 'misc'", specifier = "==3.3.4" }, - { name = "fastapi", marker = "extra == 'web'", specifier = ">=0.115.6" }, - { name = "ffmpeg-python", marker = "extra == 'web'" }, - { name = "filterpy", marker = "extra == 'perception'", specifier = ">=1.4.5" }, - { name = "gdown", marker = "extra == 'misc'", specifier = "==5.2.0" }, - { name = "googlemaps", marker = "extra == 'misc'", specifier = ">=4.10.0" }, - { name = "hydra-core", marker = "extra == 'perception'", specifier = ">=1.3.0" }, - { name = "ipykernel", marker = "extra == 'misc'" }, - { name = "kaleido", marker = "extra == 'manipulation'", specifier = ">=0.2.1" }, - { name = "langchain", marker = "extra == 'agents'", specifier = "==1.2.3" }, - { name = "langchain-chroma", marker = "extra == 'agents'", specifier = ">=1,<2" }, - { name = "langchain-core", marker = "extra == 'agents'", specifier = "==1.2.3" }, - { name = "langchain-huggingface", marker = "extra == 'agents'", specifier = ">=1,<2" }, - { name = "langchain-ollama", marker = "extra == 'agents'", specifier = ">=1,<2" }, - { name = "langchain-openai", marker = "extra == 'agents'", specifier = ">=1,<2" }, - { name = "langchain-text-splitters", marker = "extra == 'agents'", specifier = ">=1,<2" }, - { name = "lap", marker = "extra == 'perception'", specifier = ">=0.5.12" }, - { name = "lark", marker = "extra == 'misc'" }, - { name = "lazy-loader" }, - { name = "lcm", marker = "extra == 'docker'" }, - { name = "llvmlite", specifier = ">=0.42.0" }, - { name = "lxml-stubs", marker = "extra == 'dev'", specifier = ">=0.5.1,<1" }, - { name = "matplotlib", marker = "extra == 'manipulation'", specifier = ">=3.7.1" }, - { name = "mcp", marker = "extra == 'agents'", specifier = ">=1.0.0" }, - { name = "md-babel-py", marker = "extra == 'dev'", specifier = "==1.1.1" }, - { name = "moondream", marker = "extra == 'perception'" }, - { name = "mujoco", marker = "extra == 'sim'", specifier = ">=3.3.4" }, - { name = "mypy", marker = "extra == 'dev'", specifier = "==1.19.0" }, - { name = "numba", specifier = ">=0.60.0" }, - { name = "numpy", specifier = ">=1.26.4" }, - { name = "numpy", marker = "extra == 'docker'", specifier = ">=1.26.4" }, - { name = "nvidia-nvimgcodec-cu12", extras = ["all"], marker = "platform_machine == 'x86_64' and extra == 'cuda'" }, - { name = "ollama", marker = "extra == 'agents'", specifier = ">=0.6.0" }, - { name = "omegaconf", marker = "extra == 'perception'", specifier = ">=2.3.0" }, - { name = "onnx", marker = "extra == 'misc'" }, - { name = "onnxruntime", marker = "extra == 'cpu'" }, - { name = "onnxruntime-gpu", marker = "platform_machine == 'x86_64' and extra == 'cuda'", specifier = ">=1.17.1" }, - { name = "open-clip-torch", marker = "extra == 'misc'", specifier = "==3.2.0" }, - { name = "open3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'", specifier = ">=0.18.0" }, - { name = "open3d", marker = "(platform_machine != 'aarch64' and extra == 'docker') or (sys_platform != 'linux' and extra == 'docker')", specifier = ">=0.18.0" }, - { name = "open3d-unofficial-arm", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "open3d-unofficial-arm", marker = "platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'docker'" }, - { name = "openai", marker = "extra == 'agents'" }, - { name = "openai-whisper", marker = "extra == 'agents'" }, - { name = "opencv-contrib-python", marker = "extra == 'misc'", specifier = "==4.10.0.84" }, - { name = "opencv-python" }, - { name = "opencv-python-headless", marker = "extra == 'docker'" }, - { name = "pandas-stubs", marker = "extra == 'dev'", specifier = ">=2.3.2.250926,<3" }, - { name = "pillow", marker = "extra == 'perception'" }, - { name = "pin", specifier = ">=3.3.0" }, - { name = "piper-sdk", marker = "extra == 'manipulation'" }, - { name = "playground", marker = "extra == 'sim'", specifier = ">=0.0.5" }, - { name = "plotext", specifier = "==5.3.2" }, - { name = "plotly", marker = "extra == 'manipulation'", specifier = ">=5.9.0" }, - { name = "plum-dispatch", specifier = "==2.5.7" }, - { name = "plum-dispatch", marker = "extra == 'docker'", specifier = "==2.5.7" }, - { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.2.0" }, - { name = "psycopg2-binary", marker = "extra == 'psql'", specifier = ">=2.9.11" }, - { name = "py-spy", marker = "extra == 'dev'" }, - { name = "pydantic" }, - { name = "pydantic", marker = "extra == 'docker'" }, - { name = "pydantic-settings", specifier = ">=2.11.0,<3" }, - { name = "pydantic-settings", marker = "extra == 'docker'", specifier = ">=2.11.0,<3" }, - { name = "pygame", marker = "extra == 'sim'", specifier = ">=2.6.1" }, - { name = "pymavlink", marker = "extra == 'drone'" }, - { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.5" }, - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "==0.26.0" }, - { name = "pytest-env", marker = "extra == 'dev'", specifier = "==1.1.5" }, - { name = "pytest-mock", marker = "extra == 'dev'", specifier = "==3.15.0" }, - { name = "pytest-timeout", marker = "extra == 'dev'", specifier = "==2.4.0" }, - { name = "python-dotenv" }, - { name = "python-lsp-ruff", marker = "extra == 'dev'", specifier = "==2.3.0" }, - { name = "python-lsp-server", extras = ["all"], marker = "extra == 'dev'", specifier = "==1.14.0" }, - { name = "python-multipart", marker = "extra == 'misc'", specifier = "==0.0.20" }, - { name = "pyturbojpeg", specifier = "==1.8.2" }, - { name = "pyturbojpeg", marker = "extra == 'docker'" }, - { name = "pyyaml", marker = "extra == 'manipulation'", specifier = ">=6.0" }, - { name = "reactivex" }, - { name = "reactivex", marker = "extra == 'docker'" }, - { name = "requests-mock", marker = "extra == 'dev'", specifier = "==1.12.1" }, - { name = "rerun-sdk", specifier = ">=0.20.0" }, - { name = "rerun-sdk", marker = "extra == 'docker'" }, - { name = "rerun-sdk", marker = "extra == 'visualization'", specifier = ">=0.20.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = "==0.14.3" }, - { name = "scikit-learn", marker = "extra == 'misc'" }, - { name = "scipy", specifier = ">=1.15.1" }, - { name = "scipy", marker = "extra == 'docker'", specifier = ">=1.15.1" }, - { name = "sentence-transformers", marker = "extra == 'misc'" }, - { name = "sortedcontainers", specifier = "==2.4.0" }, - { name = "sortedcontainers", marker = "extra == 'docker'" }, - { name = "sounddevice", marker = "extra == 'agents'" }, - { name = "soundfile", marker = "extra == 'web'" }, - { name = "sse-starlette", marker = "extra == 'web'", specifier = ">=2.2.1" }, - { name = "structlog", specifier = ">=25.5.0,<26" }, - { name = "structlog", marker = "extra == 'docker'", specifier = ">=25.5.0,<26" }, - { name = "tensorboard", marker = "extra == 'misc'", specifier = "==2.20.0" }, - { name = "tensorzero", marker = "extra == 'misc'", specifier = "==2025.7.5" }, - { name = "terminaltexteffects", specifier = "==0.12.2" }, - { name = "terminaltexteffects", marker = "extra == 'dev'", specifier = "==0.12.2" }, - { name = "textual", specifier = "==3.7.1" }, - { name = "tiktoken", marker = "extra == 'misc'", specifier = ">=0.8.0" }, - { name = "timm", marker = "extra == 'misc'", specifier = ">=1.0.15" }, - { name = "toolz", specifier = ">=1.1.0" }, - { name = "torchreid", marker = "extra == 'misc'", specifier = "==0.2.5" }, - { name = "transformers", extras = ["torch"], marker = "extra == 'perception'", specifier = "==4.49.0" }, - { name = "typeguard", marker = "extra == 'misc'" }, - { name = "typer", specifier = ">=0.19.2,<1" }, - { name = "typer", marker = "extra == 'docker'", specifier = ">=0.19.2,<1" }, - { name = "types-colorama", marker = "extra == 'dev'", specifier = ">=0.4.15.20250801,<1" }, - { name = "types-defusedxml", marker = "extra == 'dev'", specifier = ">=0.7.0.20250822,<1" }, - { name = "types-gevent", marker = "extra == 'dev'", specifier = ">=25.4.0.20250915,<26" }, - { name = "types-greenlet", marker = "extra == 'dev'", specifier = ">=3.2.0.20250915,<4" }, - { name = "types-jmespath", marker = "extra == 'dev'", specifier = ">=1.0.2.20250809,<2" }, - { name = "types-jsonschema", marker = "extra == 'dev'", specifier = ">=4.25.1.20251009,<5" }, - { name = "types-networkx", marker = "extra == 'dev'", specifier = ">=3.5.0.20251001,<4" }, - { name = "types-protobuf", marker = "extra == 'dev'", specifier = ">=6.32.1.20250918,<7" }, - { name = "types-psutil", marker = "extra == 'dev'", specifier = ">=7.0.0.20251001,<8" }, - { name = "types-psycopg2", marker = "extra == 'dev'", specifier = ">=2.9.21.20251012" }, - { name = "types-pysocks", marker = "extra == 'dev'", specifier = ">=1.7.1.20251001,<2" }, - { name = "types-pytz", marker = "extra == 'dev'", specifier = ">=2025.2.0.20250809,<2026" }, - { name = "types-pyyaml", marker = "extra == 'dev'", specifier = ">=6.0.12.20250915,<7" }, - { name = "types-simplejson", marker = "extra == 'dev'", specifier = ">=3.20.0.20250822,<4" }, - { name = "types-tabulate", marker = "extra == 'dev'", specifier = ">=0.9.0.20241207,<1" }, - { name = "types-tensorflow", marker = "extra == 'dev'", specifier = ">=2.18.0.20251008,<3" }, - { name = "types-tqdm", marker = "extra == 'dev'", specifier = ">=4.67.0.20250809,<5" }, - { name = "ultralytics", marker = "extra == 'perception'", specifier = ">=8.3.70" }, - { name = "unitree-webrtc-connect-leshy", marker = "extra == 'unitree'", specifier = ">=2.0.7" }, - { name = "uvicorn", marker = "extra == 'web'", specifier = ">=0.34.0" }, - { name = "watchdog", marker = "extra == 'dev'", specifier = ">=3.0.0" }, - { name = "xacro", marker = "extra == 'manipulation'" }, - { name = "xarm-python-sdk", marker = "extra == 'manipulation'", specifier = ">=1.17.0" }, - { name = "xarm-python-sdk", marker = "extra == 'misc'", specifier = ">=1.17.0" }, - { name = "xformers", marker = "platform_machine == 'x86_64' and extra == 'cuda'", specifier = ">=0.0.20" }, - { name = "yapf", marker = "extra == 'misc'", specifier = "==0.40.2" }, -] -provides-extras = ["misc", "visualization", "agents", "web", "perception", "unitree", "manipulation", "cpu", "cuda", "dev", "psql", "sim", "drone", "dds", "docker", "base"] - -[[package]] -name = "dimos-lcm" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "foxglove-websocket" }, - { name = "lcm" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/d8/6e366f73f54733872d8c487a5ebd0ffd2eae2f0242d65b3552cdf71f5771/dimos_lcm-0.1.2.tar.gz", hash = "sha256:a0e193f974afdf07907be427a639e695ddd68c160e4737f847a53a1902674c30", size = 122337, upload-time = "2026-01-30T15:44:38.458Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/a9/9d938d6a84c873e3ea4765541a0babd216dfe730fc6c1044a63b4ab1097e/dimos_lcm-0.1.2-py3-none-any.whl", hash = "sha256:fb65258388e8658d0ff94577d6cb5e7c3d657070556a4289ad1b322939503552", size = 588426, upload-time = "2026-01-30T15:44:37.093Z" }, -] - -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - -[[package]] -name = "distributed" -version = "2025.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "cloudpickle" }, - { name = "dask" }, - { name = "jinja2" }, - { name = "locket" }, - { name = "msgpack" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "sortedcontainers" }, - { name = "tblib" }, - { name = "toolz" }, - { name = "tornado" }, - { name = "urllib3" }, - { name = "zict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/29/ba/45950f405d023a520a4d10753ef40209a465b86c8fdc131236ec29bcb15c/distributed-2025.5.1.tar.gz", hash = "sha256:cf1d62a2c17a0a9fc1544bd10bb7afd39f22f24aaa9e3df3209c44d2cfb16703", size = 1107874, upload-time = "2025-05-20T19:54:26.005Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/65/89601dcc7383f0e5109e59eab90677daa9abb260d821570cd6089c8894bf/distributed-2025.5.1-py3-none-any.whl", hash = "sha256:74782b965ddb24ce59c6441fa777e944b5962d82325cc41f228537b59bb7fbbe", size = 1014789, upload-time = "2025-05-20T19:54:21.935Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "dnspython" -version = "2.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "docstring-to-markdown" -version = "0.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/d8/8abe80d62c5dce1075578031bcfde07e735bcf0afe2886dd48b470162ab4/docstring_to_markdown-0.17.tar.gz", hash = "sha256:df72a112294c7492487c9da2451cae0faeee06e86008245c188c5761c9590ca3", size = 32260, upload-time = "2025-05-02T15:09:07.932Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7b/af3d0da15bed3a8665419bb3a630585756920f4ad67abfdfef26240ebcc0/docstring_to_markdown-0.17-py3-none-any.whl", hash = "sha256:fd7d5094aa83943bf5f9e1a13701866b7c452eac19765380dead666e36d3711c", size = 23479, upload-time = "2025-05-02T15:09:06.676Z" }, -] - -[[package]] -name = "docutils" -version = "0.22.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, -] - -[[package]] -name = "drake" -version = "1.45.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version < '3.11' and sys_platform == 'darwin'", -] -dependencies = [ - { name = "matplotlib", marker = "sys_platform == 'darwin'" }, - { name = "mosek", version = "11.0.24", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'darwin'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'darwin'" }, - { name = "pydot", marker = "sys_platform == 'darwin'" }, - { name = "pyyaml", marker = "sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/31/aa4f1f5523381539e1028354cc535d5a3307d28fd33872f2b403454d8391/drake-1.45.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b0d9bd6196dc6d3b0e660fc6351fcf236727a45ef6a7123f8dc96f85b8662ac3", size = 57314509, upload-time = "2025-09-16T19:02:10.195Z" }, - { url = "https://files.pythonhosted.org/packages/97/cc/a4e1909d8f69f6aaa2d572b6695a942395205f140c16cc2352b880670325/drake-1.45.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a1d429e95c43b3fe1af156489381d3129c8ef4dd95b80d8c2a2a51a74a2adb24", size = 57315511, upload-time = "2025-09-16T19:02:16.937Z" }, -] - -[[package]] -name = "drake" -version = "1.49.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "matplotlib", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "mosek", version = "11.1.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.15' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.15' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "pydot", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "pyyaml", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/26/2ce3a9caf431f24e39f8b1fc7b3ebba4faafef1d61c849db3194e8d2e21d/drake-1.49.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:6c73dbd061fcb442e82b7b5a94dadcfbf4c44949035d03394df29412114647b2", size = 41482505, upload-time = "2026-01-15T19:44:08.313Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2c/b147eaeee97986d970c0618144b28049cf078c20ba73209f4db14cf9a531/drake-1.49.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:b897f5f1516d13627ef18a8395b15f56413016d3c91c902cada76860b5cbb12c", size = 41516482, upload-time = "2026-01-15T19:44:11.342Z" }, - { url = "https://files.pythonhosted.org/packages/84/dc/c55dc5678a61e5befd3694b28e0dc5737a8422334b774a4174b517c67c22/drake-1.49.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:b9a5b528d430764ce1670918b8679cabbb209c8daa2440824ac3a9832c686591", size = 41432263, upload-time = "2026-01-15T19:44:14.486Z" }, - { url = "https://files.pythonhosted.org/packages/3f/a8/1a46831f5f802088df9cd92c204b888aef4e3659d9702128533aa4e5ebaa/drake-1.49.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:0a51abf867d534cef1343381ce79883acc606d52fc56debf2dd9e306982e8910", size = 41438880, upload-time = "2026-01-15T19:44:20.265Z" }, - { url = "https://files.pythonhosted.org/packages/d8/60/cdbc3101bb2bd57706a6b6c5a7fc68a03270f002af1d448da875f3eff5df/drake-1.49.0-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:775740e9500ab8cb2e0af0e69ab162018ac03f7553b6fe03fc6b4f03c4b01092", size = 41509337, upload-time = "2026-01-15T19:44:25.879Z" }, -] - -[[package]] -name = "durationpy" -version = "0.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/a4/e44218c2b394e31a6dd0d6b095c4e1f32d0be54c2a4b250032d717647bab/durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba", size = 3335, upload-time = "2025-05-17T13:52:37.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/0d/9feae160378a3553fa9a339b0e9c1a048e147a4127210e286ef18b730f03/durationpy-0.10-py3-none-any.whl", hash = "sha256:3b41e1b601234296b4fb368338fdcd3e13e0b4fb5b67345948f4f2bf9868b286", size = 3922, upload-time = "2025-05-17T13:52:36.463Z" }, -] - -[[package]] -name = "edgetam-dimos" -version = "1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hydra-core" }, - { name = "iopath" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "torch" }, - { name = "torchvision" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/ea/bec55e18e19b6e43ed5f18bfcb699933ab82744fa8a52209ac6e94a6d6d8/edgetam_dimos-1.0.tar.gz", hash = "sha256:4fea5fd5a5aa17f9145dc4f35abc41de9426acaa0d59cae9b467cf26e657d4a7", size = 74935, upload-time = "2026-01-19T22:53:39.159Z" } - -[[package]] -name = "eigenpy" -version = "3.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-boost" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/a5/7ec1dc873df269332c84e5b79b033fe53d55c5fd6517bd6d8bb5fb24e707/eigenpy-3.12.0.tar.gz", hash = "sha256:e9d07219df1e61e45db6e42001697c5637743b3ad3e0bfcf069fc94c5fab218d", size = 6556548, upload-time = "2025-08-23T17:54:34.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/6d/25e69e262ec336c3b51eebfae9da536f57e93970b434dc7d8506ed3ee3f7/eigenpy-3.12.0-0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:27525792572d6d2cdc7dc407b253280b7f52b9b4dda900ad9cc4b27879e251f2", size = 5635279, upload-time = "2025-08-23T17:53:58.956Z" }, - { url = "https://files.pythonhosted.org/packages/05/cf/fc8729917859ce949d7ab07f0853fe4df45555322e56765f98152e5f2bf1/eigenpy-3.12.0-0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a3e993c2adc4029673d0578dadec20c5a683c3de5b7768abf98129767e57c40a", size = 4865885, upload-time = "2025-08-23T17:54:01.012Z" }, - { url = "https://files.pythonhosted.org/packages/d0/b0/651af0c2db36f1ad62b2a40d439b65c13da8b38bd0e0a1bf67f3af6d0034/eigenpy-3.12.0-0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:291c21c38a1faeb1823e6f821be1910b7751e0fecf12047217f98c3ab748183b", size = 6200013, upload-time = "2025-08-23T17:54:03.107Z" }, - { url = "https://files.pythonhosted.org/packages/c1/90/c0fc4227cf3f02b60cbdcf36946eb1239683273057d242b97e9af7a44ee3/eigenpy-3.12.0-0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5d94b52d087d9f317e1029a48cdf170bfca7f0e1c10581b4ce79b1d067fc5e1a", size = 6004382, upload-time = "2025-08-23T17:54:04.667Z" }, - { url = "https://files.pythonhosted.org/packages/58/4c/eed4d4ab07fe3e8a05c599c4955e8053de25ba23c51a8083dabd73714835/eigenpy-3.12.0-0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e6f43554f88ff02f29480dcb278093494f4701f9a823820b98fa20c82a3d963", size = 5635357, upload-time = "2025-08-23T17:54:06.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/75/a35cb968b524d05173027b9841b3ebd209265c4f8e4040aa27d53dcc8574/eigenpy-3.12.0-0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3ed6ff1ab3c77932a5619db33c4b34945608692111a48cf7ed12e844ea495218", size = 4865912, upload-time = "2025-08-23T17:54:08.405Z" }, - { url = "https://files.pythonhosted.org/packages/6a/df/ee81fc527c3f056190e848a7741938af90293b0e6f71bab5e89ad1cc540f/eigenpy-3.12.0-0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9b842736a5d554692827ff02429a5f927bfbb84df9cf7de86eab86ae7733d315", size = 6199469, upload-time = "2025-08-23T17:54:09.858Z" }, - { url = "https://files.pythonhosted.org/packages/a6/76/569c7fec07d4dd62fd34ed47c1c048f4f32acb0a2b95d593b743a79d7872/eigenpy-3.12.0-0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a90025bb6986860e6cdb50e5dbc05b55aa7e71bff503e214a3d24842ff15da02", size = 6004355, upload-time = "2025-08-23T17:54:12.658Z" }, - { url = "https://files.pythonhosted.org/packages/3a/57/e024b4644b4a5c48cfa527c7f56064efee6286260b479d1bf2d06f616509/eigenpy-3.12.0-0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e10c04912483bb43cb3c49cf51d73138417383475b113cc4e96c426ba6925d11", size = 5626038, upload-time = "2025-08-23T17:54:14.113Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/2bf4bf06cf89b95b924482e0cd632a9c4c41043c0b8b53b58b5615239b32/eigenpy-3.12.0-0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b2a5cab1c2b7cb6cc0c798170687e168644d44988492e65b0f0ab522bda7f6a8", size = 4877878, upload-time = "2025-08-23T17:54:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/fa/d7/6b9a39ef606002f9cf20f54dd8741deb9d884d576b4997162671227cdacb/eigenpy-3.12.0-0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1b27737ffceee5915c88b408821ec3e64d1d409abf1c02ed3617cd949773cafa", size = 6193411, upload-time = "2025-08-23T17:54:17.841Z" }, - { url = "https://files.pythonhosted.org/packages/12/af/3942b89ea486bbd9bee160353b028bf98547e9bfdcb5562c975e683f8c2b/eigenpy-3.12.0-0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:15e7f3f7b4d099fc942fc5a14157022d2314aebf146fd63b927d5c339a7b0d01", size = 6012102, upload-time = "2025-08-23T17:54:19.739Z" }, - { url = "https://files.pythonhosted.org/packages/80/aa/50b418bc747273c2e7c83dfeaabbdbeaab809dc20c935e52f0bbc1c779e8/eigenpy-3.12.0-0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:e88ee2013e4f81adcb041850e60dc63bd85a2bcdefd3dbf8168f6141dfdc3174", size = 5626045, upload-time = "2025-08-23T17:54:21.308Z" }, - { url = "https://files.pythonhosted.org/packages/4a/bb/bac62e442d1727e7b9797e57f99ba9d8040c2296a979881dc9c1bab7dbc4/eigenpy-3.12.0-0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:560886481b2e38a0a7796f3e93c6e6e707c677f504e8cc723773e4a9f22299dd", size = 4877879, upload-time = "2025-08-23T17:54:22.786Z" }, - { url = "https://files.pythonhosted.org/packages/73/95/f9a4286f6f9139ba33196d5c79449981abc3767c7ab3fcdc05551f01485c/eigenpy-3.12.0-0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bd50b2a708201d439987cf55e224eb0a9656f7d1383a06f7175926aa0a8b1971", size = 6193411, upload-time = "2025-08-23T17:54:24.338Z" }, - { url = "https://files.pythonhosted.org/packages/18/c8/086b66d5310e4c62db564dbe76556f0bfa7aa0d969765de2ad554a75e6ae/eigenpy-3.12.0-0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:9bd338f32475af374c55e1803cc5e7936cc3fccf172cbb01623b846d270d8294", size = 6012109, upload-time = "2025-08-23T17:54:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/f4/53/d6c7ef75acd8ac099b1ae9e2209936fb0ada9c675dfd6762c4392be19df5/eigenpy-3.12.0-1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6d660cd9ebdff808f4e9d49027e9ae5621d19de069deef67ef226e5d48cdcfb2", size = 5415254, upload-time = "2025-10-15T20:13:13.051Z" }, - { url = "https://files.pythonhosted.org/packages/5d/31/a7358942489a31edbded67e00771ff18261c67efddb9f37bdb353633e493/eigenpy-3.12.0-1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37c05f8431f9edbd5db2ab04a01d004b894929b70c6faea0f80004f97ddb2e1f", size = 4226332, upload-time = "2025-10-15T20:13:15.092Z" }, - { url = "https://files.pythonhosted.org/packages/2e/38/4d06a01b1fe3efb9c9bc10521d9718d5a263b316306429220bd2da8e134e/eigenpy-3.12.0-1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c5cbbee1f043baed9900a99918fb4326151be0145f793f8129fd1b0b938f2974", size = 6200413, upload-time = "2025-10-15T20:13:16.952Z" }, - { url = "https://files.pythonhosted.org/packages/fe/38/5aff8d72ebaf891a07f1368176508f75cc9e7bf8967d07f7fa1baedc6ac7/eigenpy-3.12.0-1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:f579f3b23754f2156b43f27906481bc00fd46030c4e061e8e770cd02aacc3d88", size = 6037835, upload-time = "2025-10-15T20:13:18.826Z" }, - { url = "https://files.pythonhosted.org/packages/cb/40/653fc67abc9fd2fe9adcddd8d61a5f4e219e03ae952f6f4d3fd365df3671/eigenpy-3.12.0-1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd337ed66182e9afd5deb847edda7e0b0cb173b6a28d3123c375634f10888b3a", size = 5415262, upload-time = "2025-10-15T20:13:20.719Z" }, - { url = "https://files.pythonhosted.org/packages/fb/7b/2eb71b204e78d656c04037a8d31c5e57fc984323f7e5617647609f1e4cc6/eigenpy-3.12.0-1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:42e2bfd63338e4357cd0edff072b0c612fcdb8dbd89b811feba701a9cf909d0a", size = 4226325, upload-time = "2025-10-15T20:13:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/fb/ce/9d1e4b7a6dd16380893ce8252c4809c04688b530c21570ac7fc8f5a588b8/eigenpy-3.12.0-1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:54cb321401766e0e8df5870f9a497d1a74e23963d74c49df569b6cd6728ceb14", size = 6200017, upload-time = "2025-10-15T20:13:24.042Z" }, - { url = "https://files.pythonhosted.org/packages/ee/c9/c97ee17b2cdae3b1b9ce3a048469d9d5ca03ab8baf2692392cea610c7323/eigenpy-3.12.0-1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a59d2b8e3e2cc01370cb7ceff76102972b0c8369d4cdcb8b5f0c95d8860d4dcf", size = 6037983, upload-time = "2025-10-15T20:13:25.661Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e5/f0f4da14c543f9d91e15b835e48f708c839ae51e186ecebf0950ac0503f2/eigenpy-3.12.0-1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:96f5cf42a3fa538a0b7672c36e295c8f63a022bcbf5acdc9a127cc1189f151aa", size = 5460186, upload-time = "2025-10-15T20:13:27.174Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6b/b6ee7ad7f54c6f17c07f4de09686a100ef02a480ac4bc2a1ab2f2f211105/eigenpy-3.12.0-1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:165323fea20b17d5fc2d81c606a32ae85b4af0cd4b08e0f860efdb8cefd1766c", size = 4247646, upload-time = "2025-10-15T20:13:29.229Z" }, - { url = "https://files.pythonhosted.org/packages/ec/26/c7aded82e75e655fdfc879e25309b6e42f46f903f341d193f0cca1c53d19/eigenpy-3.12.0-1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f7d4fd7db04df9513fbdfff443aa4fe11a83b14dbef1d03992d94cffb9a905fe", size = 6202696, upload-time = "2025-10-15T20:13:31.403Z" }, - { url = "https://files.pythonhosted.org/packages/c4/97/a39ac2226f82cfbd4df632d2d4a9f480fbfea51fccbc5b2d5809443b47ca/eigenpy-3.12.0-1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8c1a82275bf1b478b7c201ac260e2d48c8da14ff568deeafcb8d37ef8bf798ec", size = 6047814, upload-time = "2025-10-15T20:13:33.44Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/99bb78a32f3bf3a0e3502f5e6c33887404b6ad7a77ca2b692730239fdad8/eigenpy-3.12.0-1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:8f43adceaf0f53767d974b21f822d8c9d2854e7c868e3dcfb0e5ca25c54e2281", size = 5460188, upload-time = "2025-10-15T20:13:35.15Z" }, - { url = "https://files.pythonhosted.org/packages/52/f6/89d161787fb83e4dcf54c01b8a3e43f4b4346f4249819add6efa03ec74d6/eigenpy-3.12.0-1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f9f53a5df469acbcf7055a36d5c5fe3581faa1b0468e3c78919dcc92233fcde4", size = 4247653, upload-time = "2025-10-15T20:13:37.126Z" }, - { url = "https://files.pythonhosted.org/packages/17/7c/74b63227d3bb1a2cfdd6f5ac0ddb6b359b24f1165466d9741e8c0c7624d7/eigenpy-3.12.0-1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:6e419f03d8a6e2f86a72c22950d16514c065fdfeca909afe70467c0728271162", size = 6202697, upload-time = "2025-10-15T20:13:38.813Z" }, - { url = "https://files.pythonhosted.org/packages/55/f5/a2d18432641c570618da23ae661aeb75b58f1e4db76352c43eae2d3c7794/eigenpy-3.12.0-1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:fda5e71f972af7790a8a0935ec0efafb386ab8293b9cb187e05b89f68cc964d3", size = 6047816, upload-time = "2025-10-15T20:13:40.573Z" }, - { url = "https://files.pythonhosted.org/packages/d2/3e/66f1b28849863a52c291e941f88e341a17de59ad0d64c406c195d8214c94/eigenpy-3.12.0-1-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:9a032ccee69712b4c0697034441960a2853f53d0f05bf5b5e29484d7c670d472", size = 5464486, upload-time = "2025-10-15T20:13:42.299Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/5d23552a6233e2cab6b6de182f6f838af1451feed96df9bb00ca63e068ee/eigenpy-3.12.0-1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:17c3df16edd597f0cb32616af9bd7331b82772b376d559d948e57dec85ac1edd", size = 4252388, upload-time = "2025-10-15T20:13:43.912Z" }, - { url = "https://files.pythonhosted.org/packages/07/a6/515a94095e325bb92f300353214a683395f01d14c6cded9dac9f7669d70b/eigenpy-3.12.0-1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:1af9e6a14a5ddeeff19698b0c47db029d19601ce8246dd68f6038271801a2d3e", size = 6216613, upload-time = "2025-10-15T20:13:45.746Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/95f253a4d2a684fc90d3aa1bef3a10fb1772277310d432f87ae852723195/eigenpy-3.12.0-1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4537fcc048f45e8a5ada205d0cc8798c558f34500153dc0e33862669150cd0f7", size = 6062999, upload-time = "2025-10-15T20:13:47.507Z" }, -] - -[[package]] -name = "einops" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/81/df4fbe24dff8ba3934af99044188e20a98ed441ad17a274539b74e82e126/einops-0.8.1.tar.gz", hash = "sha256:de5d960a7a761225532e0f1959e5315ebeafc0cd43394732f103ca44b9837e84", size = 54805, upload-time = "2025-02-09T03:17:00.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/62/9773de14fe6c45c23649e98b83231fffd7b9892b6cf863251dc2afa73643/einops-0.8.1-py3-none-any.whl", hash = "sha256:919387eb55330f5757c6bea9165c5ff5cfe63a642682ea788a6d472576d81737", size = 64359, upload-time = "2025-02-09T03:17:01.998Z" }, -] - -[[package]] -name = "empy" -version = "3.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/95/88ed47cb7da88569a78b7d6fb9420298df7e99997810c844a924d96d3c08/empy-3.3.4.tar.gz", hash = "sha256:73ac49785b601479df4ea18a7c79bc1304a8a7c34c02b9472cf1206ae88f01b3", size = 62857, upload-time = "2019-03-21T20:22:03.951Z" } - -[[package]] -name = "etils" -version = "1.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/a0/522bbff0f3cdd37968f90dd7f26c7aa801ed87f5ba335f156de7f2b88a48/etils-1.13.0.tar.gz", hash = "sha256:a5b60c71f95bcd2d43d4e9fb3dc3879120c1f60472bb5ce19f7a860b1d44f607", size = 106368, upload-time = "2025-07-15T10:29:10.563Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/98/87b5946356095738cb90a6df7b35ff69ac5750f6e783d5fbcc5cb3b6cbd7/etils-1.13.0-py3-none-any.whl", hash = "sha256:d9cd4f40fbe77ad6613b7348a18132cc511237b6c076dbb89105c0b520a4c6bb", size = 170603, upload-time = "2025-07-15T10:29:09.076Z" }, -] - -[package.optional-dependencies] -epath = [ - { name = "fsspec" }, - { name = "importlib-resources" }, - { name = "typing-extensions" }, - { name = "zipp" }, -] -epy = [ - { name = "typing-extensions" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - -[[package]] -name = "fastapi" -version = "0.129.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/47/75f6bea02e797abff1bca968d5997793898032d9923c1935ae2efdece642/fastapi-0.129.0.tar.gz", hash = "sha256:61315cebd2e65df5f97ec298c888f9de30430dd0612d59d6480beafbc10655af", size = 375450, upload-time = "2026-02-12T13:54:52.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/dd/d0ee25348ac58245ee9f90b6f3cbb666bf01f69be7e0911f9851bddbda16/fastapi-0.129.0-py3-none-any.whl", hash = "sha256:b4946880e48f462692b31c083be0432275cbfb6e2274566b1be91479cc1a84ec", size = 102950, upload-time = "2026-02-12T13:54:54.528Z" }, -] - -[[package]] -name = "fastcrc" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2a/79/0afaff8ff928ce9990ca883998c4ea7a7f07f2dfea3ebd6d65ba2aadfd4e/fastcrc-0.3.5.tar.gz", hash = "sha256:3705cbad6b3f283a04256f97ae899404794395090ff5966eac79fe303c13e93e", size = 11979, upload-time = "2025-12-31T18:23:09.579Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/6f/4777a0687161bd73a0be8efc6d000f30687f82aa8860f0259a0bad4a29b5/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45a04142b34d1a54891b16766955cc38fc85a2323094457e9a03f8d918a389df", size = 283015, upload-time = "2025-12-31T18:19:46.237Z" }, - { url = "https://files.pythonhosted.org/packages/57/88/66c38ffc73dd3f2e935b0d0b21a33edc79990ee7c991a76aeefb2a105628/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e44a765e411a2bda54c186d689af383686c0d78010e10944c954e6e9bfcd09f7", size = 290648, upload-time = "2025-12-31T18:20:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e3/63ceaf3792bb4d5e746510194522c4f5c028bd706f2fb04f9879592bc2b5/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebab06b7b90606e31e572392ba1ed37211f7b270db339ceca8f93762fc5c2d54", size = 408788, upload-time = "2025-12-31T18:20:23.702Z" }, - { url = "https://files.pythonhosted.org/packages/86/69/576d04272abbf2e9b86101f46701e556355c1abe0bb3de9e109710bc4b22/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f30b692f574b8b79b42f1fbd6858633e864f5ccf8c0e64cb37d3ba62c51e0d8", size = 307550, upload-time = "2025-12-31T18:21:00.849Z" }, - { url = "https://files.pythonhosted.org/packages/70/e9/89898caa3000fc1252effb8fea1b5623ae50eca18af573c5a1f5ac156174/fastcrc-0.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e93da6affe340258f1c34b13dcabc67d947b5dc4f7a26da3df86bb910baa21a0", size = 287814, upload-time = "2025-12-31T18:21:29.721Z" }, - { url = "https://files.pythonhosted.org/packages/20/73/8aeaf53c0e7f4aa77356b9f02fcb36124b71a58510733b4871838396954e/fastcrc-0.3.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:214e23ddd648aa83d2a55b22d3045ec5effc5dd3e4119957fb724f5aa6b1613d", size = 291599, upload-time = "2025-12-31T18:20:42.099Z" }, - { url = "https://files.pythonhosted.org/packages/93/51/0e153482e481a955bdbabbb5b0acf07004f09943f0e3895f0c48c8b0cfc8/fastcrc-0.3.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7f804305286f5ad547dace645c1f47aa2993c056ba81bfeb5879a07aeafac56", size = 300587, upload-time = "2025-12-31T18:21:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/8c/b8064404ef151e23112d6b1a602d6c92ef1c0920fca67f8cfd2b34d37513/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8392ee32adc4e278ba24f7eb87f7768ea0bccaccf6fd6f66dba6465001f05842", size = 465311, upload-time = "2025-12-31T18:21:52.704Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a5/3ea91a6901f486c549ad6cbb89d2ce29d0eb5b919d3ee44c9f0a6b322a55/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ff21e981ceacf9edebfebdca481b14e8662128a86e811d6d18237274a445cc94", size = 560907, upload-time = "2025-12-31T18:22:12.391Z" }, - { url = "https://files.pythonhosted.org/packages/90/39/acc1e9f012bc71a6b5d04b7f20279f11ab9d2814d7c65bb0e294d9808cb1/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85123fa2895f1608967b31e5aa675a1022fe44aecd6994d1e26221e1bcdc208d", size = 520223, upload-time = "2025-12-31T18:22:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/05/0d/9a51369da7de00afef5d7a746e88ca632f3c9c5ba623492305a5edb6dacb/fastcrc-0.3.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d027f5edcb8de021f2efd34507031978b3ea046a52681294b2eb0478bfc902a6", size = 490849, upload-time = "2025-12-31T18:22:49.818Z" }, - { url = "https://files.pythonhosted.org/packages/ea/5b/d50b6e8e04ead6cbd2c0593e8775343c55631cf4ffded8ef0ae7348b295c/fastcrc-0.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:5bf02d21694aded7e1293b4b0dbad4c9ba6c21f8a64a4e391230b56a3d341570", size = 147556, upload-time = "2025-12-31T18:23:10.838Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/5442f5ed1c5bcb030863612b7e4c7ead6e6f7158c5271dc7df31b6b31e50/fastcrc-0.3.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b43219f6b52a6aad73f33e397b504bf41e9e82c4db33483d7116624d78f79d2b", size = 258410, upload-time = "2025-12-31T18:21:46.246Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/868ecef894d2e233e6389567a59d4997ddfcac86d09fcfa887c84820bf37/fastcrc-0.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce9fb7d960c3bd167924dfca0fdc5dc1c031d0c69e54f67b9a3f3f80e416222c", size = 254154, upload-time = "2025-12-31T18:21:40.975Z" }, - { url = "https://files.pythonhosted.org/packages/85/df/9b749669136c5e813b85d02b1525afd4d1b1f100df94bc83de43645940dd/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09f9e741545a2ddff2510e11d7a81493054c281304896f82ef49e74a13f730f1", size = 282905, upload-time = "2025-12-31T18:19:47.628Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ea/751d815b5c0a1f455eba6202ffe68ba8d3d145ae06367ef17aeb95a92675/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd2f00e7a497d8bc55909ad8e4ded8b1e66e365f2334aba7e6afd6040da941b8", size = 290535, upload-time = "2025-12-31T18:20:05.084Z" }, - { url = "https://files.pythonhosted.org/packages/47/11/67625802a5fa68ed5ca237b42320a9a60098886eb3f9875ceb7035ebfbe0/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41e48f8ffe70f8c4efd6e1a8e662dc9fa30ae27b35ddd270debd095286234713", size = 408362, upload-time = "2025-12-31T18:20:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/cb/59/f9289da84866f8f9e345b96f92a55437db0d8575a706fa31dc22beff198e/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c7f2d18026a1248ab004719ded24c6cb101d53c611690c4d807605036bf235e8", size = 307457, upload-time = "2025-12-31T18:21:02.07Z" }, - { url = "https://files.pythonhosted.org/packages/3b/66/80171dc72309ab97224ab55f3402029a8a0dbf28fbb44da7402cb12fda9a/fastcrc-0.3.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56e969315376ac484b40e7a962788251c3e1dcd0d86f8e770a5a92f3d7d43af9", size = 287591, upload-time = "2025-12-31T18:21:30.974Z" }, - { url = "https://files.pythonhosted.org/packages/4e/50/f89493bd44cf682eac0ec68f378ac098d786d2aa45f2f8e8c7292130e21d/fastcrc-0.3.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8bdc486207e8d3278746a3c91adbe367f5439a4898acc758be22c13aead4241a", size = 291394, upload-time = "2025-12-31T18:20:43.412Z" }, - { url = "https://files.pythonhosted.org/packages/1a/9a/e1472e9b45e4f5774b40644f1e92870960add0832dc45d832ee9dd7a4343/fastcrc-0.3.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a488e2e74621fdd198e3fbc43e53c58c98ce4c52c9a903caf0422e219858b1a5", size = 300273, upload-time = "2025-12-31T18:21:20.035Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ba/b6f5b750a77bcd712e3a6a05e8c6b07b584738af9b264936254579ef19b0/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:94b0809c0041269501739535ff405f413fc7753145b5ab42e1ba9149656aacf6", size = 465298, upload-time = "2025-12-31T18:21:54.028Z" }, - { url = "https://files.pythonhosted.org/packages/4c/f5/711f41226ba6a8fe788f93f1123581481b34eb083a0319d26fc72deb3e45/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e20086dff65974ff6f2800e752bf34eb79ef4ab1ed9a9171c754ffad61e4da34", size = 560732, upload-time = "2025-12-31T18:22:13.8Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/ba53b7f1f0b4a4c28f75d1603bd5e535255e2c5727af206c9664f151e04a/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1c7273ba4d85d1502230a4f9afdcc618c7e87c4334006f553d3023120259c4", size = 519984, upload-time = "2025-12-31T18:22:32.271Z" }, - { url = "https://files.pythonhosted.org/packages/f8/75/7f234ed34a1cc405a0fc839d6cd01cf1744ace0a12ec8462069d35f9d9d9/fastcrc-0.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:241722fe3a6c1f8ff184f74f4fb368d194484df51eb5ee35c121492a1d758a70", size = 490533, upload-time = "2025-12-31T18:22:51.49Z" }, - { url = "https://files.pythonhosted.org/packages/29/ee/920bc7471e58dc85e6914f8d5f143da61de6158226fadcf244f0ce0d15b1/fastcrc-0.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:35a5ee5ceff7fe05bce8e5e937f80ce852506efbe345af3fc552bd7e9eed86cf", size = 147383, upload-time = "2025-12-31T18:23:12.849Z" }, - { url = "https://files.pythonhosted.org/packages/f8/6e/d57d65fb5a36fcbf6d35f759172ebf18646c2abdc3ce5300d4b1c010337a/fastcrc-0.3.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6d0a131010c608f46ad2ab1aea2b4ec2a01be90f18af8ff94b58ded7043d123e", size = 255680, upload-time = "2025-12-31T18:21:47.841Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/56a568a74f35bbd0b6b4b983b39de06ba7a21bc0502545a63d9eca8004a3/fastcrc-0.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3302c42d6356da9b0cad35e9cebff262c4542a5cdace98857a16bf7203166ed", size = 251091, upload-time = "2025-12-31T18:21:42.197Z" }, - { url = "https://files.pythonhosted.org/packages/27/bf/a7412fef1676e98ba07cb64b8a7b5b721a5b63f8fc6cccad215e52b4ac67/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8883e1ad3e530f9c97f41fbee35ae555876186475918fd42da5f78e6da674322", size = 282008, upload-time = "2025-12-31T18:19:48.897Z" }, - { url = "https://files.pythonhosted.org/packages/c2/e1/a70192cccd292a977998ff44150cf12680dc82b9f706df89f757903d275f/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15ada82abfc4d6e775b61d4591b42ce2c95994ab9139bc31ba2085ba05b32135", size = 290131, upload-time = "2025-12-31T18:20:06.695Z" }, - { url = "https://files.pythonhosted.org/packages/e2/16/5d1ac72c26494a7eb9ced6034bbdde1efbbbfbb1275c70e70748188e622b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:402c4663ecb5eb00d2ddb58407796cfb731f72e9e348f152809d6292c5229ba7", size = 407328, upload-time = "2025-12-31T18:20:26.344Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6b/c54608230fede639d3d443cd6fd08cf53457fe347f13649112816d94fd66/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4270ff2603b6d5bdac43c92e317605cb921f605a05d383917222ada402ed0b8e", size = 307459, upload-time = "2025-12-31T18:21:03.396Z" }, - { url = "https://files.pythonhosted.org/packages/c3/37/379fae277f2b73d0703996c5b78e5ebdde1a346d8c4d5bb9a6fb2fa4df6b/fastcrc-0.3.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9088dc6f0ff21535fd29ff4639ce5e7b5cb4666fe30040fbfe29614c46ef6c7", size = 286617, upload-time = "2025-12-31T18:21:32.338Z" }, - { url = "https://files.pythonhosted.org/packages/d4/25/e389d565cac63d620e9e501ee3b297b01144165eaef9fdd616fbfa1fdbd0/fastcrc-0.3.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:4b5860aaf5e1114731b63e924be35179b570016ac3fcdd051265c5665c34efa9", size = 290784, upload-time = "2025-12-31T18:20:44.585Z" }, - { url = "https://files.pythonhosted.org/packages/64/f8/2857b9e0076d4d5838f9393e5d47624fe28b9c6f154697c8e420249f3c4e/fastcrc-0.3.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a69b53da8ffbfe60099555a9f38ebb05519ba76233caf0f866ac5943efd1df3", size = 299395, upload-time = "2025-12-31T18:21:21.254Z" }, - { url = "https://files.pythonhosted.org/packages/fb/60/e69d182d150f41ca8db07c0ba5d77d371ee52ebce13537d1d487a45980aa/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a9d2b2afcfab28dbc9fa4aa516a4570524cb74d0aa734f0cf612bc9070c360d", size = 464295, upload-time = "2025-12-31T18:21:55.355Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/60f3379f11f55e2bda0f46e660b1ae772f3031e751649474c9ba7ad5e52d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c96dbf431f7214d22329650121a5f0c41377722833f255f2d926d7df0d4b1143", size = 560169, upload-time = "2025-12-31T18:22:15.081Z" }, - { url = "https://files.pythonhosted.org/packages/76/5c/8bd19ba855624aea12a6c2da5cef2cf3d12119c55acd048349583c218a7d/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:186e3f5fdfa912b43cd9700dc6be5c5c126fe8e159eb65f0757a912e0db671d4", size = 518877, upload-time = "2025-12-31T18:22:33.91Z" }, - { url = "https://files.pythonhosted.org/packages/12/54/50b211283dc54f5af3517dee0b94797f04848c844a3765fd56a5aa899a0c/fastcrc-0.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4680abf2b254599d6d21bb58c1140e4a8142d951d94240c91cc30362c26c54c5", size = 489218, upload-time = "2025-12-31T18:22:52.785Z" }, - { url = "https://files.pythonhosted.org/packages/dd/af/b77460dbe826793fc65a39b4959efa677d12be6d6680cab6b24035b82da2/fastcrc-0.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:aa8614b0340be45b280c2c4f0330a546c71a2167c4414625096254969750b17b", size = 146970, upload-time = "2025-12-31T18:23:14.477Z" }, - { url = "https://files.pythonhosted.org/packages/45/13/f13eb8e644f18c621d1003d1e34209759ed28ace2eb698809558f45f831e/fastcrc-0.3.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:846aca923cc96965f41a9ebb5c2a4172d948d67285b2e6f2af495fda55d2d624", size = 255919, upload-time = "2025-12-31T18:21:49.158Z" }, - { url = "https://files.pythonhosted.org/packages/3c/ab/72a8a7881f1ac1fd5ab8679fe29a57dbf0523d9c5ee9538da744d5e10d95/fastcrc-0.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe5d56b33a68bc647242345484281e9df7818adb7c6f690393e318a413597872", size = 251366, upload-time = "2025-12-31T18:21:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/a1/3a/605dda593c0e089b9eaf8a6b614fd3da174ecd746c7ea1212033f1ff897a/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:953093ed390ad1601288d349a0f08a48562b6b547ee184c8396b88dff50a6a5f", size = 282465, upload-time = "2025-12-31T18:19:50.36Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cb/f0bb9501c96471ec415319b342995d823a2c9482bcebff705fead1e23224/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:50679ec92e0c668b76bb1b9f5d48f7341fecc593419a60cce245f05e3631be10", size = 290304, upload-time = "2025-12-31T18:20:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/a4/6e/a59baef04c2e991e9a07494b585906aa23017d2262580c453cca29448270/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d377bb515176d08167daa5784f37f0372280042bde0974d6cdcf0874ce33fdc", size = 409004, upload-time = "2025-12-31T18:20:27.895Z" }, - { url = "https://files.pythonhosted.org/packages/86/97/9931fbc57226a83a76ee31fd197e5fb39979cb02cd975a598ab9c9a4e13d/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89e6f4df265d4443c45329857ddee4b445546c39d839e107dc471ba9398cde1d", size = 307402, upload-time = "2025-12-31T18:21:05.107Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2b/aea9bd3694fa323737cf6db7ac63a3fe21cc8987fe6d2b9f97531801688b/fastcrc-0.3.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f65d6210030a84ed3aaec0736e18908b2e499784c51f6ffd0d8f7d96de90eea1", size = 286802, upload-time = "2025-12-31T18:21:34.406Z" }, - { url = "https://files.pythonhosted.org/packages/15/03/6667737b2a24bd48a15bea1924bed3d7cd322813bc61f5ee3ea7632417fa/fastcrc-0.3.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:a1be96b4090518cd6718c24a0f012f7f385fabbd255ce6a7b0d8ec025c9fb377", size = 291114, upload-time = "2025-12-31T18:20:45.99Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/983934a2a56078a022e6f82f3fd6baf40d7e85871658590b57274179dc85/fastcrc-0.3.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63cf91059d8ab4fdb8875296cff961f7e30af81d1263ba11de4feabea980f932", size = 299618, upload-time = "2025-12-31T18:21:22.499Z" }, - { url = "https://files.pythonhosted.org/packages/e4/22/59cfae39201db940a1f97bb0fd8f5508c7065394a19a77cd6c5d6cbf2f6b/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5ad026ab9afe90effe55d2237d0357cc0edcfbb1b7cd7fa6c9c12b8649d30a9", size = 464682, upload-time = "2025-12-31T18:21:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/d8/4c/b129f316ddcbf4bf1c0745b209a45a5f2bf5bfd4ccd528893d3d25ce53f6/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2e4e0f053f9c683d11dea4282f9e0a9c5f0299883a4dd83e36eb0da83716f4f9", size = 560357, upload-time = "2025-12-31T18:22:16.694Z" }, - { url = "https://files.pythonhosted.org/packages/10/d3/c7342e8a966fb3bff7672b5727e08c54e509a470e4f96623cc5c6ff8679c/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:80fb2879c0e0bb1d20ea599e4033f48a50d3565550a484a4f3f020847d929569", size = 519044, upload-time = "2025-12-31T18:22:35.399Z" }, - { url = "https://files.pythonhosted.org/packages/02/b6/e65ba338709e49d205a612c69996b66af1bfd78e9c9278fffc0bea8613c3/fastcrc-0.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03ff342e97ff48f9f3c8aa12c48d87ed90b50b753d9cf65d2fecdb8a67cef20d", size = 489513, upload-time = "2025-12-31T18:22:54.496Z" }, - { url = "https://files.pythonhosted.org/packages/58/dd/a0338c32d8e404f32b26b6948d0b60cedc07e1fa76661c331931473ead71/fastcrc-0.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:6d59f686f253478626b9befa4dfff723d7ae5509d2aa507a1bf26cfd4ec05ae4", size = 147181, upload-time = "2025-12-31T18:23:16.014Z" }, - { url = "https://files.pythonhosted.org/packages/59/39/1a656464fca9c32722b36065cbf3ae989da8783e0d012102ade5e968b385/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2b9f060e14b291e35783372afb87227c7f1dc29f876b76f06c39552103b6545", size = 282367, upload-time = "2025-12-31T18:19:51.761Z" }, - { url = "https://files.pythonhosted.org/packages/fa/35/d9efe66a001f989e84e3d1d196f9cc8764dcead87f43e9f22b3f1ea6d1e1/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:93e2761f46567334690c4becc8adcd33c3c8bd40c0f05b1a2b151265d57aff73", size = 289402, upload-time = "2025-12-31T18:20:09.854Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ca/c10d7fc598528052463d892c4e71c01c00985cfdb86500da3550fb0b6e75/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f07abfb775b6b7c4a55819847f9f9ddd6b710ebc5e822989860771f77a08bf9c", size = 408177, upload-time = "2025-12-31T18:20:29.344Z" }, - { url = "https://files.pythonhosted.org/packages/c5/72/59bbfe8c6bdb17eb86a43a958268da3fefe3d0da67496e3112dfb84f276a/fastcrc-0.3.5-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5c57c4f70c59f6adafa8b3ae8ac1f60da42a87479d6e0eea04dbaa3c00aca6e", size = 307543, upload-time = "2025-12-31T18:21:06.85Z" }, - { url = "https://files.pythonhosted.org/packages/54/74/904760e09f5768ecbf25da8fddf57fb4fb1599b92780294f7e6172e2a398/fastcrc-0.3.5-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:03f50409fbcb118e66739012a7debcfd55dd409d6d87c32986080efdf0a70144", size = 290841, upload-time = "2025-12-31T18:20:47.685Z" }, - { url = "https://files.pythonhosted.org/packages/d0/bd/340e3d9525d442189883fce613134a5acf167d7f3e48d95036f1e0c9a2dc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89e6247432e481c420daeb670751a496680484c0c6f69e2198522d6f0f6a5b3a", size = 464580, upload-time = "2025-12-31T18:21:58.673Z" }, - { url = "https://files.pythonhosted.org/packages/4c/97/e2a90908069c89ea41b1cf7ae956fb77219d293ebe62cca32d6c2a077c16/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:9d40dcef226068888898c30ef4005c22e697360685d7e40945365bee63e15d26", size = 559567, upload-time = "2025-12-31T18:22:18.133Z" }, - { url = "https://files.pythonhosted.org/packages/91/93/b8652fabe301faf050cd19d2f6ae655a61f743687fb8dfc8e614fbf9c493/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:edabc8ee13f7581e3fb42344999a35a6a253cb65ac019969dc38aa45dac32ee8", size = 518538, upload-time = "2025-12-31T18:22:36.82Z" }, - { url = "https://files.pythonhosted.org/packages/98/c9/585216c6a93b398b3c911653eacaf05c5dc731db39b55f99415025af90bc/fastcrc-0.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:430ae0104f9eafe893f232e81834144ba31041bcc63a3eb379d22d0978c6e926", size = 489898, upload-time = "2025-12-31T18:22:55.903Z" }, - { url = "https://files.pythonhosted.org/packages/57/aa/845d6257bac319b9c1fe16648f2e3d0efa1bbbaf8a5b7b4977851d8102ae/fastcrc-0.3.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b6507652630bc1076f57fe56f265634df21f811d6882a2723be854c58664318c", size = 255565, upload-time = "2025-12-31T18:21:50.502Z" }, - { url = "https://files.pythonhosted.org/packages/f7/66/f67a5e6bf89fa7de858fd055b5a69f00029c16cabf5dcf9bc750db120257/fastcrc-0.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0a4c39b9c4a8a37f2fb0a255e3b63b640a5426c0daf3d790399ea85aaabad7f6", size = 251112, upload-time = "2025-12-31T18:21:44.753Z" }, - { url = "https://files.pythonhosted.org/packages/74/a1/c7568f21ad284e68faed0093ccb68bb5d5b248bd08f6241dedfe69ff000b/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d0621722fc4c17bdd7e772fb3fb5384d7c985bb1c8f66256a1dba374e2d12a5", size = 282301, upload-time = "2025-12-31T18:19:53.016Z" }, - { url = "https://files.pythonhosted.org/packages/35/57/da0342f2702e6b50b4d4e5befb2fcd127e82762fe30925b9160eed2184a1/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:648d63f41da1216ef1143573fef35ad2eb34913496ccec58138c2619b10ea469", size = 289811, upload-time = "2025-12-31T18:20:11.584Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d6/94e24eb87bb02546b2b809a8c05034e1e90df3179a44260451e640533a9c/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2be6196f5c4a40b7fca04ef0cc176aa30ac2e19206f362b953fe0db00ea76080", size = 406010, upload-time = "2025-12-31T18:20:30.67Z" }, - { url = "https://files.pythonhosted.org/packages/5f/3c/c4d71a07bcba4761db0c8ab70b4cb2d1fbd803f72d97e8cde188bd1cb658/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c70e985afa6ec16eebaeb0f3d6bfacb46826636f59853f1169e2eb2a336a2c5", size = 307190, upload-time = "2025-12-31T18:21:08.204Z" }, - { url = "https://files.pythonhosted.org/packages/38/7b/bba64d12b0c22d839ddb8cfafae49bb332ae925e839fff2b7752bb20d8dc/fastcrc-0.3.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562820b556d9d2070948a89cb76c34d6ec777bbcd3f70bdb89638a16b6072964", size = 286376, upload-time = "2025-12-31T18:21:35.986Z" }, - { url = "https://files.pythonhosted.org/packages/ae/f4/4fab9f16bb9e6eb6d0c74f913691c25c6f332761c255edd5f3e22a57bd65/fastcrc-0.3.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:99a333fa78aa32f2a024528828cfad71aa39533f210d121ec3962e70542d857b", size = 290450, upload-time = "2025-12-31T18:20:49.2Z" }, - { url = "https://files.pythonhosted.org/packages/41/2c/d4e4072c39f40c8a8550499722ab2539d1de1f30feb97f637d48d33325c7/fastcrc-0.3.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d752dc2285dc446fdf8e3d6e349a3428f46f7b8f059842bdabbb989332d1f3e", size = 299070, upload-time = "2025-12-31T18:21:23.761Z" }, - { url = "https://files.pythonhosted.org/packages/0d/37/12fe830bdfe3b39367645d5b2f8fb2359dc46e67346e91fdd7b9253ad501/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c996c9273c4d3b4c2902852a51b7142bd14a6c750f84bec47778225f7f8952c3", size = 464591, upload-time = "2025-12-31T18:22:00.931Z" }, - { url = "https://files.pythonhosted.org/packages/c3/95/edda45991f71b1ec6022c1649397c1b3d82d45cf3c6323684f9a97a4a9ce/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d595809fd266b037c22a22c2d72375c3f572d89200827ecaaa7c002b015b8a2e", size = 559904, upload-time = "2025-12-31T18:22:19.778Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d8/aaeb37ebc079ad945dd3aca3fae0f358d5c05af760305e6b6fef63d1c4c7/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c6ee18383827caee641d22f60be89e0a0220c1d5a00c5e8cbb744aac1d5bc876", size = 518525, upload-time = "2025-12-31T18:22:38.392Z" }, - { url = "https://files.pythonhosted.org/packages/0d/b5/7479aadffc83bb152884f65c8d543e61d2e95592d4ed1e902019fe5b80f2/fastcrc-0.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:276591e6828cd4dba70cdfc16450607d742c78119d46060a6cf90ef006a13c71", size = 489362, upload-time = "2025-12-31T18:22:57.322Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4f/4c3af4d4410aff8eef2077425406ddb20ccd3eb8b0fb5d6b6bd5fd2510a3/fastcrc-0.3.5-cp314-cp314-win32.whl", hash = "sha256:08ade4c88109a21ad098b99a9dc51bb3b689f9079828b5e390373414d0868828", size = 139114, upload-time = "2025-12-31T18:23:19.462Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/c2564781aeb0c7ae8a6554329b2f05b8a84a2376d2c0ba170ed44ddcc78c/fastcrc-0.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:01565a368ce5fe8a8578992601575313027fb16f55cf3e62c339ae89ccd6ccd2", size = 147063, upload-time = "2025-12-31T18:23:17.152Z" }, - { url = "https://files.pythonhosted.org/packages/33/79/277500a3ac37b3adf2b1c7ab59ddb72587104b7edb5d1a44f9b0a5af4704/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d169f76f8f6330ef4741eadda4cba8f0254c3ec472ed80ebb98d35fc50227d7c", size = 281969, upload-time = "2025-12-31T18:19:54.201Z" }, - { url = "https://files.pythonhosted.org/packages/08/d3/fe850eaf2e4b9be41fa4ae76c4d02bdf809a382d0d7b63cf71d84b47ecca/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d45e1940d5439f2f6fa1f8f1e4202861fb77335c7432f3fc197960af0c6f335d", size = 289745, upload-time = "2025-12-31T18:20:13.831Z" }, - { url = "https://files.pythonhosted.org/packages/f9/7e/ec4f384a15a95bbad15e359d9d63294c4842eee08f5c576ee22ff3c45035/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c8527fded11884789b0ecb9b7d93d9093e38dbfc47d4daefa948447e2397d10b", size = 407620, upload-time = "2025-12-31T18:20:31.987Z" }, - { url = "https://files.pythonhosted.org/packages/98/b8/feb49bf3a2539df193481915653da52a442395c35ffeaf1deb0a649bae87/fastcrc-0.3.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6530a28d45ca0754bbeca3a820ae0cce3ded7f3428ed67b587d3ac8ea45fc4aa", size = 307050, upload-time = "2025-12-31T18:21:09.566Z" }, - { url = "https://files.pythonhosted.org/packages/3c/00/93fe977525ccb06a491490f53042b3682f15e626263917e3e94cd66d877a/fastcrc-0.3.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4f1229f339b32987e4ad35ae500564a782ce4e62f260150928c0233f32bb6e83", size = 290287, upload-time = "2025-12-31T18:20:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/d7/e5/0b6ae9ca6cc8ae599ca298e8a42d96febbc69b52d285464bb994f0a682ff/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a56aa52c40d4293230d2751f136346d6a2b392fa2a38fe342754a6d9c238e", size = 464193, upload-time = "2025-12-31T18:22:02.338Z" }, - { url = "https://files.pythonhosted.org/packages/4a/97/7d4ed6342b244c30b81daabaa6eac591426e216455205e5c85b8566fcd19/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:5816694e6aac595cca7a4331c503ed00a22889e0f97a0fa82779ed27316c90ee", size = 559728, upload-time = "2025-12-31T18:22:21.143Z" }, - { url = "https://files.pythonhosted.org/packages/eb/4e/b9cac563d0692345bc9e6dfdc7db92695dd1b3b431ac8fe61ec1dbd6d637/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b598bb000c4290e1eb847ae20a1e07f4ad064d364c2471864fa4c8ccca0f22f6", size = 518422, upload-time = "2025-12-31T18:22:39.747Z" }, - { url = "https://files.pythonhosted.org/packages/a3/58/ef3751447b173ae84d046f90a7dac869b9ff4e11639694f60d8c04f784ea/fastcrc-0.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:56d564c7ec3dc8116c291963834f0925b4a32df1ea53d239fd470fcdde6b80e4", size = 488965, upload-time = "2025-12-31T18:22:59.006Z" }, - { url = "https://files.pythonhosted.org/packages/f7/35/77b6bc7a7e0396481b2fa73a8d72972225358e27464d2e244aa484767aa4/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:405011e48990c92b9276b449084cedab5c0d64d1a5f72f088b8a4d47de0fbbae", size = 283892, upload-time = "2025-12-31T18:19:59.267Z" }, - { url = "https://files.pythonhosted.org/packages/c2/1b/0b69cfc6fa1a4cb47a9c9a5b85018232e69f516d766bbc5bb923c175dafa/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1f4165fdc701385fa9a4bd431d9152ea0ec7943de9b48e7758ed38dc0acb4c48", size = 290946, upload-time = "2025-12-31T18:20:19.773Z" }, - { url = "https://files.pythonhosted.org/packages/17/56/6ffbb62317f053dd35901ab493e04fc52e579283d25a7741e6e0c399bd85/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b0def4dbe9587b6a4a592c52f1f66fa0e40a3d1aa9575707ec2fd959224a37", size = 408841, upload-time = "2025-12-31T18:20:37.593Z" }, - { url = "https://files.pythonhosted.org/packages/83/67/6dbd9039a0e827edbc11f5f25770a24fea979e7a0211bd880a42175170e2/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24dc361d014a98220866995f9b82019a5e9bacc4ba5802a07c3ceddcd5c4de9e", size = 308712, upload-time = "2025-12-31T18:21:14.907Z" }, - { url = "https://files.pythonhosted.org/packages/33/d5/cdc732e11d65f039c97e5bdbe06b2a52ea9b300798be86e0893b5422f224/fastcrc-0.3.5-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:20bef9df7276df191a3a9d500f1ea1508fc3b38c0a8982577d14977dabc928ad", size = 292413, upload-time = "2025-12-31T18:20:56.138Z" }, - { url = "https://files.pythonhosted.org/packages/3a/13/dda5bfb5bd11362d860c1fbe9211bd1bef1fe303c1636164bb1bd2980bf3/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e6332b6f685f9067e6fe89756246a9bb292b02ebb07127417ed470985ce99e4d", size = 466003, upload-time = "2025-12-31T18:22:07.705Z" }, - { url = "https://files.pythonhosted.org/packages/a0/35/1106f93d26e333d677c14989e9007dab348ab9d05edf36d088d0e4fb2c2b/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:e2338c08961021b74599b8ad4e2d573f1b32e9f21cdf4025cbe9835f1acec5ad", size = 561365, upload-time = "2025-12-31T18:22:26.775Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3c/1c386d858c398eb5f2b8e64d81075205bda2c60638a97a2f8186d640bbd3/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:7f1b6160e2fb9f06fb9cb9425dcbc0056fefefc42ec448d0b05a68ae37ccadca", size = 520807, upload-time = "2025-12-31T18:22:45.296Z" }, - { url = "https://files.pythonhosted.org/packages/76/0c/4ea0247b3783462206b1e9fd90f5fa43b2faee078a3b4a2ab457c4a8174e/fastcrc-0.3.5-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:33b0fdacf8a158140319bbd3a8f2aeeff7e26d8d48802ea32d5957355fd57526", size = 491332, upload-time = "2025-12-31T18:23:05.768Z" }, - { url = "https://files.pythonhosted.org/packages/80/76/587ffb201ff1ae0d891f148dab1b9e50fed1ec97d9961791d36ff6d0dc49/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f537661551c6bf334b69def2c593c6d9e09d0189ef79f283797ae7ae906d3737", size = 283824, upload-time = "2025-12-31T18:20:00.905Z" }, - { url = "https://files.pythonhosted.org/packages/c3/06/1ff2241e31777a63f92eec9b6aca7e5029b606c62b263bb4b6e71e695cb2/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b706331c2b1631dd8d551aebe1e3d17b3ab911e25b2e20e62c1d33a6193dd3fc", size = 290742, upload-time = "2025-12-31T18:20:20.967Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d0/85ed12802a49c5d136ca9e411432eef11b257330a2897e83967ff686901e/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f43950f77e6fd5d3435a9b4972cb7df73d736883ab30c3aea8db2b109c38c9c2", size = 408729, upload-time = "2025-12-31T18:20:38.831Z" }, - { url = "https://files.pythonhosted.org/packages/13/eb/1e7062d7cde651c2e6d3948a0feb826e9cf4f95478b54d2ec187aabb22f4/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d318159bbac3b5437f557e4b68daf2f2f5d0435996046fdd93d5fe8b4d577e1", size = 308351, upload-time = "2025-12-31T18:21:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/16/74/a47a8d955ff9c6c434cd2e3537bb9db60f5d0a1030702e3efa3aa0383d37/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba44180d802de71833170575ef23091bdd0a33ddc44c74bef408f589eddbe910", size = 287944, upload-time = "2025-12-31T18:21:39.647Z" }, - { url = "https://files.pythonhosted.org/packages/bf/67/406749fae0ecdd445e86f33e4994438b85fcbf754c671a72e53ce82bbf4d/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:2b5a320ec3e4cd92a8c49cd21abcaf5c5e317e9f1980fee09a1350371e593272", size = 292193, upload-time = "2025-12-31T18:20:57.676Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ca/fde844c0809f9ce9c60b17b4b3977659b0a38e9ecb17236e0dc71cb1b38d/fastcrc-0.3.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:562258863d349b1bb19f41f4d735fcc36101c332da18e6318a5773c54c73bff0", size = 300914, upload-time = "2025-12-31T18:21:28.105Z" }, - { url = "https://files.pythonhosted.org/packages/05/c3/76a684b9f8deb35e20f40953399d77474ee7c3830bd58b51455de874d2e4/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e503965227516516bfbffc42d3ddb5018b356a119bc07c009f3030ee2e7de90b", size = 465939, upload-time = "2025-12-31T18:22:09.277Z" }, - { url = "https://files.pythonhosted.org/packages/02/64/c3b6d51719d8816b1b4aa94aa745e119b4b9e6d4b02ad6856ddda67220f0/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:90890a523881e6005b1526451a8f050ad5a3302cf48084305044468314afe1ab", size = 561123, upload-time = "2025-12-31T18:22:28.135Z" }, - { url = "https://files.pythonhosted.org/packages/0d/46/296e6a81454b8d585fcbbe73bbf618856981030245704b2672e50d1910ff/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:e4ab063872bede0e88dc3ffd3815e3c4723c26f35ae6ef2cecd3acbaf7c36aff", size = 520641, upload-time = "2025-12-31T18:22:47.111Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/d5417aa573f502b7aa037a46e1279b4906511d2ad6bb93b0a531a454f393/fastcrc-0.3.5-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fa17dbea2c0984f204318d64da0c5109e8afc0f3fa218d836b42a6c4a6f6a27e", size = 491214, upload-time = "2025-12-31T18:23:07.131Z" }, -] - -[[package]] -name = "fastjsonschema" -version = "2.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, -] - -[[package]] -name = "fastrlock" -version = "0.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/b1/1c3d635d955f2b4bf34d45abf8f35492e04dbd7804e94ce65d9f928ef3ec/fastrlock-0.8.3.tar.gz", hash = "sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d", size = 79327, upload-time = "2024-12-17T11:03:39.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/02/3f771177380d8690812d5b2b7736dc6b6c8cd1c317e4572e65f823eede08/fastrlock-0.8.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd", size = 55094, upload-time = "2024-12-17T11:01:49.721Z" }, - { url = "https://files.pythonhosted.org/packages/9d/12/e201634810ac9aee59f93e3953cb39f98157d17c3fc9d44900f1209054e9/fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e", size = 49398, upload-time = "2024-12-17T11:01:53.514Z" }, - { url = "https://files.pythonhosted.org/packages/15/a1/439962ed439ff6f00b7dce14927e7830e02618f26f4653424220a646cd1c/fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62", size = 53334, upload-time = "2024-12-17T11:01:55.518Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/5e746ee6f3d7afbfbb0d794c16c71bfd5259a4e3fb1dda48baf31e46956c/fastrlock-0.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2", size = 51972, upload-time = "2024-12-17T11:02:01.384Z" }, - { url = "https://files.pythonhosted.org/packages/76/a7/8b91068f00400931da950f143fa0f9018bd447f8ed4e34bed3fe65ed55d2/fastrlock-0.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40", size = 30946, upload-time = "2024-12-17T11:02:03.491Z" }, - { url = "https://files.pythonhosted.org/packages/90/9e/647951c579ef74b6541493d5ca786d21a0b2d330c9514ba2c39f0b0b0046/fastrlock-0.8.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f", size = 55233, upload-time = "2024-12-17T11:02:04.795Z" }, - { url = "https://files.pythonhosted.org/packages/0d/ef/a13b8bab8266840bf38831d7bf5970518c02603d00a548a678763322d5bf/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5", size = 50222, upload-time = "2024-12-17T11:02:08.745Z" }, - { url = "https://files.pythonhosted.org/packages/01/e2/5e5515562b2e9a56d84659377176aef7345da2c3c22909a1897fe27e14dd/fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30", size = 54553, upload-time = "2024-12-17T11:02:10.925Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b9/ae6511e52738ba4e3a6adb7c6a20158573fbc98aab448992ece25abb0b07/fastrlock-0.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd", size = 52836, upload-time = "2024-12-17T11:02:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/88/3e/c26f8192c93e8e43b426787cec04bb46ac36e72b1033b7fe5a9267155fdf/fastrlock-0.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c", size = 31046, upload-time = "2024-12-17T11:02:15.033Z" }, - { url = "https://files.pythonhosted.org/packages/00/df/56270f2e10c1428855c990e7a7e5baafa9e1262b8e789200bd1d047eb501/fastrlock-0.8.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da", size = 55727, upload-time = "2024-12-17T11:02:17.26Z" }, - { url = "https://files.pythonhosted.org/packages/80/07/cdecb7aa976f34328372f1c4efd6c9dc1b039b3cc8d3f38787d640009a25/fastrlock-0.8.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670", size = 53924, upload-time = "2024-12-17T11:02:20.85Z" }, - { url = "https://files.pythonhosted.org/packages/62/04/9138943c2ee803d62a48a3c17b69de2f6fa27677a6896c300369e839a550/fastrlock-0.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4", size = 53261, upload-time = "2024-12-17T11:02:24.418Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4b/db35a52589764c7745a613b6943bbd018f128d42177ab92ee7dde88444f6/fastrlock-0.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c", size = 31235, upload-time = "2024-12-17T11:02:25.708Z" }, - { url = "https://files.pythonhosted.org/packages/92/74/7b13d836c3f221cff69d6f418f46c2a30c4b1fe09a8ce7db02eecb593185/fastrlock-0.8.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45", size = 54157, upload-time = "2024-12-17T11:02:29.196Z" }, - { url = "https://files.pythonhosted.org/packages/f9/4e/94480fb3fd93991dd6f4e658b77698edc343f57caa2870d77b38c89c2e3b/fastrlock-0.8.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259", size = 52535, upload-time = "2024-12-17T11:02:33.402Z" }, - { url = "https://files.pythonhosted.org/packages/63/1d/d4b7782ef59e57dd9dde69468cc245adafc3674281905e42fa98aac30a79/fastrlock-0.8.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a", size = 52044, upload-time = "2024-12-17T11:02:36.613Z" }, - { url = "https://files.pythonhosted.org/packages/28/a3/2ad0a0a69662fd4cf556ab8074f0de978ee9b56bff6ddb4e656df4aa9e8e/fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8", size = 30472, upload-time = "2024-12-17T11:02:37.983Z" }, -] - -[[package]] -name = "ffmpeg-python" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "future" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/5e/d5f9105d59c1325759d838af4e973695081fbbc97182baf73afc78dec266/ffmpeg-python-0.2.0.tar.gz", hash = "sha256:65225db34627c578ef0e11c8b1eb528bb35e024752f6f10b78c011f6f64c4127", size = 21543, upload-time = "2019-07-06T00:19:08.989Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/0c/56be52741f75bad4dc6555991fabd2e07b432d333da82c11ad701123888a/ffmpeg_python-0.2.0-py3-none-any.whl", hash = "sha256:ac441a0404e053f8b6a1113a77c0f452f1cfc62f6344a769475ffdc0f56c23c5", size = 25024, upload-time = "2019-07-06T00:19:07.215Z" }, -] - -[[package]] -name = "filelock" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/f7/5e0dec5165ca52203d9f2c248db0a72dd31d6f15aad0b1e4a874f2187452/filelock-3.23.0.tar.gz", hash = "sha256:f64442f6f4707b9385049bb490be0bc48e3ab8e74ad27d4063435252917f4d4b", size = 32798, upload-time = "2026-02-14T02:53:58.703Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/10/da216e25ef2f3c9dfa75574aa27f5f4c7e5fb5540308f04e4d8c4d834ecb/filelock-3.23.0-py3-none-any.whl", hash = "sha256:4203c3f43983c7c95e4bbb68786f184f6acb7300899bf99d686bb82d526bdf62", size = 22227, upload-time = "2026-02-14T02:53:56.122Z" }, -] - -[[package]] -name = "filterpy" -version = "1.4.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/1d/ac8914360460fafa1990890259b7fa5ef7ba4cd59014e782e4ab3ab144d8/filterpy-1.4.5.zip", hash = "sha256:4f2a4d39e4ea601b9ab42b2db08b5918a9538c168cff1c6895ae26646f3d73b1", size = 177985, upload-time = "2018-10-10T22:38:24.63Z" } - -[[package]] -name = "flake8" -version = "7.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pyflakes" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/58/16/3f2a0bb700ad65ac9663262905a025917c020a3f92f014d2ba8964b4602c/flake8-7.1.2.tar.gz", hash = "sha256:c586ffd0b41540951ae41af572e6790dbd49fc12b3aa2541685d253d9bd504bd", size = 48119, upload-time = "2025-02-16T18:45:44.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/f8/08d37b2cd89da306e3520bd27f8a85692122b42b56c0c2c3784ff09c022f/flake8-7.1.2-py2.py3-none-any.whl", hash = "sha256:1cbc62e65536f65e6d754dfe6f1bada7f5cf392d6f5db3c2b85892466c3e7c1a", size = 57745, upload-time = "2025-02-16T18:45:42.351Z" }, -] - -[[package]] -name = "flask" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, -] - -[[package]] -name = "flask-cors" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, -] - -[[package]] -name = "flask-socketio" -version = "5.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "python-socketio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/28/deac60f5c6faf9c3e0aed07aa3a92b0741c6709841aa3eba12417bbc8303/flask_socketio-5.6.0.tar.gz", hash = "sha256:42a7bc552013633875ad320e39462323b4f7334594f1658d72b6ffed99940d4c", size = 37667, upload-time = "2025-12-25T19:30:26.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/f9/6a743926417124d5c6dcbc056d569b8bde7be73596404d35881a3ff1496e/flask_socketio-5.6.0-py3-none-any.whl", hash = "sha256:894ad031d9440ca3fad388dd301ca33d13b301a2563933ca608d30979ef0a7c1", size = 18397, upload-time = "2025-12-25T19:30:24.928Z" }, -] - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, -] - -[[package]] -name = "flax" -version = "0.10.7" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "msgpack", marker = "python_full_version < '3.11'" }, - { name = "optax", marker = "python_full_version < '3.11'" }, - { name = "orbax-checkpoint", marker = "python_full_version < '3.11'" }, - { name = "pyyaml", marker = "python_full_version < '3.11'" }, - { name = "rich", marker = "python_full_version < '3.11'" }, - { name = "tensorstore", version = "0.1.78", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "treescope", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e6/76/4ea55a60a47e98fcff591238ee26ed4624cb4fdc4893aa3ebf78d0d021f4/flax-0.10.7.tar.gz", hash = "sha256:2930d6671e23076f6db3b96afacf45c5060898f5c189ecab6dda7e05d26c2085", size = 5136099, upload-time = "2025-07-02T06:10:07.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/f6/560d338687d40182c8429cf35c64cc022e0d57ba3e52191c4a78ed239b4e/flax-0.10.7-py3-none-any.whl", hash = "sha256:4033223a9a9969ba0b252e085e9714d0a1e9124ac300aaf48e92c40769c420f6", size = 456944, upload-time = "2025-07-02T06:10:05.807Z" }, -] - -[[package]] -name = "flax" -version = "0.12.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "msgpack", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "optax", marker = "python_full_version >= '3.11'" }, - { name = "orbax-checkpoint", marker = "python_full_version >= '3.11'" }, - { name = "orbax-export", marker = "python_full_version >= '3.11'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "rich", marker = "python_full_version >= '3.11'" }, - { name = "tensorstore", version = "0.1.81", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "treescope", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/81/802fd686d3f47d7560a83f73b23efff03de7e3a0342e4f0fc41680136709/flax-0.12.4.tar.gz", hash = "sha256:5e924734a0595ddfa06a824568617e5440c7948e744772cbe6101b7ae06d66a9", size = 5070824, upload-time = "2026-02-12T19:10:17.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e9/bf4bbcf9d3a5634531cb0bcbec96db13353a9113fdc424464223234780fb/flax-0.12.4-py3-none-any.whl", hash = "sha256:cf90707923cb8a6d1a542039dd61e470c94bb11d7cac2349941a07f66605b19e", size = 493441, upload-time = "2026-02-12T19:10:14.847Z" }, -] - -[[package]] -name = "fonttools" -version = "4.61.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/94/8a28707adb00bed1bf22dac16ccafe60faf2ade353dcb32c3617ee917307/fonttools-4.61.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c7db70d57e5e1089a274cbb2b1fd635c9a24de809a231b154965d415d6c6d24", size = 2854799, upload-time = "2025-12-12T17:29:27.5Z" }, - { url = "https://files.pythonhosted.org/packages/94/93/c2e682faaa5ee92034818d8f8a8145ae73eb83619600495dcf8503fa7771/fonttools-4.61.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5fe9fd43882620017add5eabb781ebfbc6998ee49b35bd7f8f79af1f9f99a958", size = 2403032, upload-time = "2025-12-12T17:29:30.115Z" }, - { url = "https://files.pythonhosted.org/packages/f1/62/1748f7e7e1ee41aa52279fd2e3a6d0733dc42a673b16932bad8e5d0c8b28/fonttools-4.61.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8db08051fc9e7d8bc622f2112511b8107d8f27cd89e2f64ec45e9825e8288da", size = 4897863, upload-time = "2025-12-12T17:29:32.535Z" }, - { url = "https://files.pythonhosted.org/packages/69/69/4ca02ee367d2c98edcaeb83fc278d20972502ee071214ad9d8ca85e06080/fonttools-4.61.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a76d4cb80f41ba94a6691264be76435e5f72f2cb3cab0b092a6212855f71c2f6", size = 4859076, upload-time = "2025-12-12T17:29:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f5/660f9e3cefa078861a7f099107c6d203b568a6227eef163dd173bfc56bdc/fonttools-4.61.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a13fc8aeb24bad755eea8f7f9d409438eb94e82cf86b08fe77a03fbc8f6a96b1", size = 4875623, upload-time = "2025-12-12T17:29:37.33Z" }, - { url = "https://files.pythonhosted.org/packages/63/d1/9d7c5091d2276ed47795c131c1bf9316c3c1ab2789c22e2f59e0572ccd38/fonttools-4.61.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b846a1fcf8beadeb9ea4f44ec5bdde393e2f1569e17d700bfc49cd69bde75881", size = 4993327, upload-time = "2025-12-12T17:29:39.781Z" }, - { url = "https://files.pythonhosted.org/packages/6f/2d/28def73837885ae32260d07660a052b99f0aa00454867d33745dfe49dbf0/fonttools-4.61.1-cp310-cp310-win32.whl", hash = "sha256:78a7d3ab09dc47ac1a363a493e6112d8cabed7ba7caad5f54dbe2f08676d1b47", size = 1502180, upload-time = "2025-12-12T17:29:42.217Z" }, - { url = "https://files.pythonhosted.org/packages/63/fa/bfdc98abb4dd2bd491033e85e3ba69a2313c850e759a6daa014bc9433b0f/fonttools-4.61.1-cp310-cp310-win_amd64.whl", hash = "sha256:eff1ac3cc66c2ac7cda1e64b4e2f3ffef474b7335f92fc3833fc632d595fcee6", size = 1550654, upload-time = "2025-12-12T17:29:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/69/12/bf9f4eaa2fad039356cc627587e30ed008c03f1cebd3034376b5ee8d1d44/fonttools-4.61.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6604b735bb12fef8e0efd5578c9fb5d3d8532d5001ea13a19cddf295673ee09", size = 2852213, upload-time = "2025-12-12T17:29:46.675Z" }, - { url = "https://files.pythonhosted.org/packages/ac/49/4138d1acb6261499bedde1c07f8c2605d1d8f9d77a151e5507fd3ef084b6/fonttools-4.61.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ce02f38a754f207f2f06557523cd39a06438ba3aafc0639c477ac409fc64e37", size = 2401689, upload-time = "2025-12-12T17:29:48.769Z" }, - { url = "https://files.pythonhosted.org/packages/e5/fe/e6ce0fe20a40e03aef906af60aa87668696f9e4802fa283627d0b5ed777f/fonttools-4.61.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77efb033d8d7ff233385f30c62c7c79271c8885d5c9657d967ede124671bbdfb", size = 5058809, upload-time = "2025-12-12T17:29:51.701Z" }, - { url = "https://files.pythonhosted.org/packages/79/61/1ca198af22f7dd22c17ab86e9024ed3c06299cfdb08170640e9996d501a0/fonttools-4.61.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:75c1a6dfac6abd407634420c93864a1e274ebc1c7531346d9254c0d8f6ca00f9", size = 5036039, upload-time = "2025-12-12T17:29:53.659Z" }, - { url = "https://files.pythonhosted.org/packages/99/cc/fa1801e408586b5fce4da9f5455af8d770f4fc57391cd5da7256bb364d38/fonttools-4.61.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0de30bfe7745c0d1ffa2b0b7048fb7123ad0d71107e10ee090fa0b16b9452e87", size = 5034714, upload-time = "2025-12-12T17:29:55.592Z" }, - { url = "https://files.pythonhosted.org/packages/bf/aa/b7aeafe65adb1b0a925f8f25725e09f078c635bc22754f3fecb7456955b0/fonttools-4.61.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58b0ee0ab5b1fc9921eccfe11d1435added19d6494dde14e323f25ad2bc30c56", size = 5158648, upload-time = "2025-12-12T17:29:57.861Z" }, - { url = "https://files.pythonhosted.org/packages/99/f9/08ea7a38663328881384c6e7777bbefc46fd7d282adfd87a7d2b84ec9d50/fonttools-4.61.1-cp311-cp311-win32.whl", hash = "sha256:f79b168428351d11e10c5aeb61a74e1851ec221081299f4cf56036a95431c43a", size = 2280681, upload-time = "2025-12-12T17:29:59.943Z" }, - { url = "https://files.pythonhosted.org/packages/07/ad/37dd1ae5fa6e01612a1fbb954f0927681f282925a86e86198ccd7b15d515/fonttools-4.61.1-cp311-cp311-win_amd64.whl", hash = "sha256:fe2efccb324948a11dd09d22136fe2ac8a97d6c1347cf0b58a911dcd529f66b7", size = 2331951, upload-time = "2025-12-12T17:30:02.254Z" }, - { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" }, - { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" }, - { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" }, - { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" }, - { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" }, - { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" }, - { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" }, - { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" }, - { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" }, - { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" }, - { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" }, - { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" }, - { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" }, - { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" }, - { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" }, - { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" }, - { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" }, - { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" }, - { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" }, - { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" }, - { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" }, - { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" }, - { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" }, - { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" }, - { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, -] - -[[package]] -name = "foxglove-websocket" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/db/b5/df32ac550eb0df9000ed78d872eb19738edecfd88f47fe08588d5066f317/foxglove_websocket-0.1.4.tar.gz", hash = "sha256:2ec8936982e478d103dd90268a572599fc0cce45a4ab95490d5bc31f7c8a8af8", size = 16616, upload-time = "2025-07-14T20:26:28.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/73/3a3e6cb864ddf98800a9236ad497d32e5b50eb1682ac659f7d669d92faec/foxglove_websocket-0.1.4-py3-none-any.whl", hash = "sha256:772e24e2c98bdfc704df53f7177c8ff5bab0abc4dac59a91463aca16debdd83a", size = 14392, upload-time = "2025-07-14T20:26:26.899Z" }, -] - -[[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]] -name = "ftfy" -version = "6.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, -] - -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - -[[package]] -name = "gdown" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "filelock" }, - { name = "requests", extra = ["socks"] }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/6a/37e6b70c5bda3161e40265861e63b64a86bfc6ca6a8f1c35328a675c84fd/gdown-5.2.0.tar.gz", hash = "sha256:2145165062d85520a3cd98b356c9ed522c5e7984d408535409fd46f94defc787", size = 284647, upload-time = "2024-05-12T06:45:12.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/70/e07c381e6488a77094f04c85c9caf1c8008cdc30778f7019bc52e5285ef0/gdown-5.2.0-py3-none-any.whl", hash = "sha256:33083832d82b1101bdd0e9df3edd0fbc0e1c5f14c9d8c38d2a35bf1683b526d6", size = 18235, upload-time = "2024-05-12T06:45:10.017Z" }, -] - -[[package]] -name = "glfw" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/72/642d4f12f61816ac96777f7360d413e3977a7dd08237d196f02da681b186/glfw-2.10.0.tar.gz", hash = "sha256:801e55d8581b34df9aa2cfea43feb06ff617576e2a8cc5dac23ee75b26d10abe", size = 31475, upload-time = "2025-09-12T08:54:38.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/1f/a9ce08b1173b0ab625ee92f0c47a5278b3e76fd367699880d8ee7d56c338/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_10_6_intel.whl", hash = "sha256:5f365a8c94bcea71ec91327e7c16e7cf739128479a18b8c1241b004b40acc412", size = 105329, upload-time = "2025-09-12T08:54:27.938Z" }, - { url = "https://files.pythonhosted.org/packages/7c/96/5a2220abcbd027eebcf8bedd28207a2de168899e51be13ba01ebdd4147a1/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-macosx_11_0_arm64.whl", hash = "sha256:5328db1a92d07abd988730517ec02aa8390d3e6ef7ce98c8b57ecba2f43a39ba", size = 102179, upload-time = "2025-09-12T08:54:29.163Z" }, - { url = "https://files.pythonhosted.org/packages/9d/41/a5bd1d9e1808f400102bd7d328c4ac17b65fb2fc8014014ec6f23d02f662/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_aarch64.whl", hash = "sha256:312c4c1dd5509613ed6bc1e95a8dbb75a36b6dcc4120f50dc3892b40172e9053", size = 230039, upload-time = "2025-09-12T08:54:30.201Z" }, - { url = "https://files.pythonhosted.org/packages/80/aa/3b503c448609dee6cb4e7138b4109338f0e65b97be107ab85562269d378d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux2014_x86_64.whl", hash = "sha256:59c53387dc08c62e8bed86bbe3a8d53ab1b27161281ffa0e7f27b64284e2627c", size = 241984, upload-time = "2025-09-12T08:54:31.347Z" }, - { url = "https://files.pythonhosted.org/packages/ac/2d/bfe39a42cad8e80b02bf5f7cae19ba67832c1810bbd3624a8e83153d74a4/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_aarch64.whl", hash = "sha256:c6f292fdaf3f9a99e598ede6582d21c523a6f51f8f5e66213849101a6bcdc699", size = 231052, upload-time = "2025-09-12T08:54:32.859Z" }, - { url = "https://files.pythonhosted.org/packages/f7/02/6e639e90f181dc9127046e00d0528f9f7ad12d428972e3a5378b9aefdb0b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-manylinux_2_28_x86_64.whl", hash = "sha256:7916034efa867927892635733a3b6af8cd95ceb10566fd7f1e0d2763c2ee8b12", size = 243525, upload-time = "2025-09-12T08:54:34.006Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/cb588ca65561defe0fc48d1df4c2ac12569b81231ae4f2b52ab37007d0bd/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win32.whl", hash = "sha256:6c9549da71b93e367b4d71438798daae1da2592039fd14204a80a1a2348ae127", size = 552685, upload-time = "2025-09-12T08:54:35.723Z" }, - { url = "https://files.pythonhosted.org/packages/86/27/00c9c96af18ac0a5eac2ff61cbe306551a2d770d7173f396d0792ee1a59e/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.p39.p310.p311.p312.p313-none-win_amd64.whl", hash = "sha256:6292d5d6634d668cd23d337e6089491d3945a9aa4ac6e1667b0003520d7caa51", size = 559466, upload-time = "2025-09-12T08:54:37.661Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/de0b33f6f00687499ca1371f22aa73396341b85bf88f1a284f9da8842493/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_10_6_intel.whl", hash = "sha256:2aab89d2d9535635ba011fc7303390685169a1aa6731ad580d08d043524b8899", size = 105326, upload-time = "2026-01-28T05:57:56.083Z" }, - { url = "https://files.pythonhosted.org/packages/b6/a6/6ea2f73ad4474896d9e38b3ffbe6ffd5a802c738392269e99e8c6621a461/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:23936202a107039b5372f0b88ae1d11080746aa1c78910a45d4a0c4cf408cfaa", size = 102180, upload-time = "2026-01-28T05:57:57.787Z" }, - { url = "https://files.pythonhosted.org/packages/58/19/d81b19e8261b9cb51b81d1402167791fef81088dfe91f0c4e9d136fdc5ca/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_aarch64.whl", hash = "sha256:7be06d0838f61df67bd54cb6266a6193d54083acb3624ff3c3812a6358406fa4", size = 230038, upload-time = "2026-01-28T05:57:59.105Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/b035636cd82198b97b51a93efe9cfc4343d6b15cefbd336a3f2be871d848/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux2014_x86_64.whl", hash = "sha256:91d36b3582a766512eff8e3b5dcc2d3ffcbf10b7cf448551085a08a10f1b8244", size = 241983, upload-time = "2026-01-28T05:58:00.352Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b4/f7b6cc022dd7c68b6c702d19da5d591f978f89c958b9bd3090615db0c739/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_aarch64.whl", hash = "sha256:27c9e9a2d5e1dc3c9e3996171d844d9df9a5a101e797cb94cce217b7afcf8fd9", size = 231053, upload-time = "2026-01-28T05:58:01.683Z" }, - { url = "https://files.pythonhosted.org/packages/5a/3f/efeb7c6801c46e11bd666a5180f0d615f74f72264212f74f39586c6fda9d/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-manylinux_2_28_x86_64.whl", hash = "sha256:ce6724bb7cb3d0543dcba17206dce909f94176e68220b8eafee72e9f92bcf542", size = 243522, upload-time = "2026-01-28T05:58:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/cf/b9/b04c3aa0aad2870cfe799f32f8b59789c98e1816bbce9e83f4823c5b840b/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win32.whl", hash = "sha256:fca724a21a372731edb290841edd28a9fb1ee490f833392752844ac807c0086a", size = 552682, upload-time = "2026-01-28T05:58:05.649Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e1/6d6816b296a529ac9b897ad228b1e084eb1f92319e96371880eebdc874a6/glfw-2.10.0-py2.py27.py3.py30.py31.py32.py33.py34.py35.py36.py37.py38.py39.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:823c0bd7770977d4b10e0ed0aef2f3682276b7c88b8b65cfc540afce5951392f", size = 559464, upload-time = "2026-01-28T05:58:07.261Z" }, -] - -[[package]] -name = "google-crc32c" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, - { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, - { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, - { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, - { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, -] - -[[package]] -name = "googlemaps" -version = "4.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fe/26/bca4d737a9acea25e94c19940a780bbf0be64a691f7caf3a68467d3a5838/googlemaps-4.10.0.tar.gz", hash = "sha256:3055fcbb1aa262a9159b589b5e6af762b10e80634ae11c59495bd44867e47d88", size = 33056, upload-time = "2023-01-26T16:45:02.501Z" } - -[[package]] -name = "grpcio" -version = "1.78.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/a8/690a085b4d1fe066130de97a87de32c45062cf2ecd218df9675add895550/grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5", size = 5946986, upload-time = "2026-02-06T09:54:34.043Z" }, - { url = "https://files.pythonhosted.org/packages/c7/1b/e5213c5c0ced9d2d92778d30529ad5bb2dcfb6c48c4e2d01b1f302d33d64/grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2", size = 11816533, upload-time = "2026-02-06T09:54:37.04Z" }, - { url = "https://files.pythonhosted.org/packages/18/37/1ba32dccf0a324cc5ace744c44331e300b000a924bf14840f948c559ede7/grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d", size = 6519964, upload-time = "2026-02-06T09:54:40.268Z" }, - { url = "https://files.pythonhosted.org/packages/ed/f5/c0e178721b818072f2e8b6fde13faaba942406c634009caf065121ce246b/grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb", size = 7198058, upload-time = "2026-02-06T09:54:42.389Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b2/40d43c91ae9cd667edc960135f9f08e58faa1576dc95af29f66ec912985f/grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7", size = 6727212, upload-time = "2026-02-06T09:54:44.91Z" }, - { url = "https://files.pythonhosted.org/packages/ed/88/9da42eed498f0efcfcd9156e48ae63c0cde3bea398a16c99fb5198c885b6/grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec", size = 7300845, upload-time = "2026-02-06T09:54:47.562Z" }, - { url = "https://files.pythonhosted.org/packages/23/3f/1c66b7b1b19a8828890e37868411a6e6925df5a9030bfa87ab318f34095d/grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a", size = 8284605, upload-time = "2026-02-06T09:54:50.475Z" }, - { url = "https://files.pythonhosted.org/packages/94/c4/ca1bd87394f7b033e88525384b4d1e269e8424ab441ea2fba1a0c5b50986/grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813", size = 7726672, upload-time = "2026-02-06T09:54:53.11Z" }, - { url = "https://files.pythonhosted.org/packages/41/09/f16e487d4cc65ccaf670f6ebdd1a17566b965c74fc3d93999d3b2821e052/grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de", size = 4076715, upload-time = "2026-02-06T09:54:55.549Z" }, - { url = "https://files.pythonhosted.org/packages/2a/32/4ce60d94e242725fd3bcc5673c04502c82a8e87b21ea411a63992dc39f8f/grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf", size = 4799157, upload-time = "2026-02-06T09:54:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" }, - { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" }, - { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" }, - { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" }, - { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" }, - { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" }, - { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" }, - { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" }, - { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" }, - { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" }, - { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" }, - { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" }, - { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" }, - { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" }, - { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" }, - { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" }, - { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" }, - { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" }, - { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" }, - { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" }, - { url = "https://files.pythonhosted.org/packages/29/f2/b56e43e3c968bfe822fa6ce5bca10d5c723aa40875b48791ce1029bb78c7/grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e", size = 5920591, upload-time = "2026-02-06T09:56:20.758Z" }, - { url = "https://files.pythonhosted.org/packages/5d/81/1f3b65bd30c334167bfa8b0d23300a44e2725ce39bba5b76a2460d85f745/grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f", size = 11813685, upload-time = "2026-02-06T09:56:24.315Z" }, - { url = "https://files.pythonhosted.org/packages/0e/1c/bbe2f8216a5bd3036119c544d63c2e592bdf4a8ec6e4a1867592f4586b26/grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724", size = 6487803, upload-time = "2026-02-06T09:56:27.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/5c/a6b2419723ea7ddce6308259a55e8e7593d88464ce8db9f4aa857aba96fa/grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b", size = 7173206, upload-time = "2026-02-06T09:56:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/df/1e/b8801345629a415ea7e26c83d75eb5dbe91b07ffe5210cc517348a8d4218/grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7", size = 6693826, upload-time = "2026-02-06T09:56:32.305Z" }, - { url = "https://files.pythonhosted.org/packages/34/84/0de28eac0377742679a510784f049738a80424b17287739fc47d63c2439e/grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452", size = 7277897, upload-time = "2026-02-06T09:56:34.915Z" }, - { url = "https://files.pythonhosted.org/packages/ca/9c/ad8685cfe20559a9edb66f735afdcb2b7d3de69b13666fdfc542e1916ebd/grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127", size = 8252404, upload-time = "2026-02-06T09:56:37.553Z" }, - { url = "https://files.pythonhosted.org/packages/3c/05/33a7a4985586f27e1de4803887c417ec7ced145ebd069bc38a9607059e2b/grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65", size = 7696837, upload-time = "2026-02-06T09:56:40.173Z" }, - { url = "https://files.pythonhosted.org/packages/73/77/7382241caf88729b106e49e7d18e3116216c778e6a7e833826eb96de22f7/grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c", size = 4142439, upload-time = "2026-02-06T09:56:43.258Z" }, - { url = "https://files.pythonhosted.org/packages/48/b2/b096ccce418882fbfda4f7496f9357aaa9a5af1896a9a7f60d9f2b275a06/grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb", size = 4929852, upload-time = "2026-02-06T09:56:45.885Z" }, -] - -[[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 = "h2" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, -] - -[[package]] -name = "hf-xet" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, - { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, - { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, - { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, - { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, -] - -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, -] - -[[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 = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, - { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, - { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, - { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, - { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, - { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, - { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, - { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, - { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, - { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, - { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, -] - -[[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.optional-dependencies] -http2 = [ - { name = "h2" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "huggingface-hub" -version = "0.36.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, -] - -[[package]] -name = "humanize" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/66/a3921783d54be8a6870ac4ccffcd15c4dc0dd7fcce51c6d63b8c63935276/humanize-4.15.0.tar.gz", hash = "sha256:1dd098483eb1c7ee8e32eb2e99ad1910baefa4b75c3aff3a82f4d78688993b10", size = 83599, upload-time = "2025-12-20T20:16:13.19Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/7b/bca5613a0c3b542420cf92bd5e5fb8ebd5435ce1011a091f66bb7693285e/humanize-4.15.0-py3-none-any.whl", hash = "sha256:b1186eb9f5a9749cd9cb8565aee77919dd7c8d076161cf44d70e59e3301e1769", size = 132203, upload-time = "2025-12-20T20:16:11.67Z" }, -] - -[[package]] -name = "hydra-core" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "omegaconf" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, -] - -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, -] - -[[package]] -name = "identify" -version = "2.6.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "ifaddr" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/ac/fb4c578f4a3256561548cd825646680edcadb9440f3f68add95ade1eb791/ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4", size = 10485, upload-time = "2022-06-15T21:40:27.561Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1f/19ebc343cc71a7ffa78f17018535adc5cbdd87afb31d7c34874680148b32/ifaddr-0.2.0-py3-none-any.whl", hash = "sha256:085e0305cfe6f16ab12d72e2024030f5d52674afad6911bb1eee207177b8a748", size = 12314, upload-time = "2022-06-15T21:40:25.756Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - -[[package]] -name = "importlib-resources" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "iopath" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "portalocker" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/73/b3d451dfc523756cf177d3ebb0af76dc7751b341c60e2a21871be400ae29/iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01", size = 42226, upload-time = "2022-07-09T19:00:50.866Z" } - -[[package]] -name = "ipykernel" -version = "7.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "comm" }, - { name = "debugpy" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "matplotlib-inline" }, - { name = "nest-asyncio" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/8d/b68b728e2d06b9e0051019640a40a9eb7a88fcd82c2e1b5ce70bef5ff044/ipykernel-7.2.0.tar.gz", hash = "sha256:18ed160b6dee2cbb16e5f3575858bc19d8f1fe6046a9a680c708494ce31d909e", size = 176046, upload-time = "2026-02-06T16:43:27.403Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/b9/e73d5d9f405cba7706c539aa8b311b49d4c2f3d698d9c12f815231169c71/ipykernel-7.2.0-py3-none-any.whl", hash = "sha256:3bbd4420d2b3cc105cbdf3756bfc04500b1e52f090a90716851f3916c62e1661", size = 118788, upload-time = "2026-02-06T16:43:25.149Z" }, -] - -[[package]] -name = "ipython" -version = "8.38.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version < '3.11'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "jedi", marker = "python_full_version < '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version < '3.11'" }, - { name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "stack-data", marker = "python_full_version < '3.11'" }, - { name = "traitlets", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/61/1810830e8b93c72dcd3c0f150c80a00c3deb229562d9423807ec92c3a539/ipython-8.38.0.tar.gz", hash = "sha256:9cfea8c903ce0867cc2f23199ed8545eb741f3a69420bfcf3743ad1cec856d39", size = 5513996, upload-time = "2026-01-05T10:59:06.901Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/df/db59624f4c71b39717c423409950ac3f2c8b2ce4b0aac843112c7fb3f721/ipython-8.38.0-py3-none-any.whl", hash = "sha256:750162629d800ac65bb3b543a14e7a74b0e88063eac9b92124d4b2aa3f6d8e86", size = 831813, upload-time = "2026-01-05T10:59:04.239Z" }, -] - -[[package]] -name = "ipython" -version = "9.10.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "decorator", marker = "python_full_version >= '3.11'" }, - { name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" }, - { name = "jedi", marker = "python_full_version >= '3.11'" }, - { name = "matplotlib-inline", marker = "python_full_version >= '3.11'" }, - { name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "prompt-toolkit", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "stack-data", marker = "python_full_version >= '3.11'" }, - { name = "traitlets", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version == '3.11.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" }, -] - -[[package]] -name = "ipython-pygments-lexers" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, -] - -[[package]] -name = "isort" -version = "7.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jax" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "opt-einsum", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cf/1e/267f59c8fb7f143c3f778c76cb7ef1389db3fd7e4540f04b9f42ca90764d/jax-0.6.2.tar.gz", hash = "sha256:a437d29038cbc8300334119692744704ca7941490867b9665406b7f90665cd96", size = 2334091, upload-time = "2025-06-17T23:10:27.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/a8/97ef0cbb7a17143ace2643d600a7b80d6705b2266fc31078229e406bdef2/jax-0.6.2-py3-none-any.whl", hash = "sha256:bb24a82dc60ccf704dcaf6dbd07d04957f68a6c686db19630dd75260d1fb788c", size = 2722396, upload-time = "2025-06-17T23:10:25.293Z" }, -] - -[[package]] -name = "jax" -version = "0.9.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "opt-einsum", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/40/f85d1feadd8f793fc1bfab726272523ef34b27302b55861ea872ec774019/jax-0.9.0.1.tar.gz", hash = "sha256:e395253449d74354fa813ff9e245acb6e42287431d8a01ff33d92e9ee57d36bd", size = 2534795, upload-time = "2026-02-05T18:47:33.088Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/1e/63ac22ec535e08129e16cb71b7eeeb8816c01d627ea1bc9105e925a71da0/jax-0.9.0.1-py3-none-any.whl", hash = "sha256:3baeaec6dc853394c272eb38a35ffba1972d67cf55d07a76bdb913bcd867e2ca", size = 2955477, upload-time = "2026-02-05T18:45:22.885Z" }, -] - -[[package]] -name = "jaxlib" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/c5/41598634c99cbebba46e6777286fb76abc449d33d50aeae5d36128ca8803/jaxlib-0.6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4601b2b5dc8c23d6afb293eacfb9aec4e1d1871cb2f29c5a151d103e73b0f8", size = 54298019, upload-time = "2025-06-17T23:10:36.916Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/db07d746cd5867d5967528e7811da53374e94f64e80a890d6a5a4b95b130/jaxlib-0.6.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:4205d098ce8efb5f7fe2fe5098bae6036094dc8d8829f5e0e0d7a9b155326336", size = 79440052, upload-time = "2025-06-17T23:10:41.282Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d8/b7ae9e819c62c1854dbc2c70540a5c041173fbc8bec5e78ab7fd615a4aee/jaxlib-0.6.2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:c087a0eb6fb7f6f8f54d56f4730328dfde5040dd3b5ddfa810e7c28ea7102b42", size = 89917034, upload-time = "2025-06-17T23:10:45.897Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/87e91bc70569ac5c3e3449eefcaf47986e892f10cfe1d5e5720dceae3068/jaxlib-0.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:153eaa51f778b60851720729d4f461a91edd9ba3932f6f3bc598d4413870038b", size = 57896337, upload-time = "2025-06-17T23:10:50.179Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/6899b0aed36a4acc51319465ddd83c7c300a062a9e236cceee00984ffe0b/jaxlib-0.6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a208ff61c58128d306bb4e5ad0858bd2b0960f2c1c10ad42c548f74a60c0020e", size = 54300346, upload-time = "2025-06-17T23:10:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/e6/03/34bb6b346609079a71942cfbf507892e3c877a06a430a0df8429c455cebc/jaxlib-0.6.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:11eae7e05bc5a79875da36324afb9eddd4baeaef2a0386caf6d4f3720b9aef28", size = 79438425, upload-time = "2025-06-17T23:10:58.356Z" }, - { url = "https://files.pythonhosted.org/packages/80/02/49b05cbab519ffd3cb79586336451fbbf8b6523f67128a794acc9f179000/jaxlib-0.6.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:335d7e3515ce78b52a410136f46aa4a7ea14d0e7d640f34e1e137409554ad0ac", size = 89920354, upload-time = "2025-06-17T23:11:03.086Z" }, - { url = "https://files.pythonhosted.org/packages/a7/7a/93b28d9452b46c15fc28dd65405672fc8a158b35d46beabaa0fe9631afb0/jaxlib-0.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:c6815509997d6b05e5c9daa7994b9ad473ce3e8c8a17bdbbcacc3c744f76f7a0", size = 57895707, upload-time = "2025-06-17T23:11:07.074Z" }, - { url = "https://files.pythonhosted.org/packages/ac/db/05e702d2534e87abf606b1067b46a273b120e6adc7d459696e3ce7399317/jaxlib-0.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:34d8a684a8be949dd87dd4acc97101b4106a0dc9ad151ec891da072319a57b99", size = 54301644, upload-time = "2025-06-17T23:11:10.977Z" }, - { url = "https://files.pythonhosted.org/packages/0d/8a/b0a96887b97a25d45ae2c30e4acecd2f95acd074c18ec737dda8c5cc7016/jaxlib-0.6.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:87ec2dc9c3ed9ab936eec8535160c5fbd2c849948559f1c5daa75f63fabe5942", size = 79439161, upload-time = "2025-06-17T23:11:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e8/71c2555431edb5dd115cf86a7b599aa7e1be26728d89ae59aa11251d299c/jaxlib-0.6.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f1dd09b481a93c1d4c750013f467f74194493ba7bd29fcd4d1cec16e3a214f65", size = 89942952, upload-time = "2025-06-17T23:11:19.181Z" }, - { url = "https://files.pythonhosted.org/packages/de/3a/06849113c844b86d20174df54735c84202ccf82cbd36d805f478c834418b/jaxlib-0.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:921dbd4db214eba19a29ba9f2450d880e08b2b2c7b968f28cc89da3e62366af4", size = 57919603, upload-time = "2025-06-17T23:11:23.207Z" }, - { url = "https://files.pythonhosted.org/packages/af/38/bed4279c2a3407820ed8bcd72dbad43c330ada35f88fafe9952b35abf785/jaxlib-0.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bff67b188133ce1f0111c7b163ac321fd646b59ed221ea489063e2e0f85cb967", size = 54300638, upload-time = "2025-06-17T23:11:26.372Z" }, - { url = "https://files.pythonhosted.org/packages/52/dc/9e35a1dc089ddf3d6be53ef2e6ba4718c5b6c0f90bccc535a20edac0c895/jaxlib-0.6.2-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:70498837caf538bd458ff6858c8bfd404db82015aba8f663670197fa9900ff02", size = 79439983, upload-time = "2025-06-17T23:11:30.016Z" }, - { url = "https://files.pythonhosted.org/packages/34/16/e93f0184b80a4e1ad38c6998aa3a2f7569c0b0152cbae39f7572393eda04/jaxlib-0.6.2-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:f94163f14c8fd3ba93ae14b631abacf14cb031bba0b59138869984b4d10375f8", size = 89941720, upload-time = "2025-06-17T23:11:34.62Z" }, - { url = "https://files.pythonhosted.org/packages/06/b9/ea50792ee0333dba764e06c305fe098bce1cb938dcb66fbe2fc47ef5dd02/jaxlib-0.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:b977604cd36c74b174d25ed685017379468138eb747d865f75e466cb273c801d", size = 57919073, upload-time = "2025-06-17T23:11:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/09/ce/9596391c104a0547fcaf6a8c72078bbae79dbc8e7f0843dc8318f6606328/jaxlib-0.6.2-cp313-cp313t-manylinux2014_aarch64.whl", hash = "sha256:39cf9555f85ae1ce2e2c1a59fc71f2eca4f9867a7cb934fef881ba56b11371d1", size = 79579638, upload-time = "2025-06-17T23:11:43.054Z" }, - { url = "https://files.pythonhosted.org/packages/10/79/f6e80f7f4cacfc9f03e64ac57ecb856b140de7c2f939b25f8dcf1aff63f9/jaxlib-0.6.2-cp313-cp313t-manylinux2014_x86_64.whl", hash = "sha256:3abd536e44b05fb1657507e3ff1fc3691f99613bae3921ecab9e82f27255f784", size = 90066675, upload-time = "2025-06-17T23:11:47.454Z" }, -] - -[[package]] -name = "jaxlib" -version = "0.9.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/fd/040321b0f4303ec7b558d69488c6130b1697c33d88dab0a0d2ccd2e0817c/jaxlib-0.9.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff2c550dab210278ed3a3b96454b19108a02e0795625be56dca5a181c9833c9", size = 56092920, upload-time = "2026-02-05T18:46:20.873Z" }, - { url = "https://files.pythonhosted.org/packages/e9/76/a558cd5e2ac8a2c16fe7f7e429dd5749cef48bc1a89941bb5b72bd3d7de3/jaxlib-0.9.0.1-cp311-cp311-manylinux_2_27_aarch64.whl", hash = "sha256:c4ac3cfd7aaacc37f37a6a332ee009dee39e3b5081bb4b473f410583436be553", size = 74767780, upload-time = "2026-02-05T18:46:23.917Z" }, - { url = "https://files.pythonhosted.org/packages/87/49/f72fb26e2feb100fd84d297a17111364b15d5979843f62b7539cd120f9bb/jaxlib-0.9.0.1-cp311-cp311-manylinux_2_27_x86_64.whl", hash = "sha256:dc95ee32ae2bd4ed947ad0218fd6576b50a60ce45b60714d7ff2fd9fa195ed9e", size = 80323754, upload-time = "2026-02-05T18:46:27.405Z" }, - { url = "https://files.pythonhosted.org/packages/55/fc/fa3c07d833a60cfb928f7a727fef25059e2e9af1dbc5d09821ad3a728292/jaxlib-0.9.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ed35e3300caa228c42897d8fbe961d6e03b797717e44eccbd3a788b5ac5c623", size = 60483840, upload-time = "2026-02-05T18:46:30.606Z" }, - { url = "https://files.pythonhosted.org/packages/c8/76/e89fd547f292663d8ce11b3247cd653a220e0d3cedbdbd094f0a8460d735/jaxlib-0.9.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3707bf0a58410da7c053c15ec6efee1fe12e70361416e055e4109b8041f4119b", size = 56104032, upload-time = "2026-02-05T18:46:33.606Z" }, - { url = "https://files.pythonhosted.org/packages/c1/92/40d4f0acecb3d6f7078b9eb468e524778a3497d0882c7ecf80509c10b7d3/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_aarch64.whl", hash = "sha256:5ea8ebd62165b6f18f89b02fab749e02f5c584c2a1c703f04592d4d803f9e981", size = 74769175, upload-time = "2026-02-05T18:46:36.767Z" }, - { url = "https://files.pythonhosted.org/packages/1d/89/0dd938e6ed65ee994a49351a13aceaea46235ffbc1db5444d9ba3a279814/jaxlib-0.9.0.1-cp312-cp312-manylinux_2_27_x86_64.whl", hash = "sha256:e0e4a0a24ef98ec021b913991fbda09aeb96481b1bc0e5300a0339aad216b226", size = 80339748, upload-time = "2026-02-05T18:46:40.148Z" }, - { url = "https://files.pythonhosted.org/packages/bb/02/265e5ccadd65fee2f0716431573d9e512e5c6aecb23f478a7a92053cf219/jaxlib-0.9.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:08733d1431238a7cf9108338ab7be898b97181cba0eef53f2f9fd3de17d20adb", size = 60508788, upload-time = "2026-02-05T18:46:43.209Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/f5a78b4d2a08e2d358e01527a3617af2df67c70231029ce1bdbb814219ff/jaxlib-0.9.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e857cafdd12e18493d96d4a290ed31aa9d99a0dc3056b4b42974c0f342c9bb0c", size = 56103168, upload-time = "2026-02-05T18:46:46.481Z" }, - { url = "https://files.pythonhosted.org/packages/47/c3/fd3a9e2f02c1a04a1a00ff74adb6dd09e34040587bbb1b51b0176151dfa1/jaxlib-0.9.0.1-cp313-cp313-manylinux_2_27_aarch64.whl", hash = "sha256:b73b85f927d9b006f07622d5676092eab916645c4804fed6568da5fb4a541dfc", size = 74768692, upload-time = "2026-02-05T18:46:49.571Z" }, - { url = "https://files.pythonhosted.org/packages/d9/48/34923a6add7dda5fb8f30409a98b638f0dbd2d9571dbbf73db958eaec44a/jaxlib-0.9.0.1-cp313-cp313-manylinux_2_27_x86_64.whl", hash = "sha256:54dd2d34c6bec4f099f888a2f7895069a47c3ba86aaa77b0b78e9c3f9ef948f1", size = 80337646, upload-time = "2026-02-05T18:46:53.299Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a9/629bed81406902653973d57de5af92842c7da63dfa8fcd84ee490c62ee94/jaxlib-0.9.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:27db7fbc49938f819f2a93fefef0bdc25bd523b499ab4d8a71ed8915c037c0b4", size = 60508306, upload-time = "2026-02-05T18:46:56.441Z" }, - { url = "https://files.pythonhosted.org/packages/45/e3/6943589aaa58d9934838e00c6149dd1fc81e0c8555e9fcc9f527648faf5c/jaxlib-0.9.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9312fcfb4c5586802c08bc1b3b2419e48aa2a4cd1356251fe791ad71edc2da2a", size = 56210697, upload-time = "2026-02-05T18:46:59.642Z" }, - { url = "https://files.pythonhosted.org/packages/7e/ff/39479759b71f1d281b77050184759ac76dfd23a3ae75132ef92d168099c5/jaxlib-0.9.0.1-cp313-cp313t-manylinux_2_27_aarch64.whl", hash = "sha256:b536512cf84a0cb031196d6d5233f7093745e87eb416e45ad96fbb764b2befed", size = 74882879, upload-time = "2026-02-05T18:47:02.708Z" }, - { url = "https://files.pythonhosted.org/packages/87/0d/e41eeddd761110d733688d6493defe776440c8f3d114419a8ecaef55601f/jaxlib-0.9.0.1-cp313-cp313t-manylinux_2_27_x86_64.whl", hash = "sha256:c4dc8828bb236532033717061d132906075452556b12d1ff6ccc10e569435dfe", size = 80438424, upload-time = "2026-02-05T18:47:06.437Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ec/54b1251cea5c74a2f0d22106f5d1c7dc9e7b6a000d6a81a88deffa34c6fe/jaxlib-0.9.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:43272e52e5c89dbc4f02c7ccb6ffa5d587a09ac8db5163cb0c43e125b7075129", size = 56101484, upload-time = "2026-02-05T18:47:09.46Z" }, - { url = "https://files.pythonhosted.org/packages/29/ce/91ba780439aa1e6bae964ea641169e8b9c9349c175fcb1a723b96ba54313/jaxlib-0.9.0.1-cp314-cp314-manylinux_2_27_aarch64.whl", hash = "sha256:82348cee1521d6123038c4c3beeafa2076c8f4ae29a233b8abff9d6dc8b44145", size = 74789558, upload-time = "2026-02-05T18:47:12.394Z" }, - { url = "https://files.pythonhosted.org/packages/ce/9b/3d7baca233c378b01fa445c9f63b260f592249ff69950baf893cea631b10/jaxlib-0.9.0.1-cp314-cp314-manylinux_2_27_x86_64.whl", hash = "sha256:e61e88032eeb31339c72ead9ed60c6153cd2222512624caadea67c350c78432e", size = 80343053, upload-time = "2026-02-05T18:47:16.042Z" }, - { url = "https://files.pythonhosted.org/packages/92/5d/80efe5295133d5114fb7b0f27bdf82bc7a2308356dde6ba77c2afbaa3a36/jaxlib-0.9.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:abd9f127d23705105683448781914f17898b2b6591a051b259e6b947d4dcb93f", size = 62826248, upload-time = "2026-02-05T18:47:19.986Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a9/f72578daa6af9bed9bda75b842c97581b31a577d7b2072daf8ba3d5a8156/jaxlib-0.9.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b01a75fbac8098cc985f6f1690bfb62f98b0785c84199287e0baaae50fa4238", size = 56209722, upload-time = "2026-02-05T18:47:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/95/ea/eefb118305dd5e1b0ad8d942f2bf43616c964d89fe491bec8628173da24d/jaxlib-0.9.0.1-cp314-cp314t-manylinux_2_27_aarch64.whl", hash = "sha256:76f23cbb109e673ea7a90781aca3e02a0c72464410c019fe14fba3c044f2b778", size = 74881382, upload-time = "2026-02-05T18:47:26.703Z" }, - { url = "https://files.pythonhosted.org/packages/0a/aa/a42fb912fd1f9c83e22dc2577cdfbf1a1b07d6660532cb44724db7a7c479/jaxlib-0.9.0.1-cp314-cp314t-manylinux_2_27_x86_64.whl", hash = "sha256:f80d30dedce96c73a7f5dcb79c4c827a1bde2304f502a56ce7e7f723df2a5398", size = 80438052, upload-time = "2026-02-05T18:47:30.039Z" }, -] - -[[package]] -name = "jaxopt" -version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3a/da/ff7d7fbd13b8ed5e8458e80308d075fc649062b9f8676d3fc56f2dc99a82/jaxopt-0.8.5.tar.gz", hash = "sha256:2790bd68ef132b216c083a8bc7a2704eceb35a92c0fc0a1e652e79dfb1e9e9ab", size = 121709, upload-time = "2025-04-14T17:59:01.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/d8/55e0901103c93d57bab3b932294c216f0cbd49054187ce29f8f13808d530/jaxopt-0.8.5-py3-none-any.whl", hash = "sha256:ff221d1a86908ec759eb1e219ee1d12bf208a70707e961bf7401076fe7cf4d5e", size = 172434, upload-time = "2025-04-14T17:59:00.342Z" }, -] - -[[package]] -name = "jaxtyping" -version = "0.3.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wadler-lindig", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/40/a2ea3ce0e3e5f540eb970de7792c90fa58fef1b27d34c83f9fa94fea4729/jaxtyping-0.3.7.tar.gz", hash = "sha256:3bd7d9beb7d3cb01a89f93f90581c6f4fff3e5c5dc3c9307e8f8687a040d10c4", size = 45721, upload-time = "2026-01-30T14:18:47.409Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/42/caf65e9a0576a3abadc537e2f831701ba9081f21317fb3be87d64451587a/jaxtyping-0.3.7-py3-none-any.whl", hash = "sha256:303ab8599edf412eeb40bf06c863e3168fa186cf0e7334703fa741ddd7046e66", size = 56101, upload-time = "2026-01-30T14:18:45.954Z" }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, -] - -[[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 = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/5a/41da76c5ea07bec1b0472b6b2fdb1b651074d504b19374d7e130e0cdfb25/jiter-0.13.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2ffc63785fd6c7977defe49b9824ae6ce2b2e2b77ce539bdaf006c26da06342e", size = 311164, upload-time = "2026-02-02T12:35:17.688Z" }, - { url = "https://files.pythonhosted.org/packages/40/cb/4a1bf994a3e869f0d39d10e11efb471b76d0ad70ecbfb591427a46c880c2/jiter-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a638816427006c1e3f0013eb66d391d7a3acda99a7b0cf091eff4497ccea33a", size = 320296, upload-time = "2026-02-02T12:35:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/09/82/acd71ca9b50ecebadc3979c541cd717cce2fe2bc86236f4fa597565d8f1a/jiter-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19928b5d1ce0ff8c1ee1b9bdef3b5bfc19e8304f1b904e436caf30bc15dc6cf5", size = 352742, upload-time = "2026-02-02T12:35:21.258Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/d1fc996f3aecfd42eb70922edecfb6dd26421c874503e241153ad41df94f/jiter-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:309549b778b949d731a2f0e1594a3f805716be704a73bf3ad9a807eed5eb5721", size = 363145, upload-time = "2026-02-02T12:35:24.653Z" }, - { url = "https://files.pythonhosted.org/packages/f1/61/a30492366378cc7a93088858f8991acd7d959759fe6138c12a4644e58e81/jiter-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bcdabaea26cb04e25df3103ce47f97466627999260290349a88c8136ecae0060", size = 487683, upload-time = "2026-02-02T12:35:26.162Z" }, - { url = "https://files.pythonhosted.org/packages/20/4e/4223cffa9dbbbc96ed821c5aeb6bca510848c72c02086d1ed3f1da3d58a7/jiter-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a377af27b236abbf665a69b2bdd680e3b5a0bd2af825cd3b81245279a7606c", size = 373579, upload-time = "2026-02-02T12:35:27.582Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c9/b0489a01329ab07a83812d9ebcffe7820a38163c6d9e7da644f926ff877c/jiter-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe49d3ff6db74321f144dff9addd4a5874d3105ac5ba7c5b77fac099cfae31ae", size = 362904, upload-time = "2026-02-02T12:35:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/05/af/53e561352a44afcba9a9bc67ee1d320b05a370aed8df54eafe714c4e454d/jiter-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2113c17c9a67071b0f820733c0893ed1d467b5fcf4414068169e5c2cabddb1e2", size = 392380, upload-time = "2026-02-02T12:35:30.385Z" }, - { url = "https://files.pythonhosted.org/packages/76/2a/dd805c3afb8ed5b326c5ae49e725d1b1255b9754b1b77dbecdc621b20773/jiter-0.13.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ab1185ca5c8b9491b55ebf6c1e8866b8f68258612899693e24a92c5fdb9455d5", size = 517939, upload-time = "2026-02-02T12:35:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/20/2a/7b67d76f55b8fe14c937e7640389612f05f9a4145fc28ae128aaa5e62257/jiter-0.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9621ca242547edc16400981ca3231e0c91c0c4c1ab8573a596cd9bb3575d5c2b", size = 551696, upload-time = "2026-02-02T12:35:33.306Z" }, - { url = "https://files.pythonhosted.org/packages/85/9c/57cdd64dac8f4c6ab8f994fe0eb04dc9fd1db102856a4458fcf8a99dfa62/jiter-0.13.0-cp310-cp310-win32.whl", hash = "sha256:a7637d92b1c9d7a771e8c56f445c7f84396d48f2e756e5978840ecba2fac0894", size = 204592, upload-time = "2026-02-02T12:35:34.58Z" }, - { url = "https://files.pythonhosted.org/packages/a7/38/f4f3ea5788b8a5bae7510a678cdc747eda0c45ffe534f9878ff37e7cf3b3/jiter-0.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:c1b609e5cbd2f52bb74fb721515745b407df26d7b800458bd97cb3b972c29e7d", size = 206016, upload-time = "2026-02-02T12:35:36.435Z" }, - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, -] - -[[package]] -name = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, -] - -[[package]] -name = "jsonpatch" -version = "1.33" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpointer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, -] - -[[package]] -name = "jsonpointer" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "jupyter-client" -version = "8.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/e4/ba649102a3bc3fbca54e7239fb924fd434c766f855693d86de0b1f2bec81/jupyter_client-8.8.0.tar.gz", hash = "sha256:d556811419a4f2d96c869af34e854e3f059b7cc2d6d01a9cd9c85c267691be3e", size = 348020, upload-time = "2026-01-08T13:55:47.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/0b/ceb7694d864abc0a047649aec263878acb9f792e1fec3e676f22dc9015e3/jupyter_client-8.8.0-py3-none-any.whl", hash = "sha256:f93a5b99c5e23a507b773d3a1136bd6e16c67883ccdbd9a829b0bbdb98cd7d7a", size = 107371, upload-time = "2026-01-08T13:55:45.562Z" }, -] - -[[package]] -name = "jupyter-core" -version = "5.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, -] - -[[package]] -name = "kaleido" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "choreographer" }, - { name = "logistro" }, - { name = "orjson" }, - { name = "packaging" }, - { name = "pytest-timeout" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/38/ad/76eec859b71eda803a88ea50ed3f270281254656bb23d19eb0a39aa706a0/kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef", size = 64160, upload-time = "2025-11-04T21:24:23.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/97/f6de8d4af54d6401d6581a686cce3e3e2371a79ba459a449104e026c08bc/kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513", size = 68997, upload-time = "2025-11-04T21:24:21.704Z" }, -] - -[[package]] -name = "kiwisolver" -version = "1.4.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/5d/8ce64e36d4e3aac5ca96996457dcf33e34e6051492399a3f1fec5657f30b/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b", size = 124159, upload-time = "2025-08-10T21:25:35.472Z" }, - { url = "https://files.pythonhosted.org/packages/96/1e/22f63ec454874378175a5f435d6ea1363dd33fb2af832c6643e4ccea0dc8/kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f", size = 66578, upload-time = "2025-08-10T21:25:36.73Z" }, - { url = "https://files.pythonhosted.org/packages/41/4c/1925dcfff47a02d465121967b95151c82d11027d5ec5242771e580e731bd/kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf", size = 65312, upload-time = "2025-08-10T21:25:37.658Z" }, - { url = "https://files.pythonhosted.org/packages/d4/42/0f333164e6307a0687d1eb9ad256215aae2f4bd5d28f4653d6cd319a3ba3/kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9", size = 1628458, upload-time = "2025-08-10T21:25:39.067Z" }, - { url = "https://files.pythonhosted.org/packages/86/b6/2dccb977d651943995a90bfe3495c2ab2ba5cd77093d9f2318a20c9a6f59/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415", size = 1225640, upload-time = "2025-08-10T21:25:40.489Z" }, - { url = "https://files.pythonhosted.org/packages/50/2b/362ebd3eec46c850ccf2bfe3e30f2fc4c008750011f38a850f088c56a1c6/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b", size = 1244074, upload-time = "2025-08-10T21:25:42.221Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bb/f09a1e66dab8984773d13184a10a29fe67125337649d26bdef547024ed6b/kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154", size = 1293036, upload-time = "2025-08-10T21:25:43.801Z" }, - { url = "https://files.pythonhosted.org/packages/ea/01/11ecf892f201cafda0f68fa59212edaea93e96c37884b747c181303fccd1/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48", size = 2175310, upload-time = "2025-08-10T21:25:45.045Z" }, - { url = "https://files.pythonhosted.org/packages/7f/5f/bfe11d5b934f500cc004314819ea92427e6e5462706a498c1d4fc052e08f/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220", size = 2270943, upload-time = "2025-08-10T21:25:46.393Z" }, - { url = "https://files.pythonhosted.org/packages/3d/de/259f786bf71f1e03e73d87e2db1a9a3bcab64d7b4fd780167123161630ad/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586", size = 2440488, upload-time = "2025-08-10T21:25:48.074Z" }, - { url = "https://files.pythonhosted.org/packages/1b/76/c989c278faf037c4d3421ec07a5c452cd3e09545d6dae7f87c15f54e4edf/kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634", size = 2246787, upload-time = "2025-08-10T21:25:49.442Z" }, - { url = "https://files.pythonhosted.org/packages/a2/55/c2898d84ca440852e560ca9f2a0d28e6e931ac0849b896d77231929900e7/kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611", size = 73730, upload-time = "2025-08-10T21:25:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/e8/09/486d6ac523dd33b80b368247f238125d027964cfacb45c654841e88fb2ae/kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536", size = 65036, upload-time = "2025-08-10T21:25:52.063Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, - { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, - { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, - { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, - { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, - { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, - { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, - { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, - { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, - { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, - { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, - { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, - { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, - { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, - { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" }, - { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" }, - { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" }, - { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" }, - { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" }, - { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" }, - { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" }, - { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" }, - { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" }, - { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" }, - { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" }, - { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" }, - { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" }, - { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" }, - { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" }, - { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" }, - { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" }, - { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" }, - { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" }, - { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" }, - { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" }, - { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" }, - { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" }, - { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" }, - { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" }, - { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" }, - { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" }, - { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" }, - { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" }, - { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" }, - { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, - { url = "https://files.pythonhosted.org/packages/a2/63/fde392691690f55b38d5dd7b3710f5353bf7a8e52de93a22968801ab8978/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527", size = 60183, upload-time = "2025-08-10T21:27:37.669Z" }, - { url = "https://files.pythonhosted.org/packages/27/b1/6aad34edfdb7cced27f371866f211332bba215bfd918ad3322a58f480d8b/kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771", size = 58675, upload-time = "2025-08-10T21:27:39.031Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1a/23d855a702bb35a76faed5ae2ba3de57d323f48b1f6b17ee2176c4849463/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e", size = 80277, upload-time = "2025-08-10T21:27:40.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/5b/5239e3c2b8fb5afa1e8508f721bb77325f740ab6994d963e61b2b7abcc1e/kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9", size = 77994, upload-time = "2025-08-10T21:27:41.181Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1c/5d4d468fb16f8410e596ed0eac02d2c68752aa7dc92997fe9d60a7147665/kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb", size = 73744, upload-time = "2025-08-10T21:27:42.254Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, - { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, - { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, -] - -[[package]] -name = "kubernetes" -version = "35.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "durationpy" }, - { name = "python-dateutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "six" }, - { name = "urllib3" }, - { name = "websocket-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" }, -] - -[[package]] -name = "langchain" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langgraph" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/78/9565319259d92818d96f30d55507ee1072fbf5c008b95a6acecf5e47c4d6/langchain-1.2.3.tar.gz", hash = "sha256:9d6171f9c3c760ca3c7c2cf8518e6f8625380962c488b41e35ebff1f1d611077", size = 548296, upload-time = "2026-01-08T20:26:30.149Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/e5/9b4f58533f8ce3013b1a993289eb11e8607d9c9d9d14699b29c6ac3b4132/langchain-1.2.3-py3-none-any.whl", hash = "sha256:5cdc7c80f672962b030c4b0d16d0d8f26d849c0ada63a4b8653a20d7505512ae", size = 106428, upload-time = "2026-01-08T20:26:29.162Z" }, -] - -[[package]] -name = "langchain-chroma" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "chromadb" }, - { name = "langchain-core" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fa/0e/54896830b7331c90788cf96b2c37858977c199da9ecdaf85cf11eb6e6bc1/langchain_chroma-1.1.0.tar.gz", hash = "sha256:8069685e7848041e998d16c8a4964256b031fd20551bf59429173415bc2adc12", size = 220382, upload-time = "2025-12-12T16:23:01.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/35/2a6d1191acaad043647e28313b0ecd161d61f09d8be37d1996a90d752c13/langchain_chroma-1.1.0-py3-none-any.whl", hash = "sha256:ff65e4a2ccefb0fb9fde2ff38705022ace402f979d557f018f6e623f7288f0fc", size = 12981, upload-time = "2025-12-12T16:23:00.196Z" }, -] - -[[package]] -name = "langchain-core" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpatch" }, - { name = "langsmith" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pyyaml" }, - { name = "tenacity" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/ea/8380184b287da43d3d2556475b985cf3e27569e9d8bbe33195600a98cabb/langchain_core-1.2.3.tar.gz", hash = "sha256:61f5197aa101cd5605879ef37f2b0ac56c079974d94d347849b8d4fe18949746", size = 803567, upload-time = "2025-12-18T20:13:10.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/57/cfc1d12e273d33d16bab7ce9a135244e6f5677a92a5a99e69a61b22b7d93/langchain_core-1.2.3-py3-none-any.whl", hash = "sha256:c3501cf0219daf67a0ae23f6d6bdf3b41ab695efd8f0f3070a566e368b8c3dc7", size = 476384, upload-time = "2025-12-18T20:13:08.998Z" }, -] - -[[package]] -name = "langchain-huggingface" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "langchain-core" }, - { name = "tokenizers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/2c/4fddeb3387baa05b6a95870ad514f649cafb46e0c0ef9caf949d974e55d2/langchain_huggingface-1.2.0.tar.gz", hash = "sha256:18a2d79955271261fb245b233fea6aa29625576e841f2b4f5bee41e51cc70949", size = 255602, upload-time = "2025-12-12T22:19:51.021Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/ce/502157ef7390a31cc67e5873ad66e737a25d1d33fcf6936e5c9a0a451409/langchain_huggingface-1.2.0-py3-none-any.whl", hash = "sha256:0ff6a17d3eb36ce2304f446e3285c74b59358703e8f7916c15bfcf9ec7b57bf1", size = 30671, upload-time = "2025-12-12T22:19:50.023Z" }, -] - -[[package]] -name = "langchain-ollama" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "ollama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/51/72cd04d74278f3575f921084f34280e2f837211dc008c9671c268c578afe/langchain_ollama-1.0.1.tar.gz", hash = "sha256:e37880c2f41cdb0895e863b1cfd0c2c840a117868b3f32e44fef42569e367443", size = 153850, upload-time = "2025-12-12T21:48:28.68Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/46/f2907da16dc5a5a6c679f83b7de21176178afad8d2ca635a581429580ef6/langchain_ollama-1.0.1-py3-none-any.whl", hash = "sha256:37eb939a4718a0255fe31e19fbb0def044746c717b01b97d397606ebc3e9b440", size = 29207, upload-time = "2025-12-12T21:48:27.832Z" }, -] - -[[package]] -name = "langchain-openai" -version = "1.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "openai" }, - { name = "tiktoken" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/67/228dc28b4498ea16422577013b5bb4ba35a1b99f8be975d6747c7a9f7e6a/langchain_openai-1.1.6.tar.gz", hash = "sha256:e306612654330ae36fb6bbe36db91c98534312afade19e140c3061fe4208dac8", size = 1038310, upload-time = "2025-12-18T17:58:52.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/5b/1f6521df83c1a8e8d3f52351883b59683e179c0aa1bec75d0a77a394c9e7/langchain_openai-1.1.6-py3-none-any.whl", hash = "sha256:c42d04a67a85cee1d994afe400800d2b09ebf714721345f0b651eb06a02c3948", size = 84701, upload-time = "2025-12-18T17:58:51.527Z" }, -] - -[[package]] -name = "langchain-text-splitters" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/42/c178dcdc157b473330eb7cc30883ea69b8ec60078c7b85e2d521054c4831/langchain_text_splitters-1.1.0.tar.gz", hash = "sha256:75e58acb7585dc9508f3cd9d9809cb14751283226c2d6e21fb3a9ae57582ca22", size = 272230, upload-time = "2025-12-14T01:15:38.659Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/1a/a84ed1c046deecf271356b0179c1b9fba95bfdaa6f934e1849dee26fad7b/langchain_text_splitters-1.1.0-py3-none-any.whl", hash = "sha256:f00341fe883358786104a5f881375ac830a4dd40253ecd42b4c10536c6e4693f", size = 34182, upload-time = "2025-12-14T01:15:37.382Z" }, -] - -[[package]] -name = "langgraph" -version = "1.0.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langgraph-checkpoint" }, - { name = "langgraph-prebuilt" }, - { name = "langgraph-sdk" }, - { name = "pydantic" }, - { name = "xxhash" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/49/e9551965d8a44dd9afdc55cbcdc5a9bd18bee6918cc2395b225d40adb77c/langgraph-1.0.8.tar.gz", hash = "sha256:2630fc578846995114fd659f8b14df9eff5a4e78c49413f67718725e88ceb544", size = 498708, upload-time = "2026-02-06T12:31:13.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/72/b0d7fc1007821a08dfc03ce232f39f209aa4aa46414ea3d125b24e35093a/langgraph-1.0.8-py3-none-any.whl", hash = "sha256:da737177c024caad7e5262642bece4f54edf4cba2c905a1d1338963f41cf0904", size = 158144, upload-time = "2026-02-06T12:31:12.489Z" }, -] - -[[package]] -name = "langgraph-checkpoint" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "ormsgpack" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/76/55a18c59dedf39688d72c4b06af73a5e3ea0d1a01bc867b88fbf0659f203/langgraph_checkpoint-4.0.0.tar.gz", hash = "sha256:814d1bd050fac029476558d8e68d87bce9009a0262d04a2c14b918255954a624", size = 137320, upload-time = "2026-01-12T20:30:26.38Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/de/ddd53b7032e623f3c7bcdab2b44e8bf635e468f62e10e5ff1946f62c9356/langgraph_checkpoint-4.0.0-py3-none-any.whl", hash = "sha256:3fa9b2635a7c5ac28b338f631abf6a030c3b508b7b9ce17c22611513b589c784", size = 46329, upload-time = "2026-01-12T20:30:25.2Z" }, -] - -[[package]] -name = "langgraph-prebuilt" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "langchain-core" }, - { name = "langgraph-checkpoint" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/59/711aecd1a50999456850dc328f3cad72b4372d8218838d8d5326f80cb76f/langgraph_prebuilt-1.0.7.tar.gz", hash = "sha256:38e097e06de810de4d0e028ffc0e432bb56d1fb417620fb1dfdc76c5e03e4bf9", size = 163692, upload-time = "2026-01-22T16:45:22.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/49/5e37abb3f38a17a3487634abc2a5da87c208cc1d14577eb8d7184b25c886/langgraph_prebuilt-1.0.7-py3-none-any.whl", hash = "sha256:e14923516504405bb5edc3977085bc9622c35476b50c1808544490e13871fe7c", size = 35324, upload-time = "2026-01-22T16:45:21.784Z" }, -] - -[[package]] -name = "langgraph-sdk" -version = "0.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/2b/2dae368ac76e315197f07ab58077aadf20833c226fbfd450d71745850314/langgraph_sdk-0.3.5.tar.gz", hash = "sha256:64669e9885a908578eed921ef9a8e52b8d0cd38db1e3e5d6d299d4e6f8830ac0", size = 177470, upload-time = "2026-02-10T16:56:09.18Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/d5/a14d957c515ba7a9713bf0f03f2b9277979c403bc50f829bdfd54ae7dc9e/langgraph_sdk-0.3.5-py3-none-any.whl", hash = "sha256:bcfa1dcbddadb604076ce46f5e08969538735e5ac47fa863d4fac5a512dab5c9", size = 70851, upload-time = "2026-02-10T16:56:07.983Z" }, -] - -[[package]] -name = "langsmith" -version = "0.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "requests-toolbelt" }, - { name = "uuid-utils" }, - { name = "xxhash" }, - { name = "zstandard" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8d/bc/8172fefad4f2da888a6d564a27d1fb7d4dbf3c640899c2b40c46235cbe98/langsmith-0.7.3.tar.gz", hash = "sha256:0223b97021af62d2cf53c8a378a27bd22e90a7327e45b353e0069ae60d5d6f9e", size = 988575, upload-time = "2026-02-13T23:25:32.916Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/9d/5a68b6b5e313ffabbb9725d18a71edb48177fd6d3ad329c07801d2a8e862/langsmith-0.7.3-py3-none-any.whl", hash = "sha256:03659bf9274e6efcead361c9c31a7849ea565ae0d6c0d73e1d8b239029eff3be", size = 325718, upload-time = "2026-02-13T23:25:31.52Z" }, -] - -[[package]] -name = "lap" -version = "0.5.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/cf/ef745c8977cbb26fba5f8433fd4bfd6bf009a90802c0a1cc7139e11f478b/lap-0.5.12.tar.gz", hash = "sha256:570b414ea7ae6c04bd49d0ec8cdac1dc5634737755784d44e37f9f668bab44fd", size = 1520169, upload-time = "2024-11-30T14:27:56.096Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/a7/d66e91ea92628f1e1572db6eb5cd0baa549ef523308f1ce469ea2b380b37/lap-0.5.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c3a38070b24531949e30d7ebc83ca533fcbef6b1d6562f035cae3b44dfbd5ec", size = 1481332, upload-time = "2024-11-30T01:20:54.008Z" }, - { url = "https://files.pythonhosted.org/packages/30/8a/a0e54a284828edc049a1d005fad835e7c8b2d2a563641ec0d3c6fb5ee6d4/lap-0.5.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a301dc9b8a30e41e4121635a0e3d0f6374a08bb9509f618d900e18d209b815c4", size = 1478472, upload-time = "2024-11-30T01:21:10.314Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d6/679d73d2552d0e36c5a2751b6509a62f1fa69d6a2976dac07568498eefde/lap-0.5.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0c1b9ab32c9ba9a94e3f139a0c30141a15fb9e71d69570a6851bbae254c299", size = 1697145, upload-time = "2024-11-30T01:21:47.91Z" }, - { url = "https://files.pythonhosted.org/packages/fa/93/dcfdcd73848c72a0aec5ff587840812764844cdb0b58dd9394e689b8bc09/lap-0.5.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f702e9fbbe3aa265708817ba9d4efb44d52f7013b792c9795f7501ecf269311a", size = 1700582, upload-time = "2024-11-30T01:22:09.43Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1d/66f32e54bbf005fe8483065b3afec4b427f2583df6ae53a2dd540c0f7227/lap-0.5.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9836f034c25b1dfeabd812b7359816911ed05fe55f53e70c30ef849adf07df02", size = 1688038, upload-time = "2024-11-30T01:22:11.863Z" }, - { url = "https://files.pythonhosted.org/packages/a9/1c/faf992abd15b643bd7d70aabcf13ef7544f11ac1167436049a3a0090ce17/lap-0.5.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0416780dbdca2769231a53fb5491bce52775299b014041296a8b5be2d00689df", size = 1697169, upload-time = "2024-11-30T01:22:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/e7/a2/9af5372d383310174f1a9e429da024ae2eaa762e6ee3fc59bdc936a1f6db/lap-0.5.12-cp310-cp310-win_amd64.whl", hash = "sha256:2d6e137e1beb779fcd6a42968feb6a122fdddf72e5b58d865191c31a01ba6804", size = 1477867, upload-time = "2024-11-30T01:22:15.57Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ad/9bb92211ea5b5b43d98f5a57b3e98ccff125ea9bc397f185d5eff1a04260/lap-0.5.12-cp310-cp310-win_arm64.whl", hash = "sha256:a40d52c5511421497ae3f82a5ca85a5442d8776ba2991c6fca146afceea7608f", size = 1467318, upload-time = "2024-11-30T01:22:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/62/ef/bc8bbc34585bcbed2b277d734008480d9ed08a6e3f2de3842ad482484e9c/lap-0.5.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d928652e77bec5a71dc4eb4fb8e15d455253b2a391ca8478ceab7d171cbaec2e", size = 1481210, upload-time = "2024-11-30T01:22:44.992Z" }, - { url = "https://files.pythonhosted.org/packages/ab/81/0d3b31d18bbdcdaab678b461d99688ec3e6a2d2cda2aa9af2ae8ed6910e1/lap-0.5.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4a0ea039fcb2fd388b5e7c1be3402c483d32d3ef8c70261c69ab969ec25cd83", size = 1478370, upload-time = "2024-11-30T01:23:00.354Z" }, - { url = "https://files.pythonhosted.org/packages/3d/90/bd6cff1b6a0c30594a7a2bf94c5f184105e8eb26fa250ce22efdeef58a3a/lap-0.5.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87c0e736c31af0a827dc642132d09c5d4f77d30f5b3f0743b9cd31ef12adb96c", size = 1718144, upload-time = "2024-11-30T01:23:03.345Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d6/97564ef3571cc2a60a6e3ee2f452514b2e549637247cb7de7004e0769864/lap-0.5.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5270141f97027776ced4b6540d51899ff151d8833b5f93f2428de36c2270a9ed", size = 1720027, upload-time = "2024-11-30T01:23:32.025Z" }, - { url = "https://files.pythonhosted.org/packages/3e/7d/73a51aeec1e22257589dad46c724d4d736aa56fdf4c0eff29c06102e21ae/lap-0.5.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:04dc4b44c633051a9942ad60c9ad3da28d7c5f09de93d6054b763c57cbc4ac90", size = 1711923, upload-time = "2024-11-30T01:23:47.213Z" }, - { url = "https://files.pythonhosted.org/packages/86/9c/c1be3d9ebe479beff3d6ee4453908a343c7a388386de28037ff2767debf9/lap-0.5.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:560ec8b9100f78d6111b0acd9ff8805e4315372f23c2dcad2f5f9f8d9c681261", size = 1720922, upload-time = "2024-11-30T01:24:14.228Z" }, - { url = "https://files.pythonhosted.org/packages/cd/4d/18c0c4edadbf9744a02131901c8a856303a901367881e44796a94190b560/lap-0.5.12-cp311-cp311-win_amd64.whl", hash = "sha256:851b9bcc898fa763d6e7c307d681dde199ca969ab00e8292fc13cff34107ea38", size = 1478202, upload-time = "2024-11-30T01:24:29.681Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d2/dcde0db492eb7a2c228e8839e831c6c5fc68f85bea586206405abd2eb44e/lap-0.5.12-cp311-cp311-win_arm64.whl", hash = "sha256:49e14fdbf4d55e7eda6dfd3aba433a91b00d87c7be4dd25059952b871b1e3399", size = 1467411, upload-time = "2024-11-30T01:24:31.92Z" }, - { url = "https://files.pythonhosted.org/packages/24/29/50a77fa27ed19b75b7599defedafd5f4a64a66bdb6255f733fdb8c9fafcb/lap-0.5.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1211fca9d16c0b1383c7a93be2045096ca5e4c306e794fcf777ac52b30f98829", size = 1481435, upload-time = "2024-11-30T01:24:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/c5/2b/41acf93603d3db57e512c77c98f4f71545602efa0574ca685608078cc0f5/lap-0.5.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8dcafbf8363308fb289d7cd3ae9df375ad090dbc2b70f5d7d038832e87d2b1a1", size = 1478195, upload-time = "2024-11-30T01:25:16.925Z" }, - { url = "https://files.pythonhosted.org/packages/3a/6e/d7644b2b2675e2c29cc473c3dde136f02f4ed30ecbc8ef89b51cbb4f7ad1/lap-0.5.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f721ed3fd2b4f6f614870d12aec48bc44c089587930512c3187c51583c811b1c", size = 1725693, upload-time = "2024-11-30T01:25:19.404Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3c/8d3f80135022a2db3eb7212fa9c735b7111dcb149d53deb62357ff2386f0/lap-0.5.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:797d9e14e517ac06337b6dca875bdf9f0d88ec4c3214ebb6d0676fed197dc13f", size = 1726953, upload-time = "2024-11-30T01:25:44.067Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e1/badf139f34ff7c7c07ba55e6f39de9ea443d9b75fd97cc4ed0ce67eeb36b/lap-0.5.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a2424daf7c7afec9b93ed02af921813ab4330826948ce780a25d94ca42df605", size = 1712981, upload-time = "2024-11-30T01:25:58.948Z" }, - { url = "https://files.pythonhosted.org/packages/ef/4a/e2d0925e5ead474709eb89c6bbb9cd188396c9e3384a1f5d2491a38aeab6/lap-0.5.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1c34c3d8aefbf7d0cb709801ccf78c6ac31f4b1dc26c169ed1496ed3cb6f4556", size = 1728876, upload-time = "2024-11-30T01:26:25.744Z" }, - { url = "https://files.pythonhosted.org/packages/46/89/73bad73b005e7f681f8cfa2c8748e9d766b91da781d07f300f86a9eb4f03/lap-0.5.12-cp312-cp312-win_amd64.whl", hash = "sha256:753ef9bd12805adbf0d09d916e6f0d271aebe3d2284a1f639bd3401329e436e5", size = 1476975, upload-time = "2024-11-30T01:26:40.341Z" }, - { url = "https://files.pythonhosted.org/packages/d9/8d/00df0c44b728119fe770e0526f850b0a9201f23bf4276568aef5b372982e/lap-0.5.12-cp312-cp312-win_arm64.whl", hash = "sha256:83e507f6def40244da3e03c71f1b1f54ceab3978cde72a84b84caadd8728977e", size = 1466243, upload-time = "2024-11-30T01:26:43.202Z" }, - { url = "https://files.pythonhosted.org/packages/e1/07/85a389eb4c6a9bf342f79811dd868ed3b6e56402f1dfa71474cec3c5ac30/lap-0.5.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c4fdbd8d94ad5da913ade49635bad3fc4352ee5621a9f785494c11df5412d6d", size = 1479752, upload-time = "2024-11-30T01:27:06.417Z" }, - { url = "https://files.pythonhosted.org/packages/b1/01/46ba9ab4b9d95b43058591094e49ef21bd7e6fe2eb5202ece0b23240b2dc/lap-0.5.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2d01113eec42174e051ee5cebb5d33ec95d37bd2c422b7a3c09bbebaf30b635", size = 1477146, upload-time = "2024-11-30T01:27:26.769Z" }, - { url = "https://files.pythonhosted.org/packages/7e/c3/9f6829a20e18c6ca3a3e97fcab815f0d888b552e3e37b892d908334d0f22/lap-0.5.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a6e8ed53cb4d85fa0875092bc17436d7eeab2c7fb3574e551c611c352fea8c8", size = 1717458, upload-time = "2024-11-30T01:27:29.936Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bb/0f3a44d7220bd48f9a313a64f4c228a02cbb0fb1f55fd449de7a0659a5e2/lap-0.5.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dd54bf8bb48c87f6276555e8014d4ea27742d84ddbb0e7b68be575f4ca438d7", size = 1720277, upload-time = "2024-11-30T01:28:05.397Z" }, - { url = "https://files.pythonhosted.org/packages/3e/48/5dcfd7f97a5ac696ad1fe750528784694c374ee64312bfbf96d14284f74a/lap-0.5.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9db0e048cfb561f21671a3603dc2761f108b3111da66a7b7d2f035974dcf966e", size = 1712562, upload-time = "2024-11-30T01:28:19.952Z" }, - { url = "https://files.pythonhosted.org/packages/77/60/ac8702518e4d7c7a284b40b1aae7b4e264a029a8476cb674067a26c17f3c/lap-0.5.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:517b8bd02e56b8466244fc4c0988aece04e6f8b11f43406ae195b4ce308733fb", size = 1724195, upload-time = "2024-11-30T01:28:46.411Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3b/62181a81af89a6e7cefca2390d1f0822f7f6b73b40393ea04000c1ac0435/lap-0.5.12-cp313-cp313-win_amd64.whl", hash = "sha256:59dba008db14f640a20f4385916def4b343fa59efb4e82066df81db5a9444d5e", size = 1476213, upload-time = "2024-11-30T01:29:03.832Z" }, - { url = "https://files.pythonhosted.org/packages/9f/4b/2db5ddb766cda2bdbf4012771d067d2b1c91e0e2d2c5ca0573efcd7ad321/lap-0.5.12-cp313-cp313-win_arm64.whl", hash = "sha256:30309f6aff8e4d616856ec8c6eec7ad5b48d2687887b931302b5c8e6dfac347a", size = 1465708, upload-time = "2024-11-30T01:29:34.141Z" }, -] - -[[package]] -name = "lark" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/34/28fff3ab31ccff1fd4f6c7c7b0ceb2b6968d8ea4950663eadcb5720591a0/lark-1.3.1.tar.gz", hash = "sha256:b426a7a6d6d53189d318f2b6236ab5d6429eaf09259f1ca33eb716eed10d2905", size = 382732, upload-time = "2025-10-27T18:25:56.653Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, -] - -[[package]] -name = "lazy-loader" -version = "0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, -] - -[[package]] -name = "lcm" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/c8/a5a6c1b0d55bf3dec92daf5db9719923e56e0fdfd9e299a4997632d54a6f/lcm-1.5.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:46021ad6e2c63d8a2ee2e22d9ccc193e8f95b8ee1084567722c6428e1e92d615", size = 3494809, upload-time = "2025-10-23T20:33:34.358Z" }, - { url = "https://files.pythonhosted.org/packages/25/35/9a7e6c619332b9a71ec2a6f53a5a83fd231f3b243789745419a64e778805/lcm-1.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:79decf56efedc81e3fe0ae5757cabec908ec17a23b792586304550e97d86be46", size = 2861313, upload-time = "2025-10-23T20:33:37.574Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/dfc70f70eaffde8e63c2227c7199ef7fdad1d411612d3082cddad2fdbab4/lcm-1.5.2-cp310-cp310-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ad0c911e67c023fb455a730f7bf94ddbc2e128c87743a603778f01033eb8a92", size = 2853116, upload-time = "2025-10-23T20:33:40.655Z" }, - { url = "https://files.pythonhosted.org/packages/d4/33/067d66c0acd06d9d00fd657e33718e2daf16a82159df8e3ff51e5273d9b3/lcm-1.5.2-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a04a7b1a623086c3a665356bc0e232185e4d247c8be07d70cbc41b0c1d9a506d", size = 2881542, upload-time = "2025-10-23T20:33:44.185Z" }, - { url = "https://files.pythonhosted.org/packages/d5/23/be8bcffbcdc7fcc04d8ce5fc16be976c65f9d767afe96ab13886ad274be4/lcm-1.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ebb81f718088ed7ad6ec73f3dc429f9cf36cc40784a5afc15648b1bf341a1b38", size = 1497534, upload-time = "2025-10-23T20:33:46.316Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c3/68e47479ff2cd2b430541b6305ccc9b0147b36b4df046a85ca71473de048/lcm-1.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:11aed9a77dffef96b3fc973737ab073d18e0389666aaf3b945ed51566c7427c7", size = 1341790, upload-time = "2025-10-23T20:33:48.006Z" }, - { url = "https://files.pythonhosted.org/packages/64/71/71a687347a9f80dbd01b6be4d8c26edf64b8aac2fc6644452f52d3573eb2/lcm-1.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:33a51c4617479350ecacc7b30ccf41918fadc3144f456d79248aaaa589fb1739", size = 1542532, upload-time = "2025-10-23T20:33:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/e6/c8/c6f3fd72383f847b150cd7a98d74bd0eb4ac38a18afa3bb7b5e771b12a57/lcm-1.5.2-cp310-cp310-win32.whl", hash = "sha256:c76e9c8de6243f9c05e4024459be1e3e497bc324a52edc8e826513082f288716", size = 4192732, upload-time = "2025-10-23T20:33:53.722Z" }, - { url = "https://files.pythonhosted.org/packages/52/ec/d2c3fcae355714994ea9ea4ae74fa242eb41f435d3bd340b4271c8e4584f/lcm-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:fa31cd789075cfb1799bd9d14841e7d82988b634533d73c8e1fabf551cdbd08e", size = 4408972, upload-time = "2025-10-23T20:33:59.654Z" }, - { url = "https://files.pythonhosted.org/packages/ed/49/9bab9e481d7ff83cd8bdc7fb5f96ec98d56f5aa4c65cf0b6266cbf787e6d/lcm-1.5.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:6014e57b4c8f08d9514c4860d6b700f69b3df5352b92271e5ea8f2cee06e8cc4", size = 3561741, upload-time = "2025-10-23T20:34:02.985Z" }, - { url = "https://files.pythonhosted.org/packages/91/4e/a123ddfe36831b64079c50b0957da12eed7dc961190b43c7ddf1f3fb39cf/lcm-1.5.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:24bf2e2895daf045101507c0fc55cb6a055430c89a6038528abb04d83db4838f", size = 3494889, upload-time = "2025-10-23T20:34:06.416Z" }, - { url = "https://files.pythonhosted.org/packages/85/a0/0d5ce44ea370f46d94bc15d51ee4889821c4e1d8664f206b33ee768d6a38/lcm-1.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e79945a1d78e9fadacd05913c3edd7cb036159a9394bb0124dfdc2fe0f74708f", size = 2861375, upload-time = "2025-10-23T20:34:09.513Z" }, - { url = "https://files.pythonhosted.org/packages/8f/56/fc2c7aeb084150688474cd503919d157b95dc9c597160562f38bac02427b/lcm-1.5.2-cp311-cp311-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cf485e679f0dc94c08c90b91221d12d14699f44c64ee967835625068ecb2a22e", size = 2853092, upload-time = "2025-10-23T20:34:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/35/71/d3ac3d849a26bd5d3487a05ca0f867a3638593984b1fac41fb6d9ca04fbd/lcm-1.5.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:288b1f1a90420bc3c21aaaa8375d72c80ff16b35848acdd207070fdb8924b152", size = 2881455, upload-time = "2025-10-23T20:34:15.407Z" }, - { url = "https://files.pythonhosted.org/packages/a1/2f/4bc34fbd695ad30f58b6999552f28744997773cd68bc59a3d85917dec6a4/lcm-1.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:46823e31741ef2f0fdbab184edbc653c6440c039ad704467577750807b74bcf4", size = 1497533, upload-time = "2025-10-23T20:34:17.506Z" }, - { url = "https://files.pythonhosted.org/packages/4f/b1/406af095fb7cd08c89cffcadfdc935c22605ceb69ab5f30dcaec98c5d6fb/lcm-1.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c25758e876e8233d5ab14fe29bba8d2d9d4baf488db01c56414417cb3f77b9c1", size = 1341790, upload-time = "2025-10-23T20:34:19.129Z" }, - { url = "https://files.pythonhosted.org/packages/7f/40/75066b7d5f2b07e58c0418813d796a9df917cfce0407dfe6ac333e0a9167/lcm-1.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4b822ab01a461364a7cf7f44ecfcd5879730e3280678ce7fd6139740c8ec9d04", size = 1542533, upload-time = "2025-10-23T20:34:21.301Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a0/ccb1ef4e3f6e20433fc5738f6f1ac011cdf79efb11cd13e68a7a67372149/lcm-1.5.2-cp311-cp311-win32.whl", hash = "sha256:0d047399e629c1f436c012373505135038918b8d340ca792acd36a21f4b65c1e", size = 4192663, upload-time = "2025-10-23T20:34:25.015Z" }, - { url = "https://files.pythonhosted.org/packages/2a/37/9d5a138375dda75afcff852fbefaf0446f16d809977ff1fce67f15c087d2/lcm-1.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:ee0b121f4e44d050e0a2247a467e2ad3a46f8ef51f3ca97a67963bc193d49311", size = 4408922, upload-time = "2025-10-23T20:34:29.097Z" }, - { url = "https://files.pythonhosted.org/packages/9a/54/035da62f6d66be55af5ff54a74b281eaf477c68bbcdcd1abd7af7d1b24f7/lcm-1.5.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:a97858daaee197c86d4b8e07be8776f9e1a0d534fdc12652843109bf4136f494", size = 3561813, upload-time = "2025-10-23T20:34:32.365Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9a/be81983818b96c6ef8b908d981d31714a927b0946c11029227e7abd9b395/lcm-1.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:29f1789cca094defbbf212384e7dae5c74db00fc0099b423b4dbb95c4bd42eef", size = 3494781, upload-time = "2025-10-23T20:34:35.598Z" }, - { url = "https://files.pythonhosted.org/packages/28/9b/dcafbbb3b9e1053642ee99271f1878c7a89a63a1bca4043f3e41276ee2db/lcm-1.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d7c36fbdd4dad337db70f89cb6235f8b75f20d4a28e683f332d23dbaa4ac1a05", size = 2861393, upload-time = "2025-10-23T20:34:41.31Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/d6704a6e95684d5567c558509b84d0badca886e7f245fa89323fb203a563/lcm-1.5.2-cp312-cp312-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ef22397ae59c20bbafd5e152f1db00830d69d5d3e3904ca0b8fc658c415e32a", size = 2852933, upload-time = "2025-10-23T20:34:45.436Z" }, - { url = "https://files.pythonhosted.org/packages/66/f6/d92a3d3bee9a7d016084da33aa558683918e5a8a1d4207fb188f901a7684/lcm-1.5.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6386c60190fb180ec40ae5a6820103b1db96d1d1996834461c6a95d116219aaa", size = 2881476, upload-time = "2025-10-23T20:34:48.571Z" }, - { url = "https://files.pythonhosted.org/packages/b3/15/38d577361788c8b0a90e53bf3a108f13d3234f1e9fcdda67191f2119c57b/lcm-1.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec234ed44e9c0f090014f65f3b82ba8b390485769073994ed7d77c7a75b06187", size = 1497521, upload-time = "2025-10-23T20:38:25.569Z" }, - { url = "https://files.pythonhosted.org/packages/35/2f/80628f6a5e1984c97755bf332cf8d0551ddf73f379e10e6b22e44aead561/lcm-1.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1f22b9beb77df7ef19725881abeb9acad9648c2b8874ed6cb9f0587442db503d", size = 1341802, upload-time = "2025-10-23T20:38:27.427Z" }, - { url = "https://files.pythonhosted.org/packages/b8/e0/2c2f3fc73fca45dd0204ae0c794d98b58f001cd1032bfa93e2fc0846c7e4/lcm-1.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f6b41b7933d0c4ad89db4e1f6b05ec7bcb99a9a65d5f961f8c1eab5819c6b50a", size = 1542621, upload-time = "2025-10-23T20:38:29.243Z" }, - { url = "https://files.pythonhosted.org/packages/26/ba/46d35b86bc23431d81826e11768e2c445e954478dc0e2f59a7f9fb84352e/lcm-1.5.2-cp312-cp312-win32.whl", hash = "sha256:8e1603ba8e1bf80d62644a1762a8785fe31211bf9c672b57f2bb7978271a3edb", size = 4192867, upload-time = "2025-10-23T20:38:32.937Z" }, - { url = "https://files.pythonhosted.org/packages/30/fa/f4d73ae40dfae33fba25a6ab70b7c70c3c49c699bf908399b0dd15e5f136/lcm-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d034c3623b878a0cb24362298fec4d908ca916d00d15133e31954eaf2d6072f4", size = 4409073, upload-time = "2025-10-23T20:38:37.067Z" }, - { url = "https://files.pythonhosted.org/packages/34/d2/bf7992a1573079329c4eb5cf34f5109046e2432b966da70eb7e6a22defe4/lcm-1.5.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:253522c62073d8b60d12a2f9efe8744c17a4d4f298de4585018320db7b2bf528", size = 3561913, upload-time = "2025-10-23T20:38:40.56Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/f26552f6f0a48dc4931d7f9cf00adbe0d972d1710a17b7c49599115b48da/lcm-1.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6f623d92091894f1e1829c5a2b973da89af27c07ea4047003b56aac2e1ad0888", size = 3494776, upload-time = "2025-10-23T20:38:43.87Z" }, - { url = "https://files.pythonhosted.org/packages/f1/e8/673ecec01037b977db6ca34a657e04ef487f6c6b13c2c71dc16c1c3b3e0a/lcm-1.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:0aba4fddc0bf8b9703af3f7a1dd7571a96ba40c5c40c04cf273a67755848bfdc", size = 2861402, upload-time = "2025-10-23T20:38:46.642Z" }, - { url = "https://files.pythonhosted.org/packages/ef/11/a20a879f4be8ba545ae748f2e41c53eac7ef16bb313339fb39ac97ae54ac/lcm-1.5.2-cp313-cp313-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3c97429fc741cd1e3b08775649af71cc6716d35a34160a6be094db89b582b179", size = 2853161, upload-time = "2025-10-23T20:38:50.244Z" }, - { url = "https://files.pythonhosted.org/packages/b9/97/d7594a34ae05618786e39c8156ab23abd10e3009d17d19c6068c66491e38/lcm-1.5.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e314e125a4a818f9a2d8e8a2dd8c39d3fe080278391490dfa808123f515cfe4b", size = 2881504, upload-time = "2025-10-23T20:38:53.035Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c8/f999d66df34b0e9db866e544ad01a2842607b6debed7ac201eecb3cebce0/lcm-1.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:55c7fb7eea47e101e4419a9c35ee386da3c3a874c84834694feb6016cffc23f9", size = 1497529, upload-time = "2025-10-23T20:38:54.9Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9d/51e79f9ed886eb0b24a9b840eab07a729d4e696cbb6ac070052966e542e4/lcm-1.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4587f97e2d2623551c092e7a9ee363cc3df65c2f6fb09bcd4d79b07b60515fde", size = 1341805, upload-time = "2025-10-23T20:38:56.555Z" }, - { url = "https://files.pythonhosted.org/packages/3e/78/9b47aa19d416bd671ce538223fcc044a5b8a5edc88f0de97e27081517f07/lcm-1.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f41574cc3da5af6d27e16ceb55cb27e4486403d3755bc3cd1cd9366ae758f9e", size = 1542632, upload-time = "2025-10-23T20:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/e8/36/0843752b05625b899063202f715f541eb4cdbeb8da86c2ee44b235865f22/lcm-1.5.2-cp313-cp313-win32.whl", hash = "sha256:b81f995c7104168d0079a24df26adf2174ff88ac99964c3cd04e0eb3d92c41fe", size = 4192684, upload-time = "2025-10-23T20:39:02.073Z" }, - { url = "https://files.pythonhosted.org/packages/f6/3e/eaf0282e8bc604c8630c99bf1cdcbffd7b7e14b4d37e6f5291e6ec0832dd/lcm-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:8c2a9646fdb446edda2a8d80d155fc6c29aa9618e8837267781f610d0da88fe5", size = 4408991, upload-time = "2025-10-23T20:39:06.066Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d7/c760e59a707c0925004f97a7282f05aee9cfe9c786a0ab82d936189f0f3e/lcm-1.5.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:d73506022bec844b4600fd0a2cddd0aa7ffbce87d16f115bcbbcf9a1d811175d", size = 3562016, upload-time = "2025-10-23T20:39:09.6Z" }, - { url = "https://files.pythonhosted.org/packages/17/a5/02c4d75bd644742677b11c45c9cb0eb45244c2b2c3b5b47dde0767f3fe41/lcm-1.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3d580d496a9b4ba8b8746b474d094876b0c7152893b4b97620ad3c44906ebe4b", size = 3494802, upload-time = "2025-10-23T20:39:13.089Z" }, - { url = "https://files.pythonhosted.org/packages/20/a9/3ec71158e6aa12b0f45daa0b456e7fd7495453a74f202259e6cdbc2a2953/lcm-1.5.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a65bf315781e3252708887a9f142d0dedd82fa446483860169f432a33d8ed0f4", size = 2861486, upload-time = "2025-10-23T20:39:15.915Z" }, - { url = "https://files.pythonhosted.org/packages/49/e3/088dd2ba297697f7566518c8027838d4b8ed32974745a8ba899bdeb7fd64/lcm-1.5.2-cp314-cp314-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23e6bf3c7b1572c3a6fb505fba4f1179dc6c0fba04ff5e117dd0c33199569300", size = 2852991, upload-time = "2025-10-23T20:39:18.729Z" }, - { url = "https://files.pythonhosted.org/packages/bd/bf/965f3552cff59b8de0fdc2f5ed6195946cdd4a0861bdf6f82736f28c37af/lcm-1.5.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49fd13d8d5e967cfe7ddcd5a8b490743b02e95238a2ab1dbf7cea701fbb3f37c", size = 2881653, upload-time = "2025-10-23T20:39:21.813Z" }, - { url = "https://files.pythonhosted.org/packages/20/21/5b93f4435625d8519fb48114bf60980cadbdecf67e036478fcb25e23d60b/lcm-1.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:599e3b400d2aff260d5f8c5386d6ae9fe7d414cc6ec869dafbf5b399ef040a3a", size = 1497598, upload-time = "2025-10-23T20:39:23.559Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/0431bb0ee7b223a70c35129e9af71c152b184d637543c1120e6cb14246c6/lcm-1.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:85257675c0b4c0035d7a11f0623537df5121411923d3d354c78547864e08aa10", size = 1341793, upload-time = "2025-10-23T20:39:25.628Z" }, - { url = "https://files.pythonhosted.org/packages/eb/42/045dd17d86e77de1547bee632dbb8d03bd6720880a232f995c0bf846e539/lcm-1.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:df99360130df5ce89b54034fb9b6b4db0a752bb6e5ab826c1fa99b4bc78d8022", size = 1542610, upload-time = "2025-10-23T20:39:27.462Z" }, - { url = "https://files.pythonhosted.org/packages/34/fd/07951d7252baa327d88e5154cefbfc3ceef17cdbacbbaf7a1d4cb4a1ec98/lcm-1.5.2-cp314-cp314-win32.whl", hash = "sha256:19617cdfa1e3f757798d3f03789dbc76148bd03aa4b07a7fa456bc3af8a74c86", size = 4237953, upload-time = "2025-10-23T20:39:31.269Z" }, - { url = "https://files.pythonhosted.org/packages/80/73/623eb9f29fe54ef2109cc9ed6d49dbdd1845c625463390c067fb2ea9c7a8/lcm-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:6ba84f4e97f61ea55bb09e8201b0bd47380332118e7199674ec9f85cb1175de3", size = 4467113, upload-time = "2025-10-23T20:39:35.195Z" }, -] - -[[package]] -name = "libcoal" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-assimp" }, - { name = "cmeel-boost" }, - { name = "cmeel-octomap" }, - { name = "cmeel-qhull" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cc/51/cb68b16abd786e3ebb5e7e64036894a6f69ea8fe45c04a433e6d5462d60e/libcoal-3.0.2.tar.gz", hash = "sha256:1d48cfdce1157d4b89cf6a7215fc1b1e120d54c4a8d975cd9f45f2c8cedec275", size = 1464086, upload-time = "2025-10-15T22:53:34.811Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/ea/6aa65497d00ec494bf1c5e121b59ad8cf6da308e0cf01271a9d7c614752c/libcoal-3.0.2-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:16702fdd13942080c42c9565eb1b692618ce4456192a5bc497c585f9142138e5", size = 1683950, upload-time = "2025-10-15T22:53:28.361Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b0/3480197ba40cf9c6de71ca7f7a81a7504a40ca77a2b8604cbcc068f8f7ca/libcoal-3.0.2-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:527c710c6215936f1a4b99ca1d01b4bb15c6b52980fa96cfa5f1fd1a7ef12393", size = 1484168, upload-time = "2025-10-15T22:53:29.933Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e5/5b9605496e48a0437152196e5f200433d3904e59c899cab799a3c27bcd4f/libcoal-3.0.2-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:ed45722c07a3d23a346211f837856549dce11167928743eb6c73bcf17a369dd6", size = 2257523, upload-time = "2025-10-15T22:53:31.214Z" }, - { url = "https://files.pythonhosted.org/packages/3c/49/c3bec783144c226b5ef3728ed66d7fc2d08c553922a3892591958284801a/libcoal-3.0.2-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:c4ca3fec02386e5c8ccc81030c44e74a546b06059886ce04bb6c16fe4628e9ba", size = 2285635, upload-time = "2025-10-15T22:53:33.142Z" }, -] - -[[package]] -name = "libpinocchio" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-boost" }, - { name = "cmeel-urdfdom" }, - { name = "libcoal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/b4431f1acdce04300d798a87b98b064c1bb56061848abd9476c7b7e9dac2/libpinocchio-3.8.0.tar.gz", hash = "sha256:687442a8316d03cbe1a5c66e20499bf3fadb59439d6207e36118eef34f73d8c8", size = 4001141, upload-time = "2025-10-16T06:34:02.405Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/71/b17ca7f4c0cb0f216441222e22c3fb8d905ba038ec5ac7c120790340da95/libpinocchio-3.8.0-0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:b8266d37482c35b5aa27240f3a0274447cd038aa219bdd6413c0bafcad822e2b", size = 4663536, upload-time = "2025-10-16T06:33:55.707Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a9/a4842e056d3f7d07c3f96f90c8f7fe7ef7e543c725f1c9498e5f4d58c47c/libpinocchio-3.8.0-0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0320c471bd4e78226cc266ad7927432f884709104fa8a253e565adbed7da8aac", size = 3781718, upload-time = "2025-10-16T06:33:57.483Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f5/950cd3be129766d6f847cb0702f73ad5f6ed2d2b5775e073f9f017d923b4/libpinocchio-3.8.0-0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:b70bc23fb9f53d0a65929c92bac8c0df836bef064225a54d009214cdd778bdb7", size = 4582702, upload-time = "2025-10-16T06:33:58.887Z" }, - { url = "https://files.pythonhosted.org/packages/28/0d/5deebded1fa71a381c9efd3ea69103a38f64d804da704148e92f4886762d/libpinocchio-3.8.0-0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:b52ca3520635f551ab2c8c9bf5e8e555b54e92c4bb948020eb4e4dc1b3f9eb0b", size = 4803646, upload-time = "2025-10-16T06:34:00.662Z" }, -] - -[[package]] -name = "librt" -version = "0.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/3f/4ca7dd7819bf8ff303aca39c3c60e5320e46e766ab7f7dd627d3b9c11bdf/librt-0.8.0.tar.gz", hash = "sha256:cb74cdcbc0103fc988e04e5c58b0b31e8e5dd2babb9182b6f9490488eb36324b", size = 177306, upload-time = "2026-02-12T14:53:54.743Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/e9/018cfd60629e0404e6917943789800aa2231defbea540a17b90cc4547b97/librt-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:db63cf3586a24241e89ca1ce0b56baaec9d371a328bd186c529b27c914c9a1ef", size = 65690, upload-time = "2026-02-12T14:51:57.761Z" }, - { url = "https://files.pythonhosted.org/packages/b5/80/8d39980860e4d1c9497ee50e5cd7c4766d8cfd90d105578eae418e8ffcbc/librt-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba9d9e60651615bc614be5e21a82cdb7b1769a029369cf4b4d861e4f19686fb6", size = 68373, upload-time = "2026-02-12T14:51:59.013Z" }, - { url = "https://files.pythonhosted.org/packages/2d/76/6e6f7a443af63977e421bd542551fec4072d9eaba02e671b05b238fe73bc/librt-0.8.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb4b3ad543084ed79f186741470b251b9d269cd8b03556f15a8d1a99a64b7de5", size = 197091, upload-time = "2026-02-12T14:52:00.642Z" }, - { url = "https://files.pythonhosted.org/packages/14/40/fa064181c231334c9f4cb69eb338132d39510c8928e84beba34b861d0a71/librt-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d2720335020219197380ccfa5c895f079ac364b4c429e96952cd6509934d8eb", size = 207350, upload-time = "2026-02-12T14:52:02.32Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/e7f8438dd226305e3e5955d495114ad01448e6a6ffc0303289b4153b5fc5/librt-0.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9726305d3e53419d27fc8cdfcd3f9571f0ceae22fa6b5ea1b3662c2e538f833e", size = 219962, upload-time = "2026-02-12T14:52:03.884Z" }, - { url = "https://files.pythonhosted.org/packages/1f/2c/74086fc5d52e77107a3cc80a9a3209be6ad1c9b6bc99969d8d9bbf9fdfe4/librt-0.8.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3d107f603b5ee7a79b6aa6f166551b99b32fb4a5303c4dfcb4222fc6a0335e", size = 212939, upload-time = "2026-02-12T14:52:05.537Z" }, - { url = "https://files.pythonhosted.org/packages/c8/ae/d6917c0ebec9bc2e0293903d6a5ccc7cdb64c228e529e96520b277318f25/librt-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41064a0c07b4cc7a81355ccc305cb097d6027002209ffca51306e65ee8293630", size = 221393, upload-time = "2026-02-12T14:52:07.164Z" }, - { url = "https://files.pythonhosted.org/packages/04/97/15df8270f524ce09ad5c19cbbe0e8f95067582507149a6c90594e7795370/librt-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c6e4c10761ddbc0d67d2f6e2753daf99908db85d8b901729bf2bf5eaa60e0567", size = 216721, upload-time = "2026-02-12T14:52:08.857Z" }, - { url = "https://files.pythonhosted.org/packages/c4/52/17cbcf9b7a1bae5016d9d3561bc7169b32c3bd216c47d934d3f270602c0c/librt-0.8.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ba581acad5ac8f33e2ff1746e8a57e001b47c6721873121bf8bbcf7ba8bd3aa4", size = 214790, upload-time = "2026-02-12T14:52:10.033Z" }, - { url = "https://files.pythonhosted.org/packages/2a/2d/010a236e8dc4d717dd545c46fd036dcced2c7ede71ef85cf55325809ff92/librt-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bdab762e2c0b48bab76f1a08acb3f4c77afd2123bedac59446aeaaeed3d086cf", size = 237384, upload-time = "2026-02-12T14:52:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/38/14/f1c0eff3df8760dee761029efb72991c554d9f3282f1048e8c3d0eb60997/librt-0.8.0-cp310-cp310-win32.whl", hash = "sha256:6a3146c63220d814c4a2c7d6a1eacc8d5c14aed0ff85115c1dfea868080cd18f", size = 54289, upload-time = "2026-02-12T14:52:12.798Z" }, - { url = "https://files.pythonhosted.org/packages/2f/0b/2684d473e64890882729f91866ed97ccc0a751a0afc3b4bf1a7b57094dbb/librt-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:bbebd2bba5c6ae02907df49150e55870fdd7440d727b6192c46b6f754723dde9", size = 61347, upload-time = "2026-02-12T14:52:13.793Z" }, - { url = "https://files.pythonhosted.org/packages/51/e9/42af181c89b65abfd557c1b017cba5b82098eef7bf26d1649d82ce93ccc7/librt-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ce33a9778e294507f3a0e3468eccb6a698b5166df7db85661543eca1cfc5369", size = 65314, upload-time = "2026-02-12T14:52:14.778Z" }, - { url = "https://files.pythonhosted.org/packages/9d/4a/15a847fca119dc0334a4b8012b1e15fdc5fc19d505b71e227eaf1bcdba09/librt-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8070aa3368559de81061ef752770d03ca1f5fc9467d4d512d405bd0483bfffe6", size = 68015, upload-time = "2026-02-12T14:52:15.797Z" }, - { url = "https://files.pythonhosted.org/packages/e1/87/ffc8dbd6ab68dd91b736c88529411a6729649d2b74b887f91f3aaff8d992/librt-0.8.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:20f73d4fecba969efc15cdefd030e382502d56bb6f1fc66b580cce582836c9fa", size = 194508, upload-time = "2026-02-12T14:52:16.835Z" }, - { url = "https://files.pythonhosted.org/packages/89/92/a7355cea28d6c48ff6ff5083ac4a2a866fb9b07b786aa70d1f1116680cd5/librt-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a512c88900bdb1d448882f5623a0b1ad27ba81a9bd75dacfe17080b72272ca1f", size = 205630, upload-time = "2026-02-12T14:52:18.58Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5e/54509038d7ac527828db95b8ba1c8f5d2649bc32fd8f39b1718ec9957dce/librt-0.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:015e2dde6e096d27c10238bf9f6492ba6c65822dfb69d2bf74c41a8e88b7ddef", size = 218289, upload-time = "2026-02-12T14:52:20.134Z" }, - { url = "https://files.pythonhosted.org/packages/6d/17/0ee0d13685cefee6d6f2d47bb643ddad3c62387e2882139794e6a5f1288a/librt-0.8.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1c25a131013eadd3c600686a0c0333eb2896483cbc7f65baa6a7ee761017aef9", size = 211508, upload-time = "2026-02-12T14:52:21.413Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/1714ef6e9325582e3727de3be27e4c1b2f428ea411d09f1396374180f130/librt-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:21b14464bee0b604d80a638cf1ee3148d84ca4cc163dcdcecb46060c1b3605e4", size = 219129, upload-time = "2026-02-12T14:52:22.61Z" }, - { url = "https://files.pythonhosted.org/packages/89/d3/2d9fe353edff91cdc0ece179348054a6fa61f3de992c44b9477cb973509b/librt-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:05a3dd3f116747f7e1a2b475ccdc6fb637fd4987126d109e03013a79d40bf9e6", size = 213126, upload-time = "2026-02-12T14:52:23.819Z" }, - { url = "https://files.pythonhosted.org/packages/ad/8e/9f5c60444880f6ad50e3ff7475e5529e787797e7f3ad5432241633733b92/librt-0.8.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fa37f99bff354ff191c6bcdffbc9d7cdd4fc37faccfc9be0ef3a4fd5613977da", size = 212279, upload-time = "2026-02-12T14:52:25.034Z" }, - { url = "https://files.pythonhosted.org/packages/fe/eb/d4a2cfa647da3022ae977f50d7eda1d91f70d7d1883cf958a4b6ef689eab/librt-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1566dbb9d1eb0987264c9b9460d212e809ba908d2f4a3999383a84d765f2f3f1", size = 234654, upload-time = "2026-02-12T14:52:26.204Z" }, - { url = "https://files.pythonhosted.org/packages/6a/31/26b978861c7983b036a3aea08bdbb2ec32bbaab1ad1d57c5e022be59afc1/librt-0.8.0-cp311-cp311-win32.whl", hash = "sha256:70defb797c4d5402166787a6b3c66dfb3fa7f93d118c0509ffafa35a392f4258", size = 54603, upload-time = "2026-02-12T14:52:27.342Z" }, - { url = "https://files.pythonhosted.org/packages/d0/78/f194ed7c48dacf875677e749c5d0d1d69a9daa7c994314a39466237fb1be/librt-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:db953b675079884ffda33d1dca7189fb961b6d372153750beb81880384300817", size = 61730, upload-time = "2026-02-12T14:52:28.31Z" }, - { url = "https://files.pythonhosted.org/packages/97/ee/ad71095478d02137b6f49469dc808c595cfe89b50985f6b39c5345f0faab/librt-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:75d1a8cab20b2043f03f7aab730551e9e440adc034d776f15f6f8d582b0a5ad4", size = 52274, upload-time = "2026-02-12T14:52:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fb/53/f3bc0c4921adb0d4a5afa0656f2c0fbe20e18e3e0295e12985b9a5dc3f55/librt-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:17269dd2745dbe8e42475acb28e419ad92dfa38214224b1b01020b8cac70b645", size = 66511, upload-time = "2026-02-12T14:52:30.34Z" }, - { url = "https://files.pythonhosted.org/packages/89/4b/4c96357432007c25a1b5e363045373a6c39481e49f6ba05234bb59a839c1/librt-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4617cef654fca552f00ce5ffdf4f4b68770f18950e4246ce94629b789b92467", size = 68628, upload-time = "2026-02-12T14:52:31.491Z" }, - { url = "https://files.pythonhosted.org/packages/47/16/52d75374d1012e8fc709216b5eaa25f471370e2a2331b8be00f18670a6c7/librt-0.8.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5cb11061a736a9db45e3c1293cfcb1e3caf205912dfa085734ba750f2197ff9a", size = 198941, upload-time = "2026-02-12T14:52:32.489Z" }, - { url = "https://files.pythonhosted.org/packages/fc/11/d5dd89e5a2228567b1228d8602d896736247424484db086eea6b8010bcba/librt-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb00bd71b448f16749909b08a0ff16f58b079e2261c2e1000f2bbb2a4f0a45", size = 210009, upload-time = "2026-02-12T14:52:33.634Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/fc1a92a77c3020ee08ce2dc48aed4b42ab7c30fb43ce488d388673b0f164/librt-0.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95a719a049f0eefaf1952673223cf00d442952273cbd20cf2ed7ec423a0ef58d", size = 224461, upload-time = "2026-02-12T14:52:34.868Z" }, - { url = "https://files.pythonhosted.org/packages/7f/98/eb923e8b028cece924c246104aa800cf72e02d023a8ad4ca87135b05a2fe/librt-0.8.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bd32add59b58fba3439d48d6f36ac695830388e3da3e92e4fc26d2d02670d19c", size = 217538, upload-time = "2026-02-12T14:52:36.078Z" }, - { url = "https://files.pythonhosted.org/packages/fd/67/24e80ab170674a1d8ee9f9a83081dca4635519dbd0473b8321deecddb5be/librt-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4f764b2424cb04524ff7a486b9c391e93f93dc1bd8305b2136d25e582e99aa2f", size = 225110, upload-time = "2026-02-12T14:52:37.301Z" }, - { url = "https://files.pythonhosted.org/packages/d8/c7/6fbdcbd1a6e5243c7989c21d68ab967c153b391351174b4729e359d9977f/librt-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f04ca50e847abc486fa8f4107250566441e693779a5374ba211e96e238f298b9", size = 217758, upload-time = "2026-02-12T14:52:38.89Z" }, - { url = "https://files.pythonhosted.org/packages/4b/bd/4d6b36669db086e3d747434430073e14def032dd58ad97959bf7e2d06c67/librt-0.8.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9ab3a3475a55b89b87ffd7e6665838e8458e0b596c22e0177e0f961434ec474a", size = 218384, upload-time = "2026-02-12T14:52:40.637Z" }, - { url = "https://files.pythonhosted.org/packages/50/2d/afe966beb0a8f179b132f3e95c8dd90738a23e9ebdba10f89a3f192f9366/librt-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e36a8da17134ffc29373775d88c04832f9ecfab1880470661813e6c7991ef79", size = 241187, upload-time = "2026-02-12T14:52:43.55Z" }, - { url = "https://files.pythonhosted.org/packages/02/d0/6172ea4af2b538462785ab1a68e52d5c99cfb9866a7caf00fdf388299734/librt-0.8.0-cp312-cp312-win32.whl", hash = "sha256:4eb5e06ebcc668677ed6389164f52f13f71737fc8be471101fa8b4ce77baeb0c", size = 54914, upload-time = "2026-02-12T14:52:44.676Z" }, - { url = "https://files.pythonhosted.org/packages/d4/cb/ceb6ed6175612a4337ad49fb01ef594712b934b4bc88ce8a63554832eb44/librt-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:0a33335eb59921e77c9acc05d0e654e4e32e45b014a4d61517897c11591094f8", size = 62020, upload-time = "2026-02-12T14:52:45.676Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/61701acbc67da74ce06ddc7ba9483e81c70f44236b2d00f6a4bfee1aacbf/librt-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:24a01c13a2a9bdad20997a4443ebe6e329df063d1978bbe2ebbf637878a46d1e", size = 52443, upload-time = "2026-02-12T14:52:47.218Z" }, - { url = "https://files.pythonhosted.org/packages/6d/32/3edb0bcb4113a9c8bdcd1750663a54565d255027657a5df9d90f13ee07fa/librt-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7f820210e21e3a8bf8fde2ae3c3d10106d4de9ead28cbfdf6d0f0f41f5b12fa1", size = 66522, upload-time = "2026-02-12T14:52:48.219Z" }, - { url = "https://files.pythonhosted.org/packages/30/ab/e8c3d05e281f5d405ebdcc5bc8ab36df23e1a4b40ac9da8c3eb9928b72b9/librt-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4831c44b8919e75ca0dfb52052897c1ef59fdae19d3589893fbd068f1e41afbf", size = 68658, upload-time = "2026-02-12T14:52:50.351Z" }, - { url = "https://files.pythonhosted.org/packages/7c/d3/74a206c47b7748bbc8c43942de3ed67de4c231156e148b4f9250869593df/librt-0.8.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88c6e75540f1f10f5e0fc5e87b4b6c290f0e90d1db8c6734f670840494764af8", size = 199287, upload-time = "2026-02-12T14:52:51.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/29/ef98a9131cf12cb95771d24e4c411fda96c89dc78b09c2de4704877ebee4/librt-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9646178cd794704d722306c2c920c221abbf080fede3ba539d5afdec16c46dad", size = 210293, upload-time = "2026-02-12T14:52:53.128Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3e/89b4968cb08c53d4c2d8b02517081dfe4b9e07a959ec143d333d76899f6c/librt-0.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e1af31a710e17891d9adf0dbd9a5fcd94901a3922a96499abdbf7ce658f4e01", size = 224801, upload-time = "2026-02-12T14:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/6d/28/f38526d501f9513f8b48d78e6be4a241e15dd4b000056dc8b3f06ee9ce5d/librt-0.8.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:507e94f4bec00b2f590fbe55f48cd518a208e2474a3b90a60aa8f29136ddbada", size = 218090, upload-time = "2026-02-12T14:52:55.758Z" }, - { url = "https://files.pythonhosted.org/packages/02/ec/64e29887c5009c24dc9c397116c680caffc50286f62bd99c39e3875a2854/librt-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f1178e0de0c271231a660fbef9be6acdfa1d596803464706862bef6644cc1cae", size = 225483, upload-time = "2026-02-12T14:52:57.375Z" }, - { url = "https://files.pythonhosted.org/packages/ee/16/7850bdbc9f1a32d3feff2708d90c56fc0490b13f1012e438532781aa598c/librt-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:71fc517efc14f75c2f74b1f0a5d5eb4a8e06aa135c34d18eaf3522f4a53cd62d", size = 218226, upload-time = "2026-02-12T14:52:58.534Z" }, - { url = "https://files.pythonhosted.org/packages/1c/4a/166bffc992d65ddefa7c47052010a87c059b44a458ebaf8f5eba384b0533/librt-0.8.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0583aef7e9a720dd40f26a2ad5a1bf2ccbb90059dac2b32ac516df232c701db3", size = 218755, upload-time = "2026-02-12T14:52:59.701Z" }, - { url = "https://files.pythonhosted.org/packages/da/5d/9aeee038bcc72a9cfaaee934463fe9280a73c5440d36bd3175069d2cb97b/librt-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d0f76fc73480d42285c609c0ea74d79856c160fa828ff9aceab574ea4ecfd7b", size = 241617, upload-time = "2026-02-12T14:53:00.966Z" }, - { url = "https://files.pythonhosted.org/packages/64/ff/2bec6b0296b9d0402aa6ec8540aa19ebcb875d669c37800cb43d10d9c3a3/librt-0.8.0-cp313-cp313-win32.whl", hash = "sha256:e79dbc8f57de360f0ed987dc7de7be814b4803ef0e8fc6d3ff86e16798c99935", size = 54966, upload-time = "2026-02-12T14:53:02.042Z" }, - { url = "https://files.pythonhosted.org/packages/08/8d/bf44633b0182996b2c7ea69a03a5c529683fa1f6b8e45c03fe874ff40d56/librt-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:25b3e667cbfc9000c4740b282df599ebd91dbdcc1aa6785050e4c1d6be5329ab", size = 62000, upload-time = "2026-02-12T14:53:03.822Z" }, - { url = "https://files.pythonhosted.org/packages/5c/fd/c6472b8e0eac0925001f75e366cf5500bcb975357a65ef1f6b5749389d3a/librt-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:e9a3a38eb4134ad33122a6d575e6324831f930a771d951a15ce232e0237412c2", size = 52496, upload-time = "2026-02-12T14:53:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/e0/13/79ebfe30cd273d7c0ce37a5f14dc489c5fb8b722a008983db2cfd57270bb/librt-0.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:421765e8c6b18e64d21c8ead315708a56fc24f44075059702e421d164575fdda", size = 66078, upload-time = "2026-02-12T14:53:06.085Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8f/d11eca40b62a8d5e759239a80636386ef88adecb10d1a050b38cc0da9f9e/librt-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:48f84830a8f8ad7918afd743fd7c4eb558728bceab7b0e38fd5a5cf78206a556", size = 68309, upload-time = "2026-02-12T14:53:07.121Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/f12ee70a3596db40ff3c88ec9eaa4e323f3b92f77505b4d900746706ec6a/librt-0.8.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f09d4884f882baa39a7e36bbf3eae124c4ca2a223efb91e567381d1c55c6b06", size = 196804, upload-time = "2026-02-12T14:53:08.164Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7e/70dbbdc0271fd626abe1671ad117bcd61a9a88cdc6a10ccfbfc703db1873/librt-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:693697133c3b32aa9b27f040e3691be210e9ac4d905061859a9ed519b1d5a376", size = 206915, upload-time = "2026-02-12T14:53:09.333Z" }, - { url = "https://files.pythonhosted.org/packages/79/13/6b9e05a635d4327608d06b3c1702166e3b3e78315846373446cf90d7b0bf/librt-0.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5512aae4648152abaf4d48b59890503fcbe86e85abc12fb9b096fe948bdd816", size = 221200, upload-time = "2026-02-12T14:53:10.68Z" }, - { url = "https://files.pythonhosted.org/packages/35/6c/e19a3ac53e9414de43a73d7507d2d766cd22d8ca763d29a4e072d628db42/librt-0.8.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:995d24caa6bbb34bcdd4a41df98ac6d1af637cfa8975cb0790e47d6623e70e3e", size = 214640, upload-time = "2026-02-12T14:53:12.342Z" }, - { url = "https://files.pythonhosted.org/packages/30/f0/23a78464788619e8c70f090cfd099cce4973eed142c4dccb99fc322283fd/librt-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b9aef96d7593584e31ef6ac1eb9775355b0099fee7651fae3a15bc8657b67b52", size = 221980, upload-time = "2026-02-12T14:53:13.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/32/38e21420c5d7aa8a8bd2c7a7d5252ab174a5a8aaec8b5551968979b747bf/librt-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4f6e975377fbc4c9567cb33ea9ab826031b6c7ec0515bfae66a4fb110d40d6da", size = 215146, upload-time = "2026-02-12T14:53:14.8Z" }, - { url = "https://files.pythonhosted.org/packages/bb/00/bd9ecf38b1824c25240b3ad982fb62c80f0a969e6679091ba2b3afb2b510/librt-0.8.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:daae5e955764be8fd70a93e9e5133c75297f8bce1e802e1d3683b98f77e1c5ab", size = 215203, upload-time = "2026-02-12T14:53:16.087Z" }, - { url = "https://files.pythonhosted.org/packages/b9/60/7559bcc5279d37810b98d4a52616febd7b8eef04391714fd6bdf629598b1/librt-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7bd68cebf3131bb920d5984f75fe302d758db33264e44b45ad139385662d7bc3", size = 237937, upload-time = "2026-02-12T14:53:17.236Z" }, - { url = "https://files.pythonhosted.org/packages/41/cc/be3e7da88f1abbe2642672af1dc00a0bccece11ca60241b1883f3018d8d5/librt-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1e6811cac1dcb27ca4c74e0ca4a5917a8e06db0d8408d30daee3a41724bfde7a", size = 50685, upload-time = "2026-02-12T14:53:18.888Z" }, - { url = "https://files.pythonhosted.org/packages/38/27/e381d0df182a8f61ef1f6025d8b138b3318cc9d18ad4d5f47c3bf7492523/librt-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:178707cda89d910c3b28bf5aa5f69d3d4734e0f6ae102f753ad79edef83a83c7", size = 57872, upload-time = "2026-02-12T14:53:19.942Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0c/ca9dfdf00554a44dea7d555001248269a4bab569e1590a91391feb863fa4/librt-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3e8b77b5f54d0937b26512774916041756c9eb3e66f1031971e626eea49d0bf4", size = 48056, upload-time = "2026-02-12T14:53:21.473Z" }, - { url = "https://files.pythonhosted.org/packages/f2/ed/6cc9c4ad24f90c8e782193c7b4a857408fd49540800613d1356c63567d7b/librt-0.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:789911e8fa40a2e82f41120c936b1965f3213c67f5a483fc5a41f5839a05dcbb", size = 68307, upload-time = "2026-02-12T14:53:22.498Z" }, - { url = "https://files.pythonhosted.org/packages/84/d8/0e94292c6b3e00b6eeea39dd44d5703d1ec29b6dafce7eea19dc8f1aedbd/librt-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2b37437e7e4ef5e15a297b36ba9e577f73e29564131d86dd75875705e97402b5", size = 70999, upload-time = "2026-02-12T14:53:23.603Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f4/6be1afcbdeedbdbbf54a7c9d73ad43e1bf36897cebf3978308cd64922e02/librt-0.8.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:671a6152edf3b924d98a5ed5e6982ec9cb30894085482acadce0975f031d4c5c", size = 220782, upload-time = "2026-02-12T14:53:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8d/f306e8caa93cfaf5c6c9e0d940908d75dc6af4fd856baa5535c922ee02b1/librt-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8992ca186a1678107b0af3d0c9303d8c7305981b9914989b9788319ed4d89546", size = 235420, upload-time = "2026-02-12T14:53:27.047Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f2/65d86bd462e9c351326564ca805e8457442149f348496e25ccd94583ffa2/librt-0.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:001e5330093d887b8b9165823eca6c5c4db183fe4edea4fdc0680bbac5f46944", size = 246452, upload-time = "2026-02-12T14:53:28.341Z" }, - { url = "https://files.pythonhosted.org/packages/03/94/39c88b503b4cb3fcbdeb3caa29672b6b44ebee8dcc8a54d49839ac280f3f/librt-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d920789eca7ef71df7f31fd547ec0d3002e04d77f30ba6881e08a630e7b2c30e", size = 238891, upload-time = "2026-02-12T14:53:29.625Z" }, - { url = "https://files.pythonhosted.org/packages/e3/c6/6c0d68190893d01b71b9569b07a1c811e280c0065a791249921c83dc0290/librt-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:82fb4602d1b3e303a58bfe6165992b5a78d823ec646445356c332cd5f5bbaa61", size = 250249, upload-time = "2026-02-12T14:53:30.93Z" }, - { url = "https://files.pythonhosted.org/packages/52/7a/f715ed9e039035d0ea637579c3c0155ab3709a7046bc408c0fb05d337121/librt-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4d3e38797eb482485b486898f89415a6ab163bc291476bd95712e42cf4383c05", size = 240642, upload-time = "2026-02-12T14:53:32.174Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3c/609000a333debf5992efe087edc6467c1fdbdddca5b610355569bbea9589/librt-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:a905091a13e0884701226860836d0386b88c72ce5c2fdfba6618e14c72be9f25", size = 239621, upload-time = "2026-02-12T14:53:33.39Z" }, - { url = "https://files.pythonhosted.org/packages/b9/df/87b0673d5c395a8f34f38569c116c93142d4dc7e04af2510620772d6bd4f/librt-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:375eda7acfce1f15f5ed56cfc960669eefa1ec8732e3e9087c3c4c3f2066759c", size = 262986, upload-time = "2026-02-12T14:53:34.617Z" }, - { url = "https://files.pythonhosted.org/packages/09/7f/6bbbe9dcda649684773aaea78b87fff4d7e59550fbc2877faa83612087a3/librt-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:2ccdd20d9a72c562ffb73098ac411de351b53a6fbb3390903b2d33078ef90447", size = 51328, upload-time = "2026-02-12T14:53:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f3/e1981ab6fa9b41be0396648b5850267888a752d025313a9e929c4856208e/librt-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:25e82d920d4d62ad741592fcf8d0f3bda0e3fc388a184cb7d2f566c681c5f7b9", size = 58719, upload-time = "2026-02-12T14:53:37.183Z" }, - { url = "https://files.pythonhosted.org/packages/94/d1/433b3c06e78f23486fe4fdd19bc134657eb30997d2054b0dbf52bbf3382e/librt-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:92249938ab744a5890580d3cb2b22042f0dce71cdaa7c1369823df62bedf7cbc", size = 48753, upload-time = "2026-02-12T14:53:38.539Z" }, -] - -[[package]] -name = "linkify-it-py" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "uc-micro-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, -] - -[[package]] -name = "llvmlite" -version = "0.46.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/74/cd/08ae687ba099c7e3d21fe2ea536500563ef1943c5105bf6ab4ee3829f68e/llvmlite-0.46.0.tar.gz", hash = "sha256:227c9fd6d09dce2783c18b754b7cd9d9b3b3515210c46acc2d3c5badd9870ceb", size = 193456, upload-time = "2025-12-08T18:15:36.295Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/a4/3959e1c61c5ca9db7921e5fd115b344c29b9d57a5dadd87bef97963ca1a5/llvmlite-0.46.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4323177e936d61ae0f73e653e2e614284d97d14d5dd12579adc92b6c2b0597b0", size = 37232766, upload-time = "2025-12-08T18:14:34.765Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/a4d916f1015106e1da876028606a8e87fd5d5c840f98c87bc2d5153b6a2f/llvmlite-0.46.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a2d461cb89537b7c20feb04c46c32e12d5ad4f0896c9dfc0f60336219ff248e", size = 56275176, upload-time = "2025-12-08T18:14:37.944Z" }, - { url = "https://files.pythonhosted.org/packages/79/7f/a7f2028805dac8c1a6fae7bda4e739b7ebbcd45b29e15bf6d21556fcd3d5/llvmlite-0.46.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b1f6595a35b7b39c3518b85a28bf18f45e075264e4b2dce3f0c2a4f232b4a910", size = 55128629, upload-time = "2025-12-08T18:14:41.674Z" }, - { url = "https://files.pythonhosted.org/packages/b2/bc/4689e1ba0c073c196b594471eb21be0aa51d9e64b911728aa13cd85ef0ae/llvmlite-0.46.0-cp310-cp310-win_amd64.whl", hash = "sha256:e7a34d4aa6f9a97ee006b504be6d2b8cb7f755b80ab2f344dda1ef992f828559", size = 38138651, upload-time = "2025-12-08T18:14:45.845Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/2ad4b2367915faeebe8447f0a057861f646dbf5fbbb3561db42c65659cf3/llvmlite-0.46.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82f3d39b16f19aa1a56d5fe625883a6ab600d5cc9ea8906cca70ce94cabba067", size = 37232766, upload-time = "2025-12-08T18:14:48.836Z" }, - { url = "https://files.pythonhosted.org/packages/12/b5/99cf8772fdd846c07da4fd70f07812a3c8fd17ea2409522c946bb0f2b277/llvmlite-0.46.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a3df43900119803bbc52720e758c76f316a9a0f34612a886862dfe0a5591a17e", size = 56275175, upload-time = "2025-12-08T18:14:51.604Z" }, - { url = "https://files.pythonhosted.org/packages/38/f2/ed806f9c003563732da156139c45d970ee435bd0bfa5ed8de87ba972b452/llvmlite-0.46.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de183fefc8022d21b0aa37fc3e90410bc3524aed8617f0ff76732fc6c3af5361", size = 55128630, upload-time = "2025-12-08T18:14:55.107Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/8f5a37a65fc9b7b17408508145edd5f86263ad69c19d3574e818f533a0eb/llvmlite-0.46.0-cp311-cp311-win_amd64.whl", hash = "sha256:e8b10bc585c58bdffec9e0c309bb7d51be1f2f15e169a4b4d42f2389e431eb93", size = 38138652, upload-time = "2025-12-08T18:14:58.171Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f8/4db016a5e547d4e054ff2f3b99203d63a497465f81ab78ec8eb2ff7b2304/llvmlite-0.46.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b9588ad4c63b4f0175a3984b85494f0c927c6b001e3a246a3a7fb3920d9a137", size = 37232767, upload-time = "2025-12-08T18:15:00.737Z" }, - { url = "https://files.pythonhosted.org/packages/aa/85/4890a7c14b4fa54400945cb52ac3cd88545bbdb973c440f98ca41591cdc5/llvmlite-0.46.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3535bd2bb6a2d7ae4012681ac228e5132cdb75fefb1bcb24e33f2f3e0c865ed4", size = 56275176, upload-time = "2025-12-08T18:15:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/6a/07/3d31d39c1a1a08cd5337e78299fca77e6aebc07c059fbd0033e3edfab45c/llvmlite-0.46.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cbfd366e60ff87ea6cc62f50bc4cd800ebb13ed4c149466f50cf2163a473d1e", size = 55128630, upload-time = "2025-12-08T18:15:07.196Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6b/d139535d7590a1bba1ceb68751bef22fadaa5b815bbdf0e858e3875726b2/llvmlite-0.46.0-cp312-cp312-win_amd64.whl", hash = "sha256:398b39db462c39563a97b912d4f2866cd37cba60537975a09679b28fbbc0fb38", size = 38138940, upload-time = "2025-12-08T18:15:10.162Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ff/3eba7eb0aed4b6fca37125387cd417e8c458e750621fce56d2c541f67fa8/llvmlite-0.46.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:30b60892d034bc560e0ec6654737aaa74e5ca327bd8114d82136aa071d611172", size = 37232767, upload-time = "2025-12-08T18:15:13.22Z" }, - { url = "https://files.pythonhosted.org/packages/0e/54/737755c0a91558364b9200702c3c9c15d70ed63f9b98a2c32f1c2aa1f3ba/llvmlite-0.46.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6cc19b051753368a9c9f31dc041299059ee91aceec81bd57b0e385e5d5bf1a54", size = 56275176, upload-time = "2025-12-08T18:15:16.339Z" }, - { url = "https://files.pythonhosted.org/packages/e6/91/14f32e1d70905c1c0aa4e6609ab5d705c3183116ca02ac6df2091868413a/llvmlite-0.46.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bca185892908f9ede48c0acd547fe4dc1bafefb8a4967d47db6cf664f9332d12", size = 55128629, upload-time = "2025-12-08T18:15:19.493Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a7/d526ae86708cea531935ae777b6dbcabe7db52718e6401e0fb9c5edea80e/llvmlite-0.46.0-cp313-cp313-win_amd64.whl", hash = "sha256:67438fd30e12349ebb054d86a5a1a57fd5e87d264d2451bcfafbbbaa25b82a35", size = 38138941, upload-time = "2025-12-08T18:15:22.536Z" }, - { url = "https://files.pythonhosted.org/packages/95/ae/af0ffb724814cc2ea64445acad05f71cff5f799bb7efb22e47ee99340dbc/llvmlite-0.46.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:d252edfb9f4ac1fcf20652258e3f102b26b03eef738dc8a6ffdab7d7d341d547", size = 37232768, upload-time = "2025-12-08T18:15:25.055Z" }, - { url = "https://files.pythonhosted.org/packages/c9/19/5018e5352019be753b7b07f7759cdabb69ca5779fea2494be8839270df4c/llvmlite-0.46.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:379fdd1c59badeff8982cb47e4694a6143bec3bb49aa10a466e095410522064d", size = 56275173, upload-time = "2025-12-08T18:15:28.109Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c9/d57877759d707e84c082163c543853245f91b70c804115a5010532890f18/llvmlite-0.46.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e8cbfff7f6db0fa2c771ad24154e2a7e457c2444d7673e6de06b8b698c3b269", size = 55128628, upload-time = "2025-12-08T18:15:31.098Z" }, - { url = "https://files.pythonhosted.org/packages/30/a8/e61a8c2b3cc7a597073d9cde1fcbb567e9d827f1db30c93cf80422eac70d/llvmlite-0.46.0-cp314-cp314-win_amd64.whl", hash = "sha256:7821eda3ec1f18050f981819756631d60b6d7ab1a6cf806d9efefbe3f4082d61", size = 39153056, upload-time = "2025-12-08T18:15:33.938Z" }, -] - -[[package]] -name = "locket" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, -] - -[[package]] -name = "logistro" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/90/bfd7a6fab22bdfafe48ed3c4831713cb77b4779d18ade5e248d5dbc0ca22/logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047", size = 8398, upload-time = "2025-11-01T02:41:18.81Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/6aa79ba3570bddd1bf7e951c6123f806751e58e8cce736bad77b2cf348d7/logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb", size = 8555, upload-time = "2025-11-01T02:41:17.587Z" }, -] - -[[package]] -name = "lsprotocol" -version = "2025.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "cattrs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/26/67b84e6ec1402f0e6764ef3d2a0aaf9a79522cc1d37738f4e5bb0b21521a/lsprotocol-2025.0.0.tar.gz", hash = "sha256:e879da2b9301e82cfc3e60d805630487ac2f7ab17492f4f5ba5aaba94fe56c29", size = 74896, upload-time = "2025-06-17T21:30:18.156Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/f0/92f2d609d6642b5f30cb50a885d2bf1483301c69d5786286500d15651ef2/lsprotocol-2025.0.0-py3-none-any.whl", hash = "sha256:f9d78f25221f2a60eaa4a96d3b4ffae011b107537facee61d3da3313880995c7", size = 76250, upload-time = "2025-06-17T21:30:19.455Z" }, -] - -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/8a/f8192a08237ef2fb1b19733f709db88a4c43bc8ab8357f01cb41a27e7f6a/lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388", size = 8590589, upload-time = "2025-09-22T04:00:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/27bcd07ae17ff5e5536e8d88f4c7d581b48963817a13de11f3ac3329bfa2/lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153", size = 4629671, upload-time = "2025-09-22T04:00:15.411Z" }, - { url = "https://files.pythonhosted.org/packages/02/5a/a7d53b3291c324e0b6e48f3c797be63836cc52156ddf8f33cd72aac78866/lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31", size = 4999961, upload-time = "2025-09-22T04:00:17.619Z" }, - { url = "https://files.pythonhosted.org/packages/f5/55/d465e9b89df1761674d8672bb3e4ae2c47033b01ec243964b6e334c6743f/lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9", size = 5157087, upload-time = "2025-09-22T04:00:19.868Z" }, - { url = "https://files.pythonhosted.org/packages/62/38/3073cd7e3e8dfc3ba3c3a139e33bee3a82de2bfb0925714351ad3d255c13/lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8", size = 5067620, upload-time = "2025-09-22T04:00:21.877Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d3/1e001588c5e2205637b08985597827d3827dbaaece16348c8822bfe61c29/lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba", size = 5406664, upload-time = "2025-09-22T04:00:23.714Z" }, - { url = "https://files.pythonhosted.org/packages/20/cf/cab09478699b003857ed6ebfe95e9fb9fa3d3c25f1353b905c9b73cfb624/lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c", size = 5289397, upload-time = "2025-09-22T04:00:25.544Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/02a2d0c38ac9a8b9f9e5e1bbd3f24b3f426044ad618b552e9549ee91bd63/lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c", size = 4772178, upload-time = "2025-09-22T04:00:27.602Z" }, - { url = "https://files.pythonhosted.org/packages/56/87/e1ceadcc031ec4aa605fe95476892d0b0ba3b7f8c7dcdf88fdeff59a9c86/lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321", size = 5358148, upload-time = "2025-09-22T04:00:29.323Z" }, - { url = "https://files.pythonhosted.org/packages/fe/13/5bb6cf42bb228353fd4ac5f162c6a84fd68a4d6f67c1031c8cf97e131fc6/lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1", size = 5112035, upload-time = "2025-09-22T04:00:31.061Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e2/ea0498552102e59834e297c5c6dff8d8ded3db72ed5e8aad77871476f073/lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34", size = 4799111, upload-time = "2025-09-22T04:00:33.11Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9e/8de42b52a73abb8af86c66c969b3b4c2a96567b6ac74637c037d2e3baa60/lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a", size = 5351662, upload-time = "2025-09-22T04:00:35.237Z" }, - { url = "https://files.pythonhosted.org/packages/28/a2/de776a573dfb15114509a37351937c367530865edb10a90189d0b4b9b70a/lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c", size = 5314973, upload-time = "2025-09-22T04:00:37.086Z" }, - { url = "https://files.pythonhosted.org/packages/50/a0/3ae1b1f8964c271b5eec91db2043cf8c6c0bce101ebb2a633b51b044db6c/lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b", size = 3611953, upload-time = "2025-09-22T04:00:39.224Z" }, - { url = "https://files.pythonhosted.org/packages/d1/70/bd42491f0634aad41bdfc1e46f5cff98825fb6185688dc82baa35d509f1a/lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0", size = 4032695, upload-time = "2025-09-22T04:00:41.402Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d0/05c6a72299f54c2c561a6c6cbb2f512e047fca20ea97a05e57931f194ac4/lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5", size = 3680051, upload-time = "2025-09-22T04:00:43.525Z" }, - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" }, - { url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" }, - { url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" }, - { url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" }, - { url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" }, - { url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" }, - { url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" }, - { url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" }, - { url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" }, - { url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" }, - { url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" }, - { url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" }, - { url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" }, - { url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" }, - { url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" }, - { url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" }, - { url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" }, - { url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" }, - { url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" }, - { url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" }, - { url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" }, - { url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" }, - { url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" }, - { url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" }, - { url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" }, - { url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" }, - { url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" }, - { url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9c/780c9a8fce3f04690b374f72f41306866b0400b9d0fdf3e17aaa37887eed/lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6", size = 3939264, upload-time = "2025-09-22T04:04:32.892Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5a/1ab260c00adf645d8bf7dec7f920f744b032f69130c681302821d5debea6/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba", size = 4216435, upload-time = "2025-09-22T04:04:34.907Z" }, - { url = "https://files.pythonhosted.org/packages/f2/37/565f3b3d7ffede22874b6d86be1a1763d00f4ea9fc5b9b6ccb11e4ec8612/lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5", size = 4325913, upload-time = "2025-09-22T04:04:37.205Z" }, - { url = "https://files.pythonhosted.org/packages/22/ec/f3a1b169b2fb9d03467e2e3c0c752ea30e993be440a068b125fc7dd248b0/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4", size = 4269357, upload-time = "2025-09-22T04:04:39.322Z" }, - { url = "https://files.pythonhosted.org/packages/77/a2/585a28fe3e67daa1cf2f06f34490d556d121c25d500b10082a7db96e3bcd/lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d", size = 4412295, upload-time = "2025-09-22T04:04:41.647Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/a57dd8bcebd7c69386c20263830d4fa72d27e6b72a229ef7a48e88952d9a/lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d", size = 3516913, upload-time = "2025-09-22T04:04:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" }, - { url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" }, - { url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, -] - -[[package]] -name = "lxml-stubs" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/da/1a3a3e5d159b249fc2970d73437496b908de8e4716a089c69591b4ffa6fd/lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d", size = 14778, upload-time = "2024-01-10T09:37:46.521Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/c9/e0f8e4e6e8a69e5959b06499582dca6349db6769cc7fdfb8a02a7c75a9ae/lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272", size = 13584, upload-time = "2024-01-10T09:37:44.931Z" }, -] - -[[package]] -name = "lz4" -version = "4.4.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/51/f1b86d93029f418033dddf9b9f79c8d2641e7454080478ee2aab5123173e/lz4-4.4.5.tar.gz", hash = "sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0", size = 172886, upload-time = "2025-11-03T13:02:36.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/45/2466d73d79e3940cad4b26761f356f19fd33f4409c96f100e01a5c566909/lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d", size = 207396, upload-time = "2025-11-03T13:01:24.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/12/7da96077a7e8918a5a57a25f1254edaf76aefb457666fcc1066deeecd609/lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f", size = 207154, upload-time = "2025-11-03T13:01:26.922Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0e/0fb54f84fd1890d4af5bc0a3c1fa69678451c1a6bd40de26ec0561bb4ec5/lz4-4.4.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3", size = 1291053, upload-time = "2025-11-03T13:01:28.396Z" }, - { url = "https://files.pythonhosted.org/packages/15/45/8ce01cc2715a19c9e72b0e423262072c17d581a8da56e0bd4550f3d76a79/lz4-4.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758", size = 1278586, upload-time = "2025-11-03T13:01:29.906Z" }, - { url = "https://files.pythonhosted.org/packages/6d/34/7be9b09015e18510a09b8d76c304d505a7cbc66b775ec0b8f61442316818/lz4-4.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1", size = 1367315, upload-time = "2025-11-03T13:01:31.054Z" }, - { url = "https://files.pythonhosted.org/packages/2a/94/52cc3ec0d41e8d68c985ec3b2d33631f281d8b748fb44955bc0384c2627b/lz4-4.4.5-cp310-cp310-win32.whl", hash = "sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc", size = 88173, upload-time = "2025-11-03T13:01:32.643Z" }, - { url = "https://files.pythonhosted.org/packages/ca/35/c3c0bdc409f551404355aeeabc8da343577d0e53592368062e371a3620e1/lz4-4.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd", size = 99492, upload-time = "2025-11-03T13:01:33.813Z" }, - { url = "https://files.pythonhosted.org/packages/1d/02/4d88de2f1e97f9d05fd3d278fe412b08969bc94ff34942f5a3f09318144a/lz4-4.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397", size = 91280, upload-time = "2025-11-03T13:01:35.081Z" }, - { url = "https://files.pythonhosted.org/packages/93/5b/6edcd23319d9e28b1bedf32768c3d1fd56eed8223960a2c47dacd2cec2af/lz4-4.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4", size = 207391, upload-time = "2025-11-03T13:01:36.644Z" }, - { url = "https://files.pythonhosted.org/packages/34/36/5f9b772e85b3d5769367a79973b8030afad0d6b724444083bad09becd66f/lz4-4.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43", size = 207146, upload-time = "2025-11-03T13:01:37.928Z" }, - { url = "https://files.pythonhosted.org/packages/04/f4/f66da5647c0d72592081a37c8775feacc3d14d2625bbdaabd6307c274565/lz4-4.4.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7", size = 1292623, upload-time = "2025-11-03T13:01:39.341Z" }, - { url = "https://files.pythonhosted.org/packages/85/fc/5df0f17467cdda0cad464a9197a447027879197761b55faad7ca29c29a04/lz4-4.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb", size = 1279982, upload-time = "2025-11-03T13:01:40.816Z" }, - { url = "https://files.pythonhosted.org/packages/25/3b/b55cb577aa148ed4e383e9700c36f70b651cd434e1c07568f0a86c9d5fbb/lz4-4.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989", size = 1368674, upload-time = "2025-11-03T13:01:42.118Z" }, - { url = "https://files.pythonhosted.org/packages/fb/31/e97e8c74c59ea479598e5c55cbe0b1334f03ee74ca97726e872944ed42df/lz4-4.4.5-cp311-cp311-win32.whl", hash = "sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d", size = 88168, upload-time = "2025-11-03T13:01:43.282Z" }, - { url = "https://files.pythonhosted.org/packages/18/47/715865a6c7071f417bef9b57c8644f29cb7a55b77742bd5d93a609274e7e/lz4-4.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004", size = 99491, upload-time = "2025-11-03T13:01:44.167Z" }, - { url = "https://files.pythonhosted.org/packages/14/e7/ac120c2ca8caec5c945e6356ada2aa5cfabd83a01e3170f264a5c42c8231/lz4-4.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b", size = 91271, upload-time = "2025-11-03T13:01:45.016Z" }, - { url = "https://files.pythonhosted.org/packages/1b/ac/016e4f6de37d806f7cc8f13add0a46c9a7cfc41a5ddc2bc831d7954cf1ce/lz4-4.4.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e", size = 207163, upload-time = "2025-11-03T13:01:45.895Z" }, - { url = "https://files.pythonhosted.org/packages/8d/df/0fadac6e5bd31b6f34a1a8dbd4db6a7606e70715387c27368586455b7fc9/lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a", size = 207150, upload-time = "2025-11-03T13:01:47.205Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/34e36cc49bb16ca73fb57fbd4c5eaa61760c6b64bce91fcb4e0f4a97f852/lz4-4.4.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5", size = 1292045, upload-time = "2025-11-03T13:01:48.667Z" }, - { url = "https://files.pythonhosted.org/packages/90/1c/b1d8e3741e9fc89ed3b5f7ef5f22586c07ed6bb04e8343c2e98f0fa7ff04/lz4-4.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e", size = 1279546, upload-time = "2025-11-03T13:01:50.159Z" }, - { url = "https://files.pythonhosted.org/packages/55/d9/e3867222474f6c1b76e89f3bd914595af69f55bf2c1866e984c548afdc15/lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e", size = 1368249, upload-time = "2025-11-03T13:01:51.273Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e7/d667d337367686311c38b580d1ca3d5a23a6617e129f26becd4f5dc458df/lz4-4.4.5-cp312-cp312-win32.whl", hash = "sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50", size = 88189, upload-time = "2025-11-03T13:01:52.605Z" }, - { url = "https://files.pythonhosted.org/packages/a5/0b/a54cd7406995ab097fceb907c7eb13a6ddd49e0b231e448f1a81a50af65c/lz4-4.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33", size = 99497, upload-time = "2025-11-03T13:01:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/6a/7e/dc28a952e4bfa32ca16fa2eb026e7a6ce5d1411fcd5986cd08c74ec187b9/lz4-4.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301", size = 91279, upload-time = "2025-11-03T13:01:54.419Z" }, - { url = "https://files.pythonhosted.org/packages/2f/46/08fd8ef19b782f301d56a9ccfd7dafec5fd4fc1a9f017cf22a1accb585d7/lz4-4.4.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c", size = 207171, upload-time = "2025-11-03T13:01:56.595Z" }, - { url = "https://files.pythonhosted.org/packages/8f/3f/ea3334e59de30871d773963997ecdba96c4584c5f8007fd83cfc8f1ee935/lz4-4.4.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a", size = 207163, upload-time = "2025-11-03T13:01:57.721Z" }, - { url = "https://files.pythonhosted.org/packages/41/7b/7b3a2a0feb998969f4793c650bb16eff5b06e80d1f7bff867feb332f2af2/lz4-4.4.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d", size = 1292136, upload-time = "2025-11-03T13:02:00.375Z" }, - { url = "https://files.pythonhosted.org/packages/89/d1/f1d259352227bb1c185288dd694121ea303e43404aa77560b879c90e7073/lz4-4.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c", size = 1279639, upload-time = "2025-11-03T13:02:01.649Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fb/ba9256c48266a09012ed1d9b0253b9aa4fe9cdff094f8febf5b26a4aa2a2/lz4-4.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64", size = 1368257, upload-time = "2025-11-03T13:02:03.35Z" }, - { url = "https://files.pythonhosted.org/packages/a5/6d/dee32a9430c8b0e01bbb4537573cabd00555827f1a0a42d4e24ca803935c/lz4-4.4.5-cp313-cp313-win32.whl", hash = "sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832", size = 88191, upload-time = "2025-11-03T13:02:04.406Z" }, - { url = "https://files.pythonhosted.org/packages/18/e0/f06028aea741bbecb2a7e9648f4643235279a770c7ffaf70bd4860c73661/lz4-4.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22", size = 99502, upload-time = "2025-11-03T13:02:05.886Z" }, - { url = "https://files.pythonhosted.org/packages/61/72/5bef44afb303e56078676b9f2486f13173a3c1e7f17eaac1793538174817/lz4-4.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9", size = 91285, upload-time = "2025-11-03T13:02:06.77Z" }, - { url = "https://files.pythonhosted.org/packages/49/55/6a5c2952971af73f15ed4ebfdd69774b454bd0dc905b289082ca8664fba1/lz4-4.4.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f", size = 207348, upload-time = "2025-11-03T13:02:08.117Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d7/fd62cbdbdccc35341e83aabdb3f6d5c19be2687d0a4eaf6457ddf53bba64/lz4-4.4.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba", size = 207340, upload-time = "2025-11-03T13:02:09.152Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/225ffadaacb4b0e0eb5fd263541edd938f16cd21fe1eae3cd6d5b6a259dc/lz4-4.4.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d", size = 1293398, upload-time = "2025-11-03T13:02:10.272Z" }, - { url = "https://files.pythonhosted.org/packages/c6/9e/2ce59ba4a21ea5dc43460cba6f34584e187328019abc0e66698f2b66c881/lz4-4.4.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67", size = 1281209, upload-time = "2025-11-03T13:02:12.091Z" }, - { url = "https://files.pythonhosted.org/packages/80/4f/4d946bd1624ec229b386a3bc8e7a85fa9a963d67d0a62043f0af0978d3da/lz4-4.4.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d", size = 1369406, upload-time = "2025-11-03T13:02:13.683Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/d429ba4720a9064722698b4b754fb93e42e625f1318b8fe834086c7c783b/lz4-4.4.5-cp313-cp313t-win32.whl", hash = "sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901", size = 88325, upload-time = "2025-11-03T13:02:14.743Z" }, - { url = "https://files.pythonhosted.org/packages/4b/85/7ba10c9b97c06af6c8f7032ec942ff127558863df52d866019ce9d2425cf/lz4-4.4.5-cp313-cp313t-win_amd64.whl", hash = "sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb", size = 99643, upload-time = "2025-11-03T13:02:15.978Z" }, - { url = "https://files.pythonhosted.org/packages/77/4d/a175459fb29f909e13e57c8f475181ad8085d8d7869bd8ad99033e3ee5fa/lz4-4.4.5-cp313-cp313t-win_arm64.whl", hash = "sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd", size = 91504, upload-time = "2025-11-03T13:02:17.313Z" }, - { url = "https://files.pythonhosted.org/packages/63/9c/70bdbdb9f54053a308b200b4678afd13efd0eafb6ddcbb7f00077213c2e5/lz4-4.4.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f", size = 207586, upload-time = "2025-11-03T13:02:18.263Z" }, - { url = "https://files.pythonhosted.org/packages/b6/cb/bfead8f437741ce51e14b3c7d404e3a1f6b409c440bad9b8f3945d4c40a7/lz4-4.4.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6", size = 207161, upload-time = "2025-11-03T13:02:19.286Z" }, - { url = "https://files.pythonhosted.org/packages/e7/18/b192b2ce465dfbeabc4fc957ece7a1d34aded0d95a588862f1c8a86ac448/lz4-4.4.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9", size = 1292415, upload-time = "2025-11-03T13:02:20.829Z" }, - { url = "https://files.pythonhosted.org/packages/67/79/a4e91872ab60f5e89bfad3e996ea7dc74a30f27253faf95865771225ccba/lz4-4.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668", size = 1279920, upload-time = "2025-11-03T13:02:22.013Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/d52c7b11eaa286d49dae619c0eec4aabc0bf3cda7a7467eb77c62c4471f3/lz4-4.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f", size = 1368661, upload-time = "2025-11-03T13:02:23.208Z" }, - { url = "https://files.pythonhosted.org/packages/f7/da/137ddeea14c2cb86864838277b2607d09f8253f152156a07f84e11768a28/lz4-4.4.5-cp314-cp314-win32.whl", hash = "sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67", size = 90139, upload-time = "2025-11-03T13:02:24.301Z" }, - { url = "https://files.pythonhosted.org/packages/18/2c/8332080fd293f8337779a440b3a143f85e374311705d243439a3349b81ad/lz4-4.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be", size = 101497, upload-time = "2025-11-03T13:02:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/ca/28/2635a8141c9a4f4bc23f5135a92bbcf48d928d8ca094088c962df1879d64/lz4-4.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7", size = 93812, upload-time = "2025-11-03T13:02:26.133Z" }, -] - -[[package]] -name = "markdown" -version = "3.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, -] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[package.optional-dependencies] -linkify = [ - { name = "linkify-it-py" }, -] -plugins = [ - { name = "mdit-py-plugins" }, -] - -[[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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, - { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, - { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, - { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, - { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, - { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, -] - -[[package]] -name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, -] - -[[package]] -name = "matplotlib" -version = "3.10.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "contourpy", version = "1.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "contourpy", version = "1.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "cycler" }, - { name = "fonttools" }, - { name = "kiwisolver" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "pyparsing" }, - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/be/a30bd917018ad220c400169fba298f2bb7003c8ccbc0c3e24ae2aacad1e8/matplotlib-3.10.8-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:00270d217d6b20d14b584c521f810d60c5c78406dc289859776550df837dcda7", size = 8239828, upload-time = "2025-12-10T22:55:02.313Z" }, - { url = "https://files.pythonhosted.org/packages/58/27/ca01e043c4841078e82cf6e80a6993dfecd315c3d79f5f3153afbb8e1ec6/matplotlib-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37b3c1cc42aa184b3f738cfa18c1c1d72fd496d85467a6cf7b807936d39aa656", size = 8128050, upload-time = "2025-12-10T22:55:04.997Z" }, - { url = "https://files.pythonhosted.org/packages/cb/aa/7ab67f2b729ae6a91bcf9dcac0affb95fb8c56f7fd2b2af894ae0b0cf6fa/matplotlib-3.10.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee40c27c795bda6a5292e9cff9890189d32f7e3a0bf04e0e3c9430c4a00c37df", size = 8700452, upload-time = "2025-12-10T22:55:07.47Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/2d5817b0acee3c49b7e7ccfbf5b273f284957cc8e270adf36375db353190/matplotlib-3.10.8-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a48f2b74020919552ea25d222d5cc6af9ca3f4eb43a93e14d068457f545c2a17", size = 9534928, upload-time = "2025-12-10T22:55:10.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5b/8e66653e9f7c39cb2e5cab25fce4810daffa2bff02cbf5f3077cea9e942c/matplotlib-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f254d118d14a7f99d616271d6c3c27922c092dac11112670b157798b89bf4933", size = 9586377, upload-time = "2025-12-10T22:55:12.362Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e2/fd0bbadf837f81edb0d208ba8f8cb552874c3b16e27cb91a31977d90875d/matplotlib-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:f9b587c9c7274c1613a30afabf65a272114cd6cdbe67b3406f818c79d7ab2e2a", size = 8128127, upload-time = "2025-12-10T22:55:14.436Z" }, - { url = "https://files.pythonhosted.org/packages/f8/86/de7e3a1cdcfc941483af70609edc06b83e7c8a0e0dc9ac325200a3f4d220/matplotlib-3.10.8-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6be43b667360fef5c754dda5d25a32e6307a03c204f3c0fc5468b78fa87b4160", size = 8251215, upload-time = "2025-12-10T22:55:16.175Z" }, - { url = "https://files.pythonhosted.org/packages/fd/14/baad3222f424b19ce6ad243c71de1ad9ec6b2e4eb1e458a48fdc6d120401/matplotlib-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2b336e2d91a3d7006864e0990c83b216fcdca64b5a6484912902cef87313d78", size = 8139625, upload-time = "2025-12-10T22:55:17.712Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a0/7024215e95d456de5883e6732e708d8187d9753a21d32f8ddb3befc0c445/matplotlib-3.10.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:efb30e3baaea72ce5928e32bab719ab4770099079d66726a62b11b1ef7273be4", size = 8712614, upload-time = "2025-12-10T22:55:20.8Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f4/b8347351da9a5b3f41e26cf547252d861f685c6867d179a7c9d60ad50189/matplotlib-3.10.8-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d56a1efd5bfd61486c8bc968fa18734464556f0fb8e51690f4ac25d85cbbbbc2", size = 9540997, upload-time = "2025-12-10T22:55:23.258Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c0/c7b914e297efe0bc36917bf216b2acb91044b91e930e878ae12981e461e5/matplotlib-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:238b7ce5717600615c895050239ec955d91f321c209dd110db988500558e70d6", size = 9596825, upload-time = "2025-12-10T22:55:25.217Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d3/a4bbc01c237ab710a1f22b4da72f4ff6d77eb4c7735ea9811a94ae239067/matplotlib-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:18821ace09c763ec93aef5eeff087ee493a24051936d7b9ebcad9662f66501f9", size = 8135090, upload-time = "2025-12-10T22:55:27.162Z" }, - { url = "https://files.pythonhosted.org/packages/89/dd/a0b6588f102beab33ca6f5218b31725216577b2a24172f327eaf6417d5c9/matplotlib-3.10.8-cp311-cp311-win_arm64.whl", hash = "sha256:bab485bcf8b1c7d2060b4fcb6fc368a9e6f4cd754c9c2fea281f4be21df394a2", size = 8012377, upload-time = "2025-12-10T22:55:29.185Z" }, - { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" }, - { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" }, - { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" }, - { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" }, - { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" }, - { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" }, - { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" }, - { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" }, - { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" }, - { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" }, - { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" }, - { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" }, - { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" }, - { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" }, - { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" }, - { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" }, - { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" }, - { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" }, - { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" }, - { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" }, - { url = "https://files.pythonhosted.org/packages/f5/43/31d59500bb950b0d188e149a2e552040528c13d6e3d6e84d0cccac593dcd/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f97aeb209c3d2511443f8797e3e5a569aebb040d4f8bc79aa3ee78a8fb9e3dd8", size = 8237252, upload-time = "2025-12-10T22:56:39.529Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2c/615c09984f3c5f907f51c886538ad785cf72e0e11a3225de2c0f9442aecc/matplotlib-3.10.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fb061f596dad3a0f52b60dc6a5dec4a0c300dec41e058a7efe09256188d170b7", size = 8124693, upload-time = "2025-12-10T22:56:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/e1/2757277a1c56041e1fc104b51a0f7b9a4afc8eb737865d63cababe30bc61/matplotlib-3.10.8-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12d90df9183093fcd479f4172ac26b322b1248b15729cb57f42f71f24c7e37a3", size = 8702205, upload-time = "2025-12-10T22:56:43.415Z" }, - { url = "https://files.pythonhosted.org/packages/04/30/3afaa31c757f34b7725ab9d2ba8b48b5e89c2019c003e7d0ead143aabc5a/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6da7c2ce169267d0d066adcf63758f0604aa6c3eebf67458930f9d9b79ad1db1", size = 8249198, upload-time = "2025-12-10T22:56:45.584Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/6334aec331f57485a642a7c8be03cb286f29111ae71c46c38b363230063c/matplotlib-3.10.8-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9153c3292705be9f9c64498a8872118540c3f4123d1a1c840172edf262c8be4a", size = 8136817, upload-time = "2025-12-10T22:56:47.339Z" }, - { url = "https://files.pythonhosted.org/packages/73/e4/6d6f14b2a759c622f191b2d67e9075a3f56aaccb3be4bb9bb6890030d0a0/matplotlib-3.10.8-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae029229a57cd1e8fe542485f27e7ca7b23aa9e8944ddb4985d0bc444f1eca2", size = 8713867, upload-time = "2025-12-10T22:56:48.954Z" }, -] - -[[package]] -name = "matplotlib-inline" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, -] - -[[package]] -name = "mcp" -version = "1.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" }, -] - -[[package]] -name = "md-babel-py" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/b3/f814d429edf2848ba03079a3f6da443e6d45b984a7fc22766cb73939d289/md_babel_py-1.1.1.tar.gz", hash = "sha256:826fea96b7415eeaab7607ed5e8eb6d7723f22b9f1005af1b7da12f68766123d", size = 30547, upload-time = "2026-01-20T06:27:32.496Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/4a/dbe497b41432a98c7d4f043cf112410957553ce27e56bc366714695f53a9/md_babel_py-1.1.1-py3-none-any.whl", hash = "sha256:4df82011f123f13b6f9979226e69b0ce06209d94e4c029b60eeb2f54a709d2d0", size = 25836, upload-time = "2026-01-20T06:27:31.514Z" }, -] - -[[package]] -name = "mdit-py-plugins" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, -] - -[[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 = "mediapy" -version = "1.2.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.10.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "matplotlib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/eb/8a0499fb1a2f373f97e2b4df91797507c3971c42c59f1610bed090c57ddc/mediapy-1.2.6.tar.gz", hash = "sha256:2c866cfa0a170213f771b1dd5584a2e82d8d0dc0fa94982f83e29aae27e49c83", size = 28143, upload-time = "2026-02-03T10:29:31.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/8c/52f0299f1675cdfa1ab39a6028a2e5adf9032ae1118c9895c84b08af162b/mediapy-1.2.6-py3-none-any.whl", hash = "sha256:0a0ea00eb0da83c3c54d588b49c49a41ba456174aa33e530ffe13e17269c9072", size = 27494, upload-time = "2026-02-03T10:29:30.245Z" }, -] - -[[package]] -name = "ml-collections" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/f8/1a9ae6696dbb6bc9c44ddf5c5e84710d77fe9a35a57e8a06722e1836a4a6/ml_collections-1.1.0.tar.gz", hash = "sha256:0ac1ac6511b9f1566863e0bb0afad0c64e906ea278ad3f4d2144a55322671f6f", size = 61356, upload-time = "2025-04-17T08:25:02.247Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/8a/18d4ff2c7bd83f30d6924bd4ad97abf418488c3f908dea228d6f0961ad68/ml_collections-1.1.0-py3-none-any.whl", hash = "sha256:23b6fa4772aac1ae745a96044b925a5746145a70734f087eaca6626e92c05cbc", size = 76707, upload-time = "2025-04-17T08:24:59.038Z" }, -] - -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, - { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, - { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, - { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, - { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, - { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, - { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, - { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, - { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, - { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, - { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, - { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, - { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, - { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, - { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, - { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, - { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, - { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, - { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, -] - -[[package]] -name = "mmh3" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/af/f28c2c2f51f31abb4725f9a64bc7863d5f491f6539bd26aee2a1d21a649e/mmh3-5.2.0.tar.gz", hash = "sha256:1efc8fec8478e9243a78bb993422cf79f8ff85cb4cf6b79647480a31e0d950a8", size = 33582, upload-time = "2025-07-29T07:43:48.49Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/2b/870f0ff5ecf312c58500f45950751f214b7068665e66e9bfd8bc2595587c/mmh3-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:81c504ad11c588c8629536b032940f2a359dda3b6cbfd4ad8f74cb24dcd1b0bc", size = 56119, upload-time = "2025-07-29T07:41:39.117Z" }, - { url = "https://files.pythonhosted.org/packages/3b/88/eb9a55b3f3cf43a74d6bfa8db0e2e209f966007777a1dc897c52c008314c/mmh3-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b898cecff57442724a0f52bf42c2de42de63083a91008fb452887e372f9c328", size = 40634, upload-time = "2025-07-29T07:41:40.626Z" }, - { url = "https://files.pythonhosted.org/packages/d1/4c/8e4b3878bf8435c697d7ce99940a3784eb864521768069feaccaff884a17/mmh3-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be1374df449465c9f2500e62eee73a39db62152a8bdfbe12ec5b5c1cd451344d", size = 40080, upload-time = "2025-07-29T07:41:41.791Z" }, - { url = "https://files.pythonhosted.org/packages/45/ac/0a254402c8c5ca424a0a9ebfe870f5665922f932830f0a11a517b6390a09/mmh3-5.2.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0d753ad566c721faa33db7e2e0eddd74b224cdd3eaf8481d76c926603c7a00e", size = 95321, upload-time = "2025-07-29T07:41:42.659Z" }, - { url = "https://files.pythonhosted.org/packages/39/8e/29306d5eca6dfda4b899d22c95b5420db4e0ffb7e0b6389b17379654ece5/mmh3-5.2.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dfbead5575f6470c17e955b94f92d62a03dfc3d07f2e6f817d9b93dc211a1515", size = 101220, upload-time = "2025-07-29T07:41:43.572Z" }, - { url = "https://files.pythonhosted.org/packages/49/f7/0dd1368e531e52a17b5b8dd2f379cce813bff2d0978a7748a506f1231152/mmh3-5.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7434a27754049144539d2099a6d2da5d88b8bdeedf935180bf42ad59b3607aa3", size = 103991, upload-time = "2025-07-29T07:41:44.914Z" }, - { url = "https://files.pythonhosted.org/packages/35/06/abc7122c40f4abbfcef01d2dac6ec0b77ede9757e5be8b8a40a6265b1274/mmh3-5.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cadc16e8ea64b5d9a47363013e2bea469e121e6e7cb416a7593aeb24f2ad122e", size = 110894, upload-time = "2025-07-29T07:41:45.849Z" }, - { url = "https://files.pythonhosted.org/packages/f4/2f/837885759afa4baccb8e40456e1cf76a4f3eac835b878c727ae1286c5f82/mmh3-5.2.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d765058da196f68dc721116cab335e696e87e76720e6ef8ee5a24801af65e63d", size = 118327, upload-time = "2025-07-29T07:41:47.224Z" }, - { url = "https://files.pythonhosted.org/packages/40/cc/5683ba20a21bcfb3f1605b1c474f46d30354f728a7412201f59f453d405a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8b0c53fe0994beade1ad7c0f13bd6fec980a0664bfbe5a6a7d64500b9ab76772", size = 101701, upload-time = "2025-07-29T07:41:48.259Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/99ab3fb940150aec8a26dbdfc39b200b5592f6aeb293ec268df93e054c30/mmh3-5.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:49037d417419863b222ae47ee562b2de9c3416add0a45c8d7f4e864be8dc4f89", size = 96712, upload-time = "2025-07-29T07:41:49.467Z" }, - { url = "https://files.pythonhosted.org/packages/61/04/d7c4cb18f1f001ede2e8aed0f9dbbfad03d161c9eea4fffb03f14f4523e5/mmh3-5.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6ecb4e750d712abde046858ee6992b65c93f1f71b397fce7975c3860c07365d2", size = 110302, upload-time = "2025-07-29T07:41:50.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/bf/4dac37580cfda74425a4547500c36fa13ef581c8a756727c37af45e11e9a/mmh3-5.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:382a6bb3f8c6532ea084e7acc5be6ae0c6effa529240836d59352398f002e3fc", size = 111929, upload-time = "2025-07-29T07:41:51.348Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b1/49f0a582c7a942fb71ddd1ec52b7d21d2544b37d2b2d994551346a15b4f6/mmh3-5.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7733ec52296fc1ba22e9b90a245c821adbb943e98c91d8a330a2254612726106", size = 100111, upload-time = "2025-07-29T07:41:53.139Z" }, - { url = "https://files.pythonhosted.org/packages/dc/94/ccec09f438caeb2506f4c63bb3b99aa08a9e09880f8fc047295154756210/mmh3-5.2.0-cp310-cp310-win32.whl", hash = "sha256:127c95336f2a98c51e7682341ab7cb0be3adb9df0819ab8505a726ed1801876d", size = 40783, upload-time = "2025-07-29T07:41:54.463Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f4/8d39a32c8203c1cdae88fdb04d1ea4aa178c20f159df97f4c5a2eaec702c/mmh3-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:419005f84ba1cab47a77465a2a843562dadadd6671b8758bf179d82a15ca63eb", size = 41549, upload-time = "2025-07-29T07:41:55.295Z" }, - { url = "https://files.pythonhosted.org/packages/cc/a1/30efb1cd945e193f62574144dd92a0c9ee6463435e4e8ffce9b9e9f032f0/mmh3-5.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:d22c9dcafed659fadc605538946c041722b6d1104fe619dbf5cc73b3c8a0ded8", size = 39335, upload-time = "2025-07-29T07:41:56.194Z" }, - { url = "https://files.pythonhosted.org/packages/f7/87/399567b3796e134352e11a8b973cd470c06b2ecfad5468fe580833be442b/mmh3-5.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7901c893e704ee3c65f92d39b951f8f34ccf8e8566768c58103fb10e55afb8c1", size = 56107, upload-time = "2025-07-29T07:41:57.07Z" }, - { url = "https://files.pythonhosted.org/packages/c3/09/830af30adf8678955b247d97d3d9543dd2fd95684f3cd41c0cd9d291da9f/mmh3-5.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5f5536b1cbfa72318ab3bfc8a8188b949260baed186b75f0abc75b95d8c051", size = 40635, upload-time = "2025-07-29T07:41:57.903Z" }, - { url = "https://files.pythonhosted.org/packages/07/14/eaba79eef55b40d653321765ac5e8f6c9ac38780b8a7c2a2f8df8ee0fb72/mmh3-5.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cedac4f4054b8f7859e5aed41aaa31ad03fce6851901a7fdc2af0275ac533c10", size = 40078, upload-time = "2025-07-29T07:41:58.772Z" }, - { url = "https://files.pythonhosted.org/packages/bb/26/83a0f852e763f81b2265d446b13ed6d49ee49e1fc0c47b9655977e6f3d81/mmh3-5.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eb756caf8975882630ce4e9fbbeb9d3401242a72528230422c9ab3a0d278e60c", size = 97262, upload-time = "2025-07-29T07:41:59.678Z" }, - { url = "https://files.pythonhosted.org/packages/00/7d/b7133b10d12239aeaebf6878d7eaf0bf7d3738c44b4aba3c564588f6d802/mmh3-5.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:097e13c8b8a66c5753c6968b7640faefe85d8e38992703c1f666eda6ef4c3762", size = 103118, upload-time = "2025-07-29T07:42:01.197Z" }, - { url = "https://files.pythonhosted.org/packages/7b/3e/62f0b5dce2e22fd5b7d092aba285abd7959ea2b17148641e029f2eab1ffa/mmh3-5.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7c0c7845566b9686480e6a7e9044db4afb60038d5fabd19227443f0104eeee4", size = 106072, upload-time = "2025-07-29T07:42:02.601Z" }, - { url = "https://files.pythonhosted.org/packages/66/84/ea88bb816edfe65052c757a1c3408d65c4201ddbd769d4a287b0f1a628b2/mmh3-5.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:61ac226af521a572700f863d6ecddc6ece97220ce7174e311948ff8c8919a363", size = 112925, upload-time = "2025-07-29T07:42:03.632Z" }, - { url = "https://files.pythonhosted.org/packages/2e/13/c9b1c022807db575fe4db806f442d5b5784547e2e82cff36133e58ea31c7/mmh3-5.2.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:582f9dbeefe15c32a5fa528b79b088b599a1dfe290a4436351c6090f90ddebb8", size = 120583, upload-time = "2025-07-29T07:42:04.991Z" }, - { url = "https://files.pythonhosted.org/packages/8a/5f/0e2dfe1a38f6a78788b7eb2b23432cee24623aeabbc907fed07fc17d6935/mmh3-5.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ebfc46b39168ab1cd44670a32ea5489bcbc74a25795c61b6d888c5c2cf654ed", size = 99127, upload-time = "2025-07-29T07:42:05.929Z" }, - { url = "https://files.pythonhosted.org/packages/77/27/aefb7d663b67e6a0c4d61a513c83e39ba2237e8e4557fa7122a742a23de5/mmh3-5.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1556e31e4bd0ac0c17eaf220be17a09c171d7396919c3794274cb3415a9d3646", size = 98544, upload-time = "2025-07-29T07:42:06.87Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/a21cc9b1a7c6e92205a1b5fa030cdf62277d177570c06a239eca7bd6dd32/mmh3-5.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81df0dae22cd0da87f1c978602750f33d17fb3d21fb0f326c89dc89834fea79b", size = 106262, upload-time = "2025-07-29T07:42:07.804Z" }, - { url = "https://files.pythonhosted.org/packages/43/18/db19ae82ea63c8922a880e1498a75342311f8aa0c581c4dd07711473b5f7/mmh3-5.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:eba01ec3bd4a49b9ac5ca2bc6a73ff5f3af53374b8556fcc2966dd2af9eb7779", size = 109824, upload-time = "2025-07-29T07:42:08.735Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f5/41dcf0d1969125fc6f61d8618b107c79130b5af50b18a4651210ea52ab40/mmh3-5.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e9a011469b47b752e7d20de296bb34591cdfcbe76c99c2e863ceaa2aa61113d2", size = 97255, upload-time = "2025-07-29T07:42:09.706Z" }, - { url = "https://files.pythonhosted.org/packages/32/b3/cce9eaa0efac1f0e735bb178ef9d1d2887b4927fe0ec16609d5acd492dda/mmh3-5.2.0-cp311-cp311-win32.whl", hash = "sha256:bc44fc2b886243d7c0d8daeb37864e16f232e5b56aaec27cc781d848264cfd28", size = 40779, upload-time = "2025-07-29T07:42:10.546Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e9/3fa0290122e6d5a7041b50ae500b8a9f4932478a51e48f209a3879fe0b9b/mmh3-5.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ebf241072cf2777a492d0e09252f8cc2b3edd07dfdb9404b9757bffeb4f2cee", size = 41549, upload-time = "2025-07-29T07:42:11.399Z" }, - { url = "https://files.pythonhosted.org/packages/3a/54/c277475b4102588e6f06b2e9095ee758dfe31a149312cdbf62d39a9f5c30/mmh3-5.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:b5f317a727bba0e633a12e71228bc6a4acb4f471a98b1c003163b917311ea9a9", size = 39336, upload-time = "2025-07-29T07:42:12.209Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6a/d5aa7edb5c08e0bd24286c7d08341a0446f9a2fbbb97d96a8a6dd81935ee/mmh3-5.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:384eda9361a7bf83a85e09447e1feafe081034af9dd428893701b959230d84be", size = 56141, upload-time = "2025-07-29T07:42:13.456Z" }, - { url = "https://files.pythonhosted.org/packages/08/49/131d0fae6447bc4a7299ebdb1a6fb9d08c9f8dcf97d75ea93e8152ddf7ab/mmh3-5.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c9da0d568569cc87315cb063486d761e38458b8ad513fedd3dc9263e1b81bcd", size = 40681, upload-time = "2025-07-29T07:42:14.306Z" }, - { url = "https://files.pythonhosted.org/packages/8f/6f/9221445a6bcc962b7f5ff3ba18ad55bba624bacdc7aa3fc0a518db7da8ec/mmh3-5.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86d1be5d63232e6eb93c50881aea55ff06eb86d8e08f9b5417c8c9b10db9db96", size = 40062, upload-time = "2025-07-29T07:42:15.08Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d4/6bb2d0fef81401e0bb4c297d1eb568b767de4ce6fc00890bc14d7b51ecc4/mmh3-5.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf7bee43e17e81671c447e9c83499f53d99bf440bc6d9dc26a841e21acfbe094", size = 97333, upload-time = "2025-07-29T07:42:16.436Z" }, - { url = "https://files.pythonhosted.org/packages/44/e0/ccf0daff8134efbb4fbc10a945ab53302e358c4b016ada9bf97a6bdd50c1/mmh3-5.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7aa18cdb58983ee660c9c400b46272e14fa253c675ed963d3812487f8ca42037", size = 103310, upload-time = "2025-07-29T07:42:17.796Z" }, - { url = "https://files.pythonhosted.org/packages/02/63/1965cb08a46533faca0e420e06aff8bbaf9690a6f0ac6ae6e5b2e4544687/mmh3-5.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9d032488fcec32d22be6542d1a836f00247f40f320844dbb361393b5b22773", size = 106178, upload-time = "2025-07-29T07:42:19.281Z" }, - { url = "https://files.pythonhosted.org/packages/c2/41/c883ad8e2c234013f27f92061200afc11554ea55edd1bcf5e1accd803a85/mmh3-5.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1861fb6b1d0453ed7293200139c0a9011eeb1376632e048e3766945b13313c5", size = 113035, upload-time = "2025-07-29T07:42:20.356Z" }, - { url = "https://files.pythonhosted.org/packages/df/b5/1ccade8b1fa625d634a18bab7bf08a87457e09d5ec8cf83ca07cbea9d400/mmh3-5.2.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99bb6a4d809aa4e528ddfe2c85dd5239b78b9dd14be62cca0329db78505e7b50", size = 120784, upload-time = "2025-07-29T07:42:21.377Z" }, - { url = "https://files.pythonhosted.org/packages/77/1c/919d9171fcbdcdab242e06394464ccf546f7d0f3b31e0d1e3a630398782e/mmh3-5.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1f8d8b627799f4e2fcc7c034fed8f5f24dc7724ff52f69838a3d6d15f1ad4765", size = 99137, upload-time = "2025-07-29T07:42:22.344Z" }, - { url = "https://files.pythonhosted.org/packages/66/8a/1eebef5bd6633d36281d9fc83cf2e9ba1ba0e1a77dff92aacab83001cee4/mmh3-5.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b5995088dd7023d2d9f310a0c67de5a2b2e06a570ecfd00f9ff4ab94a67cde43", size = 98664, upload-time = "2025-07-29T07:42:23.269Z" }, - { url = "https://files.pythonhosted.org/packages/13/41/a5d981563e2ee682b21fb65e29cc0f517a6734a02b581359edd67f9d0360/mmh3-5.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1a5f4d2e59d6bba8ef01b013c472741835ad961e7c28f50c82b27c57748744a4", size = 106459, upload-time = "2025-07-29T07:42:24.238Z" }, - { url = "https://files.pythonhosted.org/packages/24/31/342494cd6ab792d81e083680875a2c50fa0c5df475ebf0b67784f13e4647/mmh3-5.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fd6e6c3d90660d085f7e73710eab6f5545d4854b81b0135a3526e797009dbda3", size = 110038, upload-time = "2025-07-29T07:42:25.629Z" }, - { url = "https://files.pythonhosted.org/packages/28/44/efda282170a46bb4f19c3e2b90536513b1d821c414c28469a227ca5a1789/mmh3-5.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c4a2f3d83879e3de2eb8cbf562e71563a8ed15ee9b9c2e77ca5d9f73072ac15c", size = 97545, upload-time = "2025-07-29T07:42:27.04Z" }, - { url = "https://files.pythonhosted.org/packages/68/8f/534ae319c6e05d714f437e7206f78c17e66daca88164dff70286b0e8ea0c/mmh3-5.2.0-cp312-cp312-win32.whl", hash = "sha256:2421b9d665a0b1ad724ec7332fb5a98d075f50bc51a6ff854f3a1882bd650d49", size = 40805, upload-time = "2025-07-29T07:42:28.032Z" }, - { url = "https://files.pythonhosted.org/packages/b8/f6/f6abdcfefcedab3c964868048cfe472764ed358c2bf6819a70dd4ed4ed3a/mmh3-5.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d80005b7634a3a2220f81fbeb94775ebd12794623bb2e1451701ea732b4aa3", size = 41597, upload-time = "2025-07-29T07:42:28.894Z" }, - { url = "https://files.pythonhosted.org/packages/15/fd/f7420e8cbce45c259c770cac5718badf907b302d3a99ec587ba5ce030237/mmh3-5.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:3d6bfd9662a20c054bc216f861fa330c2dac7c81e7fb8307b5e32ab5b9b4d2e0", size = 39350, upload-time = "2025-07-29T07:42:29.794Z" }, - { url = "https://files.pythonhosted.org/packages/d8/fa/27f6ab93995ef6ad9f940e96593c5dd24744d61a7389532b0fec03745607/mmh3-5.2.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:e79c00eba78f7258e5b354eccd4d7907d60317ced924ea4a5f2e9d83f5453065", size = 40874, upload-time = "2025-07-29T07:42:30.662Z" }, - { url = "https://files.pythonhosted.org/packages/11/9c/03d13bcb6a03438bc8cac3d2e50f80908d159b31a4367c2e1a7a077ded32/mmh3-5.2.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:956127e663d05edbeec54df38885d943dfa27406594c411139690485128525de", size = 42012, upload-time = "2025-07-29T07:42:31.539Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/0865d9765408a7d504f1789944e678f74e0888b96a766d578cb80b040999/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:c3dca4cb5b946ee91b3d6bb700d137b1cd85c20827f89fdf9c16258253489044", size = 39197, upload-time = "2025-07-29T07:42:32.374Z" }, - { url = "https://files.pythonhosted.org/packages/3e/12/76c3207bd186f98b908b6706c2317abb73756d23a4e68ea2bc94825b9015/mmh3-5.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e651e17bfde5840e9e4174b01e9e080ce49277b70d424308b36a7969d0d1af73", size = 39840, upload-time = "2025-07-29T07:42:33.227Z" }, - { url = "https://files.pythonhosted.org/packages/5d/0d/574b6cce5555c9f2b31ea189ad44986755eb14e8862db28c8b834b8b64dc/mmh3-5.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:9f64bf06f4bf623325fda3a6d02d36cd69199b9ace99b04bb2d7fd9f89688504", size = 40644, upload-time = "2025-07-29T07:42:34.099Z" }, - { url = "https://files.pythonhosted.org/packages/52/82/3731f8640b79c46707f53ed72034a58baad400be908c87b0088f1f89f986/mmh3-5.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ddc63328889bcaee77b743309e5c7d2d52cee0d7d577837c91b6e7cc9e755e0b", size = 56153, upload-time = "2025-07-29T07:42:35.031Z" }, - { url = "https://files.pythonhosted.org/packages/4f/34/e02dca1d4727fd9fdeaff9e2ad6983e1552804ce1d92cc796e5b052159bb/mmh3-5.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb0fdc451fb6d86d81ab8f23d881b8d6e37fc373a2deae1c02d27002d2ad7a05", size = 40684, upload-time = "2025-07-29T07:42:35.914Z" }, - { url = "https://files.pythonhosted.org/packages/8f/36/3dee40767356e104967e6ed6d102ba47b0b1ce2a89432239b95a94de1b89/mmh3-5.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b29044e1ffdb84fe164d0a7ea05c7316afea93c00f8ed9449cf357c36fc4f814", size = 40057, upload-time = "2025-07-29T07:42:36.755Z" }, - { url = "https://files.pythonhosted.org/packages/31/58/228c402fccf76eb39a0a01b8fc470fecf21965584e66453b477050ee0e99/mmh3-5.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:58981d6ea9646dbbf9e59a30890cbf9f610df0e4a57dbfe09215116fd90b0093", size = 97344, upload-time = "2025-07-29T07:42:37.675Z" }, - { url = "https://files.pythonhosted.org/packages/34/82/fc5ce89006389a6426ef28e326fc065b0fbaaed230373b62d14c889f47ea/mmh3-5.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7e5634565367b6d98dc4aa2983703526ef556b3688ba3065edb4b9b90ede1c54", size = 103325, upload-time = "2025-07-29T07:42:38.591Z" }, - { url = "https://files.pythonhosted.org/packages/09/8c/261e85777c6aee1ebd53f2f17e210e7481d5b0846cd0b4a5c45f1e3761b8/mmh3-5.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0271ac12415afd3171ab9a3c7cbfc71dee2c68760a7dc9d05bf8ed6ddfa3a7a", size = 106240, upload-time = "2025-07-29T07:42:39.563Z" }, - { url = "https://files.pythonhosted.org/packages/70/73/2f76b3ad8a3d431824e9934403df36c0ddacc7831acf82114bce3c4309c8/mmh3-5.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:45b590e31bc552c6f8e2150ff1ad0c28dd151e9f87589e7eaf508fbdd8e8e908", size = 113060, upload-time = "2025-07-29T07:42:40.585Z" }, - { url = "https://files.pythonhosted.org/packages/9f/b9/7ea61a34e90e50a79a9d87aa1c0b8139a7eaf4125782b34b7d7383472633/mmh3-5.2.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bdde97310d59604f2a9119322f61b31546748499a21b44f6715e8ced9308a6c5", size = 120781, upload-time = "2025-07-29T07:42:41.618Z" }, - { url = "https://files.pythonhosted.org/packages/0f/5b/ae1a717db98c7894a37aeedbd94b3f99e6472a836488f36b6849d003485b/mmh3-5.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc9c5f280438cf1c1a8f9abb87dc8ce9630a964120cfb5dd50d1e7ce79690c7a", size = 99174, upload-time = "2025-07-29T07:42:42.587Z" }, - { url = "https://files.pythonhosted.org/packages/e3/de/000cce1d799fceebb6d4487ae29175dd8e81b48e314cba7b4da90bcf55d7/mmh3-5.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c903e71fd8debb35ad2a4184c1316b3cb22f64ce517b4e6747f25b0a34e41266", size = 98734, upload-time = "2025-07-29T07:42:43.996Z" }, - { url = "https://files.pythonhosted.org/packages/79/19/0dc364391a792b72fbb22becfdeacc5add85cc043cd16986e82152141883/mmh3-5.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:eed4bba7ff8a0d37106ba931ab03bdd3915fbb025bcf4e1f0aa02bc8114960c5", size = 106493, upload-time = "2025-07-29T07:42:45.07Z" }, - { url = "https://files.pythonhosted.org/packages/3c/b1/bc8c28e4d6e807bbb051fefe78e1156d7f104b89948742ad310612ce240d/mmh3-5.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:1fdb36b940e9261aff0b5177c5b74a36936b902f473180f6c15bde26143681a9", size = 110089, upload-time = "2025-07-29T07:42:46.122Z" }, - { url = "https://files.pythonhosted.org/packages/3b/a2/d20f3f5c95e9c511806686c70d0a15479cc3941c5f322061697af1c1ff70/mmh3-5.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7303aab41e97adcf010a09efd8f1403e719e59b7705d5e3cfed3dd7571589290", size = 97571, upload-time = "2025-07-29T07:42:47.18Z" }, - { url = "https://files.pythonhosted.org/packages/7b/23/665296fce4f33488deec39a750ffd245cfc07aafb0e3ef37835f91775d14/mmh3-5.2.0-cp313-cp313-win32.whl", hash = "sha256:03e08c6ebaf666ec1e3d6ea657a2d363bb01effd1a9acfe41f9197decaef0051", size = 40806, upload-time = "2025-07-29T07:42:48.166Z" }, - { url = "https://files.pythonhosted.org/packages/59/b0/92e7103f3b20646e255b699e2d0327ce53a3f250e44367a99dc8be0b7c7a/mmh3-5.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:7fddccd4113e7b736706e17a239a696332360cbaddf25ae75b57ba1acce65081", size = 41600, upload-time = "2025-07-29T07:42:49.371Z" }, - { url = "https://files.pythonhosted.org/packages/99/22/0b2bd679a84574647de538c5b07ccaa435dbccc37815067fe15b90fe8dad/mmh3-5.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa0c966ee727aad5406d516375593c5f058c766b21236ab8985693934bb5085b", size = 39349, upload-time = "2025-07-29T07:42:50.268Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ca/a20db059a8a47048aaf550da14a145b56e9c7386fb8280d3ce2962dcebf7/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:e5015f0bb6eb50008bed2d4b1ce0f2a294698a926111e4bb202c0987b4f89078", size = 39209, upload-time = "2025-07-29T07:42:51.559Z" }, - { url = "https://files.pythonhosted.org/packages/98/dd/e5094799d55c7482d814b979a0fd608027d0af1b274bfb4c3ea3e950bfd5/mmh3-5.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e0f3ed828d709f5b82d8bfe14f8856120718ec4bd44a5b26102c3030a1e12501", size = 39843, upload-time = "2025-07-29T07:42:52.536Z" }, - { url = "https://files.pythonhosted.org/packages/f4/6b/7844d7f832c85400e7cc89a1348e4e1fdd38c5a38415bb5726bbb8fcdb6c/mmh3-5.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:f35727c5118aba95f0397e18a1a5b8405425581bfe53e821f0fb444cbdc2bc9b", size = 40648, upload-time = "2025-07-29T07:42:53.392Z" }, - { url = "https://files.pythonhosted.org/packages/1f/bf/71f791f48a21ff3190ba5225807cbe4f7223360e96862c376e6e3fb7efa7/mmh3-5.2.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bc244802ccab5220008cb712ca1508cb6a12f0eb64ad62997156410579a1770", size = 56164, upload-time = "2025-07-29T07:42:54.267Z" }, - { url = "https://files.pythonhosted.org/packages/70/1f/f87e3d34d83032b4f3f0f528c6d95a98290fcacf019da61343a49dccfd51/mmh3-5.2.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ff3d50dc3fe8a98059f99b445dfb62792b5d006c5e0b8f03c6de2813b8376110", size = 40692, upload-time = "2025-07-29T07:42:55.234Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e2/db849eaed07117086f3452feca8c839d30d38b830ac59fe1ce65af8be5ad/mmh3-5.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:37a358cc881fe796e099c1db6ce07ff757f088827b4e8467ac52b7a7ffdca647", size = 40068, upload-time = "2025-07-29T07:42:56.158Z" }, - { url = "https://files.pythonhosted.org/packages/df/6b/209af927207af77425b044e32f77f49105a0b05d82ff88af6971d8da4e19/mmh3-5.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b9a87025121d1c448f24f27ff53a5fe7b6ef980574b4a4f11acaabe702420d63", size = 97367, upload-time = "2025-07-29T07:42:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e0/78adf4104c425606a9ce33fb351f790c76a6c2314969c4a517d1ffc92196/mmh3-5.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ba55d6ca32eeef8b2625e1e4bfc3b3db52bc63014bd7e5df8cc11bf2b036b12", size = 103306, upload-time = "2025-07-29T07:42:58.522Z" }, - { url = "https://files.pythonhosted.org/packages/a3/79/c2b89f91b962658b890104745b1b6c9ce38d50a889f000b469b91eeb1b9e/mmh3-5.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9ff37ba9f15637e424c2ab57a1a590c52897c845b768e4e0a4958084ec87f22", size = 106312, upload-time = "2025-07-29T07:42:59.552Z" }, - { url = "https://files.pythonhosted.org/packages/4b/14/659d4095528b1a209be90934778c5ffe312177d51e365ddcbca2cac2ec7c/mmh3-5.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a094319ec0db52a04af9fdc391b4d39a1bc72bc8424b47c4411afb05413a44b5", size = 113135, upload-time = "2025-07-29T07:43:00.745Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6f/cd7734a779389a8a467b5c89a48ff476d6f2576e78216a37551a97e9e42a/mmh3-5.2.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5584061fd3da584659b13587f26c6cad25a096246a481636d64375d0c1f6c07", size = 120775, upload-time = "2025-07-29T07:43:02.124Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ca/8256e3b96944408940de3f9291d7e38a283b5761fe9614d4808fcf27bd62/mmh3-5.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecbfc0437ddfdced5e7822d1ce4855c9c64f46819d0fdc4482c53f56c707b935", size = 99178, upload-time = "2025-07-29T07:43:03.182Z" }, - { url = "https://files.pythonhosted.org/packages/8a/32/39e2b3cf06b6e2eb042c984dab8680841ac2a0d3ca6e0bea30db1f27b565/mmh3-5.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:7b986d506a8e8ea345791897ba5d8ba0d9d8820cd4fc3e52dbe6de19388de2e7", size = 98738, upload-time = "2025-07-29T07:43:04.207Z" }, - { url = "https://files.pythonhosted.org/packages/61/d3/7bbc8e0e8cf65ebbe1b893ffa0467b7ecd1bd07c3bbf6c9db4308ada22ec/mmh3-5.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:38d899a156549da8ef6a9f1d6f7ef231228d29f8f69bce2ee12f5fba6d6fd7c5", size = 106510, upload-time = "2025-07-29T07:43:05.656Z" }, - { url = "https://files.pythonhosted.org/packages/10/99/b97e53724b52374e2f3859046f0eb2425192da356cb19784d64bc17bb1cf/mmh3-5.2.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d86651fa45799530885ba4dab3d21144486ed15285e8784181a0ab37a4552384", size = 110053, upload-time = "2025-07-29T07:43:07.204Z" }, - { url = "https://files.pythonhosted.org/packages/ac/62/3688c7d975ed195155671df68788c83fed6f7909b6ec4951724c6860cb97/mmh3-5.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c463d7c1c4cfc9d751efeaadd936bbba07b5b0ed81a012b3a9f5a12f0872bd6e", size = 97546, upload-time = "2025-07-29T07:43:08.226Z" }, - { url = "https://files.pythonhosted.org/packages/ca/3b/c6153250f03f71a8b7634cded82939546cdfba02e32f124ff51d52c6f991/mmh3-5.2.0-cp314-cp314-win32.whl", hash = "sha256:bb4fe46bdc6104fbc28db7a6bacb115ee6368ff993366bbd8a2a7f0076e6f0c0", size = 41422, upload-time = "2025-07-29T07:43:09.216Z" }, - { url = "https://files.pythonhosted.org/packages/74/01/a27d98bab083a435c4c07e9d1d720d4c8a578bf4c270bae373760b1022be/mmh3-5.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:7c7f0b342fd06044bedd0b6e72177ddc0076f54fd89ee239447f8b271d919d9b", size = 42135, upload-time = "2025-07-29T07:43:10.183Z" }, - { url = "https://files.pythonhosted.org/packages/cb/c9/dbba5507e95429b8b380e2ba091eff5c20a70a59560934dff0ad8392b8c8/mmh3-5.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:3193752fc05ea72366c2b63ff24b9a190f422e32d75fdeae71087c08fff26115", size = 39879, upload-time = "2025-07-29T07:43:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/b5/d1/c8c0ef839c17258b9de41b84f663574fabcf8ac2007b7416575e0f65ff6e/mmh3-5.2.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:69fc339d7202bea69ef9bd7c39bfdf9fdabc8e6822a01eba62fb43233c1b3932", size = 57696, upload-time = "2025-07-29T07:43:11.989Z" }, - { url = "https://files.pythonhosted.org/packages/2f/55/95e2b9ff201e89f9fe37036037ab61a6c941942b25cdb7b6a9df9b931993/mmh3-5.2.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:12da42c0a55c9d86ab566395324213c319c73ecb0c239fad4726324212b9441c", size = 41421, upload-time = "2025-07-29T07:43:13.269Z" }, - { url = "https://files.pythonhosted.org/packages/77/79/9be23ad0b7001a4b22752e7693be232428ecc0a35068a4ff5c2f14ef8b20/mmh3-5.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7f9034c7cf05ddfaac8d7a2e63a3c97a840d4615d0a0e65ba8bdf6f8576e3be", size = 40853, upload-time = "2025-07-29T07:43:14.888Z" }, - { url = "https://files.pythonhosted.org/packages/ac/1b/96b32058eda1c1dee8264900c37c359a7325c1f11f5ff14fd2be8e24eff9/mmh3-5.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11730eeb16dfcf9674fdea9bb6b8e6dd9b40813b7eb839bc35113649eef38aeb", size = 109694, upload-time = "2025-07-29T07:43:15.816Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6f/a2ae44cd7dad697b6dea48390cbc977b1e5ca58fda09628cbcb2275af064/mmh3-5.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:932a6eec1d2e2c3c9e630d10f7128d80e70e2d47fe6b8c7ea5e1afbd98733e65", size = 117438, upload-time = "2025-07-29T07:43:16.865Z" }, - { url = "https://files.pythonhosted.org/packages/a0/08/bfb75451c83f05224a28afeaf3950c7b793c0b71440d571f8e819cfb149a/mmh3-5.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca975c51c5028947bbcfc24966517aac06a01d6c921e30f7c5383c195f87991", size = 120409, upload-time = "2025-07-29T07:43:18.207Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ea/8b118b69b2ff8df568f742387d1a159bc654a0f78741b31437dd047ea28e/mmh3-5.2.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b0b58215befe0f0e120b828f7645e97719bbba9f23b69e268ed0ac7adde8645", size = 125909, upload-time = "2025-07-29T07:43:19.39Z" }, - { url = "https://files.pythonhosted.org/packages/3e/11/168cc0b6a30650032e351a3b89b8a47382da541993a03af91e1ba2501234/mmh3-5.2.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29c2b9ce61886809d0492a274a5a53047742dea0f703f9c4d5d223c3ea6377d3", size = 135331, upload-time = "2025-07-29T07:43:20.435Z" }, - { url = "https://files.pythonhosted.org/packages/31/05/e3a9849b1c18a7934c64e831492c99e67daebe84a8c2f2c39a7096a830e3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a367d4741ac0103f8198c82f429bccb9359f543ca542b06a51f4f0332e8de279", size = 110085, upload-time = "2025-07-29T07:43:21.92Z" }, - { url = "https://files.pythonhosted.org/packages/d9/d5/a96bcc306e3404601418b2a9a370baec92af84204528ba659fdfe34c242f/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5a5dba98e514fb26241868f6eb90a7f7ca0e039aed779342965ce24ea32ba513", size = 111195, upload-time = "2025-07-29T07:43:23.066Z" }, - { url = "https://files.pythonhosted.org/packages/af/29/0fd49801fec5bff37198684e0849b58e0dab3a2a68382a357cfffb0fafc3/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:941603bfd75a46023807511c1ac2f1b0f39cccc393c15039969806063b27e6db", size = 116919, upload-time = "2025-07-29T07:43:24.178Z" }, - { url = "https://files.pythonhosted.org/packages/2d/04/4f3c32b0a2ed762edca45d8b46568fc3668e34f00fb1e0a3b5451ec1281c/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:132dd943451a7c7546978863d2f5a64977928410782e1a87d583cb60eb89e667", size = 123160, upload-time = "2025-07-29T07:43:25.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/76/3d29eaa38821730633d6a240d36fa8ad2807e9dfd432c12e1a472ed211eb/mmh3-5.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f698733a8a494466432d611a8f0d1e026f5286dee051beea4b3c3146817e35d5", size = 110206, upload-time = "2025-07-29T07:43:26.699Z" }, - { url = "https://files.pythonhosted.org/packages/44/1c/ccf35892684d3a408202e296e56843743e0b4fb1629e59432ea88cdb3909/mmh3-5.2.0-cp314-cp314t-win32.whl", hash = "sha256:6d541038b3fc360ec538fc116de87462627944765a6750308118f8b509a8eec7", size = 41970, upload-time = "2025-07-29T07:43:27.666Z" }, - { url = "https://files.pythonhosted.org/packages/75/b2/b9e4f1e5adb5e21eb104588fcee2cd1eaa8308255173481427d5ecc4284e/mmh3-5.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e912b19cf2378f2967d0c08e86ff4c6c360129887f678e27e4dde970d21b3f4d", size = 43063, upload-time = "2025-07-29T07:43:28.582Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455, upload-time = "2025-07-29T07:43:29.563Z" }, -] - -[[package]] -name = "moondream" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a5/d7/85e4d020c4d00f4842b35773e4442fe5cea310e4ebc6a1856e55d3e1a658/moondream-0.2.0.tar.gz", hash = "sha256:402655cc23b94490512caa1cf9f250fc34d133dfdbac201f78b32cbdeabdae0d", size = 97837, upload-time = "2025-11-25T18:22:04.477Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/cf/369278487161c8d8eadd1a6cee8b0bd629936a1b263bbeccf71342b24dc8/moondream-0.2.0-py3-none-any.whl", hash = "sha256:ca722763bddcce7c13faf87fa3e6b834f86f7bea22bc8794fc1fe15f2d826d93", size = 96169, upload-time = "2025-11-25T18:22:03.465Z" }, -] - -[[package]] -name = "more-itertools" -version = "10.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, -] - -[[package]] -name = "mosek" -version = "11.0.24" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version < '3.11' and sys_platform == 'darwin'", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and sys_platform == 'darwin'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and sys_platform == 'darwin'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/e7/d04ea5c587fd8b491fbe9377fafa5feb063bb28a3a6949fb393a62230d9d/mosek-11.0.24-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7f2ab70ad3357f9187c96237d0c49187f82f5885250a5e211b6aa20cb0a7207f", size = 8345311, upload-time = "2025-06-25T10:51:51.777Z" }, -] - -[[package]] -name = "mosek" -version = "11.1.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/e9/253e759e6e00b9cfbb4e95e7fe079b0e971b3c81c75f059bf2c2be3216e9/mosek-11.1.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:5c3566d2a603d94a1773bcd27097c8390dba1d9a1543534f3527deb56f1d0a55", size = 15359313, upload-time = "2026-01-07T08:22:00.805Z" }, - { url = "https://files.pythonhosted.org/packages/41/ea/17bb932e0d307c31de685ba817a3cba822e2757a9810e7cc516778c2baa3/mosek-11.1.2-cp39-abi3-manylinux_2_27_aarch64.whl", hash = "sha256:67c13d56a9b7adf2670e4ed6fb62aa92560ae2ff1050f6e756d0d3f82c42c19f", size = 11073007, upload-time = "2026-01-07T08:22:03.118Z" }, - { url = "https://files.pythonhosted.org/packages/f2/67/6f2b6e544cf5e284c7f0baebffbc82b55e7db5b7ed5d711b621fa965d4df/mosek-11.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:ad81cfd53af508db89241c7869ddce7ceaae13ef057f7b98007d57dccbb63c92", size = 11191977, upload-time = "2026-01-07T08:22:05.845Z" }, -] - -[[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 = "msgpack" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4d/f2/bfb55a6236ed8725a96b0aa3acbd0ec17588e6a2c3b62a93eb513ed8783f/msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e", size = 173581, upload-time = "2025-10-08T09:15:56.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/a2/3b68a9e769db68668b25c6108444a35f9bd163bb848c0650d516761a59c0/msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2", size = 81318, upload-time = "2025-10-08T09:14:38.722Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/2b720cc341325c00be44e1ed59e7cfeae2678329fbf5aa68f5bda57fe728/msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87", size = 83786, upload-time = "2025-10-08T09:14:40.082Z" }, - { url = "https://files.pythonhosted.org/packages/71/e5/c2241de64bfceac456b140737812a2ab310b10538a7b34a1d393b748e095/msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251", size = 398240, upload-time = "2025-10-08T09:14:41.151Z" }, - { url = "https://files.pythonhosted.org/packages/b7/09/2a06956383c0fdebaef5aa9246e2356776f12ea6f2a44bd1368abf0e46c4/msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a", size = 406070, upload-time = "2025-10-08T09:14:42.821Z" }, - { url = "https://files.pythonhosted.org/packages/0e/74/2957703f0e1ef20637d6aead4fbb314330c26f39aa046b348c7edcf6ca6b/msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f", size = 393403, upload-time = "2025-10-08T09:14:44.38Z" }, - { url = "https://files.pythonhosted.org/packages/a5/09/3bfc12aa90f77b37322fc33e7a8a7c29ba7c8edeadfa27664451801b9860/msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f", size = 398947, upload-time = "2025-10-08T09:14:45.56Z" }, - { url = "https://files.pythonhosted.org/packages/4b/4f/05fcebd3b4977cb3d840f7ef6b77c51f8582086de5e642f3fefee35c86fc/msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9", size = 64769, upload-time = "2025-10-08T09:14:47.334Z" }, - { url = "https://files.pythonhosted.org/packages/d0/3e/b4547e3a34210956382eed1c85935fff7e0f9b98be3106b3745d7dec9c5e/msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa", size = 71293, upload-time = "2025-10-08T09:14:48.665Z" }, - { url = "https://files.pythonhosted.org/packages/2c/97/560d11202bcd537abca693fd85d81cebe2107ba17301de42b01ac1677b69/msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c", size = 82271, upload-time = "2025-10-08T09:14:49.967Z" }, - { url = "https://files.pythonhosted.org/packages/83/04/28a41024ccbd67467380b6fb440ae916c1e4f25e2cd4c63abe6835ac566e/msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0", size = 84914, upload-time = "2025-10-08T09:14:50.958Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/b817349db6886d79e57a966346cf0902a426375aadc1e8e7a86a75e22f19/msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296", size = 416962, upload-time = "2025-10-08T09:14:51.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/e0/6cc2e852837cd6086fe7d8406af4294e66827a60a4cf60b86575a4a65ca8/msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef", size = 426183, upload-time = "2025-10-08T09:14:53.477Z" }, - { url = "https://files.pythonhosted.org/packages/25/98/6a19f030b3d2ea906696cedd1eb251708e50a5891d0978b012cb6107234c/msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c", size = 411454, upload-time = "2025-10-08T09:14:54.648Z" }, - { url = "https://files.pythonhosted.org/packages/b7/cd/9098fcb6adb32187a70b7ecaabf6339da50553351558f37600e53a4a2a23/msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e", size = 422341, upload-time = "2025-10-08T09:14:56.328Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ae/270cecbcf36c1dc85ec086b33a51a4d7d08fc4f404bdbc15b582255d05ff/msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e", size = 64747, upload-time = "2025-10-08T09:14:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/2a/79/309d0e637f6f37e83c711f547308b91af02b72d2326ddd860b966080ef29/msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68", size = 71633, upload-time = "2025-10-08T09:14:59.177Z" }, - { url = "https://files.pythonhosted.org/packages/73/4d/7c4e2b3d9b1106cd0aa6cb56cc57c6267f59fa8bfab7d91df5adc802c847/msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406", size = 64755, upload-time = "2025-10-08T09:15:00.48Z" }, - { url = "https://files.pythonhosted.org/packages/ad/bd/8b0d01c756203fbab65d265859749860682ccd2a59594609aeec3a144efa/msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa", size = 81939, upload-time = "2025-10-08T09:15:01.472Z" }, - { url = "https://files.pythonhosted.org/packages/34/68/ba4f155f793a74c1483d4bdef136e1023f7bcba557f0db4ef3db3c665cf1/msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb", size = 85064, upload-time = "2025-10-08T09:15:03.764Z" }, - { url = "https://files.pythonhosted.org/packages/f2/60/a064b0345fc36c4c3d2c743c82d9100c40388d77f0b48b2f04d6041dbec1/msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f", size = 417131, upload-time = "2025-10-08T09:15:05.136Z" }, - { url = "https://files.pythonhosted.org/packages/65/92/a5100f7185a800a5d29f8d14041f61475b9de465ffcc0f3b9fba606e4505/msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42", size = 427556, upload-time = "2025-10-08T09:15:06.837Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/ffe21d1bf7d9991354ad93949286f643b2bb6ddbeab66373922b44c3b8cc/msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9", size = 404920, upload-time = "2025-10-08T09:15:08.179Z" }, - { url = "https://files.pythonhosted.org/packages/ff/41/8543ed2b8604f7c0d89ce066f42007faac1eaa7d79a81555f206a5cdb889/msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620", size = 415013, upload-time = "2025-10-08T09:15:09.83Z" }, - { url = "https://files.pythonhosted.org/packages/41/0d/2ddfaa8b7e1cee6c490d46cb0a39742b19e2481600a7a0e96537e9c22f43/msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029", size = 65096, upload-time = "2025-10-08T09:15:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ec/d431eb7941fb55a31dd6ca3404d41fbb52d99172df2e7707754488390910/msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b", size = 72708, upload-time = "2025-10-08T09:15:12.554Z" }, - { url = "https://files.pythonhosted.org/packages/c5/31/5b1a1f70eb0e87d1678e9624908f86317787b536060641d6798e3cf70ace/msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69", size = 64119, upload-time = "2025-10-08T09:15:13.589Z" }, - { url = "https://files.pythonhosted.org/packages/6b/31/b46518ecc604d7edf3a4f94cb3bf021fc62aa301f0cb849936968164ef23/msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf", size = 81212, upload-time = "2025-10-08T09:15:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/92/dc/c385f38f2c2433333345a82926c6bfa5ecfff3ef787201614317b58dd8be/msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7", size = 84315, upload-time = "2025-10-08T09:15:15.543Z" }, - { url = "https://files.pythonhosted.org/packages/d3/68/93180dce57f684a61a88a45ed13047558ded2be46f03acb8dec6d7c513af/msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999", size = 412721, upload-time = "2025-10-08T09:15:16.567Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ba/459f18c16f2b3fc1a1ca871f72f07d70c07bf768ad0a507a698b8052ac58/msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e", size = 424657, upload-time = "2025-10-08T09:15:17.825Z" }, - { url = "https://files.pythonhosted.org/packages/38/f8/4398c46863b093252fe67368b44edc6c13b17f4e6b0e4929dbf0bdb13f23/msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162", size = 402668, upload-time = "2025-10-08T09:15:19.003Z" }, - { url = "https://files.pythonhosted.org/packages/28/ce/698c1eff75626e4124b4d78e21cca0b4cc90043afb80a507626ea354ab52/msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794", size = 419040, upload-time = "2025-10-08T09:15:20.183Z" }, - { url = "https://files.pythonhosted.org/packages/67/32/f3cd1667028424fa7001d82e10ee35386eea1408b93d399b09fb0aa7875f/msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c", size = 65037, upload-time = "2025-10-08T09:15:21.416Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/1ed8277f8653c40ebc65985180b007879f6a836c525b3885dcc6448ae6cb/msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9", size = 72631, upload-time = "2025-10-08T09:15:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/e5/db/0314e4e2db56ebcf450f277904ffd84a7988b9e5da8d0d61ab2d057df2b6/msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84", size = 64118, upload-time = "2025-10-08T09:15:23.402Z" }, - { url = "https://files.pythonhosted.org/packages/22/71/201105712d0a2ff07b7873ed3c220292fb2ea5120603c00c4b634bcdafb3/msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00", size = 81127, upload-time = "2025-10-08T09:15:24.408Z" }, - { url = "https://files.pythonhosted.org/packages/1b/9f/38ff9e57a2eade7bf9dfee5eae17f39fc0e998658050279cbb14d97d36d9/msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939", size = 84981, upload-time = "2025-10-08T09:15:25.812Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a9/3536e385167b88c2cc8f4424c49e28d49a6fc35206d4a8060f136e71f94c/msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e", size = 411885, upload-time = "2025-10-08T09:15:27.22Z" }, - { url = "https://files.pythonhosted.org/packages/2f/40/dc34d1a8d5f1e51fc64640b62b191684da52ca469da9cd74e84936ffa4a6/msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931", size = 419658, upload-time = "2025-10-08T09:15:28.4Z" }, - { url = "https://files.pythonhosted.org/packages/3b/ef/2b92e286366500a09a67e03496ee8b8ba00562797a52f3c117aa2b29514b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014", size = 403290, upload-time = "2025-10-08T09:15:29.764Z" }, - { url = "https://files.pythonhosted.org/packages/78/90/e0ea7990abea5764e4655b8177aa7c63cdfa89945b6e7641055800f6c16b/msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2", size = 415234, upload-time = "2025-10-08T09:15:31.022Z" }, - { url = "https://files.pythonhosted.org/packages/72/4e/9390aed5db983a2310818cd7d3ec0aecad45e1f7007e0cda79c79507bb0d/msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717", size = 66391, upload-time = "2025-10-08T09:15:32.265Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f1/abd09c2ae91228c5f3998dbd7f41353def9eac64253de3c8105efa2082f7/msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b", size = 73787, upload-time = "2025-10-08T09:15:33.219Z" }, - { url = "https://files.pythonhosted.org/packages/6a/b0/9d9f667ab48b16ad4115c1935d94023b82b3198064cb84a123e97f7466c1/msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af", size = 66453, upload-time = "2025-10-08T09:15:34.225Z" }, - { url = "https://files.pythonhosted.org/packages/16/67/93f80545eb1792b61a217fa7f06d5e5cb9e0055bed867f43e2b8e012e137/msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a", size = 85264, upload-time = "2025-10-08T09:15:35.61Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/33c8a24959cf193966ef11a6f6a2995a65eb066bd681fd085afd519a57ce/msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b", size = 89076, upload-time = "2025-10-08T09:15:36.619Z" }, - { url = "https://files.pythonhosted.org/packages/fc/6b/62e85ff7193663fbea5c0254ef32f0c77134b4059f8da89b958beb7696f3/msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245", size = 435242, upload-time = "2025-10-08T09:15:37.647Z" }, - { url = "https://files.pythonhosted.org/packages/c1/47/5c74ecb4cc277cf09f64e913947871682ffa82b3b93c8dad68083112f412/msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90", size = 432509, upload-time = "2025-10-08T09:15:38.794Z" }, - { url = "https://files.pythonhosted.org/packages/24/a4/e98ccdb56dc4e98c929a3f150de1799831c0a800583cde9fa022fa90602d/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20", size = 415957, upload-time = "2025-10-08T09:15:40.238Z" }, - { url = "https://files.pythonhosted.org/packages/da/28/6951f7fb67bc0a4e184a6b38ab71a92d9ba58080b27a77d3e2fb0be5998f/msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27", size = 422910, upload-time = "2025-10-08T09:15:41.505Z" }, - { url = "https://files.pythonhosted.org/packages/f0/03/42106dcded51f0a0b5284d3ce30a671e7bd3f7318d122b2ead66ad289fed/msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b", size = 75197, upload-time = "2025-10-08T09:15:42.954Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/d0071e94987f8db59d4eeb386ddc64d0bb9b10820a8d82bcd3e53eeb2da6/msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff", size = 85772, upload-time = "2025-10-08T09:15:43.954Z" }, - { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, -] - -[[package]] -name = "mujoco" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "etils", extra = ["epath"] }, - { name = "glfw" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pyopengl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/0d/005f0d49ad5878f0611a7c018550b8504d480a7a17ad7e6773ff47d8627a/mujoco-3.5.0.tar.gz", hash = "sha256:5c85a6fc7560ab5fa4534f35ff459e12dc3609681f307e457dbb49b6217f4d73", size = 912543, upload-time = "2026-02-13T01:02:51.554Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/20/9e0595e653543df3e4233bc3ad7e50b371b81dbe48d45ffbc867ed7c379d/mujoco-3.5.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:c4324161cb4f334dd984fbb4a4f7d7db9f914f40d06174b02dcf05463d8275e4", size = 7088320, upload-time = "2026-02-13T01:02:06.745Z" }, - { url = "https://files.pythonhosted.org/packages/8d/6b/fdac8ed97086e12ac930fb44e419eda1626e339010df73678cb1f22527d7/mujoco-3.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f3803ff0dd7bc04d6c47d53a794343843bde06f0aeefeac28bb62b4cf2baab3", size = 7093261, upload-time = "2026-02-13T01:02:09.857Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/abcd9cc6ee7802f97c729ae0ccd517c68f04882f5db755b178e199511dc2/mujoco-3.5.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e13560991c779a139b53151733a0a6f3420ef09459b32d90302c2661c1b20992", size = 6637850, upload-time = "2026-02-13T01:02:11.808Z" }, - { url = "https://files.pythonhosted.org/packages/ca/d6/a5a7b615b257867b7c97db6b3ce07dec9351d5d9d5a5aca881cbb583d7a3/mujoco-3.5.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b12896ae906f157e18d8b1b7c24a8b72d2576fffa09869047150f186e92b33", size = 7079429, upload-time = "2026-02-13T01:02:13.738Z" }, - { url = "https://files.pythonhosted.org/packages/7e/91/d82dd3c16892e1b0e27a2f537eec8aad54d91d939cb3cd37db2e8c09ecc2/mujoco-3.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:2328358d2f0031175897092560dd6d04b14bab1cc22caa145ce99b843c17daa2", size = 5624454, upload-time = "2026-02-13T01:02:15.714Z" }, - { url = "https://files.pythonhosted.org/packages/8b/47/e923589301c197c3ea0776b60cc0d57383b3cc51639ca75e4e4b6c5334d6/mujoco-3.5.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:6b3ae97c3f84d093e84dc445a093c893d9f4b6f6bbb1a441e56d77074c450553", size = 7100854, upload-time = "2026-02-13T01:02:17.649Z" }, - { url = "https://files.pythonhosted.org/packages/82/02/aa6057ac4c50fb36558208005d6da19518f9a7857ef9b5fd2ed8f9262fe2/mujoco-3.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fbb00809de98e8a65f2002745c5bca39076f8118b0fe08e973e7a99603c92b", size = 7105779, upload-time = "2026-02-13T01:02:19.621Z" }, - { url = "https://files.pythonhosted.org/packages/94/8a/8d87db2cf09a95ff4dcac1bd8eb6ccb95680804eff8f2f70f1d7a11e1980/mujoco-3.5.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a8d48990172d3b1eb51f20cd08f537c488686b2bc370c504333c07c04595f5d", size = 6651006, upload-time = "2026-02-13T01:02:22.197Z" }, - { url = "https://files.pythonhosted.org/packages/47/14/d5bf98385354318ec2e6c466a8c7cf7fd76f8b711ed6d11d155e2baa81fb/mujoco-3.5.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba54826121c6857fc4ca82df642d9a89174ce5537677c6ead34844bb692437e3", size = 7094833, upload-time = "2026-02-13T01:02:24.517Z" }, - { url = "https://files.pythonhosted.org/packages/b8/98/c1fac334cc764068e6c5d7eb01d6ed2a3392bab51952c816888b2dfe78c2/mujoco-3.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:ec0e35678773b34ee8b15741c34a745e027db062efcae790315aa83a5581c505", size = 5649612, upload-time = "2026-02-13T01:02:26.45Z" }, - { url = "https://files.pythonhosted.org/packages/f9/f0/4772421643f1c5aaf46d9e500a8716f59b02c8bf30bfa92cb8a763159efb/mujoco-3.5.0-cp312-cp312-macosx_10_16_x86_64.whl", hash = "sha256:ec0587cc423385a8d45343a981df58511cb69758ba99164a71567af2d41be3c9", size = 7100581, upload-time = "2026-02-13T01:02:29.182Z" }, - { url = "https://files.pythonhosted.org/packages/e1/d4/d0032323f58a9b8080b8464c6aade8d5ac2e101dbed1de64a38b3913b446/mujoco-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:94cf4285b46bc2d74fbe86e39a93ecfb3b0e584477fff7e38d293d47b88576e7", size = 7046132, upload-time = "2026-02-13T01:02:31.606Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7b/c1612ec68d98e5f3dbc5b8a21ff5d40ab52409fcc89ea7afc8a197983297/mujoco-3.5.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12bfb2bb70f760e0d51fd59f3c43b2906c7660a23954fd717321da52ba85a617", size = 6677917, upload-time = "2026-02-13T01:02:34.13Z" }, - { url = "https://files.pythonhosted.org/packages/c8/8a/229e4db3692be55532e155e2ca6a1363752243ee79df0e7e22ba00f716cf/mujoco-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66fe37276644c28fab497929c55580725de81afc6d511a40cc27525a8dd99efa", size = 7170882, upload-time = "2026-02-13T01:02:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/02/37/527d83610b878f27c01dd762e0e41aaa62f095c607f0500ac7f724a2c7a5/mujoco-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:4b3a62af174ab59b9b6d816dca0786b7fd85ac081d6c2a931a2b22dd6e821f50", size = 5721886, upload-time = "2026-02-13T01:02:39.544Z" }, - { url = "https://files.pythonhosted.org/packages/87/2a/371033684e4ddcda47c97661fb6e9617c0e5e3749af082a9b4d5d1bf9f27/mujoco-3.5.0-cp313-cp313-macosx_10_16_x86_64.whl", hash = "sha256:74b05ec4a6a3d728b2da6944d2ae17cac4af9b7a9293f2c2e9e7332fa7535714", size = 7100778, upload-time = "2026-02-13T01:02:41.456Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c9/26bd4979d503d03f7a6ded851c3094a5708cb534cf0dc80b4db6672da2b0/mujoco-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82416804ae96c69ed779330bd4f4af0a43632e2bbbcc60e5b193642db48e84ca", size = 7046419, upload-time = "2026-02-13T01:02:43.397Z" }, - { url = "https://files.pythonhosted.org/packages/cd/46/34b49e5cfcc6a25ad8af669e170c00b77cfaae99fca12c6586ed4e6cedb7/mujoco-3.5.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b591ed76e800713cd485dd38ec681b3065bde253b25350cfbe708e43a8a7bda", size = 6678488, upload-time = "2026-02-13T01:02:45.702Z" }, - { url = "https://files.pythonhosted.org/packages/16/47/93c7ac3a9630b49c55d76b0d02aa565543e2f62cecd885f8f574f5c745e7/mujoco-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a956520adb275ce8e878da29e2586eac3affc7b7ac772065ef01f2380a9e8784", size = 7171277, upload-time = "2026-02-13T01:02:47.59Z" }, - { url = "https://files.pythonhosted.org/packages/ab/53/54a0815d43c83e1074cfc7da98a3dea88d7dda48c03edfd225a387a3767b/mujoco-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:646b26f545cfdd60ae65ee90d44f63f50fc7ea5b8242777964ef0148830e72df", size = 5721537, upload-time = "2026-02-13T01:02:49.636Z" }, -] - -[[package]] -name = "mujoco-mjx" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "etils", extra = ["epath"] }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "mujoco" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "trimesh" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/3c/fc471adb5c83bb657c3634cf37c8c5cb5bb37c204d02192a4ee215132d1e/mujoco_mjx-3.5.0.tar.gz", hash = "sha256:42bdf3e80c0c4dfcfc78af97034f836d5292742e450a43a0dd9d44ada1e4bdc0", size = 6907429, upload-time = "2026-02-13T01:04:23.208Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dd/ec/ba408121d07200f4d588ae83033a99dcd197bba47e35e50165d260f2ef6c/mujoco_mjx-3.5.0-py3-none-any.whl", hash = "sha256:633aa801f84fa2becc17ea124d95ad3e34f59fdfaa3720b7ec18b427f3c5bf46", size = 6992318, upload-time = "2026-02-13T01:04:21.21Z" }, -] - -[[package]] -name = "mypy" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "librt" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" }, - { url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" }, - { url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" }, - { url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" }, - { url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" }, - { url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" }, - { url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" }, - { url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" }, - { url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" }, - { url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" }, - { url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" }, - { url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" }, - { url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" }, - { url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" }, - { url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" }, - { url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" }, - { url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" }, - { url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" }, - { url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" }, - { url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f4/7751f32f56916f7f8c229fe902cbdba3e4dd3f3ea9e8b872be97e7fc546d/mypy-1.19.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f2e36bed3c6d9b5f35d28b63ca4b727cb0228e480826ffc8953d1892ddc8999d", size = 13185236, upload-time = "2025-11-28T15:45:20.696Z" }, - { url = "https://files.pythonhosted.org/packages/35/31/871a9531f09e78e8d145032355890384f8a5b38c95a2c7732d226b93242e/mypy-1.19.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a18d8abdda14035c5718acb748faec09571432811af129bf0d9e7b2d6699bf18", size = 12213902, upload-time = "2025-11-28T15:46:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/58/b8/af221910dd40eeefa2077a59107e611550167b9994693fc5926a0b0f87c0/mypy-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75e60aca3723a23511948539b0d7ed514dda194bc3755eae0bfc7a6b4887aa7", size = 12738600, upload-time = "2025-11-28T15:44:22.521Z" }, - { url = "https://files.pythonhosted.org/packages/11/9f/c39e89a3e319c1d9c734dedec1183b2cc3aefbab066ec611619002abb932/mypy-1.19.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f44f2ae3c58421ee05fe609160343c25f70e3967f6e32792b5a78006a9d850f", size = 13592639, upload-time = "2025-11-28T15:48:08.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/6d/ffaf5f01f5e284d9033de1267e6c1b8f3783f2cf784465378a86122e884b/mypy-1.19.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63ea6a00e4bd6822adbfc75b02ab3653a17c02c4347f5bb0cf1d5b9df3a05835", size = 13799132, upload-time = "2025-11-28T15:47:06.032Z" }, - { url = "https://files.pythonhosted.org/packages/fe/b0/c33921e73aaa0106224e5a34822411bea38046188eb781637f5a5b07e269/mypy-1.19.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ad925b14a0bb99821ff6f734553294aa6a3440a8cb082fe1f5b84dfb662afb1", size = 10269832, upload-time = "2025-11-28T15:47:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" }, -] - -[[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 = "narwhals" -version = "2.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/6f/713be67779028d482c6e0f2dde5bc430021b2578a4808c1c9f6d7ad48257/narwhals-2.16.0.tar.gz", hash = "sha256:155bb45132b370941ba0396d123cf9ed192bf25f39c4cea726f2da422ca4e145", size = 618268, upload-time = "2026-02-02T10:31:00.545Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/cc/7cb74758e6df95e0c4e1253f203b6dd7f348bf2f29cf89e9210a2416d535/narwhals-2.16.0-py3-none-any.whl", hash = "sha256:846f1fd7093ac69d63526e50732033e86c30ea0026a44d9b23991010c7d1485d", size = 443951, upload-time = "2026-02-02T10:30:58.635Z" }, -] - -[[package]] -name = "nbformat" -version = "5.10.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, -] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, -] - -[[package]] -name = "networkx" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, -] - -[[package]] -name = "networkx" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -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 = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - -[[package]] -name = "numba" -version = "0.63.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "llvmlite" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/60/0145d479b2209bd8fdae5f44201eceb8ce5a23e0ed54c71f57db24618665/numba-0.63.1.tar.gz", hash = "sha256:b320aa675d0e3b17b40364935ea52a7b1c670c9037c39cf92c49502a75902f4b", size = 2761666, upload-time = "2025-12-10T02:57:39.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/ce/5283d4ffa568f795bb0fd61ee1f0efc0c6094b94209259167fc8d4276bde/numba-0.63.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6d6bf5bf00f7db629305caaec82a2ffb8abe2bf45eaad0d0738dc7de4113779", size = 2680810, upload-time = "2025-12-10T02:56:55.269Z" }, - { url = "https://files.pythonhosted.org/packages/0f/72/a8bda517e26d912633b32626333339b7c769ea73a5c688365ea5f88fd07e/numba-0.63.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08653d0dfc9cc9c4c9a8fba29ceb1f2d5340c3b86c4a7e5e07e42b643bc6a2f4", size = 3739735, upload-time = "2025-12-10T02:56:57.922Z" }, - { url = "https://files.pythonhosted.org/packages/ca/17/1913b7c1173b2db30fb7a9696892a7c4c59aeee777a9af6859e9e01bac51/numba-0.63.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09eebf5650246ce2a4e9a8d38270e2d4b0b0ae978103bafb38ed7adc5ea906e", size = 3446707, upload-time = "2025-12-10T02:56:59.837Z" }, - { url = "https://files.pythonhosted.org/packages/b4/77/703db56c3061e9fdad5e79c91452947fdeb2ec0bdfe4affe9b144e7025e0/numba-0.63.1-cp310-cp310-win_amd64.whl", hash = "sha256:f8bba17421d865d8c0f7be2142754ebce53e009daba41c44cf6909207d1a8d7d", size = 2747374, upload-time = "2025-12-10T02:57:07.908Z" }, - { url = "https://files.pythonhosted.org/packages/70/90/5f8614c165d2e256fbc6c57028519db6f32e4982475a372bbe550ea0454c/numba-0.63.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b33db00f18ccc790ee9911ce03fcdfe9d5124637d1ecc266f5ae0df06e02fec3", size = 2680501, upload-time = "2025-12-10T02:57:09.797Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9d/d0afc4cf915edd8eadd9b2ab5b696242886ee4f97720d9322650d66a88c6/numba-0.63.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d31ea186a78a7c0f6b1b2a3fe68057fdb291b045c52d86232b5383b6cf4fc25", size = 3744945, upload-time = "2025-12-10T02:57:11.697Z" }, - { url = "https://files.pythonhosted.org/packages/05/a9/d82f38f2ab73f3be6f838a826b545b80339762ee8969c16a8bf1d39395a8/numba-0.63.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed3bb2fbdb651d6aac394388130a7001aab6f4541837123a4b4ab8b02716530c", size = 3450827, upload-time = "2025-12-10T02:57:13.709Z" }, - { url = "https://files.pythonhosted.org/packages/18/3f/a9b106e93c5bd7434e65f044bae0d204e20aa7f7f85d72ceb872c7c04216/numba-0.63.1-cp311-cp311-win_amd64.whl", hash = "sha256:1ecbff7688f044b1601be70113e2fb1835367ee0b28ffa8f3adf3a05418c5c87", size = 2747262, upload-time = "2025-12-10T02:57:15.664Z" }, - { url = "https://files.pythonhosted.org/packages/14/9c/c0974cd3d00ff70d30e8ff90522ba5fbb2bcee168a867d2321d8d0457676/numba-0.63.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2819cd52afa5d8d04e057bdfd54367575105f8829350d8fb5e4066fb7591cc71", size = 2680981, upload-time = "2025-12-10T02:57:17.579Z" }, - { url = "https://files.pythonhosted.org/packages/cb/70/ea2bc45205f206b7a24ee68a159f5097c9ca7e6466806e7c213587e0c2b1/numba-0.63.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5cfd45dbd3d409e713b1ccfdc2ee72ca82006860254429f4ef01867fdba5845f", size = 3801656, upload-time = "2025-12-10T02:57:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/0d/82/4f4ba4fd0f99825cbf3cdefd682ca3678be1702b63362011de6e5f71f831/numba-0.63.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69a599df6976c03b7ecf15d05302696f79f7e6d10d620367407517943355bcb0", size = 3501857, upload-time = "2025-12-10T02:57:20.721Z" }, - { url = "https://files.pythonhosted.org/packages/af/fd/6540456efa90b5f6604a86ff50dabefb187e43557e9081adcad3be44f048/numba-0.63.1-cp312-cp312-win_amd64.whl", hash = "sha256:bbad8c63e4fc7eb3cdb2c2da52178e180419f7969f9a685f283b313a70b92af3", size = 2750282, upload-time = "2025-12-10T02:57:22.474Z" }, - { url = "https://files.pythonhosted.org/packages/57/f7/e19e6eff445bec52dde5bed1ebb162925a8e6f988164f1ae4b3475a73680/numba-0.63.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0bd4fd820ef7442dcc07da184c3f54bb41d2bdb7b35bacf3448e73d081f730dc", size = 2680954, upload-time = "2025-12-10T02:57:24.145Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/1e222edba1e20e6b113912caa9b1665b5809433cbcb042dfd133c6f1fd38/numba-0.63.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53de693abe4be3bd4dee38e1c55f01c55ff644a6a3696a3670589e6e4c39cde2", size = 3809736, upload-time = "2025-12-10T02:57:25.836Z" }, - { url = "https://files.pythonhosted.org/packages/76/0a/590bad11a8b3feeac30a24d01198d46bdb76ad15c70d3a530691ce3cae58/numba-0.63.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81227821a72a763c3d4ac290abbb4371d855b59fdf85d5af22a47c0e86bf8c7e", size = 3508854, upload-time = "2025-12-10T02:57:27.438Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f5/3800384a24eed1e4d524669cdbc0b9b8a628800bb1e90d7bd676e5f22581/numba-0.63.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb227b07c2ac37b09432a9bda5142047a2d1055646e089d4a240a2643e508102", size = 2750228, upload-time = "2025-12-10T02:57:30.36Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/53be2aa8a55ee2608ebe1231789cbb217f6ece7f5e1c685d2f0752e95a5b/numba-0.63.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f180883e5508940cc83de8a8bea37fc6dd20fbe4e5558d4659b8b9bef5ff4731", size = 2681153, upload-time = "2025-12-10T02:57:32.016Z" }, - { url = "https://files.pythonhosted.org/packages/13/91/53e59c86759a0648282368d42ba732c29524a745fd555ed1fb1df83febbe/numba-0.63.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0938764afa82a47c0e895637a6c55547a42c9e1d35cac42285b1fa60a8b02bb", size = 3778718, upload-time = "2025-12-10T02:57:33.764Z" }, - { url = "https://files.pythonhosted.org/packages/6c/0c/2be19eba50b0b7636f6d1f69dfb2825530537708a234ba1ff34afc640138/numba-0.63.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f90a929fa5094e062d4e0368ede1f4497d5e40f800e80aa5222c4734236a2894", size = 3478712, upload-time = "2025-12-10T02:57:35.518Z" }, - { url = "https://files.pythonhosted.org/packages/0d/5f/4d0c9e756732577a52211f31da13a3d943d185f7fb90723f56d79c696caa/numba-0.63.1-cp314-cp314-win_amd64.whl", hash = "sha256:8d6d5ce85f572ed4e1a135dbb8c0114538f9dd0e3657eeb0bb64ab204cbe2a8f", size = 2752161, upload-time = "2025-12-10T02:57:37.12Z" }, -] - -[[package]] -name = "numpy" -version = "2.2.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -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/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, - { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, - { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, - { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, - { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, - { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, - { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, - { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, - { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, - { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, - { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, - { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, - { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, - { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, - { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, - { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, - { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, - { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, - { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, - { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, - { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, - { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, - { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, - { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, - { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, - { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, - { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, - { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, -] - -[[package]] -name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" }, - { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" }, - { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" }, - { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, - { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, - { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, - { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, - { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, - { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, - { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, - { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, - { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, - { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, - { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, - { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, - { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, - { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, - { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, - { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, - { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, - { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, - { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, - { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, - { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, - { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, - { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, - { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, - { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, - { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, - { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, - { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, - { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, - { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, - { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, - { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, - { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, - { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" }, - { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" }, - { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -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-cublas-cu12" -version = "12.9.1.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/6c/90d3f532f608a03a13c1d6c16c266ffa3828e8011b1549d3b61db2ad59f5/nvidia_cublas_cu12-12.9.1.4-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:7a950dae01add3b415a5a5cdc4ec818fb5858263e9cca59004bb99fdbbd3a5d6", size = 575006342, upload-time = "2025-06-05T20:04:16.902Z" }, - { url = "https://files.pythonhosted.org/packages/45/a1/a17fade6567c57452cfc8f967a40d1035bb9301db52f27808167fbb2be2f/nvidia_cublas_cu12-12.9.1.4-py3-none-win_amd64.whl", hash = "sha256:1e5fee10662e6e52bd71dec533fbbd4971bb70a5f24f3bc3793e5c2e9dc640bf", size = 553153899, upload-time = "2025-06-05T20:13:35.556Z" }, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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" }, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -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-cuda-runtime-cu12" -version = "12.9.79" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/e0/0279bd94539fda525e0c8538db29b72a5a8495b0c12173113471d28bce78/nvidia_cuda_runtime_cu12-12.9.79-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83469a846206f2a733db0c42e223589ab62fd2fabac4432d2f8802de4bded0a4", size = 3515012, upload-time = "2025-06-05T20:00:35.519Z" }, - { url = "https://files.pythonhosted.org/packages/59/df/e7c3a360be4f7b93cee39271b792669baeb3846c58a4df6dfcf187a7ffab/nvidia_cuda_runtime_cu12-12.9.79-py3-none-win_amd64.whl", hash = "sha256:8e018af8fa02363876860388bd10ccb89eb9ab8fb0aa749aaf58430a9f7c4891", size = 3591604, upload-time = "2025-06-05T20:11:17.036Z" }, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -wheels = [ - { 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 = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -wheels = [ - { 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" }, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -wheels = [ - { 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 = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')" }, -] -wheels = [ - { 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/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-libnvcomp-cu12" -version = "5.1.0.21" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/ab/844fcbaa46cc1242632b4b94b4ffc210ec3d8d8f30ad8f7f1c27767389a9/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:68de61183edb9a870c9a608273a2b5da97dea18e3552096c61fafd9bb2689db0", size = 28958714, upload-time = "2025-12-02T19:01:40.466Z" }, - { url = "https://files.pythonhosted.org/packages/c4/cc/c6e92d9587b9ad63c08b1b94c5ae2216319491d0bd4f40f2a9a431d4841f/nvidia_libnvcomp_cu12-5.1.0.21-py3-none-win_amd64.whl", hash = "sha256:1352c7c4264ee5357f8f20e4a8da7f2f91debe21d8968f44576a7f4b51f91533", size = 28490640, upload-time = "2025-12-02T19:07:28.096Z" }, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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-nvimgcodec-cu12" -version = "0.7.0.11" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/b4/f06528ebcb82da84f4a96efe7a210c277767cb86ad2f61f8b1a17d17f251/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:32d3457859c5784e4c0f6a2f56b6a9afec8fe646cec1cbe4bb5c320948d92dfe", size = 33735220, upload-time = "2025-12-02T09:30:02.546Z" }, - { url = "https://files.pythonhosted.org/packages/be/79/95b36049a9504d59d79929e9f3bec001b270f29aec8486e5fb9783a9502c/nvidia_nvimgcodec_cu12-0.7.0.11-py3-none-win_amd64.whl", hash = "sha256:495e07e071fcb2115f7f1948a04f6c51f96d61b83c614af753f7cc1bf369a46c", size = 18448810, upload-time = "2025-12-02T09:20:33.838Z" }, -] - -[package.optional-dependencies] -all = [ - { name = "nvidia-libnvcomp-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "nvidia-nvjpeg-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "nvidia-nvjpeg2k-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "nvidia-nvtiff-cu12", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, -] - -[[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" }, -] - -[[package]] -name = "nvidia-nvjpeg-cu12" -version = "12.4.0.76" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/68/d3526394584134a23f2500833c62d3352e1feda7547041f4612b1a183aa3/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3888f10b32fbd58e80166c48e01073732d752fa5f167b7cb5b9615f1c6375a20", size = 5313609, upload-time = "2025-06-05T20:10:43.92Z" }, - { url = "https://files.pythonhosted.org/packages/bc/28/e05bb8e6cdb98e79c6822f8bbd7154a26d8102412b3a0bfd5e4c7c52db8c/nvidia_nvjpeg_cu12-12.4.0.76-py3-none-win_amd64.whl", hash = "sha256:21923726db667bd53050d0de88320983ff423322b7f376057dd943e487c40abc", size = 4741398, upload-time = "2025-06-05T20:16:19.152Z" }, -] - -[[package]] -name = "nvidia-nvjpeg2k-cu12" -version = "0.9.1.47" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/91/41abf44089ceb8b29479cdef2ca952277cc6667d40affedd39c3f1744d7e/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6672c85e47ab61ffe3d19da8a41fd597155852e6e219ddc90a133623b54f7818", size = 7402941, upload-time = "2025-11-13T18:13:28.977Z" }, - { url = "https://files.pythonhosted.org/packages/01/b2/ab62e6c008f3080743477de31da22eb83b374c37fe5d387e7435e507914f/nvidia_nvjpeg2k_cu12-0.9.1.47-py3-none-win_amd64.whl", hash = "sha256:ebb5d34d68beb70c2718c769996d9d8e49a2d9acacc79f6235c07649a4045e97", size = 6973975, upload-time = "2025-11-13T18:25:26.611Z" }, -] - -[[package]] -name = "nvidia-nvshmem-cu12" -version = "3.4.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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-nvtiff-cu12" -version = "0.6.0.78" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/4b/24805e9c56936dd57a1830b65b53234853f429cea5edbcbfdf853ceebdcf/nvidia_nvtiff_cu12-0.6.0.78-py3-none-manylinux2014_x86_64.whl", hash = "sha256:b48517578de6f1a6e806e00ef0da6d673036957560efbe9fa2934707d5d18c00", size = 2518414, upload-time = "2025-11-13T18:16:55.401Z" }, - { url = "https://files.pythonhosted.org/packages/45/48/1d818455e6c6182354fb5b17a6c9d7dcfb002e64e258554fe3410ea44510/nvidia_nvtiff_cu12-0.6.0.78-py3-none-win_amd64.whl", hash = "sha256:daf9035b5efc315ef904b449564d1d9d9a502f38e115cf5757d98f9c52a284d0", size = 2055719, upload-time = "2025-11-13T18:29:01.023Z" }, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { 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 = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - -[[package]] -name = "ollama" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620, upload-time = "2025-11-13T23:02:17.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354, upload-time = "2025-11-13T23:02:16.292Z" }, -] - -[[package]] -name = "omegaconf" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, -] - -[[package]] -name = "onnx" -version = "1.20.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ml-dtypes" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "protobuf" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/cc/4ba3c80cfaffdb541dc5a23eaccb045a627361e94ecaeba30496270f15b3/onnx-1.20.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be", size = 17904206, upload-time = "2026-01-10T01:38:58.574Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fc/3a1c4ae2cd5cfab2d0ebc1842769b04b417fe13946144a7c8ce470dd9c85/onnx-1.20.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38", size = 17414849, upload-time = "2026-01-10T01:39:01.494Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ab/5017945291b981f2681fb620f2d5b6070e02170c648770711ef1eac79d56/onnx-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031", size = 17513600, upload-time = "2026-01-10T01:39:04.348Z" }, - { url = "https://files.pythonhosted.org/packages/2e/b0/063e79dc365972af876d786bacc6acd8909691af2b9296615ff74ad182f3/onnx-1.20.1-cp310-cp310-win32.whl", hash = "sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826", size = 16239159, upload-time = "2026-01-10T01:39:07.254Z" }, - { url = "https://files.pythonhosted.org/packages/2a/73/a992271eb3683e676239d71b5a78ad3cf4d06d2223c387e701bf305da199/onnx-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a", size = 16391718, upload-time = "2026-01-10T01:39:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/0c/38/1a0e74d586c08833404100f5c052f92732fb5be417c0b2d7cb0838443bfe/onnx-1.20.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095", size = 17904965, upload-time = "2026-01-10T01:39:13.532Z" }, - { url = "https://files.pythonhosted.org/packages/96/25/64b076e9684d17335f80b15b3bf502f7a8e1a89f08a6b208d4f2861b3011/onnx-1.20.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945", size = 17415179, upload-time = "2026-01-10T01:39:16.516Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d5/6743b409421ced20ad5af1b3a7b4c4e568689ffaca86db431692fca409a6/onnx-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0", size = 17513672, upload-time = "2026-01-10T01:39:19.35Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6b/dae82e6fdb2043302f29adca37522312ea2be55b75907b59be06fbdffe87/onnx-1.20.1-cp311-cp311-win32.whl", hash = "sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e", size = 16239336, upload-time = "2026-01-10T01:39:22.506Z" }, - { url = "https://files.pythonhosted.org/packages/8e/17/a0d7863390c1f2067d7c02dcc1477034965c32aaa1407bfcf775305ffee4/onnx-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf", size = 16392120, upload-time = "2026-01-10T01:39:25.106Z" }, - { url = "https://files.pythonhosted.org/packages/aa/72/9b879a46eb7a3322223791f36bf9c25d95da9ed93779eabb75a560f22e5b/onnx-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2", size = 16346923, upload-time = "2026-01-10T01:39:27.782Z" }, - { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, - { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, - { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, - { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, - { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, - { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" }, - { url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" }, - { url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" }, -] - -[[package]] -name = "onnxruntime" -version = "1.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flatbuffers" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "protobuf" }, - { name = "sympy" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/88/d9757c62a0f96b5193f8d447a141eefd14498c404cc5caf1a6f3233cf102/onnxruntime-1.24.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:79b3119ab9f4f3817062e6dbe7f4a44937de93905e3a31ba34313d18cb49e7be", size = 17212018, upload-time = "2026-02-05T17:32:13.986Z" }, - { url = "https://files.pythonhosted.org/packages/7b/61/b3305c39144e19dbe8791802076b29b4b592b09de03d0e340c1314bfd408/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86bc43e922b1f581b3de26a3dc402149c70e5542fceb5bec6b3a85542dbeb164", size = 15018703, upload-time = "2026-02-05T17:30:53.846Z" }, - { url = "https://files.pythonhosted.org/packages/94/d6/d273b75fe7825ea3feed321dd540aef33d8a1380ddd8ac3bb70a8ed000fe/onnxruntime-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1cabe71ca14dcfbf812d312aab0a704507ac909c137ee6e89e4908755d0fc60e", size = 17096352, upload-time = "2026-02-05T17:31:29.057Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/0616101a3938bfe2918ea60b581a9bbba61ffc255c63388abb0885f7ce18/onnxruntime-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:3273c330f5802b64b4103e87b5bbc334c0355fff1b8935d8910b0004ce2f20c8", size = 12493235, upload-time = "2026-02-05T17:32:04.451Z" }, - { url = "https://files.pythonhosted.org/packages/c8/30/437de870e4e1c6d237a2ca5e11f54153531270cb5c745c475d6e3d5c5dcf/onnxruntime-1.24.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:7307aab9e2e879c0171f37e0eb2808a5b4aec7ba899bb17c5f0cedfc301a8ac2", size = 17211043, upload-time = "2026-02-05T17:32:16.909Z" }, - { url = "https://files.pythonhosted.org/packages/21/60/004401cd86525101ad8aa9eec301327426555d7a77fac89fd991c3c7aae6/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:780add442ce2d4175fafb6f3102cdc94243acffa3ab16eacc03dd627cc7b1b54", size = 15016224, upload-time = "2026-02-05T17:30:56.791Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a1/43ad01b806a1821d1d6f98725edffcdbad54856775643718e9124a09bfbe/onnxruntime-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6119526eda12613f0d0498e2ae59563c247c370c9cef74c2fc93133dde157", size = 17098191, upload-time = "2026-02-05T17:31:31.87Z" }, - { url = "https://files.pythonhosted.org/packages/ff/37/5beb65270864037d5c8fb25cfe6b23c48b618d1f4d06022d425cbf29bd9c/onnxruntime-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0af2f1cfcfff9094971c7eb1d1dfae7ccf81af197493c4dc4643e4342c0946", size = 12493108, upload-time = "2026-02-05T17:32:07.076Z" }, - { url = "https://files.pythonhosted.org/packages/95/77/7172ecfcbdabd92f338e694f38c325f6fab29a38fa0a8c3d1c85b9f4617c/onnxruntime-1.24.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:82e367770e8fba8a87ba9f4c04bb527e6d4d7204540f1390f202c27a3b759fb4", size = 17211381, upload-time = "2026-02-05T17:31:09.601Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/532a0d75b93bbd0da0e108b986097ebe164b84fbecfdf2ddbf7c8a3a2e83/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1099f3629832580fedf415cfce2462a56cc9ca2b560d6300c24558e2ac049134", size = 15016000, upload-time = "2026-02-05T17:31:00.116Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b5/40606c7bce0702975a077bc6668cd072cd77695fc5c0b3fcf59bdb1fe65e/onnxruntime-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6361dda4270f3939a625670bd67ae0982a49b7f923207450e28433abc9c3a83b", size = 17097637, upload-time = "2026-02-05T17:31:34.787Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/9e8f7933796b466241b934585723c700d8fb6bde2de856e65335193d7c93/onnxruntime-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:bd1e4aefe73b6b99aa303cd72562ab6de3cccb09088100f8ad1c974be13079c7", size = 12492467, upload-time = "2026-02-05T17:32:09.834Z" }, - { url = "https://files.pythonhosted.org/packages/fb/8a/ee07d86e35035f9fed42497af76435f5a613d4e8b6c537ea0f8ef9fa85da/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88a2b54dca00c90fca6303eedf13d49b5b4191d031372c2e85f5cffe4d86b79e", size = 15025407, upload-time = "2026-02-05T17:31:02.251Z" }, - { url = "https://files.pythonhosted.org/packages/fd/9e/ab3e1dda4b126313d240e1aaa87792ddb1f5ba6d03ca2f093a7c4af8c323/onnxruntime-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2dfbba602da840615ed5b431facda4b3a43b5d8276cf9e0dbf13d842df105838", size = 17099810, upload-time = "2026-02-05T17:31:37.537Z" }, - { url = "https://files.pythonhosted.org/packages/87/23/167d964414cee2af9c72af323b28d2c4cb35beed855c830a23f198265c79/onnxruntime-1.24.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:890c503ca187bc883c3aa72c53f2a604ec8e8444bdd1bf6ac243ec6d5e085202", size = 17214004, upload-time = "2026-02-05T17:31:11.917Z" }, - { url = "https://files.pythonhosted.org/packages/b4/24/6e5558fdd51027d6830cf411bc003ae12c64054826382e2fab89e99486a0/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da1b84b3bdeec543120df169e5e62a1445bf732fc2c7fb036c2f8a4090455e8", size = 15017034, upload-time = "2026-02-05T17:31:04.331Z" }, - { url = "https://files.pythonhosted.org/packages/91/d4/3cb1c9eaae1103265ed7eb00a3eaeb0d9ba51dc88edc398b7071c9553bed/onnxruntime-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:557753ec345efa227c6a65139f3d29c76330fcbd54cc10dd1b64232ebb939c13", size = 17097531, upload-time = "2026-02-05T17:31:40.303Z" }, - { url = "https://files.pythonhosted.org/packages/0f/da/4522b199c12db7c5b46aaf265ee0d741abe65ea912f6c0aaa2cc18a4654d/onnxruntime-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:ea4942104805e868f3ddddfa1fbb58b04503a534d489ab2d1452bbfa345c78c2", size = 12795556, upload-time = "2026-02-05T17:32:11.886Z" }, - { url = "https://files.pythonhosted.org/packages/a1/53/3b8969417276b061ff04502ccdca9db4652d397abbeb06c9f6ae05cec9ca/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea8963a99e0f10489acdf00ef3383c3232b7e44aa497b063c63be140530d9f85", size = 15025434, upload-time = "2026-02-05T17:31:06.942Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/cfcf009eb38d90cc628c087b6506b3dfe1263387f3cbbf8d272af4fef957/onnxruntime-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34488aa760fb5c2e6d06a7ca9241124eb914a6a06f70936a14c669d1b3df9598", size = 17099815, upload-time = "2026-02-05T17:31:43.092Z" }, -] - -[[package]] -name = "onnxruntime-gpu" -version = "1.24.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flatbuffers", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, - { name = "packaging", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "protobuf", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "sympy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/c7/07d06175f1124fc89e8b7da30d70eb8e0e1400d90961ae1cbea9da69e69b/onnxruntime_gpu-1.24.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac4bfc90c376516b13d709764ab257e4e3d78639bf6a2ccfc826e9db4a5c7ddf", size = 252616647, upload-time = "2026-02-05T17:24:02.993Z" }, - { url = "https://files.pythonhosted.org/packages/8c/9a/47c2a873bf5fc307cda696e8a8cb54b7c709f5a4b3f9e2b4a636066a63c2/onnxruntime_gpu-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:ccd800875cb6c04ce623154c7fa312da21631ef89a9543c9a21593817cfa3473", size = 207089749, upload-time = "2026-02-05T17:23:59.5Z" }, - { url = "https://files.pythonhosted.org/packages/db/a8/fb1a36a052321a839cc9973f6cfd630709412a24afff2d7315feb3efc4b8/onnxruntime_gpu-1.24.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:710bf83751e6761584ad071102af3cbffd4b42bb77b2e3caacfb54ffbaa0666b", size = 252628733, upload-time = "2026-02-05T17:24:12.926Z" }, - { url = "https://files.pythonhosted.org/packages/52/65/48f694b81a963f3ee575041d5f2879b15268f5e7e14d90c3e671836c9646/onnxruntime_gpu-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:b128a42b3fa098647765ba60c2af9d4bf839181307cfac27da649364feb37f7b", size = 207089008, upload-time = "2026-02-05T17:24:07.126Z" }, - { url = "https://files.pythonhosted.org/packages/7a/e7/4e19062e95d3701c0d32c228aa848ba4a1cc97651e53628d978dba8e1267/onnxruntime_gpu-1.24.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:db9acb0d0e59d93b4fa6b7fd44284ece4408d0acee73235d43ed343f8cee7ee5", size = 252629216, upload-time = "2026-02-05T17:24:24.604Z" }, - { url = "https://files.pythonhosted.org/packages/c4/82/223d7120d8a98b07c104ddecfb0cc2536188e566a4e9c2dee7572453f89c/onnxruntime_gpu-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:59fdb40743f0722f3b859209f649ea160ca6bb42799e43f49b70a3ec5fc8c4ad", size = 207089285, upload-time = "2026-02-05T17:24:18.497Z" }, - { url = "https://files.pythonhosted.org/packages/ac/82/3159e57f09d7e6c8ad47d8ba8d5bd7494f383bc1071481cf38c9c8142bf9/onnxruntime_gpu-1.24.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88ca04e1dffea2d4c3c79cf4de7f429e99059d085f21b3e775a8d36380cd5186", size = 252633977, upload-time = "2026-02-05T17:24:33.568Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b4/51ad0ab878ff1456a831a0566b4db982a904e22f138e4b2c5f021bac517f/onnxruntime_gpu-1.24.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ced66900b1f48bddb62b5233925c3b56f8e008e2c34ebf8c060b20cae5842bcf", size = 252629039, upload-time = "2026-02-05T17:24:43.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/46/336d4e09a6af66532eedde5c8f03a73eaa91a046b408522259ab6a604363/onnxruntime_gpu-1.24.1-cp314-cp314-win_amd64.whl", hash = "sha256:129f6ae8b331a6507759597cd317b23e94aed6ead1da951f803c3328f2990b0c", size = 209487551, upload-time = "2026-02-05T17:24:26.373Z" }, - { url = "https://files.pythonhosted.org/packages/6a/94/a3b20276261f5e64dbd72bda656af988282cff01f18c2685953600e2f810/onnxruntime_gpu-1.24.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2cee7e12b0f4813c62f9a48df83fd01d066cc970400c832252cf3c155a6957", size = 252633096, upload-time = "2026-02-05T17:24:53.248Z" }, -] - -[[package]] -name = "open-clip-torch" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ftfy" }, - { name = "huggingface-hub" }, - { name = "regex" }, - { name = "safetensors" }, - { name = "timm" }, - { name = "torch" }, - { name = "torchvision" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/46/fb8be250fa7fcfc56fbeb41583645e18d868268f67fbbbeb8ed62a8ff18a/open_clip_torch-3.2.0.tar.gz", hash = "sha256:62b7743012ccc40fb7c64819fa762fba0a13dd74585ac733babe58c2974c2506", size = 1502853, upload-time = "2025-09-21T17:32:08.289Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/91/397327cc1597fa317942cc15bef414175eee4b3c2263b34407c57f3521f9/open_clip_torch-3.2.0-py3-none-any.whl", hash = "sha256:e1f5b3ecbadb6d8ea64b1f887db23efee9739e7c0d0075a8a2a3cabae8fed8d1", size = 1546677, upload-time = "2025-09-21T17:32:06.269Z" }, -] - -[[package]] -name = "open3d" -version = "0.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "addict", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "configargparse", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "dash", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "flask", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "matplotlib", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "nbformat", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, - { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "pandas", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, - { name = "pillow", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "pyquaternion", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "pyyaml", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, - { name = "tqdm", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, - { name = "werkzeug", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/4b/91e8a4100adf0ccd2f7ad21dd24c2e3d8f12925396528d0462cfb1735e5a/open3d-0.19.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:f7128ded206e07987cc29d0917195fb64033dea31e0d60dead3629b33d3c175f", size = 103086005, upload-time = "2025-01-08T07:25:56.755Z" }, - { url = "https://files.pythonhosted.org/packages/c7/45/13bc9414ee9db611cba90b9efa69f66f246560e8ade575f1ee5b7f7b5d31/open3d-0.19.0-cp310-cp310-manylinux_2_31_x86_64.whl", hash = "sha256:5b60234fa6a56a20caf1560cad4e914133c8c198d74d7b839631c90e8592762e", size = 447678387, upload-time = "2025-01-08T07:21:55.27Z" }, - { url = "https://files.pythonhosted.org/packages/bc/1c/0219416429f88ebc94fcb269fb186b153affe5b91dffe8f9062330d7776d/open3d-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:18bb8b86e5fa9e582ed11b9651ff6e4a782e6778c9b8bfc344fc866dc8b5f49c", size = 69150378, upload-time = "2025-01-08T07:27:10.462Z" }, - { url = "https://files.pythonhosted.org/packages/a7/37/8d1746fcb58c37a9bd868fdca9a36c25b3c277bd764b7146419d11d2a58d/open3d-0.19.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:117702467bfb1602e9ae0ee5e2c7bcf573ebcd227b36a26f9f08425b52c89929", size = 103098641, upload-time = "2025-01-08T07:26:12.371Z" }, - { url = "https://files.pythonhosted.org/packages/bc/50/339bae21d0078cc3d3735e8eaf493a353a17dcc95d76bcefaa8edcf723d3/open3d-0.19.0-cp311-cp311-manylinux_2_31_x86_64.whl", hash = "sha256:678017392f6cc64a19d83afeb5329ffe8196893de2432f4c258eaaa819421bb5", size = 447683616, upload-time = "2025-01-08T07:22:48.098Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3c/358f1cc5b034dc6a785408b7aa7643e503229d890bcbc830cda9fce778b1/open3d-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:02091c309708f09da1167d2ea475e05d19f5e81dff025145f3afd9373cbba61f", size = 69151111, upload-time = "2025-01-08T07:27:22.662Z" }, - { url = "https://files.pythonhosted.org/packages/37/c5/286c605e087e72ad83eab130451ce13b768caa4374d926dc735edc20da5a/open3d-0.19.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:9e4a8d29443ba4c83010d199d56c96bf553dd970d3351692ab271759cbe2d7ac", size = 103202754, upload-time = "2025-01-08T07:26:27.169Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/3723e5ade77c234a1650db11cbe59fe25c4f5af6c224f8ea22ff088bb36a/open3d-0.19.0-cp312-cp312-manylinux_2_31_x86_64.whl", hash = "sha256:01e4590dc2209040292ebe509542fbf2bf869ea60bcd9be7a3fe77b65bad3192", size = 447665185, upload-time = "2025-01-08T07:23:39.769Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c4/35a6e0a35aa72420e75dc28d54b24beaff79bcad150423e47c67d2ad8773/open3d-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:665839837e1d3a62524804c31031462c3b548a2b6ed55214e6deb91522844f97", size = 69169961, upload-time = "2025-01-08T07:27:35.392Z" }, -] - -[[package]] -name = "open3d-unofficial-arm" -version = "0.19.0.post5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "configargparse", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "dash", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "flask", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "nbformat", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'" }, - { name = "werkzeug", marker = "platform_machine == 'aarch64' and sys_platform == 'linux'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/87/95d3cf9017a0e89a708e611d003abeb66c88d7947fa7238962971cc8b0cb/open3d_unofficial_arm-0.19.0.post5-cp310-cp310-manylinux_2_35_aarch64.whl", hash = "sha256:26bc160f3326a74b232f026d741a576bf0d1fa7b1d5128c5e979d7b4d2d1b983", size = 48230542, upload-time = "2026-02-10T08:37:33.928Z" }, - { url = "https://files.pythonhosted.org/packages/b7/98/e5f803c0ccc23ff68eee12d4b43aa48514dca604e3805f243f399050bd64/open3d_unofficial_arm-0.19.0.post5-cp311-cp311-manylinux_2_35_aarch64.whl", hash = "sha256:003db3e400cd8053e9428c6082af72e73082a28b3e69e9c49f69f83cf5205bb4", size = 48233477, upload-time = "2026-02-10T08:37:47.281Z" }, - { url = "https://files.pythonhosted.org/packages/36/36/df78b304227d7249f3cdeaf2444da17d5826a2c7a679e71084b3aa0d1b9a/open3d_unofficial_arm-0.19.0.post5-cp312-cp312-manylinux_2_35_aarch64.whl", hash = "sha256:984d7f5757e9cb2f849ce43f43046a30a82c221be0778149642cdfe450bd3664", size = 48221813, upload-time = "2026-02-10T08:37:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/fa/93/25b667f4dea742d870cce76b404aab46ebd47bd66a3efc162bc86e4c81fc/open3d_unofficial_arm-0.19.0.post5-cp313-cp313-manylinux_2_35_aarch64.whl", hash = "sha256:ced1653305fa052015fea3c9d1d7672ce2ebb8f2251dfe0258ee7073e5932da7", size = 48223510, upload-time = "2026-02-10T08:38:00.654Z" }, -] - -[[package]] -name = "openai" -version = "2.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/e5/3d197a0947a166649f566706d7a4c8f7fe38f1fa7b24c9bcffe4c7591d44/openai-2.21.0.tar.gz", hash = "sha256:81b48ce4b8bbb2cc3af02047ceb19561f7b1dc0d4e52d1de7f02abfd15aa59b7", size = 644374, upload-time = "2026-02-14T00:12:01.577Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/56/0a89092a453bb2c676d66abee44f863e742b2110d4dbb1dbcca3f7e5fc33/openai-2.21.0-py3-none-any.whl", hash = "sha256:0bc1c775e5b1536c294eded39ee08f8407656537ccc71b1004104fe1602e267c", size = 1103065, upload-time = "2026-02-14T00:11:59.603Z" }, -] - -[[package]] -name = "openai-whisper" -version = "20250625" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "more-itertools" }, - { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tiktoken" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "triton", marker = "(platform_machine == 'x86_64' and sys_platform == 'linux') or sys_platform == 'linux2'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/35/8e/d36f8880bcf18ec026a55807d02fe4c7357da9f25aebd92f85178000c0dc/openai_whisper-20250625.tar.gz", hash = "sha256:37a91a3921809d9f44748ffc73c0a55c9f366c85a3ef5c2ae0cc09540432eb96", size = 803191, upload-time = "2025-06-26T01:06:13.34Z" } - -[[package]] -name = "opencv-contrib-python" -version = "4.10.0.84" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/33/7b8ec6c4d45e678b26297e4a5e76464a93033a9adcc8c17eac01097065f6/opencv-contrib-python-4.10.0.84.tar.gz", hash = "sha256:4a3eae0ed9cadf1abe9293a6938a25a540e2fd6d7fc308595caa5896c8b36a0c", size = 150433857, upload-time = "2024-06-17T18:30:50.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/64/c1194510eaed272d86b53a08c790ca6ed1c450f06d401c49c8145fc46d40/opencv_contrib_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:ee4b0919026d8c533aeb69b16c6ec4a891a2f6844efaa14121bf68838753209c", size = 63667391, upload-time = "2024-06-18T04:57:54.718Z" }, - { url = "https://files.pythonhosted.org/packages/09/94/d077c4c976c2d7a88812fd55396e92edae0e0c708689dbd8c8f508920e47/opencv_contrib_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:dea80d4db73b8acccf9e16b5744bf3654f47b22745074263f0a6c10de26c5ef5", size = 66278032, upload-time = "2024-06-17T19:34:23.718Z" }, - { url = "https://files.pythonhosted.org/packages/f8/76/f76fe74b864f3cfa737173ca12e8890aad8369e980006fb8a0b6cd14c6c7/opencv_contrib_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:040575b69e4f3aa761676bace4e3d1b8485fbfaf77ef77b266ab6bda5a3b5e9b", size = 47384495, upload-time = "2024-06-17T20:00:39.027Z" }, - { url = "https://files.pythonhosted.org/packages/b0/e0/8f5d065ebb2e5941d289c5f653f944318f9e418bc5167bc6a346ab5e0f6a/opencv_contrib_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a261223db41f6e512d76deaf21c8fcfb4fbbcbc2de62ca7f74a05f2c9ee489ef", size = 68681489, upload-time = "2024-06-17T18:30:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/36/30/7041bd7350cb1a26fa80415a7664b6f04f7ccbf0c12b9318d564cdf35932/opencv_contrib_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2a36257ec1375d1bec2a62177ea39828ff9804de6831ee39646bdc875c343cec", size = 34506122, upload-time = "2024-06-17T18:28:29.922Z" }, - { url = "https://files.pythonhosted.org/packages/a7/9e/7110d2c5d543ab03b9581dbb1f8e2429863e44e0c9b4960b766f230c1279/opencv_contrib_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:47ec3160dae75f70e099b286d1a2e086d20dac8b06e759f60eaf867e6bdecba7", size = 45541421, upload-time = "2024-06-17T18:28:46.012Z" }, -] - -[[package]] -name = "opencv-python" -version = "4.13.0.92" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" }, - { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" }, - { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" }, - { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" }, - { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" }, - { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" }, -] - -[[package]] -name = "opencv-python-headless" -version = "4.13.0.92" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -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 = "opentelemetry-api" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-common" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-proto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/9d/22d241b66f7bbde88a3bfa6847a351d2c46b84de23e71222c6aae25c7050/opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464", size = 20409, upload-time = "2025-12-11T13:32:40.885Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/02/ffc3e143d89a27ac21fd557365b98bd0653b98de8a101151d5805b5d4c33/opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde", size = 18366, upload-time = "2025-12-11T13:32:20.2Z" }, -] - -[[package]] -name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "googleapis-common-protos" }, - { name = "grpcio" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/48/b329fed2c610c2c32c9366d9dc597202c9d1e58e631c137ba15248d8850f/opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad", size = 24650, upload-time = "2025-12-11T13:32:41.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/a3/cc9b66575bd6597b98b886a2067eea2693408d2d5f39dad9ab7fc264f5f3/opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18", size = 19766, upload-time = "2025-12-11T13:32:21.027Z" }, -] - -[[package]] -name = "opentelemetry-proto" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/1d/f25d76d8260c156c40c97c9ed4511ec0f9ce353f8108ca6e7561f82a06b2/opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8", size = 46152, upload-time = "2025-12-11T13:32:48.681Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/95/b40c96a7b5203005a0b03d8ce8cd212ff23f1793d5ba289c87a097571b18/opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007", size = 72535, upload-time = "2025-12-11T13:32:33.866Z" }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" }, -] - -[[package]] -name = "opt-einsum" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, -] - -[[package]] -name = "optax" -version = "0.2.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "chex", version = "0.1.90", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "chex", version = "0.1.91", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/3b/90c11f740a3538200b61cd2b7d9346959cb9e31e0bdea3d2f886b7262203/optax-0.2.6.tar.gz", hash = "sha256:ba8d1e12678eba2657484d6feeca4fb281b8066bdfd5efbfc0f41b87663109c0", size = 269660, upload-time = "2025-09-15T22:41:24.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/ec/19c6cc6064c7fc8f0cd6d5b37c4747849e66040c6ca98f86565efc2c227c/optax-0.2.6-py3-none-any.whl", hash = "sha256:f875251a5ab20f179d4be57478354e8e21963373b10f9c3b762b94dcb8c36d91", size = 367782, upload-time = "2025-09-15T22:41:22.825Z" }, -] - -[[package]] -name = "orbax-checkpoint" -version = "0.11.32" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "aiofiles" }, - { name = "etils", extra = ["epath", "epy"] }, - { name = "humanize" }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "msgpack" }, - { name = "nest-asyncio" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "protobuf" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "simplejson" }, - { name = "tensorstore", version = "0.1.78", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "tensorstore", version = "0.1.81", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6c/5f/1733e1143696319f311bc4de48da2e306a1f62f0925f9fe9d797b8ba8abe/orbax_checkpoint-0.11.32.tar.gz", hash = "sha256:523dcf61e93c7187c6b80fd50f3177114c0b957ea62cbb5c869c0b3e3d1a7dfc", size = 431601, upload-time = "2026-01-20T16:46:06.307Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/17/aae3144258f30920741ec91dbff0ff54665e572da50e6445ef437e08ec32/orbax_checkpoint-0.11.32-py3-none-any.whl", hash = "sha256:f0bfe9f9b1ce2c32c8f5dfab63393e51de525d41352abc17c7e21f9cc731d7a9", size = 634424, upload-time = "2026-01-20T16:46:04.382Z" }, -] - -[[package]] -name = "orbax-export" -version = "0.0.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py", marker = "python_full_version >= '3.11'" }, - { name = "dataclasses-json", marker = "python_full_version >= '3.11'" }, - { name = "etils", marker = "python_full_version >= '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxlib", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jaxtyping", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "orbax-checkpoint", marker = "python_full_version >= '3.11'" }, - { name = "protobuf", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/c8/ed7ac3c3c687bf129d7469b016c2b3d8777379f4ea453474e50ee41ce5cb/orbax_export-0.0.8.tar.gz", hash = "sha256:544eef564e2a6f17cd11b1167febe348b7b7cf56d9575de994a33d5613dd568a", size = 124980, upload-time = "2025-09-17T15:41:14.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/a9/3a755a58c8b6a36fe7e9e66bb6b93967ff49cdbc77cca8eacb2cf66435e9/orbax_export-0.0.8-py3-none-any.whl", hash = "sha256:f8037e1666ad28411cdb08d0668a2737b1281a32902c623ceda12109a089bc36", size = 180487, upload-time = "2025-09-17T15:41:12.928Z" }, -] - -[[package]] -name = "orjson" -version = "3.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1a/a373746fa6d0e116dd9e54371a7b54622c44d12296d5d0f3ad5e3ff33490/orjson-3.11.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a02c833f38f36546ba65a452127633afce4cf0dd7296b753d3bb54e55e5c0174", size = 229140, upload-time = "2026-02-02T15:37:06.082Z" }, - { url = "https://files.pythonhosted.org/packages/52/a2/fa129e749d500f9b183e8a3446a193818a25f60261e9ce143ad61e975208/orjson-3.11.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b63c6e6738d7c3470ad01601e23376aa511e50e1f3931395b9f9c722406d1a67", size = 128670, upload-time = "2026-02-02T15:37:08.002Z" }, - { url = "https://files.pythonhosted.org/packages/08/93/1e82011cd1e0bd051ef9d35bed1aa7fb4ea1f0a055dc2c841b46b43a9ebd/orjson-3.11.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:043d3006b7d32c7e233b8cfb1f01c651013ea079e08dcef7189a29abd8befe11", size = 123832, upload-time = "2026-02-02T15:37:09.191Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d8/a26b431ef962c7d55736674dddade876822f3e33223c1f47a36879350d04/orjson-3.11.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57036b27ac8a25d81112eb0cc9835cd4833c5b16e1467816adc0015f59e870dc", size = 129171, upload-time = "2026-02-02T15:37:11.112Z" }, - { url = "https://files.pythonhosted.org/packages/a7/19/f47819b84a580f490da260c3ee9ade214cf4cf78ac9ce8c1c758f80fdfc9/orjson-3.11.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:733ae23ada68b804b222c44affed76b39e30806d38660bf1eb200520d259cc16", size = 141967, upload-time = "2026-02-02T15:37:12.282Z" }, - { url = "https://files.pythonhosted.org/packages/5b/cd/37ece39a0777ba077fdcdbe4cccae3be8ed00290c14bf8afdc548befc260/orjson-3.11.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5fdfad2093bdd08245f2e204d977facd5f871c88c4a71230d5bcbd0e43bf6222", size = 130991, upload-time = "2026-02-02T15:37:13.465Z" }, - { url = "https://files.pythonhosted.org/packages/8f/ed/f2b5d66aa9b6b5c02ff5f120efc7b38c7c4962b21e6be0f00fd99a5c348e/orjson-3.11.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cededd6738e1c153530793998e31c05086582b08315db48ab66649768f326baa", size = 133674, upload-time = "2026-02-02T15:37:14.694Z" }, - { url = "https://files.pythonhosted.org/packages/c4/6e/baa83e68d1aa09fa8c3e5b2c087d01d0a0bd45256de719ed7bc22c07052d/orjson-3.11.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:14f440c7268c8f8633d1b3d443a434bd70cb15686117ea6beff8fdc8f5917a1e", size = 138722, upload-time = "2026-02-02T15:37:16.501Z" }, - { url = "https://files.pythonhosted.org/packages/0c/47/7f8ef4963b772cd56999b535e553f7eb5cd27e9dd6c049baee6f18bfa05d/orjson-3.11.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3a2479753bbb95b0ebcf7969f562cdb9668e6d12416a35b0dda79febf89cdea2", size = 409056, upload-time = "2026-02-02T15:37:17.895Z" }, - { url = "https://files.pythonhosted.org/packages/38/eb/2df104dd2244b3618f25325a656f85cc3277f74bbd91224752410a78f3c7/orjson-3.11.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:71924496986275a737f38e3f22b4e0878882b3f7a310d2ff4dc96e812789120c", size = 144196, upload-time = "2026-02-02T15:37:19.349Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2a/ee41de0aa3a6686598661eae2b4ebdff1340c65bfb17fcff8b87138aab21/orjson-3.11.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4a9eefdc70bf8bf9857f0290f973dec534ac84c35cd6a7f4083be43e7170a8f", size = 134979, upload-time = "2026-02-02T15:37:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fa/92fc5d3d402b87a8b28277a9ed35386218a6a5287c7fe5ee9b9f02c53fb2/orjson-3.11.7-cp310-cp310-win32.whl", hash = "sha256:ae9e0b37a834cef7ce8f99de6498f8fad4a2c0bf6bfc3d02abd8ed56aa15b2de", size = 127968, upload-time = "2026-02-02T15:37:23.178Z" }, - { url = "https://files.pythonhosted.org/packages/07/29/a576bf36d73d60df06904d3844a9df08e25d59eba64363aaf8ec2f9bff41/orjson-3.11.7-cp310-cp310-win_amd64.whl", hash = "sha256:d772afdb22555f0c58cfc741bdae44180122b3616faa1ecadb595cd526e4c993", size = 125128, upload-time = "2026-02-02T15:37:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, -] - -[[package]] -name = "ormsgpack" -version = "1.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/fa/a91f70829ebccf6387c4946e0a1a109f6ba0d6a28d65f628bedfad94b890/ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657", size = 378262, upload-time = "2026-01-18T20:55:22.284Z" }, - { url = "https://files.pythonhosted.org/packages/5f/62/3698a9a0c487252b5c6a91926e5654e79e665708ea61f67a8bdeceb022bf/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163", size = 203034, upload-time = "2026-01-18T20:55:53.324Z" }, - { url = "https://files.pythonhosted.org/packages/66/3a/f716f64edc4aec2744e817660b317e2f9bb8de372338a95a96198efa1ac1/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a", size = 210538, upload-time = "2026-01-18T20:55:20.097Z" }, - { url = "https://files.pythonhosted.org/packages/72/30/a436be9ce27d693d4e19fa94900028067133779f09fc45776db3f689c822/ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2", size = 212401, upload-time = "2026-01-18T20:55:46.447Z" }, - { url = "https://files.pythonhosted.org/packages/10/c5/cde98300fd33fee84ca71de4751b19aeeca675f0cf3c0ec4b043f40f3b76/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd", size = 387080, upload-time = "2026-01-18T20:56:00.884Z" }, - { url = "https://files.pythonhosted.org/packages/6a/31/30bf445ef827546747c10889dd254b3d84f92b591300efe4979d792f4c41/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c", size = 482346, upload-time = "2026-01-18T20:55:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f5/e1745ddf4fa246c921b5ca253636c4c700ff768d78032f79171289159f6e/ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b", size = 425178, upload-time = "2026-01-18T20:55:27.106Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a2/e6532ed7716aed03dede8df2d0d0d4150710c2122647d94b474147ccd891/ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f", size = 117183, upload-time = "2026-01-18T20:55:55.52Z" }, - { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, - { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, - { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, - { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, - { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, - { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, - { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, - { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, - { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, - { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, - { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, - { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, - { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, - { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, - { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, - { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, - { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, - { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, - { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, - { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, - { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, - { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, - { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, - { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, - { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, - { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, -] - -[[package]] -name = "overrides" -version = "7.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, -] - -[[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" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "python-dateutil", marker = "python_full_version < '3.11'" }, - { name = "pytz", marker = "python_full_version < '3.11'" }, - { name = "tzdata", marker = "python_full_version < '3.11'" }, -] -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/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, - { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, - { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, - { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, - { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, - { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, - { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, - { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, - { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, - { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, - { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, -] - -[[package]] -name = "pandas" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, - { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, - { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, - { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, - { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, - { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, - { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, - { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, - { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, - { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, - { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, - { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, - { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, - { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, - { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, - { url = "https://files.pythonhosted.org/packages/6f/fa/7f0ac4ca8877c57537aaff2a842f8760e630d8e824b730eb2e859ffe96ca/pandas-3.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b78d646249b9a2bc191040988c7bb524c92fa8534fb0898a0741d7e6f2ffafa6", size = 10307129, upload-time = "2026-01-21T15:50:52.877Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/28a221815dcea4c0c9414dfc845e34a84a6a7dabc6da3194498ed5ba4361/pandas-3.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bc9cba7b355cb4162442a88ce495e01cb605f17ac1e27d6596ac963504e0305f", size = 9850201, upload-time = "2026-01-21T15:50:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/ba/da/53bbc8c5363b7e5bd10f9ae59ab250fc7a382ea6ba08e4d06d8694370354/pandas-3.0.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c9a1a149aed3b6c9bf246033ff91e1b02d529546c5d6fb6b74a28fea0cf4c70", size = 10354031, upload-time = "2026-01-21T15:50:57.463Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a3/51e02ebc2a14974170d51e2410dfdab58870ea9bcd37cda15bd553d24dc4/pandas-3.0.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95683af6175d884ee89471842acfca29172a85031fccdabc35e50c0984470a0e", size = 10861165, upload-time = "2026-01-21T15:50:59.32Z" }, - { url = "https://files.pythonhosted.org/packages/a5/fe/05a51e3cac11d161472b8297bd41723ea98013384dd6d76d115ce3482f9b/pandas-3.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1fbbb5a7288719e36b76b4f18d46ede46e7f916b6c8d9915b756b0a6c3f792b3", size = 11359359, upload-time = "2026-01-21T15:51:02.014Z" }, - { url = "https://files.pythonhosted.org/packages/ee/56/ba620583225f9b85a4d3e69c01df3e3870659cc525f67929b60e9f21dcd1/pandas-3.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8e8b9808590fa364416b49b2a35c1f4cf2785a6c156935879e57f826df22038e", size = 11912907, upload-time = "2026-01-21T15:51:05.175Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8c/c6638d9f67e45e07656b3826405c5cc5f57f6fd07c8b2572ade328c86e22/pandas-3.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:98212a38a709feb90ae658cb6227ea3657c22ba8157d4b8f913cd4c950de5e7e", size = 9732138, upload-time = "2026-01-21T15:51:07.569Z" }, - { url = "https://files.pythonhosted.org/packages/7b/bf/bd1335c3bf1770b6d8fed2799993b11c4971af93bb1b729b9ebbc02ca2ec/pandas-3.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:177d9df10b3f43b70307a149d7ec49a1229a653f907aa60a48f1877d0e6be3be", size = 9033568, upload-time = "2026-01-21T15:51:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/8e/c6/f5e2171914d5e29b9171d495344097d54e3ffe41d2d85d8115baba4dc483/pandas-3.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2713810ad3806767b89ad3b7b69ba153e1c6ff6d9c20f9c2140379b2a98b6c98", size = 10741936, upload-time = "2026-01-21T15:51:11.693Z" }, - { url = "https://files.pythonhosted.org/packages/51/88/9a0164f99510a1acb9f548691f022c756c2314aad0d8330a24616c14c462/pandas-3.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:15d59f885ee5011daf8335dff47dcb8a912a27b4ad7826dc6cbe809fd145d327", size = 10393884, upload-time = "2026-01-21T15:51:14.197Z" }, - { url = "https://files.pythonhosted.org/packages/e0/53/b34d78084d88d8ae2b848591229da8826d1e65aacf00b3abe34023467648/pandas-3.0.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24e6547fb64d2c92665dd2adbfa4e85fa4fd70a9c070e7cfb03b629a0bbab5eb", size = 10310740, upload-time = "2026-01-21T15:51:16.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d3/bee792e7c3d6930b74468d990604325701412e55d7aaf47460a22311d1a5/pandas-3.0.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48ee04b90e2505c693d3f8e8f524dab8cb8aaf7ddcab52c92afa535e717c4812", size = 10700014, upload-time = "2026-01-21T15:51:18.818Z" }, - { url = "https://files.pythonhosted.org/packages/55/db/2570bc40fb13aaed1cbc3fbd725c3a60ee162477982123c3adc8971e7ac1/pandas-3.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66f72fb172959af42a459e27a8d8d2c7e311ff4c1f7db6deb3b643dbc382ae08", size = 11323737, upload-time = "2026-01-21T15:51:20.784Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2e/297ac7f21c8181b62a4cccebad0a70caf679adf3ae5e83cb676194c8acc3/pandas-3.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4a4a400ca18230976724a5066f20878af785f36c6756e498e94c2a5e5d57779c", size = 11771558, upload-time = "2026-01-21T15:51:22.977Z" }, - { url = "https://files.pythonhosted.org/packages/0a/46/e1c6876d71c14332be70239acce9ad435975a80541086e5ffba2f249bcf6/pandas-3.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:940eebffe55528074341a5a36515f3e4c5e25e958ebbc764c9502cfc35ba3faa", size = 10473771, upload-time = "2026-01-21T15:51:25.285Z" }, - { url = "https://files.pythonhosted.org/packages/c0/db/0270ad9d13c344b7a36fa77f5f8344a46501abf413803e885d22864d10bf/pandas-3.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:597c08fb9fef0edf1e4fa2f9828dd27f3d78f9b8c9b4a748d435ffc55732310b", size = 10312075, upload-time = "2026-01-21T15:51:28.5Z" }, - { url = "https://files.pythonhosted.org/packages/09/9f/c176f5e9717f7c91becfe0f55a52ae445d3f7326b4a2cf355978c51b7913/pandas-3.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:447b2d68ac5edcbf94655fe909113a6dba6ef09ad7f9f60c80477825b6c489fe", size = 9900213, upload-time = "2026-01-21T15:51:30.955Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e7/63ad4cc10b257b143e0a5ebb04304ad806b4e1a61c5da25f55896d2ca0f4/pandas-3.0.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debb95c77ff3ed3ba0d9aa20c3a2f19165cc7956362f9873fce1ba0a53819d70", size = 10428768, upload-time = "2026-01-21T15:51:33.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/0e/4e4c2d8210f20149fd2248ef3fff26623604922bd564d915f935a06dd63d/pandas-3.0.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fedabf175e7cd82b69b74c30adbaa616de301291a5231138d7242596fc296a8d", size = 10882954, upload-time = "2026-01-21T15:51:35.287Z" }, - { url = "https://files.pythonhosted.org/packages/c6/60/c9de8ac906ba1f4d2250f8a951abe5135b404227a55858a75ad26f84db47/pandas-3.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:412d1a89aab46889f3033a386912efcdfa0f1131c5705ff5b668dda88305e986", size = 11430293, upload-time = "2026-01-21T15:51:37.57Z" }, - { url = "https://files.pythonhosted.org/packages/a1/69/806e6637c70920e5787a6d6896fd707f8134c2c55cd761e7249a97b7dc5a/pandas-3.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e979d22316f9350c516479dd3a92252be2937a9531ed3a26ec324198a99cdd49", size = 11952452, upload-time = "2026-01-21T15:51:39.618Z" }, - { url = "https://files.pythonhosted.org/packages/cb/de/918621e46af55164c400ab0ef389c9d969ab85a43d59ad1207d4ddbe30a5/pandas-3.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:083b11415b9970b6e7888800c43c82e81a06cd6b06755d84804444f0007d6bb7", size = 9851081, upload-time = "2026-01-21T15:51:41.758Z" }, - { url = "https://files.pythonhosted.org/packages/91/a1/3562a18dd0bd8c73344bfa26ff90c53c72f827df119d6d6b1dacc84d13e3/pandas-3.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:5db1e62cb99e739fa78a28047e861b256d17f88463c76b8dafc7c1338086dca8", size = 9174610, upload-time = "2026-01-21T15:51:44.312Z" }, - { url = "https://files.pythonhosted.org/packages/ce/26/430d91257eaf366f1737d7a1c158677caaf6267f338ec74e3a1ec444111c/pandas-3.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:697b8f7d346c68274b1b93a170a70974cdc7d7354429894d5927c1effdcccd73", size = 10761999, upload-time = "2026-01-21T15:51:46.899Z" }, - { url = "https://files.pythonhosted.org/packages/ec/1a/954eb47736c2b7f7fe6a9d56b0cb6987773c00faa3c6451a43db4beb3254/pandas-3.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8cb3120f0d9467ed95e77f67a75e030b67545bcfa08964e349252d674171def2", size = 10410279, upload-time = "2026-01-21T15:51:48.89Z" }, - { url = "https://files.pythonhosted.org/packages/20/fc/b96f3a5a28b250cd1b366eb0108df2501c0f38314a00847242abab71bb3a/pandas-3.0.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33fd3e6baa72899746b820c31e4b9688c8e1b7864d7aec2de7ab5035c285277a", size = 10330198, upload-time = "2026-01-21T15:51:51.015Z" }, - { url = "https://files.pythonhosted.org/packages/90/b3/d0e2952f103b4fbef1ef22d0c2e314e74fc9064b51cee30890b5e3286ee6/pandas-3.0.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8942e333dc67ceda1095227ad0febb05a3b36535e520154085db632c40ad084", size = 10728513, upload-time = "2026-01-21T15:51:53.387Z" }, - { url = "https://files.pythonhosted.org/packages/76/81/832894f286df828993dc5fd61c63b231b0fb73377e99f6c6c369174cf97e/pandas-3.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:783ac35c4d0fe0effdb0d67161859078618b1b6587a1af15928137525217a721", size = 11345550, upload-time = "2026-01-21T15:51:55.329Z" }, - { url = "https://files.pythonhosted.org/packages/34/a0/ed160a00fb4f37d806406bc0a79a8b62fe67f29d00950f8d16203ff3409b/pandas-3.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:125eb901e233f155b268bbef9abd9afb5819db74f0e677e89a61b246228c71ac", size = 11799386, upload-time = "2026-01-21T15:51:57.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/c8/2ac00d7255252c5e3cf61b35ca92ca25704b0188f7454ca4aec08a33cece/pandas-3.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b86d113b6c109df3ce0ad5abbc259fe86a1bd4adfd4a31a89da42f84f65509bb", size = 10873041, upload-time = "2026-01-21T15:52:00.034Z" }, - { url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" }, -] - -[[package]] -name = "pandas-stubs" -version = "2.3.3.260113" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "types-pytz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl", hash = "sha256:ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3", size = 168246, upload-time = "2026-01-13T22:30:15.244Z" }, -] - -[[package]] -name = "parso" -version = "0.8.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/76/a1e769043c0c0c9fe391b702539d594731a4362334cdf4dc25d0c09761e7/parso-0.8.6.tar.gz", hash = "sha256:2b9a0332696df97d454fa67b81618fd69c35a7b90327cbe6ba5c92d2c68a7bfd", size = 401621, upload-time = "2026-02-09T15:45:24.425Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl", hash = "sha256:2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff", size = 106894, upload-time = "2026-02-09T15:45:21.391Z" }, -] - -[[package]] -name = "partd" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "locket" }, - { name = "toolz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, -] - -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess", marker = "sys_platform != 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, -] - -[[package]] -name = "pillow" -version = "10.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", size = 3509271, upload-time = "2024-07-01T09:45:22.07Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", size = 3375658, upload-time = "2024-07-01T09:45:25.292Z" }, - { url = "https://files.pythonhosted.org/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", size = 4332075, upload-time = "2024-07-01T09:45:27.94Z" }, - { url = "https://files.pythonhosted.org/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", size = 4444808, upload-time = "2024-07-01T09:45:30.305Z" }, - { url = "https://files.pythonhosted.org/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", size = 4356290, upload-time = "2024-07-01T09:45:32.868Z" }, - { url = "https://files.pythonhosted.org/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", size = 4525163, upload-time = "2024-07-01T09:45:35.279Z" }, - { url = "https://files.pythonhosted.org/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", size = 4463100, upload-time = "2024-07-01T09:45:37.74Z" }, - { url = "https://files.pythonhosted.org/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", size = 4592880, upload-time = "2024-07-01T09:45:39.89Z" }, - { url = "https://files.pythonhosted.org/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", size = 2235218, upload-time = "2024-07-01T09:45:42.771Z" }, - { url = "https://files.pythonhosted.org/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", size = 2554487, upload-time = "2024-07-01T09:45:45.176Z" }, - { url = "https://files.pythonhosted.org/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1", size = 2243219, upload-time = "2024-07-01T09:45:47.274Z" }, - { url = "https://files.pythonhosted.org/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", size = 3509265, upload-time = "2024-07-01T09:45:49.812Z" }, - { url = "https://files.pythonhosted.org/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", size = 3375655, upload-time = "2024-07-01T09:45:52.462Z" }, - { url = "https://files.pythonhosted.org/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", size = 4340304, upload-time = "2024-07-01T09:45:55.006Z" }, - { url = "https://files.pythonhosted.org/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", size = 4452804, upload-time = "2024-07-01T09:45:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", size = 4365126, upload-time = "2024-07-01T09:46:00.713Z" }, - { url = "https://files.pythonhosted.org/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", size = 4533541, upload-time = "2024-07-01T09:46:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", size = 4471616, upload-time = "2024-07-01T09:46:05.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", size = 4600802, upload-time = "2024-07-01T09:46:08.145Z" }, - { url = "https://files.pythonhosted.org/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", size = 2235213, upload-time = "2024-07-01T09:46:10.211Z" }, - { url = "https://files.pythonhosted.org/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", size = 2554498, upload-time = "2024-07-01T09:46:12.685Z" }, - { url = "https://files.pythonhosted.org/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", size = 2243219, upload-time = "2024-07-01T09:46:14.83Z" }, - { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" }, - { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" }, - { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" }, - { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" }, - { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" }, - { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" }, - { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" }, - { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" }, - { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" }, - { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" }, - { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" }, - { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" }, - { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" }, - { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" }, - { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" }, - { url = "https://files.pythonhosted.org/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", size = 3493889, upload-time = "2024-07-01T09:48:04.815Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", size = 3346160, upload-time = "2024-07-01T09:48:07.206Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", size = 3435020, upload-time = "2024-07-01T09:48:09.66Z" }, - { url = "https://files.pythonhosted.org/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", size = 3490539, upload-time = "2024-07-01T09:48:12.529Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", size = 3476125, upload-time = "2024-07-01T09:48:14.891Z" }, - { url = "https://files.pythonhosted.org/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", size = 3579373, upload-time = "2024-07-01T09:48:17.601Z" }, - { url = "https://files.pythonhosted.org/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", size = 2554661, upload-time = "2024-07-01T09:48:20.293Z" }, -] - -[[package]] -name = "pin" -version = "3.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cmeel" }, - { name = "cmeel-boost" }, - { name = "cmeel-urdfdom" }, - { name = "coal" }, - { name = "libpinocchio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/99/4e7393e8035985405e89bc61dc0037f9bd1792c7a0295192aa3791bf4844/pin-3.8.0.tar.gz", hash = "sha256:f3889867d6fb968299696e94974138d6668600663b8650723a59fe062356fece", size = 4000900, upload-time = "2025-10-16T14:04:29.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/6b/0d280cc9753acb1bca1ffad8138f1c3939a797a336b9b058a051267b4aea/pin-3.8.0-0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:92046a8b0599d2396e0f5303f81f76ad306315d7a45cc44bb1ad8afacc59760c", size = 5634231, upload-time = "2025-10-16T14:03:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/c1/df/b7c9cbb484a0c096e7b4beb22fed4c5bf77c5bb042fe22702ce9c3757bb7/pin-3.8.0-0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2565eebc9dd2f84181cddc66c356f2896a64162ed1eadc7d3a60e6a034d6a5ae", size = 5420549, upload-time = "2025-10-16T14:03:51.642Z" }, - { url = "https://files.pythonhosted.org/packages/c3/2e/1cb2fc19cd5ee830a9bc992956d9ef83a3dcee347edbb56d8c35d069b374/pin-3.8.0-0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4b2e0ae3f5b06538f78f84e385c9d5d2a8470828b108520a1cf0657f658521e8", size = 7242690, upload-time = "2025-10-16T14:03:53.369Z" }, - { url = "https://files.pythonhosted.org/packages/78/9a/8f93ca590dab6058283d0cc3ee776ba3a72f6d8662e3c7e3b6b9424faee0/pin-3.8.0-0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d9a48b99f8d3b085575f88944f1537a048fcd262da3efc52fed732b220e1422f", size = 7402696, upload-time = "2025-10-16T14:03:55.133Z" }, - { url = "https://files.pythonhosted.org/packages/59/36/921da84d53048ab2cc443da6d745e03494a447a5f41dfe65f8c948b26cfa/pin-3.8.0-0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d6ef1dc90aa2af1cbe616fd29671bfab60b860def2d7f4fc8fd9ffe5f95033a8", size = 5634235, upload-time = "2025-10-16T14:03:56.692Z" }, - { url = "https://files.pythonhosted.org/packages/d4/aa/a2dbe963f20ebc89ab8f1adc6ac4a6bbe8d82383f056edc478607b349021/pin-3.8.0-0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5c34d3b5f1307d94a94ca86e6563b5cc3c0a92bfbe17d63f408ea6e98d5befe", size = 5420564, upload-time = "2025-10-16T14:03:58.027Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/5bc1f519b56f2c546e8035cf1dc42451d40d86d5d1f693c2786fbb57ae8a/pin-3.8.0-0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:80eba5dd6e8eb91211b98170e15c25eb2927eb6c3bd561b755b33185b1ce301e", size = 7241049, upload-time = "2025-10-16T14:03:59.978Z" }, - { url = "https://files.pythonhosted.org/packages/d1/35/14336eca99c7403e011fb3d6e20d51494ba8e1b03689f63ecea0e17f4beb/pin-3.8.0-0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:9b22136ddc544d13ee56e1fcf7ffc57b16f2e28ff5484b01241e16268e19afa4", size = 7402020, upload-time = "2025-10-16T14:04:01.512Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1e/620cd7711ee033ada46f0efefc5b59587aed8ece33dbb5701954990f0a47/pin-3.8.0-0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:93fd4b3c11b450f448120a5f5891dd1f810612cb3624fa9aee9795f1efc95427", size = 5682834, upload-time = "2025-10-16T14:04:03.389Z" }, - { url = "https://files.pythonhosted.org/packages/74/7e/036ccc91f29e406ed102f4189508881f78d859d70d5ba0b553e35d72db3b/pin-3.8.0-0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd074b97d8045cbadc6774983152bbae90347c42b8b478fe9b077f7261b2807d", size = 5451808, upload-time = "2025-10-16T14:04:05.046Z" }, - { url = "https://files.pythonhosted.org/packages/b7/67/85bf2cc80697a50e74fd2c58cc28038f557632c3ca6caef2779797dbfd6c/pin-3.8.0-0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:cfccb5d4e6a8b8a337a091762d6c09f1a6945fb6feb37968d076fd01c5631e6d", size = 7166942, upload-time = "2025-10-16T14:04:06.46Z" }, - { url = "https://files.pythonhosted.org/packages/38/a4/17aa94538ddd552767abacf29c271a7b29a4659c89a7eda140fea9507e39/pin-3.8.0-0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4d00f51d24464c61975073ee9dfbb02b0ac92c9393454c0c61086f919024f635", size = 7336970, upload-time = "2025-10-16T14:04:07.921Z" }, - { url = "https://files.pythonhosted.org/packages/79/bb/adec2172e3bce5f42539a910f4c619ffad43fe206e40e21ad02093a08cb6/pin-3.8.0-0-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:735e0d4db389048cf23ae5e38d2d6991393ad42b8f0b226bfb21b44ffd29a3b0", size = 5682835, upload-time = "2025-10-16T14:04:09.554Z" }, - { url = "https://files.pythonhosted.org/packages/b0/de/4a93ee6a684057507eedfefa0f0e63240cca25d9053836e5e01ff045a2e0/pin-3.8.0-0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3a5c403821f450298ec235362ff006daa7963d7c618a34c8693fd8660573961c", size = 5451811, upload-time = "2025-10-16T14:04:11.253Z" }, - { url = "https://files.pythonhosted.org/packages/92/ef/670dd481925f4805a22138993f6e8bd08a4c717939a60a2efb554b54a6a6/pin-3.8.0-0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b2b2b07d14194ae0178f7ea2a1427599ea57b700ee2e30e6703594f9ad055831", size = 7166941, upload-time = "2025-10-16T14:04:13.751Z" }, - { url = "https://files.pythonhosted.org/packages/39/5e/96c3b0b4480b09f44582ad79c51d3bc644cefaf9961433ea396e8da29590/pin-3.8.0-0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:976970948a3c5bcdd2807239cf072e232c88e29d0db3a49ed7a73bf18b7c59e3", size = 7336973, upload-time = "2025-10-16T14:04:15.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/53/3828fe93db30851cc03ada6d6f6f2b93493940e6b43afcad247342c0d20e/pin-3.8.0-0-cp314-cp314-macosx_10_9_x86_64.whl", hash = "sha256:a7e7b277087f80bd16a3e03c6d6b8f7000bcb5cf58bc871085c3fd4db0384078", size = 5698064, upload-time = "2025-10-16T14:04:16.811Z" }, - { url = "https://files.pythonhosted.org/packages/eb/e4/477dfc034f337b94ad11cb3e48d9301abdf142b83568371c07abb27a3069/pin-3.8.0-0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f8261ac76c0a474e1bc40cfe04a67e24dfbc33f26cd84dae6c65cd35509f3127", size = 5467147, upload-time = "2025-10-16T14:04:18.56Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/23030667d13743a532de3bbdfcf73213c1516ede1b41198fc836675963ab/pin-3.8.0-0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:ba8d2d1e9c8faa2d8ffbb58b0cb57f79fdb9e6750d139ec5030525e67a30fd47", size = 7193153, upload-time = "2025-10-16T14:04:20.095Z" }, - { url = "https://files.pythonhosted.org/packages/51/aa/3ed32e4204194ee171ce1259ba6c86eb28373ffb139465ba0bd3b5796191/pin-3.8.0-0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:07a3b2f3bacd9510fc9a6dc8aefba286f89ded2ebbf398d4a55671de32aa76d9", size = 7350015, upload-time = "2025-10-16T14:04:21.65Z" }, -] - -[[package]] -name = "piper-sdk" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-can" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/c4/06172af8276170ff0f484e2065f853eef53d7ced3cc822730f55ea110f3b/piper_sdk-0.6.1.tar.gz", hash = "sha256:2a154870992379f5048caf70662fdbb29f11b7cb17846d6a23afc07cd3d57217", size = 161302, upload-time = "2025-10-30T06:38:53.054Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/0c/4473a7a9aca9c50798abec6a77e8e5e714ad968399db3d2b86162a05177c/piper_sdk-0.6.1-py3-none-any.whl", hash = "sha256:743557e1b8dfe685f2c33d728ab28c3ff510de8860d6494e54ed5d801493d65c", size = 193748, upload-time = "2025-10-30T06:38:51.368Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/9b/20c8288dc591129bf9dd7be2c91aec6ef23e450605c3403716bd6c74833e/platformdirs-4.8.0.tar.gz", hash = "sha256:c1d4a51ab04087041dd602707fbe7ee8b62b64e590f30e336e5c99c2d0c542d2", size = 27607, upload-time = "2026-02-14T01:52:03.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/f0/227a7d1b8d80ae55c4b47f271c0870dd7a153aa65353bf71921265df2300/platformdirs-4.8.0-py3-none-any.whl", hash = "sha256:1c1328b4d2ea997bbcb904175a9bde14e824a3fa79f751ea3888d63d7d727557", size = 20647, upload-time = "2026-02-14T01:52:01.915Z" }, -] - -[[package]] -name = "playground" -version = "0.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "brax" }, - { name = "etils" }, - { name = "flax", version = "0.10.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "flax", version = "0.12.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jax", version = "0.6.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jax", version = "0.9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "lxml" }, - { name = "mediapy" }, - { name = "ml-collections" }, - { name = "mujoco" }, - { name = "mujoco-mjx" }, - { name = "orbax-checkpoint" }, - { name = "tqdm" }, - { name = "warp-lang" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/48/7ef4a08d57c431e7bea8a54c7853726de6cfcc50584442377acb6be615a6/playground-0.1.0.tar.gz", hash = "sha256:30d31d59528005e13f938cbcd5ce40c831553313aa7f861bfa3c9640115f46cf", size = 9894110, upload-time = "2026-01-08T22:18:36.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/c9/59e9cd044c234b7eb0f9f5480ddec55ffcef79327f78b2004aa7ec80fd76/playground-0.1.0-py3-none-any.whl", hash = "sha256:06e8fd567bab346adfdd31bd13042d0dd121af9cdce28a4155b30d79cf99a91e", size = 10044265, upload-time = "2026-01-08T22:18:34.116Z" }, -] - -[[package]] -name = "plotext" -version = "5.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/d7/f75f397af966fe252d0d34ffd3cae765317fce2134f925f95e7d6725d1ce/plotext-5.3.2.tar.gz", hash = "sha256:52d1e932e67c177bf357a3f0fe6ce14d1a96f7f7d5679d7b455b929df517068e", size = 61967, upload-time = "2024-09-24T15:13:37.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/1e/12fe7c40cd2099a1f454518754ed229b01beaf3bbb343127f0cc13ce6c22/plotext-5.3.2-py3-none-any.whl", hash = "sha256:394362349c1ddbf319548cfac17ca65e6d5dfc03200c40dfdc0503b3e95a2283", size = 64047, upload-time = "2024-09-24T15:13:36.296Z" }, -] - -[[package]] -name = "plotly" -version = "6.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "narwhals" }, - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/4f/8a10a9b9f5192cb6fdef62f1d77fa7d834190b2c50c0cd256bd62879212b/plotly-6.5.2.tar.gz", hash = "sha256:7478555be0198562d1435dee4c308268187553cc15516a2f4dd034453699e393", size = 7015695, upload-time = "2026-01-14T21:26:51.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/67/f95b5460f127840310d2187f916cf0023b5875c0717fdf893f71e1325e87/plotly-6.5.2-py3-none-any.whl", hash = "sha256:91757653bd9c550eeea2fa2404dba6b85d1e366d54804c340b2c874e5a7eb4a4", size = 9895973, upload-time = "2026-01-14T21:26:47.135Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "plum-dispatch" -version = "2.5.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beartype" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/46/ab3928e864b0a88a8ae6987b3da3b7ae32fe0a610264f33272139275dab5/plum_dispatch-2.5.7.tar.gz", hash = "sha256:a7908ad5563b93f387e3817eb0412ad40cfbad04bc61d869cf7a76cd58a3895d", size = 35452, upload-time = "2025-01-17T20:07:31.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/31/21609a9be48e877bc33b089a7f495c853215def5aeb9564a31c210d9d769/plum_dispatch-2.5.7-py3-none-any.whl", hash = "sha256:06471782eea0b3798c1e79dca2af2165bafcfa5eb595540b514ddd81053b1ede", size = 42612, upload-time = "2025-01-17T20:07:26.461Z" }, -] - -[[package]] -name = "polars" -version = "1.38.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "polars-runtime-32" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/5e/208a24471a433bcd0e9a6889ac49025fd4daad2815c8220c5bd2576e5f1b/polars-1.38.1.tar.gz", hash = "sha256:803a2be5344ef880ad625addfb8f641995cfd777413b08a10de0897345778239", size = 717667, upload-time = "2026-02-06T18:13:23.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/49/737c1a6273c585719858261753da0b688454d1b634438ccba8a9c4eb5aab/polars-1.38.1-py3-none-any.whl", hash = "sha256:a29479c48fed4984d88b656486d221f638cba45d3e961631a50ee5fdde38cb2c", size = 810368, upload-time = "2026-02-06T18:11:55.819Z" }, -] - -[[package]] -name = "polars-runtime-32" -version = "1.38.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/07/4b/04d6b3fb7cf336fbe12fbc4b43f36d1783e11bb0f2b1e3980ec44878df06/polars_runtime_32-1.38.1.tar.gz", hash = "sha256:04f20ed1f5c58771f34296a27029dc755a9e4b1390caeaef8f317e06fdfce2ec", size = 2812631, upload-time = "2026-02-06T18:13:25.206Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/a2/a00defbddadd8cf1042f52380dcba6b6592b03bac8e3b34c436b62d12d3b/polars_runtime_32-1.38.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:18154e96044724a0ac38ce155cf63aa03c02dd70500efbbf1a61b08cadd269ef", size = 44108001, upload-time = "2026-02-06T18:11:58.127Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fb/599ff3709e6a303024efd7edfd08cf8de55c6ac39527d8f41cbc4399385f/polars_runtime_32-1.38.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c49acac34cc4049ed188f1eb67d6ff3971a39b4af7f7b734b367119970f313ac", size = 40230140, upload-time = "2026-02-06T18:12:01.181Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8c/3ac18d6f89dc05fe2c7c0ee1dc5b81f77a5c85ad59898232c2500fe2ebbf/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef2ef2626a954e010e006cc8e4de467ecf32d08008f130cea1c78911f545323", size = 41994039, upload-time = "2026-02-06T18:12:04.332Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5a/61d60ec5cc0ab37cbd5a699edb2f9af2875b7fdfdfb2a4608ca3cc5f0448/polars_runtime_32-1.38.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5f7a8125e2d50e2e060296551c929aec09be23a9edcb2b12ca923f555a5ba", size = 45755804, upload-time = "2026-02-06T18:12:07.846Z" }, - { url = "https://files.pythonhosted.org/packages/91/54/02cd4074c98c361ccd3fec3bcb0bd68dbc639c0550c42a4436b0ff0f3ccf/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:10d19cd9863e129273b18b7fcaab625b5c8143c2d22b3e549067b78efa32e4fa", size = 42159605, upload-time = "2026-02-06T18:12:10.919Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f3/b2a5e720cc56eaa38b4518e63aa577b4bbd60e8b05a00fe43ca051be5879/polars_runtime_32-1.38.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:61e8d73c614b46a00d2f853625a7569a2e4a0999333e876354ac81d1bf1bb5e2", size = 45336615, upload-time = "2026-02-06T18:12:14.074Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/ee2e4b7de948090cfb3df37d401c521233daf97bfc54ddec5d61d1d31618/polars_runtime_32-1.38.1-cp310-abi3-win_amd64.whl", hash = "sha256:08c2b3b93509c1141ac97891294ff5c5b0c548a373f583eaaea873a4bf506437", size = 45680732, upload-time = "2026-02-06T18:12:19.097Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/72c216f4ab0c82b907009668f79183ae029116ff0dd245d56ef58aac48e7/polars_runtime_32-1.38.1-cp310-abi3-win_arm64.whl", hash = "sha256:6d07d0cc832bfe4fb54b6e04218c2c27afcfa6b9498f9f6bbf262a00d58cc7c4", size = 41639413, upload-time = "2026-02-06T18:12:22.044Z" }, -] - -[[package]] -name = "portalocker" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, -] - -[[package]] -name = "posthog" -version = "5.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backoff" }, - { name = "distro" }, - { name = "python-dateutil" }, - { name = "requests" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - -[[package]] -name = "protobuf" -version = "6.33.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, - { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, - { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, - { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, - { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, -] - -[[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/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, - { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, - { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, - { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, - { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, - { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, - { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, - { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, - { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, - { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, - { 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 = "psycopg2-binary" -version = "2.9.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/f2/8e377d29c2ecf99f6062d35ea606b036e8800720eccfec5fe3dd672c2b24/psycopg2_binary-2.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6fe6b47d0b42ce1c9f1fa3e35bb365011ca22e39db37074458f27921dca40f2", size = 3756506, upload-time = "2025-10-10T11:10:30.144Z" }, - { url = "https://files.pythonhosted.org/packages/24/cc/dc143ea88e4ec9d386106cac05023b69668bd0be20794c613446eaefafe5/psycopg2_binary-2.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6c0e4262e089516603a09474ee13eabf09cb65c332277e39af68f6233911087", size = 3863943, upload-time = "2025-10-10T11:10:34.586Z" }, - { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, - { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, - { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, - { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, - { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, - { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, - { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, - { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, - { url = "https://files.pythonhosted.org/packages/4b/34/aa03d327739c1be70e09d01182619aca8ebab5970cd0cfa50dd8b9cec2ac/psycopg2_binary-2.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:763c93ef1df3da6d1a90f86ea7f3f806dc06b21c198fa87c3c25504abec9404a", size = 3863957, upload-time = "2025-10-10T11:11:16.932Z" }, - { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, - { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, - { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, - { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, - { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, - { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, - { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, - { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, - { url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, - { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, - { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, - { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, - { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, - { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, - { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" }, - { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, - { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, - { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, - { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, - { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, - { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, - { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, - { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" }, - { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, - { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, - { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, - { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, - { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, - { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, - { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, -] - -[[package]] -name = "py-cpuinfo" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/a8/d832f7293ebb21690860d2e01d8115e5ff6f2ae8bbdc953f0eb0fa4bd2c7/py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690", size = 104716, upload-time = "2022-10-25T20:38:06.303Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a9/023730ba63db1e494a271cb018dcd361bd2c917ba7004c3e49d5daf795a2/py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5", size = 22335, upload-time = "2022-10-25T20:38:27.636Z" }, -] - -[[package]] -name = "py-spy" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/e2/ff811a367028b87e86714945bb9ecb5c1cc69114a8039a67b3a862cef921/py_spy-0.4.1.tar.gz", hash = "sha256:e53aa53daa2e47c2eef97dd2455b47bb3a7e7f962796a86cc3e7dbde8e6f4db4", size = 244726, upload-time = "2025-07-31T19:33:25.172Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/e3/3a32500d845bdd94f6a2b4ed6244982f42ec2bc64602ea8fcfe900678ae7/py_spy-0.4.1-py2.py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:809094208c6256c8f4ccadd31e9a513fe2429253f48e20066879239ba12cd8cc", size = 3682508, upload-time = "2025-07-31T19:33:13.753Z" }, - { url = "https://files.pythonhosted.org/packages/4f/bf/e4d280e9e0bec71d39fc646654097027d4bbe8e04af18fb68e49afcff404/py_spy-0.4.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:1fb8bf71ab8df95a95cc387deed6552934c50feef2cf6456bc06692a5508fd0c", size = 1796395, upload-time = "2025-07-31T19:33:15.325Z" }, - { url = "https://files.pythonhosted.org/packages/df/79/9ed50bb0a9de63ed023aa2db8b6265b04a7760d98c61eb54def6a5fddb68/py_spy-0.4.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee776b9d512a011d1ad3907ed53ae32ce2f3d9ff3e1782236554e22103b5c084", size = 2034938, upload-time = "2025-07-31T19:33:17.194Z" }, - { url = "https://files.pythonhosted.org/packages/53/a5/36862e3eea59f729dfb70ee6f9e14b051d8ddce1aa7e70e0b81d9fe18536/py_spy-0.4.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:532d3525538254d1859b49de1fbe9744df6b8865657c9f0e444bf36ce3f19226", size = 2658968, upload-time = "2025-07-31T19:33:18.916Z" }, - { url = "https://files.pythonhosted.org/packages/08/f8/9ea0b586b065a623f591e5e7961282ec944b5fbbdca33186c7c0296645b3/py_spy-0.4.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4972c21890b6814017e39ac233c22572c4a61fd874524ebc5ccab0f2237aee0a", size = 2147541, upload-time = "2025-07-31T19:33:20.565Z" }, - { url = "https://files.pythonhosted.org/packages/68/fb/bc7f639aed026bca6e7beb1e33f6951e16b7d315594e7635a4f7d21d63f4/py_spy-0.4.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6a80ec05eb8a6883863a367c6a4d4f2d57de68466f7956b6367d4edd5c61bb29", size = 2763338, upload-time = "2025-07-31T19:33:22.202Z" }, - { url = "https://files.pythonhosted.org/packages/e1/da/fcc9a9fcd4ca946ff402cff20348e838b051d69f50f5d1f5dca4cd3c5eb8/py_spy-0.4.1-py2.py3-none-win_amd64.whl", hash = "sha256:d92e522bd40e9bf7d87c204033ce5bb5c828fca45fa28d970f58d71128069fdc", size = 1818784, upload-time = "2025-07-31T19:33:23.802Z" }, -] - -[[package]] -name = "pyarrow" -version = "23.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/2f/23e042a5aa99bcb15e794e14030e8d065e00827e846e53a66faec73c7cd6/pyarrow-23.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:cbdc2bf5947aa4d462adcf8453cf04aee2f7932653cb67a27acd96e5e8528a67", size = 34281861, upload-time = "2026-01-18T16:13:34.332Z" }, - { url = "https://files.pythonhosted.org/packages/8b/65/1651933f504b335ec9cd8f99463718421eb08d883ed84f0abd2835a16cad/pyarrow-23.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4d38c836930ce15cd31dce20114b21ba082da231c884bdc0a7b53e1477fe7f07", size = 35825067, upload-time = "2026-01-18T16:13:42.549Z" }, - { url = "https://files.pythonhosted.org/packages/84/ec/d6fceaec050c893f4e35c0556b77d4cc9973fcc24b0a358a5781b1234582/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:4222ff8f76919ecf6c716175a0e5fddb5599faeed4c56d9ea41a2c42be4998b2", size = 44458539, upload-time = "2026-01-18T16:13:52.975Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d9/369f134d652b21db62fe3ec1c5c2357e695f79eb67394b8a93f3a2b2cffa/pyarrow-23.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:87f06159cbe38125852657716889296c83c37b4d09a5e58f3d10245fd1f69795", size = 47535889, upload-time = "2026-01-18T16:14:03.693Z" }, - { url = "https://files.pythonhosted.org/packages/a3/95/f37b6a252fdbf247a67a78fb3f61a529fe0600e304c4d07741763d3522b1/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1675c374570d8b91ea6d4edd4608fa55951acd44e0c31bd146e091b4005de24f", size = 48157777, upload-time = "2026-01-18T16:14:12.483Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/fb94923108c9c6415dab677cf1f066d3307798eafc03f9a65ab4abc61056/pyarrow-23.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:247374428fde4f668f138b04031a7e7077ba5fa0b5b1722fdf89a017bf0b7ee0", size = 50580441, upload-time = "2026-01-18T16:14:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/ae/78/897ba6337b517fc8e914891e1bd918da1c4eb8e936a553e95862e67b80f6/pyarrow-23.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:de53b1bd3b88a2ee93c9af412c903e57e738c083be4f6392288294513cd8b2c1", size = 27530028, upload-time = "2026-01-18T16:14:27.353Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c0/57fe251102ca834fee0ef69a84ad33cc0ff9d5dfc50f50b466846356ecd7/pyarrow-23.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5574d541923efcbfdf1294a2746ae3b8c2498a2dc6cd477882f6f4e7b1ac08d3", size = 34276762, upload-time = "2026-01-18T16:14:34.128Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4e/24130286548a5bc250cbed0b6bbf289a2775378a6e0e6f086ae8c68fc098/pyarrow-23.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:2ef0075c2488932e9d3c2eb3482f9459c4be629aa673b725d5e3cf18f777f8e4", size = 35821420, upload-time = "2026-01-18T16:14:40.699Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/a869e8529d487aa2e842d6c8865eb1e2c9ec33ce2786eb91104d2c3e3f10/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:65666fc269669af1ef1c14478c52222a2aa5c907f28b68fb50a203c777e4f60c", size = 44457412, upload-time = "2026-01-18T16:14:49.051Z" }, - { url = "https://files.pythonhosted.org/packages/36/81/1de4f0edfa9a483bbdf0082a05790bd6a20ed2169ea12a65039753be3a01/pyarrow-23.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:4d85cb6177198f3812db4788e394b757223f60d9a9f5ad6634b3e32be1525803", size = 47534285, upload-time = "2026-01-18T16:14:56.748Z" }, - { url = "https://files.pythonhosted.org/packages/f2/04/464a052d673b5ece074518f27377861662449f3c1fdb39ce740d646fd098/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1a9ff6fa4141c24a03a1a434c63c8fa97ce70f8f36bccabc18ebba905ddf0f17", size = 48157913, upload-time = "2026-01-18T16:15:05.114Z" }, - { url = "https://files.pythonhosted.org/packages/f4/1b/32a4de9856ee6688c670ca2def588382e573cce45241a965af04c2f61687/pyarrow-23.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:84839d060a54ae734eb60a756aeacb62885244aaa282f3c968f5972ecc7b1ecc", size = 50582529, upload-time = "2026-01-18T16:15:12.846Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/d6581f03e9b9e44ea60b52d1750ee1a7678c484c06f939f45365a45f7eef/pyarrow-23.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:a149a647dbfe928ce8830a713612aa0b16e22c64feac9d1761529778e4d4eaa5", size = 27542646, upload-time = "2026-01-18T16:15:18.89Z" }, - { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" }, - { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" }, - { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" }, - { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" }, - { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" }, - { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" }, - { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" }, - { url = "https://files.pythonhosted.org/packages/66/34/564db447d083ec7ff93e0a883a597d2f214e552823bfc178a2d0b1f2c257/pyarrow-23.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ad96a597547af7827342ffb3c503c8316e5043bb09b47a84885ce39394c96e00", size = 34184630, upload-time = "2026-01-18T16:16:22.141Z" }, - { url = "https://files.pythonhosted.org/packages/aa/3a/3999daebcb5e6119690c92a621c4d78eef2ffba7a0a1b56386d2875fcd77/pyarrow-23.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b9edf990df77c2901e79608f08c13fbde60202334a4fcadb15c1f57bf7afee43", size = 35796820, upload-time = "2026-01-18T16:16:29.441Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ee/39195233056c6a8d0976d7d1ac1cd4fe21fb0ec534eca76bc23ef3f60e11/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:36d1b5bc6ddcaff0083ceec7e2561ed61a51f49cce8be079ee8ed406acb6fdef", size = 44438735, upload-time = "2026-01-18T16:16:38.79Z" }, - { url = "https://files.pythonhosted.org/packages/2c/41/6a7328ee493527e7afc0c88d105ecca69a3580e29f2faaeac29308369fd7/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4292b889cd224f403304ddda8b63a36e60f92911f89927ec8d98021845ea21be", size = 47557263, upload-time = "2026-01-18T16:16:46.248Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ee/34e95b21ee84db494eae60083ddb4383477b31fb1fd19fd866d794881696/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dfd9e133e60eaa847fd80530a1b89a052f09f695d0b9c34c235ea6b2e0924cf7", size = 48153529, upload-time = "2026-01-18T16:16:53.412Z" }, - { url = "https://files.pythonhosted.org/packages/52/88/8a8d83cea30f4563efa1b7bf51d241331ee5cd1b185a7e063f5634eca415/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832141cc09fac6aab1cd3719951d23301396968de87080c57c9a7634e0ecd068", size = 50598851, upload-time = "2026-01-18T16:17:01.133Z" }, - { url = "https://files.pythonhosted.org/packages/c6/4c/2929c4be88723ba025e7b3453047dc67e491c9422965c141d24bab6b5962/pyarrow-23.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:7a7d067c9a88faca655c71bcc30ee2782038d59c802d57950826a07f60d83c4c", size = 27577747, upload-time = "2026-01-18T16:18:02.413Z" }, - { url = "https://files.pythonhosted.org/packages/64/52/564a61b0b82d72bd68ec3aef1adda1e3eba776f89134b9ebcb5af4b13cb6/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ce9486e0535a843cf85d990e2ec5820a47918235183a5c7b8b97ed7e92c2d47d", size = 34446038, upload-time = "2026-01-18T16:17:07.861Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/232d4f9855fd1de0067c8a7808a363230d223c83aeee75e0fe6eab851ba9/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:075c29aeaa685fd1182992a9ed2499c66f084ee54eea47da3eb76e125e06064c", size = 35921142, upload-time = "2026-01-18T16:17:15.401Z" }, - { url = "https://files.pythonhosted.org/packages/96/f2/60af606a3748367b906bb82d41f0032e059f075444445d47e32a7ff1df62/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:799965a5379589510d888be3094c2296efd186a17ca1cef5b77703d4d5121f53", size = 44490374, upload-time = "2026-01-18T16:17:23.93Z" }, - { url = "https://files.pythonhosted.org/packages/ff/2d/7731543050a678ea3a413955a2d5d80d2a642f270aa57a3cb7d5a86e3f46/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef7cac8fe6fccd8b9e7617bfac785b0371a7fe26af59463074e4882747145d40", size = 47527896, upload-time = "2026-01-18T16:17:33.393Z" }, - { url = "https://files.pythonhosted.org/packages/5a/90/f3342553b7ac9879413aed46500f1637296f3c8222107523a43a1c08b42a/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15a414f710dc927132dd67c361f78c194447479555af57317066ee5116b90e9e", size = 48210401, upload-time = "2026-01-18T16:17:42.012Z" }, - { url = "https://files.pythonhosted.org/packages/f3/da/9862ade205ecc46c172b6ce5038a74b5151c7401e36255f15975a45878b2/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e0d2e6915eca7d786be6a77bf227fbc06d825a75b5b5fe9bcbef121dec32685", size = 50579677, upload-time = "2026-01-18T16:17:50.241Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4c/f11f371f5d4740a5dafc2e11c76bcf42d03dfdb2d68696da97de420b6963/pyarrow-23.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4b317ea6e800b5704e5e5929acb6e2dc13e9276b708ea97a39eb8b345aa2658b", size = 27631889, upload-time = "2026-01-18T16:17:56.55Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/15aec78bcf43a0c004067bd33eb5352836a29a49db8581fc56f2b6ca88b7/pyarrow-23.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:20b187ed9550d233a872074159f765f52f9d92973191cd4b93f293a19efbe377", size = 34213265, upload-time = "2026-01-18T16:18:07.904Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/deb2c594bbba41c37c5d9aa82f510376998352aa69dfcb886cb4b18ad80f/pyarrow-23.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:18ec84e839b493c3886b9b5e06861962ab4adfaeb79b81c76afbd8d84c7d5fda", size = 35819211, upload-time = "2026-01-18T16:18:13.94Z" }, - { url = "https://files.pythonhosted.org/packages/e0/e5/ee82af693cb7b5b2b74f6524cdfede0e6ace779d7720ebca24d68b57c36b/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e438dd3f33894e34fd02b26bd12a32d30d006f5852315f611aa4add6c7fab4bc", size = 44502313, upload-time = "2026-01-18T16:18:20.367Z" }, - { url = "https://files.pythonhosted.org/packages/9c/86/95c61ad82236495f3c31987e85135926ba3ec7f3819296b70a68d8066b49/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a244279f240c81f135631be91146d7fa0e9e840e1dfed2aba8483eba25cd98e6", size = 47585886, upload-time = "2026-01-18T16:18:27.544Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6e/a72d901f305201802f016d015de1e05def7706fff68a1dedefef5dc7eff7/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c4692e83e42438dba512a570c6eaa42be2f8b6c0f492aea27dec54bdc495103a", size = 48207055, upload-time = "2026-01-18T16:18:35.425Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/5de029c537630ca18828db45c30e2a78da03675a70ac6c3528203c416fe3/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f30f898dfe44ea69654a35c93e8da4cef6606dc4c72394068fd95f8e9f54a", size = 50619812, upload-time = "2026-01-18T16:18:43.553Z" }, - { url = "https://files.pythonhosted.org/packages/59/8d/2af846cd2412e67a087f5bda4a8e23dfd4ebd570f777db2e8686615dafc1/pyarrow-23.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:5b86bb649e4112fb0614294b7d0a175c7513738876b89655605ebb87c804f861", size = 28263851, upload-time = "2026-01-18T16:19:38.567Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7f/caab863e587041156f6786c52e64151b7386742c8c27140f637176e9230e/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ebc017d765d71d80a3f8584ca0566b53e40464586585ac64176115baa0ada7d3", size = 34463240, upload-time = "2026-01-18T16:18:49.755Z" }, - { url = "https://files.pythonhosted.org/packages/c9/fa/3a5b8c86c958e83622b40865e11af0857c48ec763c11d472c87cd518283d/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:0800cc58a6d17d159df823f87ad66cefebf105b982493d4bad03ee7fab84b993", size = 35935712, upload-time = "2026-01-18T16:18:55.626Z" }, - { url = "https://files.pythonhosted.org/packages/c5/08/17a62078fc1a53decb34a9aa79cf9009efc74d63d2422e5ade9fed2f99e3/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a7c68c722da9bb5b0f8c10e3eae71d9825a4b429b40b32709df5d1fa55beb3d", size = 44503523, upload-time = "2026-01-18T16:19:03.958Z" }, - { url = "https://files.pythonhosted.org/packages/cc/70/84d45c74341e798aae0323d33b7c39194e23b1abc439ceaf60a68a7a969a/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:bd5556c24622df90551063ea41f559b714aa63ca953db884cfb958559087a14e", size = 47542490, upload-time = "2026-01-18T16:19:11.208Z" }, - { url = "https://files.pythonhosted.org/packages/61/d9/d1274b0e6f19e235de17441e53224f4716574b2ca837022d55702f24d71d/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54810f6e6afc4ffee7c2e0051b61722fbea9a4961b46192dcfae8ea12fa09059", size = 48233605, upload-time = "2026-01-18T16:19:19.544Z" }, - { url = "https://files.pythonhosted.org/packages/39/07/e4e2d568cb57543d84482f61e510732820cddb0f47c4bb7df629abfed852/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:14de7d48052cf4b0ed174533eafa3cfe0711b8076ad70bede32cf59f744f0d7c", size = 50603979, upload-time = "2026-01-18T16:19:26.717Z" }, - { url = "https://files.pythonhosted.org/packages/72/9c/47693463894b610f8439b2e970b82ef81e9599c757bf2049365e40ff963c/pyarrow-23.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:427deac1f535830a744a4f04a6ac183a64fcac4341b3f618e693c41b7b98d2b0", size = 28338905, upload-time = "2026-01-18T16:19:32.93Z" }, -] - -[[package]] -name = "pyaudio" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/1d/8878c7752febb0f6716a7e1a52cb92ac98871c5aa522cba181878091607c/PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87", size = 47066, upload-time = "2023-11-07T07:11:48.806Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/90/1553487277e6aa25c0b7c2c38709cdd2b49e11c66c0b25c6e8b7b6638c72/PyAudio-0.2.14-cp310-cp310-win32.whl", hash = "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61", size = 144624, upload-time = "2023-11-07T07:11:33.599Z" }, - { url = "https://files.pythonhosted.org/packages/27/bc/719d140ee63cf4b0725016531d36743a797ffdbab85e8536922902c9349a/PyAudio-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83", size = 164069, upload-time = "2023-11-07T07:11:35.439Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f0/b0eab89eafa70a86b7b566a4df2f94c7880a2d483aa8de1c77d335335b5b/PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289", size = 144624, upload-time = "2023-11-07T07:11:36.94Z" }, - { url = "https://files.pythonhosted.org/packages/82/d8/f043c854aad450a76e476b0cf9cda1956419e1dacf1062eb9df3c0055abe/PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903", size = 164070, upload-time = "2023-11-07T07:11:38.579Z" }, - { url = "https://files.pythonhosted.org/packages/8d/45/8d2b76e8f6db783f9326c1305f3f816d4a12c8eda5edc6a2e1d03c097c3b/PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b", size = 144750, upload-time = "2023-11-07T07:11:40.142Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6a/d25812e5f79f06285767ec607b39149d02aa3b31d50c2269768f48768930/PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3", size = 164126, upload-time = "2023-11-07T07:11:41.539Z" }, - { url = "https://files.pythonhosted.org/packages/3a/77/66cd37111a87c1589b63524f3d3c848011d21ca97828422c7fde7665ff0d/PyAudio-0.2.14-cp313-cp313-win32.whl", hash = "sha256:95328285b4dab57ea8c52a4a996cb52be6d629353315be5bfda403d15932a497", size = 150982, upload-time = "2024-11-20T19:12:12.404Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8b/7f9a061c1cc2b230f9ac02a6003fcd14c85ce1828013aecbaf45aa988d20/PyAudio-0.2.14-cp313-cp313-win_amd64.whl", hash = "sha256:692d8c1446f52ed2662120bcd9ddcb5aa2b71f38bda31e58b19fb4672fffba69", size = 173655, upload-time = "2024-11-20T19:12:13.616Z" }, -] - -[[package]] -name = "pybase64" -version = "1.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/47/16d7af6fae7803f4c691856bc0d8d433ccf30e106432e2ef7707ee19a38a/pybase64-1.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f63aa7f29139b8a05ce5f97cdb7fad63d29071e5bdc8a638a343311fe996112a", size = 38241, upload-time = "2025-12-06T13:22:27.396Z" }, - { url = "https://files.pythonhosted.org/packages/4d/3e/268beb8d2240ab55396af4d1b45d2494935982212549b92a5f5b57079bd3/pybase64-1.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5943ec1ae87a8b4fe310905bb57205ea4330c75e2c628433a7d9dd52295b588", size = 31672, upload-time = "2025-12-06T13:22:28.854Z" }, - { url = "https://files.pythonhosted.org/packages/80/14/4365fa33222edcc46b6db4973f9e22bda82adfb6ab2a01afff591f1e41c8/pybase64-1.4.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5f2b8aef86f35cd5894c13681faf433a1fffc5b2e76544dcb5416a514a1a8347", size = 65978, upload-time = "2025-12-06T13:22:30.191Z" }, - { url = "https://files.pythonhosted.org/packages/1c/22/e89739d8bc9b96c68ead44b4eec42fe555683d9997e4ba65216d384920fc/pybase64-1.4.3-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6ec7e53dd09b0a8116ccf5c3265c7c7fce13c980747525be76902aef36a514a", size = 68903, upload-time = "2025-12-06T13:22:31.29Z" }, - { url = "https://files.pythonhosted.org/packages/77/e1/7e59a19f8999cdefe9eb0d56bfd701dd38263b0f6fb4a4d29fce165a1b36/pybase64-1.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7528604cd69c538e1dbaafded46e9e4915a2adcd6f2a60fcef6390d87ca922ea", size = 57516, upload-time = "2025-12-06T13:22:32.395Z" }, - { url = "https://files.pythonhosted.org/packages/42/ad/f47dc7e6fe32022b176868b88b671a32dab389718c8ca905cab79280aaaf/pybase64-1.4.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:4ec645f32b50593879031e09158f8681a1db9f5df0f72af86b3969a1c5d1fa2b", size = 54533, upload-time = "2025-12-06T13:22:33.457Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/7ab312b5a324833953b00e47b23eb4f83d45bd5c5c854b4b4e51b2a0cf5b/pybase64-1.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:634a000c5b3485ccc18bb9b244e0124f74b6fbc7f43eade815170237a7b34c64", size = 57187, upload-time = "2025-12-06T13:22:34.566Z" }, - { url = "https://files.pythonhosted.org/packages/2c/84/80acab1fcbaaae103e6b862ef5019192c8f2cd8758433595a202179a0d1d/pybase64-1.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:309ea32ad07639a485580af1be0ad447a434deb1924e76adced63ac2319cfe15", size = 57730, upload-time = "2025-12-06T13:22:35.581Z" }, - { url = "https://files.pythonhosted.org/packages/1f/24/84256d472400ea3163d7d69c44bb7e2e1027f0f1d4d20c47629a7dc4578e/pybase64-1.4.3-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:d10d517566b748d3f25f6ac7162af779360c1c6426ad5f962927ee205990d27c", size = 53036, upload-time = "2025-12-06T13:22:36.621Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0f/33aecbed312ee0431798a73fa25e00dedbffdd91389ee23121fed397c550/pybase64-1.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74cc0f4d835400857cc5c6d27ec854f7949491e07a04e6d66e2137812831f4c", size = 56321, upload-time = "2025-12-06T13:22:37.7Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1c/a341b050746658cbec8cab3c733aeb3ef52ce8f11e60d0d47adbdf729ebf/pybase64-1.4.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1b591d774ac09d5eb73c156a03277cb271438fbd8042bae4109ff3a827cd218c", size = 50114, upload-time = "2025-12-06T13:22:38.752Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d3/f7e6680ae6dc4ddff39112ad66e0fa6b2ec346e73881bafc08498c560bc0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5eb588d35a04302ef6157d17db62354a787ac6f8b1585dd0b90c33d63a97a550", size = 66570, upload-time = "2025-12-06T13:22:40.221Z" }, - { url = "https://files.pythonhosted.org/packages/4c/71/774748eecc7fe23869b7e5df028e3c4c2efa16b506b83ea3fa035ea95dc2/pybase64-1.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df8b122d5be2c96962231cc4831d9c2e1eae6736fb12850cec4356d8b06fe6f8", size = 55700, upload-time = "2025-12-06T13:22:41.289Z" }, - { url = "https://files.pythonhosted.org/packages/b3/91/dd15075bb2fe0086193e1cd4bad80a43652c38d8a572f9218d46ba721802/pybase64-1.4.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:31b7a85c661fc591bbcce82fb8adaebe2941e6a83b08444b0957b77380452a4b", size = 52491, upload-time = "2025-12-06T13:22:42.628Z" }, - { url = "https://files.pythonhosted.org/packages/7b/27/f357d63ea3774c937fc47160e040419ed528827aa3d4306d5ec9826259c0/pybase64-1.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e6d7beaae65979fef250e25e66cf81c68a8f81910bcda1a2f43297ab486a7e4e", size = 53957, upload-time = "2025-12-06T13:22:44.615Z" }, - { url = "https://files.pythonhosted.org/packages/b3/c3/243693771701a54e67ff5ccbf4c038344f429613f5643169a7befc51f007/pybase64-1.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4a6276bc3a3962d172a2b5aba544d89881c4037ea954517b86b00892c703d007", size = 68422, upload-time = "2025-12-06T13:22:45.641Z" }, - { url = "https://files.pythonhosted.org/packages/75/95/f987081bf6bc1d1eda3012dae1b06ad427732ef9933a632cb8b58f9917f8/pybase64-1.4.3-cp310-cp310-win32.whl", hash = "sha256:4bdd07ef017515204ee6eaab17e1ad05f83c0ccb5af8ae24a0fe6d9cb5bb0b7a", size = 33622, upload-time = "2025-12-06T13:22:47.348Z" }, - { url = "https://files.pythonhosted.org/packages/79/28/c169a769fe90128f16d394aad87b2096dd4bf2f035ae0927108a46b617df/pybase64-1.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:5db0b6bbda15110db2740c61970a8fda3bf9c93c3166a3f57f87c7865ed1125c", size = 35799, upload-time = "2025-12-06T13:22:48.731Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f2/bdbe6af0bd4f3fe5bc70e77ead7f7d523bb9d3ca3ad50ac42b9adbb9ca14/pybase64-1.4.3-cp310-cp310-win_arm64.whl", hash = "sha256:f96367dfc82598569aa02b1103ebd419298293e59e1151abda2b41728703284b", size = 31158, upload-time = "2025-12-06T13:22:50.021Z" }, - { url = "https://files.pythonhosted.org/packages/2b/63/21e981e9d3f1f123e0b0ee2130112b1956cad9752309f574862c7ae77c08/pybase64-1.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:70b0d4a4d54e216ce42c2655315378b8903933ecfa32fced453989a92b4317b2", size = 38237, upload-time = "2025-12-06T13:22:52.159Z" }, - { url = "https://files.pythonhosted.org/packages/92/fb/3f448e139516404d2a3963915cc10dc9dde7d3a67de4edba2f827adfef17/pybase64-1.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8127f110cdee7a70e576c5c9c1d4e17e92e76c191869085efbc50419f4ae3c72", size = 31673, upload-time = "2025-12-06T13:22:53.241Z" }, - { url = "https://files.pythonhosted.org/packages/3c/fb/bb06a5b9885e7d853ac1e801c4d8abfdb4c8506deee33e53d55aa6690e67/pybase64-1.4.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f9ef0388878bc15a084bd9bf73ec1b2b4ee513d11009b1506375e10a7aae5032", size = 68331, upload-time = "2025-12-06T13:22:54.197Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/8d60b9ec5e658185fc2ee3333e01a6e30d717cf677b24f47cbb3a859d13c/pybase64-1.4.3-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95a57cccf106352a72ed8bc8198f6820b16cc7d55aa3867a16dea7011ae7c218", size = 71370, upload-time = "2025-12-06T13:22:55.517Z" }, - { url = "https://files.pythonhosted.org/packages/ac/29/a3e5c1667cc8c38d025a4636855de0fc117fc62e2afeb033a3c6f12c6a22/pybase64-1.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cd1c47dfceb9c7bd3de210fb4e65904053ed2d7c9dce6d107f041ff6fbd7e21", size = 59834, upload-time = "2025-12-06T13:22:56.682Z" }, - { url = "https://files.pythonhosted.org/packages/a9/00/8ffcf9810bd23f3984698be161cf7edba656fd639b818039a7be1d6405d4/pybase64-1.4.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9fe9922698f3e2f72874b26890d53a051c431d942701bb3a37aae94da0b12107", size = 56652, upload-time = "2025-12-06T13:22:57.724Z" }, - { url = "https://files.pythonhosted.org/packages/81/62/379e347797cdea4ab686375945bc77ad8d039c688c0d4d0cfb09d247beb9/pybase64-1.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:af5f4bd29c86b59bb4375e0491d16ec8a67548fa99c54763aaedaf0b4b5a6632", size = 59382, upload-time = "2025-12-06T13:22:58.758Z" }, - { url = "https://files.pythonhosted.org/packages/c6/f2/9338ffe2f487086f26a2c8ca175acb3baa86fce0a756ff5670a0822bb877/pybase64-1.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c302f6ca7465262908131411226e02100f488f531bb5e64cb901aa3f439bccd9", size = 59990, upload-time = "2025-12-06T13:23:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/f9/a4/85a6142b65b4df8625b337727aa81dc199642de3d09677804141df6ee312/pybase64-1.4.3-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2f3f439fa4d7fde164ebbbb41968db7d66b064450ab6017c6c95cef0afa2b349", size = 54923, upload-time = "2025-12-06T13:23:02.369Z" }, - { url = "https://files.pythonhosted.org/packages/ac/00/e40215d25624012bf5b7416ca37f168cb75f6dd15acdb91ea1f2ea4dc4e7/pybase64-1.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a23c6866551043f8b681a5e1e0d59469148b2920a3b4fc42b1275f25ea4217a", size = 58664, upload-time = "2025-12-06T13:23:03.378Z" }, - { url = "https://files.pythonhosted.org/packages/b0/73/d7e19a63e795c13837f2356268d95dc79d1180e756f57ced742a1e52fdeb/pybase64-1.4.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:56e6526f8565642abc5f84338cc131ce298a8ccab696b19bdf76fa6d7dc592ef", size = 52338, upload-time = "2025-12-06T13:23:04.458Z" }, - { url = "https://files.pythonhosted.org/packages/f2/32/3c746d7a310b69bdd9df77ffc85c41b80bce00a774717596f869b0d4a20e/pybase64-1.4.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6a792a8b9d866ffa413c9687d9b611553203753987a3a582d68cbc51cf23da45", size = 68993, upload-time = "2025-12-06T13:23:05.526Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b3/63cec68f9d6f6e4c0b438d14e5f1ef536a5fe63ce14b70733ac5e31d7ab8/pybase64-1.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:62ad29a5026bb22cfcd1ca484ec34b0a5ced56ddba38ceecd9359b2818c9c4f9", size = 58055, upload-time = "2025-12-06T13:23:06.931Z" }, - { url = "https://files.pythonhosted.org/packages/d5/cb/7acf7c3c06f9692093c07f109668725dc37fb9a3df0fa912b50add645195/pybase64-1.4.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11b9d1d2d32ec358c02214363b8fc3651f6be7dd84d880ecd597a6206a80e121", size = 54430, upload-time = "2025-12-06T13:23:07.936Z" }, - { url = "https://files.pythonhosted.org/packages/33/39/4eb33ff35d173bfff4002e184ce8907f5d0a42d958d61cd9058ef3570179/pybase64-1.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0aebaa7f238caa0a0d373616016e2040c6c879ebce3ba7ab3c59029920f13640", size = 56272, upload-time = "2025-12-06T13:23:09.253Z" }, - { url = "https://files.pythonhosted.org/packages/19/97/a76d65c375a254e65b730c6f56bf528feca91305da32eceab8bcc08591e6/pybase64-1.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e504682b20c63c2b0c000e5f98a80ea867f8d97642e042a5a39818e44ba4d599", size = 70904, upload-time = "2025-12-06T13:23:10.336Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2c/8338b6d3da3c265002839e92af0a80d6db88385c313c73f103dfb800c857/pybase64-1.4.3-cp311-cp311-win32.whl", hash = "sha256:e9a8b81984e3c6fb1db9e1614341b0a2d98c0033d693d90c726677db1ffa3a4c", size = 33639, upload-time = "2025-12-06T13:23:11.9Z" }, - { url = "https://files.pythonhosted.org/packages/39/dc/32efdf2f5927e5449cc341c266a1bbc5fecd5319a8807d9c5405f76e6d02/pybase64-1.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:a90a8fa16a901fabf20de824d7acce07586e6127dc2333f1de05f73b1f848319", size = 35797, upload-time = "2025-12-06T13:23:13.174Z" }, - { url = "https://files.pythonhosted.org/packages/da/59/eda4f9cb0cbce5a45f0cd06131e710674f8123a4d570772c5b9694f88559/pybase64-1.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:61d87de5bc94d143622e94390ec3e11b9c1d4644fe9be3a81068ab0f91056f59", size = 31160, upload-time = "2025-12-06T13:23:15.696Z" }, - { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, - { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, - { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, - { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, - { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, - { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, - { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, - { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, - { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, - { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, - { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, - { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, - { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, - { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, - { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, - { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, - { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, - { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, - { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, - { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, - { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, - { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, - { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, - { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, - { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, - { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, - { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, - { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, - { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, - { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, - { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, - { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, - { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, - { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, - { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, - { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, - { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, - { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, - { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, - { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, - { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, - { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, - { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, - { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, - { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, - { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, - { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, - { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, - { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, - { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, - { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, - { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, - { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, - { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, - { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, - { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, - { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, - { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, - { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, - { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, - { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, - { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, - { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, - { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, - { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, - { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, - { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, - { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, - { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, - { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, - { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, - { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, - { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, - { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, - { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, - { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, - { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7c/545fd4935a0e1ddd7147f557bf8157c73eecec9cffd523382fa7af2557de/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_10_9_x86_64.whl", hash = "sha256:d27c1dfdb0c59a5e758e7a98bd78eaca5983c22f4a811a36f4f980d245df4611", size = 38393, upload-time = "2025-12-06T13:26:19.535Z" }, - { url = "https://files.pythonhosted.org/packages/c3/ca/ae7a96be9ddc96030d4e9dffc43635d4e136b12058b387fd47eb8301b60f/pybase64-1.4.3-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0f1a0c51d6f159511e3431b73c25db31095ee36c394e26a4349e067c62f434e5", size = 32109, upload-time = "2025-12-06T13:26:20.72Z" }, - { url = "https://files.pythonhosted.org/packages/bf/44/d4b7adc7bf4fd5b52d8d099121760c450a52c390223806b873f0b6a2d551/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a492518f3078a4e3faaef310697d21df9c6bc71908cebc8c2f6fbfa16d7d6b1f", size = 43227, upload-time = "2025-12-06T13:26:21.845Z" }, - { url = "https://files.pythonhosted.org/packages/08/86/2ba2d8734ef7939debeb52cf9952e457ba7aa226cae5c0e6dd631f9b851f/pybase64-1.4.3-graalpy311-graalpy242_311_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae1a0f47784fd16df90d8acc32011c8d5fcdd9ab392c9ec49543e5f6a9c43a4", size = 35804, upload-time = "2025-12-06T13:26:23.149Z" }, - { url = "https://files.pythonhosted.org/packages/4f/5b/19c725dc3aaa6281f2ce3ea4c1628d154a40dd99657d1381995f8096768b/pybase64-1.4.3-graalpy311-graalpy242_311_native-win_amd64.whl", hash = "sha256:03cea70676ffbd39a1ab7930a2d24c625b416cacc9d401599b1d29415a43ab6a", size = 35880, upload-time = "2025-12-06T13:26:24.663Z" }, - { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, - { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, - { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, - { url = "https://files.pythonhosted.org/packages/2a/cf/6e712491bd665ea8633efb0b484121893ea838d8e830e06f39f2aae37e58/pybase64-1.4.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94cf50c36bb2f8618982ee5a978c4beed9db97d35944fa96e8586dd953c7994a", size = 38007, upload-time = "2025-12-06T13:26:32.804Z" }, - { url = "https://files.pythonhosted.org/packages/38/c0/9272cae1c49176337dcdbd97511e2843faae1aaf5a5fb48569093c6cd4ce/pybase64-1.4.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:01bc3ff5ca1341685c6d2d945b035f442f7b9c3b068a5c6ee8408a41fda5754e", size = 31538, upload-time = "2025-12-06T13:26:34.001Z" }, - { url = "https://files.pythonhosted.org/packages/20/f2/17546f97befe429c73f622bbd869ceebb518c40fdb0dec4c4f98312e80a5/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:03d0aa3761a99034960496280c02aa063f856a3cc9b33771bc4eab0e4e72b5c2", size = 40682, upload-time = "2025-12-06T13:26:35.168Z" }, - { url = "https://files.pythonhosted.org/packages/92/a0/464b36d5dfb61f3da17858afaeaa876a9342d58e9f17803ce7f28b5de9e8/pybase64-1.4.3-pp310-pypy310_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ca5b1ce768520acd6440280cdab35235b27ad2faacfcec064bc9c3377066ef1", size = 41306, upload-time = "2025-12-06T13:26:36.351Z" }, - { url = "https://files.pythonhosted.org/packages/07/c9/a748dfc0969a8d960ecf1e82c8a2a16046ffec22f8e7ece582aa3b1c6cf9/pybase64-1.4.3-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3caa1e2ddad1c50553ffaaa1c86b74b3f9fbd505bea9970326ab88fc68c4c184", size = 35452, upload-time = "2025-12-06T13:26:37.772Z" }, - { url = "https://files.pythonhosted.org/packages/95/b7/4d37bd3577d1aa6c732dc099087fe027c48873e223de3784b095e5653f8b/pybase64-1.4.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bd47076f736b27a8b0f9b30d93b6bb4f5af01b0dc8971f883ed3b75934f39a99", size = 36125, upload-time = "2025-12-06T13:26:39.78Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/160dded493c00d3376d4ad0f38a2119c5345de4a6693419ad39c3565959b/pybase64-1.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:277de6e03cc9090fb359365c686a2a3036d23aee6cd20d45d22b8c89d1247f17", size = 37939, upload-time = "2025-12-06T13:26:41.014Z" }, - { url = "https://files.pythonhosted.org/packages/b7/b8/a0f10be8d648d6f8f26e560d6e6955efa7df0ff1e009155717454d76f601/pybase64-1.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab1dd8b1ed2d1d750260ed58ab40defaa5ba83f76a30e18b9ebd5646f6247ae5", size = 31466, upload-time = "2025-12-06T13:26:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/d3/22/832a2f9e76cdf39b52e01e40d8feeb6a04cf105494f2c3e3126d0149717f/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:bd4d2293de9fd212e294c136cec85892460b17d24e8c18a6ba18750928037750", size = 40681, upload-time = "2025-12-06T13:26:43.782Z" }, - { url = "https://files.pythonhosted.org/packages/12/d7/6610f34a8972415fab3bb4704c174a1cc477bffbc3c36e526428d0f3957d/pybase64-1.4.3-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2af6d0d3a691911cc4c9a625f3ddcd3af720738c21be3d5c72de05629139d393", size = 41294, upload-time = "2025-12-06T13:26:44.936Z" }, - { url = "https://files.pythonhosted.org/packages/64/25/ed24400948a6c974ab1374a233cb7e8af0a5373cea0dd8a944627d17c34a/pybase64-1.4.3-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5cfc8c49a28322d82242088378f8542ce97459866ba73150b062a7073e82629d", size = 35447, upload-time = "2025-12-06T13:26:46.098Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2b/e18ee7c5ee508a82897f021c1981533eca2940b5f072fc6ed0906c03a7a7/pybase64-1.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:debf737e09b8bf832ba86f5ecc3d3dbd0e3021d6cd86ba4abe962d6a5a77adb3", size = 36134, upload-time = "2025-12-06T13:26:47.35Z" }, -] - -[[package]] -name = "pycodestyle" -version = "2.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232, upload-time = "2024-08-04T20:26:54.576Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284, upload-time = "2024-08-04T20:26:53.173Z" }, -] - -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, - { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, - { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, - { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, - { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, - { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, - { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, - { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, - { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, - { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, - { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, - { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, - { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, - { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, - { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, - { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, - { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[package]] -name = "pydocstyle" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "snowballstemmer" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/d5385ca59fd065e3c6a5fe19f9bc9d5ea7f2509fa8c9c22fb6b2031dd953/pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1", size = 36796, upload-time = "2023-01-17T20:29:19.838Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/ea/99ddefac41971acad68f14114f38261c1f27dac0b3ec529824ebc739bdaa/pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019", size = 38038, upload-time = "2023-01-17T20:29:18.094Z" }, -] - -[[package]] -name = "pydot" -version = "4.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyparsing", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/32/a7125fb28c4261a627f999d5fb4afff25b523800faed2c30979949d6facd/pydot-4.0.1-py3-none-any.whl", hash = "sha256:869c0efadd2708c0be1f916eb669f3d664ca684bc57ffb7ecc08e70d5e93fee6", size = 37087, upload-time = "2025-06-17T20:09:55.25Z" }, -] - -[[package]] -name = "pydub" -version = "0.25.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, -] - -[[package]] -name = "pyee" -version = "13.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/95/03/1fd98d5841cd7964a27d729ccf2199602fe05eb7a405c1462eb7277945ed/pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37", size = 31250, upload-time = "2025-03-17T18:53:15.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/4d/b9add7c84060d4c1906abe9a7e5359f2a60f7a9a4f67268b2766673427d8/pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", size = 15730, upload-time = "2025-03-17T18:53:14.532Z" }, -] - -[[package]] -name = "pyflakes" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" }, -] - -[[package]] -name = "pygame" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/cc/08bba60f00541f62aaa252ce0cfbd60aebd04616c0b9574f755b583e45ae/pygame-2.6.1.tar.gz", hash = "sha256:56fb02ead529cee00d415c3e007f75e0780c655909aaa8e8bf616ee09c9feb1f", size = 14808125, upload-time = "2024-09-29T13:41:34.698Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/0b/334c7c50a2979e15f2a027a41d1ca78ee730d5b1c7f7f4b26d7cb899839d/pygame-2.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9beeb647e555afb5657111fa83acb74b99ad88761108eaea66472e8b8547b55b", size = 13109297, upload-time = "2024-09-29T14:25:34.709Z" }, - { url = "https://files.pythonhosted.org/packages/dc/48/f8b1069788d1bd42e63a960d74d3355242480b750173a42b2749687578ca/pygame-2.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10e3d2a55f001f6c0a6eb44aa79ea7607091c9352b946692acedb2ac1482f1c9", size = 12375837, upload-time = "2024-09-29T14:25:50.538Z" }, - { url = "https://files.pythonhosted.org/packages/bc/33/a1310386b8913ce1bdb90c33fa536970e299ad57eb35785f1d71ea1e2ad3/pygame-2.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:816e85000c5d8b02a42b9834f761a5925ef3377d2924e3a7c4c143d2990ce5b8", size = 13607860, upload-time = "2024-09-29T11:10:44.173Z" }, - { url = "https://files.pythonhosted.org/packages/88/0f/4e37b115056e43714e7550054dd3cd7f4d552da54d7fc58a2fb1407acda5/pygame-2.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a78fd030d98faab4a8e27878536fdff7518d3e062a72761c552f624ebba5a5f", size = 14304696, upload-time = "2024-09-29T11:39:46.724Z" }, - { url = "https://files.pythonhosted.org/packages/11/b3/de6ed93ae483cf3bac8f950a955e83f7ffe59651fd804d100fff65d66d6c/pygame-2.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da3ad64d685f84a34ebe5daacb39fff14f1251acb34c098d760d63fee768f50c", size = 13977684, upload-time = "2024-09-29T11:39:49.921Z" }, - { url = "https://files.pythonhosted.org/packages/d3/05/d86440aa879708c41844bafc6b3eb42c6d8cf54082482499b53139133e2a/pygame-2.6.1-cp310-cp310-win32.whl", hash = "sha256:9dd5c054d4bd875a8caf978b82672f02bec332f52a833a76899220c460bb4b58", size = 10251775, upload-time = "2024-09-29T11:40:34.952Z" }, - { url = "https://files.pythonhosted.org/packages/38/88/8de61324775cf2c844a51d8db14a8a6d2a9092312f27678f6eaa3a460376/pygame-2.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:00827aba089355925902d533f9c41e79a799641f03746c50a374dc5c3362e43d", size = 10618801, upload-time = "2024-09-29T12:13:25.284Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ca/8f367cb9fe734c4f6f6400e045593beea2635cd736158f9fabf58ee14e3c/pygame-2.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:20349195326a5e82a16e351ed93465a7845a7e2a9af55b7bc1b2110ea3e344e1", size = 13113753, upload-time = "2024-09-29T14:26:13.751Z" }, - { url = "https://files.pythonhosted.org/packages/83/47/6edf2f890139616b3219be9cfcc8f0cb8f42eb15efd59597927e390538cb/pygame-2.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3935459109da4bb0b3901da9904f0a3e52028a3332a355d298b1673a334cf21", size = 12378146, upload-time = "2024-09-29T14:26:22.456Z" }, - { url = "https://files.pythonhosted.org/packages/00/9e/0d8aa8cf93db2d2ee38ebaf1c7b61d0df36ded27eb726221719c150c673d/pygame-2.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31dbdb5d0217f32764797d21c2752e258e5fb7e895326538d82b5f75a0cd856", size = 13611760, upload-time = "2024-09-29T11:10:47.317Z" }, - { url = "https://files.pythonhosted.org/packages/d7/9e/d06adaa5cc65876bcd7a24f59f67e07f7e4194e6298130024ed3fb22c456/pygame-2.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:173badf82fa198e6888017bea40f511cb28e69ecdd5a72b214e81e4dcd66c3b1", size = 14298054, upload-time = "2024-09-29T11:39:53.891Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/9ae2852ebd3a7cc7d9ae7ff7919ab983e4a5c1b7a14e840732f23b2b48f6/pygame-2.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce8cc108b92de9b149b344ad2e25eedbe773af0dc41dfb24d1f07f679b558c60", size = 13977107, upload-time = "2024-09-29T11:39:56.831Z" }, - { url = "https://files.pythonhosted.org/packages/31/df/6788fd2e9a864d0496a77670e44a7c012184b7a5382866ab0e60c55c0f28/pygame-2.6.1-cp311-cp311-win32.whl", hash = "sha256:811e7b925146d8149d79193652cbb83e0eca0aae66476b1cb310f0f4226b8b5c", size = 10250863, upload-time = "2024-09-29T11:44:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/d2/55/ca3eb851aeef4f6f2e98a360c201f0d00bd1ba2eb98e2c7850d80aabc526/pygame-2.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:91476902426facd4bb0dad4dc3b2573bc82c95c71b135e0daaea072ed528d299", size = 10622016, upload-time = "2024-09-29T12:17:01.545Z" }, - { url = "https://files.pythonhosted.org/packages/92/16/2c602c332f45ff9526d61f6bd764db5096ff9035433e2172e2d2cadae8db/pygame-2.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4ee7f2771f588c966fa2fa8b829be26698c9b4836f82ede5e4edc1a68594942e", size = 13118279, upload-time = "2024-09-29T14:26:30.427Z" }, - { url = "https://files.pythonhosted.org/packages/cd/53/77ccbc384b251c6e34bfd2e734c638233922449a7844e3c7a11ef91cee39/pygame-2.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c8040ea2ab18c6b255af706ec01355c8a6b08dc48d77fd4ee783f8fc46a843bf", size = 12384524, upload-time = "2024-09-29T14:26:49.996Z" }, - { url = "https://files.pythonhosted.org/packages/06/be/3ed337583f010696c3b3435e89a74fb29d0c74d0931e8f33c0a4246307a9/pygame-2.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47a6938de93fa610accd4969e638c2aebcb29b2fca518a84c3a39d91ab47116", size = 13587123, upload-time = "2024-09-29T11:10:50.072Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/b015586a450db59313535662991b34d24c1f0c0dc149cc5f496573900f4e/pygame-2.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33006f784e1c7d7e466fcb61d5489da59cc5f7eb098712f792a225df1d4e229d", size = 14275532, upload-time = "2024-09-29T11:39:59.356Z" }, - { url = "https://files.pythonhosted.org/packages/b9/f2/d31e6ad42d657af07be2ffd779190353f759a07b51232b9e1d724f2cda46/pygame-2.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1206125f14cae22c44565c9d333607f1d9f59487b1f1432945dfc809aeaa3e88", size = 13952653, upload-time = "2024-09-29T11:40:01.781Z" }, - { url = "https://files.pythonhosted.org/packages/f3/42/8ea2a6979e6fa971702fece1747e862e2256d4a8558fe0da6364dd946c53/pygame-2.6.1-cp312-cp312-win32.whl", hash = "sha256:84fc4054e25262140d09d39e094f6880d730199710829902f0d8ceae0213379e", size = 10252421, upload-time = "2024-09-29T11:14:26.877Z" }, - { url = "https://files.pythonhosted.org/packages/5f/90/7d766d54bb95939725e9a9361f9c06b0cfbe3fe100aa35400f0a461a278a/pygame-2.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:3a9e7396be0d9633831c3f8d5d82dd63ba373ad65599628294b7a4f8a5a01a65", size = 10624591, upload-time = "2024-09-29T11:52:54.489Z" }, - { url = "https://files.pythonhosted.org/packages/e1/91/718acf3e2a9d08a6ddcc96bd02a6f63c99ee7ba14afeaff2a51c987df0b9/pygame-2.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6039f3a55d800db80e8010f387557b528d34d534435e0871326804df2a62f2", size = 13090765, upload-time = "2024-09-29T14:27:02.377Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c6/9cb315de851a7682d9c7568a41ea042ee98d668cb8deadc1dafcab6116f0/pygame-2.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2a3a1288e2e9b1e5834e425bedd5ba01a3cd4902b5c2bff8ed4a740ccfe98171", size = 12381704, upload-time = "2024-09-29T14:27:10.228Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8f/617a1196e31ae3b46be6949fbaa95b8c93ce15e0544266198c2266cc1b4d/pygame-2.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27eb17e3dc9640e4b4683074f1890e2e879827447770470c2aba9f125f74510b", size = 13581091, upload-time = "2024-09-29T11:30:27.653Z" }, - { url = "https://files.pythonhosted.org/packages/3b/87/2851a564e40a2dad353f1c6e143465d445dab18a95281f9ea458b94f3608/pygame-2.6.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1623180e70a03c4a734deb9bac50fc9c82942ae84a3a220779062128e75f3b", size = 14273844, upload-time = "2024-09-29T11:40:04.138Z" }, - { url = "https://files.pythonhosted.org/packages/85/b5/aa23aa2e70bcba42c989c02e7228273c30f3b44b9b264abb93eaeff43ad7/pygame-2.6.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef07c0103d79492c21fced9ad68c11c32efa6801ca1920ebfd0f15fb46c78b1c", size = 13951197, upload-time = "2024-09-29T11:40:06.785Z" }, - { url = "https://files.pythonhosted.org/packages/a6/06/29e939b34d3f1354738c7d201c51c250ad7abefefaf6f8332d962ff67c4b/pygame-2.6.1-cp313-cp313-win32.whl", hash = "sha256:3acd8c009317190c2bfd81db681ecef47d5eb108c2151d09596d9c7ea9df5c0e", size = 10249309, upload-time = "2024-09-29T11:10:23.329Z" }, - { url = "https://files.pythonhosted.org/packages/7e/11/17f7f319ca91824b86557e9303e3b7a71991ef17fd45286bf47d7f0a38e6/pygame-2.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:813af4fba5d0b2cb8e58f5d95f7910295c34067dcc290d34f1be59c48bd1ea6a", size = 10620084, upload-time = "2024-09-29T11:48:51.587Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pylibsrtp" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/a6/6e532bec974aaecbf9fe4e12538489fb1c28456e65088a50f305aeab9f89/pylibsrtp-1.0.0.tar.gz", hash = "sha256:b39dff075b263a8ded5377f2490c60d2af452c9f06c4d061c7a2b640612b34d4", size = 10858, upload-time = "2025-10-13T16:12:31.552Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/af/89e61a62fa3567f1b7883feb4d19e19564066c2fcd41c37e08d317b51881/pylibsrtp-1.0.0-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:822c30ea9e759b333dc1f56ceac778707c51546e97eb874de98d7d378c000122", size = 1865017, upload-time = "2025-10-13T16:12:15.62Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0e/8d215484a9877adcf2459a8b28165fc89668b034565277fd55d666edd247/pylibsrtp-1.0.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:aaad74e5c8cbc1c32056c3767fea494c1e62b3aea2c908eda2a1051389fdad76", size = 2182739, upload-time = "2025-10-13T16:12:17.121Z" }, - { url = "https://files.pythonhosted.org/packages/57/3f/76a841978877ae13eac0d4af412c13bbd5d83b3df2c1f5f2175f2e0f68e5/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9209b86e662ebbd17c8a9e8549ba57eca92a3e87fb5ba8c0e27b8c43cd08a767", size = 2732922, upload-time = "2025-10-13T16:12:18.348Z" }, - { url = "https://files.pythonhosted.org/packages/0e/14/cf5d2a98a66fdfe258f6b036cda570f704a644fa861d7883a34bc359501e/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:293c9f2ac21a2bd689c477603a1aa235d85cf252160e6715f0101e42a43cbedc", size = 2434534, upload-time = "2025-10-13T16:12:20.074Z" }, - { url = "https://files.pythonhosted.org/packages/bd/08/a3f6e86c04562f7dce6717cd2206a0f84ca85c5e38121d998e0e330194c3/pylibsrtp-1.0.0-cp310-abi3-manylinux_2_28_i686.whl", hash = "sha256:81fb8879c2e522021a7cbd3f4bda1b37c192e1af939dfda3ff95b4723b329663", size = 2345818, upload-time = "2025-10-13T16:12:21.439Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d5/130c2b5b4b51df5631684069c6f0a6761c59d096a33d21503ac207cf0e47/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4ddb562e443cf2e557ea2dfaeef0d7e6b90e96dd38eb079b4ab2c8e34a79f50b", size = 2774490, upload-time = "2025-10-13T16:12:22.659Z" }, - { url = "https://files.pythonhosted.org/packages/91/e3/715a453bfee3bea92a243888ad359094a7727cc6d393f21281320fe7798c/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f02e616c9dfab2b03b32d8cc7b748f9d91814c0211086f987629a60f05f6e2cc", size = 2372603, upload-time = "2025-10-13T16:12:24.036Z" }, - { url = "https://files.pythonhosted.org/packages/e3/56/52fa74294254e1f53a4ff170ee2006e57886cf4bb3db46a02b4f09e1d99f/pylibsrtp-1.0.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c134fa09e7b80a5b7fed626230c5bc257fd771bd6978e754343e7a61d96bc7e6", size = 2451269, upload-time = "2025-10-13T16:12:25.475Z" }, - { url = "https://files.pythonhosted.org/packages/1e/51/2e9b34f484cbdd3bac999bf1f48b696d7389433e900639089e8fc4e0da0d/pylibsrtp-1.0.0-cp310-abi3-win32.whl", hash = "sha256:bae377c3b402b17b9bbfbfe2534c2edba17aa13bea4c64ce440caacbe0858b55", size = 1247503, upload-time = "2025-10-13T16:12:27.39Z" }, - { url = "https://files.pythonhosted.org/packages/c3/70/43db21af194580aba2d9a6d4c7bd8c1a6e887fa52cd810b88f89096ecad2/pylibsrtp-1.0.0-cp310-abi3-win_amd64.whl", hash = "sha256:8d6527c4a78a39a8d397f8862a8b7cdad4701ee866faf9de4ab8c70be61fd34d", size = 1601659, upload-time = "2025-10-13T16:12:29.037Z" }, - { url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" }, -] - -[[package]] -name = "pylint" -version = "4.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomlkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d2/b081da1a8930d00e3fc06352a1d449aaf815d4982319fab5d8cdb2e9ab35/pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2", size = 1571735, upload-time = "2025-11-30T13:29:04.315Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/92/d40f5d937517cc489ad848fc4414ecccc7592e4686b9071e09e64f5e378e/pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0", size = 536425, upload-time = "2025-11-30T13:29:02.53Z" }, -] - -[[package]] -name = "pymavlink" -version = "2.4.49" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastcrc" }, - { name = "lxml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/a2/0a4ce323178f60f869e42a2ed3844bead7b685807674bef966a39661606e/pymavlink-2.4.49.tar.gz", hash = "sha256:d7cf10d5592d038a18aa972711177ebb88be2143efcc258df630b0513e9da2c2", size = 6172115, upload-time = "2025-08-01T23:33:10.372Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/ab/f41e116c3753398fa773acfcd576bccceff4d1da2534ec0bfbcbf0433337/pymavlink-2.4.49-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e7c92b066ff05587dbdbc517f20eb7f98f2aa13916223fcd6daed54a8d1b7bc5", size = 6289986, upload-time = "2025-08-01T23:31:28.865Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/d61d3454884048227307f638bc91efd06832c0f273791d3b445e12d3789f/pymavlink-2.4.49-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be3607975eb7392b89a847c6810a4b5e2e3ea6935c7875dfb1289de39ccb4aea", size = 6225493, upload-time = "2025-08-01T23:31:30.773Z" }, - { url = "https://files.pythonhosted.org/packages/66/4a/fbf69e38bdd7e0b4803cb61c7ddfc9c9cc5b501843b5c822527e072192a1/pymavlink-2.4.49-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29943b071e75c4caa87fb7b1e15cdcde712d31dbd1c1babac6b45264ba1a6c97", size = 6222403, upload-time = "2025-08-01T23:31:32.804Z" }, - { url = "https://files.pythonhosted.org/packages/21/6d/a326e64e59ad7b54ca1e7b26e54ce13da7a2237865f6c446a9d442d9ffcb/pymavlink-2.4.49-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:55f0de705a985dc47b384a3aae1b1425217614c604b6defffac9f247c98c99af", size = 6336320, upload-time = "2025-08-01T23:31:34.348Z" }, - { url = "https://files.pythonhosted.org/packages/b1/26/e73f67f0a21564b68cae454b49b6536e8139bcff5fff629291a656951920/pymavlink-2.4.49-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e03ae4c4a9d3cd261a09c1dcc3b83e4d0493c9d4a8a438219f7133b1bd6d74a", size = 6348490, upload-time = "2025-08-01T23:31:35.866Z" }, - { url = "https://files.pythonhosted.org/packages/7f/60/74b123aca08a005500f2d7bddd76add7be34d62e15792c104674010cb444/pymavlink-2.4.49-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86ffb6df2e29cb5365a2ec875ec903e4cbd516523d43eaacec2e9fca97567e0d", size = 6348133, upload-time = "2025-08-01T23:31:37.119Z" }, - { url = "https://files.pythonhosted.org/packages/d8/e1/c6c8554a7e2b384672353345fbff06a066a2eb238e814978d0f414e804ce/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a74d5fc8a825d2ffe48921f5d10a053408d44608012fb19161747fa28c8b5383", size = 6341961, upload-time = "2025-08-01T23:31:40.139Z" }, - { url = "https://files.pythonhosted.org/packages/04/b9/0634eb528d57892a81a4bed81917f80fc5f60df0a14112be2045b0365a3c/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cdf11ac8ac1158fc9428cd1a2b5128b504549920b28d5dbbb052310179f501d6", size = 6339292, upload-time = "2025-08-01T23:31:41.698Z" }, - { url = "https://files.pythonhosted.org/packages/6d/12/834979e873d65332ec5a25be49245042b3bbd8b0e1ad093fcb90328b23f5/pymavlink-2.4.49-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b30bfb76ff520508297836b8d9c11787c33897cee2099188e07c54b0678db6d5", size = 6346117, upload-time = "2025-08-01T23:31:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/1f/4e/336df7081a8303036bab84cb96f520218fe2f117df77c8deea6c31d7f682/pymavlink-2.4.49-cp310-cp310-win32.whl", hash = "sha256:4269be350ecb674e90df91539aded072b2ff7153c2cf4b9fdadd9e49cd67d040", size = 6230571, upload-time = "2025-08-01T23:31:44.705Z" }, - { url = "https://files.pythonhosted.org/packages/cb/17/873c51e5966318c61ceee6c1e4631be795f3ec70e824569ba1433da67d2f/pymavlink-2.4.49-cp310-cp310-win_amd64.whl", hash = "sha256:dcf50ea5da306025dd5ddd45ed603e5f8a06c292dd22a143c9ff4292627ca338", size = 6241529, upload-time = "2025-08-01T23:31:46.256Z" }, - { url = "https://files.pythonhosted.org/packages/04/06/6503887e624b09a02d0a1203a4cedb979ab42a8fa42432760c5ba836c622/pymavlink-2.4.49-cp310-cp310-win_arm64.whl", hash = "sha256:5e4a91f6dabe4c7087ad3a6564c00992b38a336be021f093a01910efbbe2efb2", size = 6231870, upload-time = "2025-08-01T23:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/a7/44/2cf9031edf12949b400e3e1db8f29e55f91f04c2ed31e8bf36e0c6be78f7/pymavlink-2.4.49-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6d1c16ee2b913cbd9768709bb9d6716320b317f632cd588fa112e45309c62a33", size = 6289780, upload-time = "2025-08-01T23:31:49.072Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ee/2d5843485a072d0b1f6f37461ce6144c739f976e65f972082b0299dc233a/pymavlink-2.4.49-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85155f395a238764eab374edaff901cfe24ecc884c0bc1ed94818d89ab30c6b4", size = 6225400, upload-time = "2025-08-01T23:31:50.559Z" }, - { url = "https://files.pythonhosted.org/packages/b0/f3/01d67e77aa776928ff89d35de5a2a2039489a0b77a0ad8c2b1ccb4dceb9e/pymavlink-2.4.49-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf1d869a6730f9cce98779258b203ebd084ba905b34c2dc0e0507789571903c0", size = 6222298, upload-time = "2025-08-01T23:31:51.729Z" }, - { url = "https://files.pythonhosted.org/packages/4b/de/e4f25fee7948652ea9cb61500256d800bb7635d44258b4e85a8900ff4228/pymavlink-2.4.49-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0a1867635ada32078bca118dd2b521bec71276a7a5d08e6afb0077cab409cd14", size = 6359387, upload-time = "2025-08-01T23:31:53.042Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4e/2b2fbadf4f9e941fcf2141c9499c444cd003b8bb6a1ff0a52a1a4f37929f/pymavlink-2.4.49-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31b77118c7a3fa6adb9ec3e2401d26c4f2988e9c5a0b37f5a96526a4cecb81aa", size = 6368235, upload-time = "2025-08-01T23:31:54.294Z" }, - { url = "https://files.pythonhosted.org/packages/50/23/c6c9b75009433fcaa3ba40115b7ade2e0c9206826f916d1b15c7bfa7ae17/pymavlink-2.4.49-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58e954576b0323535336d347a7bb1c96794e1f2a966afd48e2fd304c5d40ab65", size = 6367486, upload-time = "2025-08-01T23:31:55.558Z" }, - { url = "https://files.pythonhosted.org/packages/89/3d/27695922636033890b1f2ff2a5c05d4ba413dbfc4c120de2f4768e9efc40/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:86d9c9dc261f1f7b785c45b6db002f6a9738ec8923065f83a6065fc0585a116e", size = 6360966, upload-time = "2025-08-01T23:31:56.865Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/c04174858b9ab5534bafab6a1b61046bea7fcd7036afebba74c543083eab/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:813f59d4942d5c635369869436440124e10fed5ee97c85dab146d081acc04763", size = 6362035, upload-time = "2025-08-01T23:31:58.378Z" }, - { url = "https://files.pythonhosted.org/packages/60/60/4f121e717dd627f37554e88e7435fe21edbb79ce17ff4f3c1bc4bbc51ff3/pymavlink-2.4.49-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a99736ed9e42935f2d9e0cba1c315320d77ed8fb2153c4dbf8778a521101ddf", size = 6364653, upload-time = "2025-08-01T23:31:59.561Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f1/d6a74f1fa88307d0feb8812bf09d193dbb7819d32fca031086dfcbf6bf63/pymavlink-2.4.49-cp311-cp311-win32.whl", hash = "sha256:5e14316b7bc02b93d509aa719ae6600bbc8f8fc4a8b62062d129089e5c07fb62", size = 6230360, upload-time = "2025-08-01T23:32:01.109Z" }, - { url = "https://files.pythonhosted.org/packages/32/72/2e145ed2f76852fe0dbf9db8d9cc0d7c802ed23cb75cbe1fd3a30ae19956/pymavlink-2.4.49-cp311-cp311-win_amd64.whl", hash = "sha256:c5702142ad5727fce926c54233016e076fb4288cd8211954cc9efdc523f9714a", size = 6241353, upload-time = "2025-08-01T23:32:02.346Z" }, - { url = "https://files.pythonhosted.org/packages/66/46/c8eb26b1ef82378fc30ea0c6b128422f0a69e1ec0e8e0feeae30bd28028b/pymavlink-2.4.49-cp311-cp311-win_arm64.whl", hash = "sha256:e69036e0556a688aeb6a4a5acb4737bbf275713090f6839dda36db4cabbb676b", size = 6231600, upload-time = "2025-08-01T23:32:03.647Z" }, - { url = "https://files.pythonhosted.org/packages/e8/81/062427da96311d359ed799af8569b7b3ffa25c333fb4a961478ce5a4735f/pymavlink-2.4.49-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b7925330a4bb30bcc732971cfeb1aa54515efd28f4588d7abc942967d7a2298b", size = 6291309, upload-time = "2025-08-01T23:32:04.972Z" }, - { url = "https://files.pythonhosted.org/packages/ca/f2/8bfed758e2efc2d6f28259634d94c09b10e50c62cd5914ac888ce268378d/pymavlink-2.4.49-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bf4c13703bc6dcbc70083a12aaec71f3a36a6b607290e93f59f2b004ebd02784", size = 6226353, upload-time = "2025-08-01T23:32:06.311Z" }, - { url = "https://files.pythonhosted.org/packages/e0/27/78419c2ae5489fdd996f6af0c1e4bd6dceaa5a5b155a367851926da7b05f/pymavlink-2.4.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8522d652fef8fb03c7ee50abd2686ffe0262cbec06136ae230f3a88cccdff21c", size = 6222943, upload-time = "2025-08-01T23:32:07.609Z" }, - { url = "https://files.pythonhosted.org/packages/ed/e6/c9ae05436ed219bb9f2b505d7c82474173c8ebcd28ff8f55833213d732a2/pymavlink-2.4.49-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f1b6e29972792a1da3fafde911355631b8a62735297a2f3c5209aa985919917a", size = 6376049, upload-time = "2025-08-01T23:32:08.949Z" }, - { url = "https://files.pythonhosted.org/packages/36/6f/eb93cc44e2653044eb5bbfa7ce0f808611e42d56106a4d6d5de4db8bb211/pymavlink-2.4.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8b13b9ac195e9fa8f01cda21638e26af9c5a90e3475ddb43fd2b9e396913f6b", size = 6388174, upload-time = "2025-08-01T23:32:10.194Z" }, - { url = "https://files.pythonhosted.org/packages/87/44/34099ab9e4e41db4b2ec9f05c3d8e7726ef3d5a2ae8cfb6f90596c4d82fb/pymavlink-2.4.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a87b508a9e9215afdb809189224731b4b34153f3879226fd94b8f485ac626ab", size = 6390472, upload-time = "2025-08-01T23:32:11.444Z" }, - { url = "https://files.pythonhosted.org/packages/d0/51/0146c0008feb5d8a7721870489b4c19fd30a1e49433be7a83624dc961f90/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31ca1c1e60a21f240abf35258df30e7b5ee954a055bbe7584f0ebabb48dd8c40", size = 6376189, upload-time = "2025-08-01T23:32:12.921Z" }, - { url = "https://files.pythonhosted.org/packages/c4/51/aa4b51cd9948eca7b63359ad392d8cd69b393bd781830c4a518a98aede33/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f854d1d730f40d4efa52d8901413af1b23d16187e941b76d55f0dcc0208d641d", size = 6378697, upload-time = "2025-08-01T23:32:14.471Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b6/dec8f9f7e1769894b7b11c8900b0a13cf13fb9cee2c45d7f9f5a785b3f39/pymavlink-2.4.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16c915365a21b7734c794ba97fa804ae6db52411bf62d21b877a51df2183dfab", size = 6384644, upload-time = "2025-08-01T23:32:16.134Z" }, - { url = "https://files.pythonhosted.org/packages/ee/2e/3db53612dab0bfa31eca8e9162489f44c9f9e23c183a2b263d707eb5ddc7/pymavlink-2.4.49-cp312-cp312-win32.whl", hash = "sha256:af7e84aec82f00fd574c2a0dbe11fb1a4c3cbf26f294ca0ef3807dcc5670567e", size = 6230813, upload-time = "2025-08-01T23:32:17.732Z" }, - { url = "https://files.pythonhosted.org/packages/bf/47/fe857933a464b5a07bf72e2a1d2e92a87ad9d96915f48f86c9475333a63d/pymavlink-2.4.49-cp312-cp312-win_amd64.whl", hash = "sha256:246e227ca8535de98a4b93876a14b9ced7bfc82c70458e480725a715aa6b6bf3", size = 6242451, upload-time = "2025-08-01T23:32:19.063Z" }, - { url = "https://files.pythonhosted.org/packages/25/ca/995d1201925ad49fb6b174a9d488f1d90b77256b1088ebd3d7f192b0f65a/pymavlink-2.4.49-cp312-cp312-win_arm64.whl", hash = "sha256:c7415592166d9cbd4434775828b00c71bebf292c8367744d861e3ccd2dab9f3e", size = 6231742, upload-time = "2025-08-01T23:32:20.707Z" }, - { url = "https://files.pythonhosted.org/packages/26/10/67756d987b1aefd991664ce0a996ee3bf69ed7aaf8c7319ff6012a4dc8a2/pymavlink-2.4.49-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f7cfaf3cc1abd611c0757d4b7e56eaf5b4cfa54510a3178b26ebbd9d3443b9d7", size = 6290269, upload-time = "2025-08-01T23:32:22.168Z" }, - { url = "https://files.pythonhosted.org/packages/c2/02/9e63467d65da78fed03981c86e5b7877fcf163a98372ba5ef03015e3798c/pymavlink-2.4.49-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db9c0d00e79946ecf1ac89847f32712ef546994342f44b3e9a68e59cfbc85bef", size = 6225761, upload-time = "2025-08-01T23:32:23.419Z" }, - { url = "https://files.pythonhosted.org/packages/3b/7e/46a5512964043ada02914657610c885b083375dd169dea172870f4dd73b0/pymavlink-2.4.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37937d5dfd2ddc2a64ea64687380278ac9c49e1644ea125f1e8a5caf4e1f2ebd", size = 6222450, upload-time = "2025-08-01T23:32:24.803Z" }, - { url = "https://files.pythonhosted.org/packages/db/c6/1b63a8c4d35887edc979805b324240ff4b847e9d912b323d71613e8f1971/pymavlink-2.4.49-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8100f2f1f53b094611531df2cfb25f1c8e8fdee01f095eb8ee18976994663cf6", size = 6368072, upload-time = "2025-08-01T23:32:26.068Z" }, - { url = "https://files.pythonhosted.org/packages/29/69/94348757424a94c5a3e87f41d4c05a168bc5de2549afdbea1d4424a318dc/pymavlink-2.4.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2db4b88f38aa1ba4c0653a8c5938364bfe78a008e8d02627534015142bf774", size = 6379869, upload-time = "2025-08-01T23:32:27.337Z" }, - { url = "https://files.pythonhosted.org/packages/91/a7/792925eadc046ae580ab444181a06e8d51d38204a81a9274460f90009b88/pymavlink-2.4.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7fe9286fd5b2db05277d30d1ea6b9b3a9ea010a99aff04d451705cc4be6a7e6", size = 6382786, upload-time = "2025-08-01T23:32:28.518Z" }, - { url = "https://files.pythonhosted.org/packages/b9/71/d7b1d280dda800ac386fd54dcded6344b518a8266a918729512e46e39f6b/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d49309e00d4d434f2e414c166b18ef18496987a13a613864f89a19ca190ef0d0", size = 6368732, upload-time = "2025-08-01T23:32:29.794Z" }, - { url = "https://files.pythonhosted.org/packages/23/89/b75ef8eea1e31ec07f13fe71883b08cdc2bce0c33418218cebb03e55124a/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7104eef554b01d6c180e1a532dc494c4b1d74e48b0b725328ec39f042982e172", size = 6370950, upload-time = "2025-08-01T23:32:31.041Z" }, - { url = "https://files.pythonhosted.org/packages/f6/57/3cb77e3f593e27dc63bd74357b3c3b57075af74771c4446275097f0865f2/pymavlink-2.4.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:795e6628f9ecf0b06e3b7b65f8fcf477ec1603971d590cffd4640cff1852da23", size = 6376423, upload-time = "2025-08-01T23:32:32.345Z" }, - { url = "https://files.pythonhosted.org/packages/41/bb/49b83c6d212751c88a29cebe413c940ee1d0b7991a667710689eb0cd648e/pymavlink-2.4.49-cp313-cp313-win32.whl", hash = "sha256:9f14bbe1ce3d5c0af4994f0f76d1a8d0c2f915d7dcb7645c1ecba42eeff89536", size = 6230635, upload-time = "2025-08-01T23:32:33.613Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c4/d3e9e414dd7ba0124ef07d33d9492cc01db1b76ae3cec45443ec4d6a7935/pymavlink-2.4.49-cp313-cp313-win_amd64.whl", hash = "sha256:9777a0375ebcda0efda3f4eae6d8d2e5ce6de8e26c2f0ac7be1a016d0d386b82", size = 6242260, upload-time = "2025-08-01T23:32:35.256Z" }, - { url = "https://files.pythonhosted.org/packages/e5/36/52616b4fdd076177f1ba22e6ef40782b48e14efb47fce2c3bd4f8496ec23/pymavlink-2.4.49-cp313-cp313-win_arm64.whl", hash = "sha256:712ee4240a9489c6dab6158882c7e1f37516c5951db5841cd408ad7b4c6db0d4", size = 6231575, upload-time = "2025-08-01T23:32:36.845Z" }, -] - -[[package]] -name = "pyopengl" -version = "3.1.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/16/912b7225d56284859cd9a672827f18be43f8012f8b7b932bc4bd959a298e/pyopengl-3.1.10.tar.gz", hash = "sha256:c4a02d6866b54eb119c8e9b3fb04fa835a95ab802dd96607ab4cdb0012df8335", size = 1915580, upload-time = "2025-08-18T02:33:01.76Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/e4/1ba6f44e491c4eece978685230dde56b14d51a0365bc1b774ddaa94d14cd/pyopengl-3.1.10-py3-none-any.whl", hash = "sha256:794a943daced39300879e4e47bd94525280685f42dbb5a998d336cfff151d74f", size = 3194996, upload-time = "2025-08-18T02:32:59.902Z" }, -] - -[[package]] -name = "pyopenssl" -version = "25.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/be/97b83a464498a79103036bc74d1038df4a7ef0e402cfaf4d5e113fb14759/pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329", size = 184073, upload-time = "2025-09-17T00:32:21.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/81/ef2b1dfd1862567d573a4fdbc9f969067621764fbb74338496840a1d2977/pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6", size = 57268, upload-time = "2025-09-17T00:32:19.474Z" }, -] - -[[package]] -name = "pyparsing" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, -] - -[[package]] -name = "pypika" -version = "0.51.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" }, -] - -[[package]] -name = "pyproject-hooks" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, -] - -[[package]] -name = "pyquaternion" -version = "0.9.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/3d092aa20efaedacb89c3221a92c6491be5b28f618a2c36b52b53e7446c2/pyquaternion-0.9.9.tar.gz", hash = "sha256:b1f61af219cb2fe966b5fb79a192124f2e63a3f7a777ac3cadf2957b1a81bea8", size = 15530, upload-time = "2020-10-05T01:31:30.327Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/b3/d8482e8cacc8ea15a356efea13d22ce1c5914a9ee36622ba250523240bf2/pyquaternion-0.9.9-py3-none-any.whl", hash = "sha256:e65f6e3f7b1fdf1a9e23f82434334a1ae84f14223eee835190cd2e841f8172ec", size = 14361, upload-time = "2020-10-05T01:31:37.575Z" }, -] - -[[package]] -name = "pysocks" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" }, -] - -[[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, -] - -[[package]] -name = "pytest-env" -version = "1.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/31/27f28431a16b83cab7a636dce59cf397517807d247caa38ee67d65e71ef8/pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf", size = 8911, upload-time = "2024-09-17T22:39:18.566Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/b8/87cfb16045c9d4092cfcf526135d73b88101aac83bc1adcf82dfb5fd3833/pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30", size = 6141, upload-time = "2024-09-17T22:39:16.942Z" }, -] - -[[package]] -name = "pytest-mock" -version = "3.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/99/3323ee5c16b3637b4d941c362182d3e749c11e400bea31018c42219f3a98/pytest_mock-3.15.0.tar.gz", hash = "sha256:ab896bd190316b9d5d87b277569dfcdf718b2d049a2ccff5f7aca279c002a1cf", size = 33838, upload-time = "2025-09-04T20:57:48.679Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/b3/7fefc43fb706380144bcd293cc6e446e6f637ddfa8b83f48d1734156b529/pytest_mock-3.15.0-py3-none-any.whl", hash = "sha256:ef2219485fb1bd256b00e7ad7466ce26729b30eadfc7cbcdb4fa9a92ca68db6f", size = 10050, upload-time = "2025-09-04T20:57:47.274Z" }, -] - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, -] - -[[package]] -name = "python-can" -version = "4.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "typing-extensions" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/f9/a9d99d36dd33be5badb747801c9255c3c526171a5542092eaacc73350fb8/python_can-4.6.1.tar.gz", hash = "sha256:290fea135d04b8504ebff33889cc6d301e2181a54099116609f940825ffe5005", size = 1206049, upload-time = "2025-08-12T07:44:58.314Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/34/e4ac153acdbcfba7f48bc73d6586a74c91cc919fcc2e29acbf81be329d1f/python_can-4.6.1-py3-none-any.whl", hash = "sha256:17f95255868a95108dcfcb90565a684dad32d5a3ebb35afd14f739e18c84ff6c", size = 276996, upload-time = "2025-08-12T07:44:56.55Z" }, -] - -[[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 = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-engineio" -version = "4.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "simple-websocket" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, -] - -[[package]] -name = "python-lsp-jsonrpc" -version = "1.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ujson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/b6/fd92e2ea4635d88966bb42c20198df1a981340f07843b5e3c6694ba3557b/python-lsp-jsonrpc-1.1.2.tar.gz", hash = "sha256:4688e453eef55cd952bff762c705cedefa12055c0aec17a06f595bcc002cc912", size = 15298, upload-time = "2023-09-23T17:48:30.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/d9/656659d5b5d5f402b2b174cd0ba9bc827e07ce3c0bf88da65424baf64af8/python_lsp_jsonrpc-1.1.2-py3-none-any.whl", hash = "sha256:7339c2e9630ae98903fdaea1ace8c47fba0484983794d6aafd0bd8989be2b03c", size = 8805, upload-time = "2023-09-23T17:48:28.804Z" }, -] - -[[package]] -name = "python-lsp-ruff" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cattrs" }, - { name = "lsprotocol" }, - { name = "python-lsp-server" }, - { name = "ruff" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/79/2f6322c47bd2956447e0a6787084b4110b4473e3d2501b86aa47c802e6a0/python_lsp_ruff-2.3.0.tar.gz", hash = "sha256:647745b7f3010ac101e3c53a797b8f9deb1f52228b608d70ad0e8e056978c3b7", size = 17268, upload-time = "2025-09-29T20:14:02.994Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/c0/761e359e255fce641c263a3c3e43f7685d1667139e9d35a376c1cc9f6f70/python_lsp_ruff-2.3.0-py3-none-any.whl", hash = "sha256:b858b698fbaff5670f6d5e6c66afc632908f78639d73dc85dedd33ae5fdd204f", size = 12039, upload-time = "2025-09-29T20:14:01.56Z" }, -] - -[[package]] -name = "python-lsp-server" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "black" }, - { name = "docstring-to-markdown" }, - { name = "jedi" }, - { name = "pluggy" }, - { name = "python-lsp-jsonrpc" }, - { name = "ujson" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b4/b5/b989d41c63390dfc2bf63275ab543b82fed076723d912055e77ccbae1422/python_lsp_server-1.14.0.tar.gz", hash = "sha256:509c445fc667f41ffd3191cb7512a497bf7dd76c14ceb1ee2f6c13ebe71f9a6b", size = 121536, upload-time = "2025-12-06T16:12:20.86Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/cf/587f913335e3855e0ddca2aee7c3f9d5de2d75a1e23434891e9f74783bcd/python_lsp_server-1.14.0-py3-none-any.whl", hash = "sha256:a71a917464effc48f4c70363f90b8520e5e3ba8201428da80b97a7ceb259e32a", size = 77060, upload-time = "2025-12-06T16:12:19.46Z" }, -] - -[package.optional-dependencies] -all = [ - { name = "autopep8" }, - { name = "flake8" }, - { name = "mccabe" }, - { name = "pycodestyle" }, - { name = "pydocstyle" }, - { name = "pyflakes" }, - { name = "pylint" }, - { name = "rope" }, - { name = "whatthepatch" }, - { name = "yapf" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, -] - -[[package]] -name = "python-socketio" -version = "5.16.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bidict" }, - { name = "python-engineio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, -] - -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, - { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, - { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, - { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - -[[package]] -name = "pytoolconfig" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/dc/abf70d2c2bcac20e8c71a7cdf6d44e4ddba4edf65acb179248d554d743db/pytoolconfig-1.3.1.tar.gz", hash = "sha256:51e6bd1a6f108238ae6aab6a65e5eed5e75d456be1c2bf29b04e5c1e7d7adbae", size = 16655, upload-time = "2024-01-11T16:25:11.914Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/44/da239917f5711ca7105f7d7f9e2765716dd883b241529beafc0f28504725/pytoolconfig-1.3.1-py3-none-any.whl", hash = "sha256:5d8cea8ae1996938ec3eaf44567bbc5ef1bc900742190c439a44a704d6e1b62b", size = 17022, upload-time = "2024-01-11T16:25:10.589Z" }, -] - -[package.optional-dependencies] -global = [ - { name = "platformdirs" }, -] - -[[package]] -name = "pyturbojpeg" -version = "1.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d2/e8/0cbd6e4f086a3b9261b2539ab5ddb1e3ba0c94d45b47832594d4b4607586/PyTurboJPEG-1.8.2.tar.gz", hash = "sha256:b7d9625bbb2121b923228fc70d0c2b010b386687501f5b50acec4501222e152b", size = 12694, upload-time = "2025-06-22T07:26:45.861Z" } - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, - { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, -] - -[[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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "pyzmq" -version = "27.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/b9/52aa9ec2867528b54f1e60846728d8b4d84726630874fee3a91e66c7df81/pyzmq-27.1.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:508e23ec9bc44c0005c4946ea013d9317ae00ac67778bd47519fdf5a0e930ff4", size = 1329850, upload-time = "2025-09-08T23:07:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/99/64/5653e7b7425b169f994835a2b2abf9486264401fdef18df91ddae47ce2cc/pyzmq-27.1.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:507b6f430bdcf0ee48c0d30e734ea89ce5567fd7b8a0f0044a369c176aa44556", size = 906380, upload-time = "2025-09-08T23:07:29.78Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/7d713284dbe022f6440e391bd1f3c48d9185673878034cfb3939cdf333b2/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf7b38f9fd7b81cb6d9391b2946382c8237fd814075c6aa9c3b746d53076023b", size = 666421, upload-time = "2025-09-08T23:07:31.263Z" }, - { url = "https://files.pythonhosted.org/packages/30/76/8f099f9d6482450428b17c4d6b241281af7ce6a9de8149ca8c1c649f6792/pyzmq-27.1.0-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ff0b279b40d687691a6217c12242ee71f0fba28bf8626ff50e3ef0f4410e1e", size = 854149, upload-time = "2025-09-08T23:07:33.17Z" }, - { url = "https://files.pythonhosted.org/packages/59/f0/37fbfff06c68016019043897e4c969ceab18bde46cd2aca89821fcf4fb2e/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:677e744fee605753eac48198b15a2124016c009a11056f93807000ab11ce6526", size = 1655070, upload-time = "2025-09-08T23:07:35.205Z" }, - { url = "https://files.pythonhosted.org/packages/47/14/7254be73f7a8edc3587609554fcaa7bfd30649bf89cd260e4487ca70fdaa/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dd2fec2b13137416a1c5648b7009499bcc8fea78154cd888855fa32514f3dad1", size = 2033441, upload-time = "2025-09-08T23:07:37.432Z" }, - { url = "https://files.pythonhosted.org/packages/22/dc/49f2be26c6f86f347e796a4d99b19167fc94503f0af3fd010ad262158822/pyzmq-27.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08e90bb4b57603b84eab1d0ca05b3bbb10f60c1839dc471fc1c9e1507bef3386", size = 1891529, upload-time = "2025-09-08T23:07:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3e/154fb963ae25be70c0064ce97776c937ecc7d8b0259f22858154a9999769/pyzmq-27.1.0-cp310-cp310-win32.whl", hash = "sha256:a5b42d7a0658b515319148875fcb782bbf118dd41c671b62dae33666c2213bda", size = 567276, upload-time = "2025-09-08T23:07:40.695Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/f4ab56c8c595abcb26b2be5fd9fa9e6899c1e5ad54964e93ae8bb35482be/pyzmq-27.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0bb87227430ee3aefcc0ade2088100e528d5d3298a0a715a64f3d04c60ba02f", size = 632208, upload-time = "2025-09-08T23:07:42.298Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e3/be2cc7ab8332bdac0522fdb64c17b1b6241a795bee02e0196636ec5beb79/pyzmq-27.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:9a916f76c2ab8d045b19f2286851a38e9ac94ea91faf65bd64735924522a8b32", size = 559766, upload-time = "2025-09-08T23:07:43.869Z" }, - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" }, - { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" }, - { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" }, - { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" }, - { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" }, - { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" }, - { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" }, - { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" }, - { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" }, - { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" }, - { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" }, - { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" }, - { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" }, - { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" }, - { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" }, - { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" }, - { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" }, - { url = "https://files.pythonhosted.org/packages/f3/81/a65e71c1552f74dec9dff91d95bafb6e0d33338a8dfefbc88aa562a20c92/pyzmq-27.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c17e03cbc9312bee223864f1a2b13a99522e0dc9f7c5df0177cd45210ac286e6", size = 836266, upload-time = "2025-09-08T23:09:40.048Z" }, - { url = "https://files.pythonhosted.org/packages/58/ed/0202ca350f4f2b69faa95c6d931e3c05c3a397c184cacb84cb4f8f42f287/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f328d01128373cb6763823b2b4e7f73bdf767834268c565151eacb3b7a392f90", size = 800206, upload-time = "2025-09-08T23:09:41.902Z" }, - { url = "https://files.pythonhosted.org/packages/47/42/1ff831fa87fe8f0a840ddb399054ca0009605d820e2b44ea43114f5459f4/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1790386614232e1b3a40a958454bdd42c6d1811837b15ddbb052a032a43f62", size = 567747, upload-time = "2025-09-08T23:09:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/5c4d6807434751e3f21231bee98109aa57b9b9b55e058e450d0aef59b70f/pyzmq-27.1.0-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:448f9cb54eb0cee4732b46584f2710c8bc178b0e5371d9e4fc8125201e413a74", size = 747371, upload-time = "2025-09-08T23:09:45.575Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/78ce193dbf03567eb8c0dc30e3df2b9e56f12a670bf7eb20f9fb532c7e8a/pyzmq-27.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:05b12f2d32112bf8c95ef2e74ec4f1d4beb01f8b5e703b38537f8849f92cb9ba", size = 544862, upload-time = "2025-09-08T23:09:47.448Z" }, - { url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" }, - { url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, -] - -[[package]] -name = "reactivex" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/af/38a4b62468e4c5bd50acf511d86fe62e65a466aa6abb55b1d59a4a9e57f3/reactivex-4.1.0.tar.gz", hash = "sha256:c7499e3c802bccaa20839b3e17355a7d939573fded3f38ba3d4796278a169a3d", size = 113482, upload-time = "2025-11-05T21:44:24.557Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/9e/3c2f5d3abb6c5d82f7696e1e3c69b7279049e928596ce82ed25ca97a08f3/reactivex-4.1.0-py3-none-any.whl", hash = "sha256:485750ec8d9b34bcc8ff4318971d234dc4f595058a1b4435a74aefef4b2bc9bd", size = 218588, upload-time = "2025-11-05T21:44:23.015Z" }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - -[[package]] -name = "regex" -version = "2026.1.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, - { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, - { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, - { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, - { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, - { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, - { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, - { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, - { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, - { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, - { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, - { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, - { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, - { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, - { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, - { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, - { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, - { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, - { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, - { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, - { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, - { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, - { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, - { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, - { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, - { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, - { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, - { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, - { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, - { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, - { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, - { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, - { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, - { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, - { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, - { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, - { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, - { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, - { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, - { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, - { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, - { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, - { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, - { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, - { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, - { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, - { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, - { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, - { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, - { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, - { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, - { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, - { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, - { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, - { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, - { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, - { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, - { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, - { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, - { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, - { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, - { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, - { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, - { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, - { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, - { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, - { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, - { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, - { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, - { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, - { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, - { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, - { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, - { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, - { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, - { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, - { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, - { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, - { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, - { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, - { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, - { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, - { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, - { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, - { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, - { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, - { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, - { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[package.optional-dependencies] -socks = [ - { name = "pysocks" }, -] - -[[package]] -name = "requests-mock" -version = "1.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/32/587625f91f9a0a3d84688bf9cfc4b2480a7e8ec327cefd0ff2ac891fd2cf/requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401", size = 60901, upload-time = "2024-03-29T03:54:29.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/ec/889fbc557727da0c34a33850950310240f2040f3b1955175fdb2b36a8910/requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563", size = 27695, upload-time = "2024-03-29T03:54:27.64Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - -[[package]] -name = "rerun-sdk" -version = "0.29.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "pyarrow" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/d1/6b31d12e726732dced50806b1cb0b5fb55c478ee4ac23d68f50db888cf2c/rerun_sdk-0.29.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ead2b4bb93cac553c9b524442e49ba5f34c30ab9db2225e1ed2ce2ee235ea46b", size = 112371441, upload-time = "2026-02-12T19:31:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/dc/81/9b3619b37c8a7492ccbe9ea172dedc5ffb66b83ded82b8f443c1958fe1c0/rerun_sdk-0.29.2-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a97f5601cb50c14ec665525c0cf65056167de1306958a0526ff1e8d384320076", size = 120304992, upload-time = "2026-02-12T19:31:12.499Z" }, - { url = "https://files.pythonhosted.org/packages/63/43/2590293ce7985cbb88f9fdd67b90c36b954116f6c75639b378f098b3ff61/rerun_sdk-0.29.2-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:392a7f2c3db660716b660f4b164f9b73a076100378781a3a2551edf290d00c23", size = 125305451, upload-time = "2026-02-12T19:31:17.319Z" }, - { url = "https://files.pythonhosted.org/packages/bc/06/b73e04344f2220d48c0583270a54873bca3b93ab476cf09629941afac8e5/rerun_sdk-0.29.2-cp310-abi3-win_amd64.whl", hash = "sha256:a3ccfbac8df89519a075f9dc3499a9e715c653a19a17de00d39fd218a589e009", size = 108289765, upload-time = "2026-02-12T19:31:22.616Z" }, -] - -[[package]] -name = "retrying" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c8/5a/b17e1e257d3e6f2e7758930e1256832c9ddd576f8631781e6a072914befa/retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39", size = 11411, upload-time = "2025-08-03T03:35:25.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/f3/6cd296376653270ac1b423bb30bd70942d9916b6978c6f40472d6ac038e7/retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59", size = 10859, upload-time = "2025-08-03T03:35:23.829Z" }, -] - -[[package]] -name = "rich" -version = "14.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" }, -] - -[[package]] -name = "rich-click" -version = "1.9.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/27/091e140ea834272188e63f8dd6faac1f5c687582b687197b3e0ec3c78ebf/rich_click-1.9.7.tar.gz", hash = "sha256:022997c1e30731995bdbc8ec2f82819340d42543237f033a003c7b1f843fc5dc", size = 74838, upload-time = "2026-01-31T04:29:27.707Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/e5/d708d262b600a352abe01c2ae360d8ff75b0af819b78e9af293191d928e6/rich_click-1.9.7-py3-none-any.whl", hash = "sha256:2f99120fca78f536e07b114d3b60333bc4bb2a0969053b1250869bcdc1b5351b", size = 71491, upload-time = "2026-01-31T04:29:26.777Z" }, -] - -[[package]] -name = "rope" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytoolconfig", extra = ["global"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/3a/85e60d154f26ecdc1d47a63ac58bd9f32a5a9f3f771f6672197f02a00ade/rope-1.14.0.tar.gz", hash = "sha256:8803e3b667315044f6270b0c69a10c0679f9f322ed8efe6245a93ceb7658da69", size = 296801, upload-time = "2025-07-12T17:46:07.786Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/35/130469d1901da2b3a5a377539b4ffcd8a5c983f1c9e3ba5ffdd8d71ae314/rope-1.14.0-py3-none-any.whl", hash = "sha256:00a7ea8c0c376fc0b053b2f2f8ef3bfb8b50fecf1ebf3eb80e4f8bd7f1941918", size = 207143, upload-time = "2025-07-12T17:46:05.928Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - -[[package]] -name = "ruff" -version = "0.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/62/50b7727004dfe361104dfbf898c45a9a2fdfad8c72c04ae62900224d6ecf/ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153", size = 5558687, upload-time = "2025-10-31T00:26:26.878Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/8e/0c10ff1ea5d4360ab8bfca4cb2c9d979101a391f3e79d2616c9bf348cd26/ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371", size = 12535613, upload-time = "2025-10-31T00:25:44.302Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c8/6724f4634c1daf52409fbf13fefda64aa9c8f81e44727a378b7b73dc590b/ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654", size = 12855812, upload-time = "2025-10-31T00:25:47.793Z" }, - { url = "https://files.pythonhosted.org/packages/de/03/db1bce591d55fd5f8a08bb02517fa0b5097b2ccabd4ea1ee29aa72b67d96/ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14", size = 11944026, upload-time = "2025-10-31T00:25:49.657Z" }, - { url = "https://files.pythonhosted.org/packages/0b/75/4f8dbd48e03272715d12c87dc4fcaaf21b913f0affa5f12a4e9c6f8a0582/ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed", size = 12356818, upload-time = "2025-10-31T00:25:51.949Z" }, - { url = "https://files.pythonhosted.org/packages/ec/9b/506ec5b140c11d44a9a4f284ea7c14ebf6f8b01e6e8917734a3325bff787/ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc", size = 12336745, upload-time = "2025-10-31T00:25:54.248Z" }, - { url = "https://files.pythonhosted.org/packages/c7/e1/c560d254048c147f35e7f8131d30bc1f63a008ac61595cf3078a3e93533d/ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd", size = 13101684, upload-time = "2025-10-31T00:25:56.253Z" }, - { url = "https://files.pythonhosted.org/packages/a5/32/e310133f8af5cd11f8cc30f52522a3ebccc5ea5bff4b492f94faceaca7a8/ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb", size = 14535000, upload-time = "2025-10-31T00:25:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/a2/a1/7b0470a22158c6d8501eabc5e9b6043c99bede40fa1994cadf6b5c2a61c7/ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20", size = 14156450, upload-time = "2025-10-31T00:26:00.889Z" }, - { url = "https://files.pythonhosted.org/packages/0a/96/24bfd9d1a7f532b560dcee1a87096332e461354d3882124219bcaff65c09/ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0", size = 13568414, upload-time = "2025-10-31T00:26:03.291Z" }, - { url = "https://files.pythonhosted.org/packages/a7/e7/138b883f0dfe4ad5b76b58bf4ae675f4d2176ac2b24bdd81b4d966b28c61/ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e", size = 13315293, upload-time = "2025-10-31T00:26:05.708Z" }, - { url = "https://files.pythonhosted.org/packages/33/f4/c09bb898be97b2eb18476b7c950df8815ef14cf956074177e9fbd40b7719/ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5", size = 13539444, upload-time = "2025-10-31T00:26:08.09Z" }, - { url = "https://files.pythonhosted.org/packages/9c/aa/b30a1db25fc6128b1dd6ff0741fa4abf969ded161599d07ca7edd0739cc0/ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e", size = 12252581, upload-time = "2025-10-31T00:26:10.297Z" }, - { url = "https://files.pythonhosted.org/packages/da/13/21096308f384d796ffe3f2960b17054110a9c3828d223ca540c2b7cc670b/ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e", size = 12307503, upload-time = "2025-10-31T00:26:12.646Z" }, - { url = "https://files.pythonhosted.org/packages/cb/cc/a350bac23f03b7dbcde3c81b154706e80c6f16b06ff1ce28ed07dc7b07b0/ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa", size = 12675457, upload-time = "2025-10-31T00:26:15.044Z" }, - { url = "https://files.pythonhosted.org/packages/cb/76/46346029fa2f2078826bc88ef7167e8c198e58fe3126636e52f77488cbba/ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f", size = 13403980, upload-time = "2025-10-31T00:26:17.81Z" }, - { url = "https://files.pythonhosted.org/packages/9f/a4/35f1ef68c4e7b236d4a5204e3669efdeefaef21f0ff6a456792b3d8be438/ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7", size = 12500045, upload-time = "2025-10-31T00:26:20.503Z" }, - { url = "https://files.pythonhosted.org/packages/03/15/51960ae340823c9859fb60c63301d977308735403e2134e17d1d2858c7fb/ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f", size = 13594005, upload-time = "2025-10-31T00:26:22.533Z" }, - { url = "https://files.pythonhosted.org/packages/b7/73/4de6579bac8e979fca0a77e54dec1f1e011a0d268165eb8a9bc0982a6564/ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1", size = 12590017, upload-time = "2025-10-31T00:26:24.52Z" }, -] - -[[package]] -name = "safetensors" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, - { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, - { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, - { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, - { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, - { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, - { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, - { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/3e/daed796fd69cce768b8788401cc464ea90b306fb196ae1ffed0b98182859/scikit_learn-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b33579c10a3081d076ab403df4a4190da4f4432d443521674637677dc91e61f", size = 9336221, upload-time = "2025-09-09T08:20:19.328Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ce/af9d99533b24c55ff4e18d9b7b4d9919bbc6cd8f22fe7a7be01519a347d5/scikit_learn-1.7.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:36749fb62b3d961b1ce4fedf08fa57a1986cd409eff2d783bca5d4b9b5fce51c", size = 8653834, upload-time = "2025-09-09T08:20:22.073Z" }, - { url = "https://files.pythonhosted.org/packages/58/0e/8c2a03d518fb6bd0b6b0d4b114c63d5f1db01ff0f9925d8eb10960d01c01/scikit_learn-1.7.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7a58814265dfc52b3295b1900cfb5701589d30a8bb026c7540f1e9d3499d5ec8", size = 9660938, upload-time = "2025-09-09T08:20:24.327Z" }, - { url = "https://files.pythonhosted.org/packages/2b/75/4311605069b5d220e7cf5adabb38535bd96f0079313cdbb04b291479b22a/scikit_learn-1.7.2-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a847fea807e278f821a0406ca01e387f97653e284ecbd9750e3ee7c90347f18", size = 9477818, upload-time = "2025-09-09T08:20:26.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9b/87961813c34adbca21a6b3f6b2bea344c43b30217a6d24cc437c6147f3e8/scikit_learn-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:ca250e6836d10e6f402436d6463d6c0e4d8e0234cfb6a9a47835bd392b852ce5", size = 8886969, upload-time = "2025-09-09T08:20:29.329Z" }, - { url = "https://files.pythonhosted.org/packages/43/83/564e141eef908a5863a54da8ca342a137f45a0bfb71d1d79704c9894c9d1/scikit_learn-1.7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7509693451651cd7361d30ce4e86a1347493554f172b1c72a39300fa2aea79e", size = 9331967, upload-time = "2025-09-09T08:20:32.421Z" }, - { url = "https://files.pythonhosted.org/packages/18/d6/ba863a4171ac9d7314c4d3fc251f015704a2caeee41ced89f321c049ed83/scikit_learn-1.7.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:0486c8f827c2e7b64837c731c8feff72c0bd2b998067a8a9cbc10643c31f0fe1", size = 8648645, upload-time = "2025-09-09T08:20:34.436Z" }, - { url = "https://files.pythonhosted.org/packages/ef/0e/97dbca66347b8cf0ea8b529e6bb9367e337ba2e8be0ef5c1a545232abfde/scikit_learn-1.7.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89877e19a80c7b11a2891a27c21c4894fb18e2c2e077815bcade10d34287b20d", size = 9715424, upload-time = "2025-09-09T08:20:36.776Z" }, - { url = "https://files.pythonhosted.org/packages/f7/32/1f3b22e3207e1d2c883a7e09abb956362e7d1bd2f14458c7de258a26ac15/scikit_learn-1.7.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8da8bf89d4d79aaec192d2bda62f9b56ae4e5b4ef93b6a56b5de4977e375c1f1", size = 9509234, upload-time = "2025-09-09T08:20:38.957Z" }, - { url = "https://files.pythonhosted.org/packages/9f/71/34ddbd21f1da67c7a768146968b4d0220ee6831e4bcbad3e03dd3eae88b6/scikit_learn-1.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:9b7ed8d58725030568523e937c43e56bc01cadb478fc43c042a9aca1dacb3ba1", size = 8894244, upload-time = "2025-09-09T08:20:41.166Z" }, - { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, - { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, - { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, - { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, - { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, - { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, - { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, - { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, - { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, - { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, - { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, - { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, - { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, - { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, - { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, - { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, - { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, - { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, - { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, - { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, - { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, - { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, - { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, - { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, - { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, - { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, - { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, - { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, - { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, - { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, - { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, - { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, - { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, - { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, - { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, - { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, - { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, - { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, - { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, - { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, -] - -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, -] - -[[package]] -name = "scipy" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, - { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, - { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, - { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, - { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, - { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, - { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, - { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, - { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, - { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, - { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, - { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, - { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" }, - { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" }, - { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" }, - { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" }, - { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" }, - { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" }, - { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" }, - { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" }, - { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" }, - { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" }, - { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" }, - { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" }, - { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" }, - { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" }, - { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" }, - { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" }, - { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" }, - { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" }, - { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" }, - { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" }, - { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" }, - { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" }, - { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" }, - { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" }, - { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" }, - { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" }, - { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" }, - { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" }, - { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" }, - { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" }, - { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" }, - { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" }, - { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" }, - { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" }, - { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" }, -] - -[[package]] -name = "sentence-transformers" -version = "5.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "transformers" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/bc/0bc9c0ec1cf83ab2ec6e6f38667d167349b950fff6dd2086b79bd360eeca/sentence_transformers-5.2.2.tar.gz", hash = "sha256:7033ee0a24bc04c664fd490abf2ef194d387b3a58a97adcc528783ff505159fa", size = 381607, upload-time = "2026-01-27T11:11:02.658Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/21/7e925890636791386e81b52878134f114d63072e79fffe14cdcc5e7a5e6a/sentence_transformers-5.2.2-py3-none-any.whl", hash = "sha256:280ac54bffb84c110726b4d8848ba7b7c60813b9034547f8aea6e9a345cd1c23", size = 494106, upload-time = "2026-01-27T11:11:00.983Z" }, -] - -[[package]] -name = "setuptools" -version = "81.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, -] - -[[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 = "simple-websocket" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wsproto" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b0/d4/bfa032f961103eba93de583b161f0e6a5b63cebb8f2c7d0c6e6efe1e3d2e/simple_websocket-1.1.0.tar.gz", hash = "sha256:7939234e7aa067c534abdab3a9ed933ec9ce4691b0713c78acb195560aa52ae4", size = 17300, upload-time = "2024-10-10T22:39:31.412Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/59/0782e51887ac6b07ffd1570e0364cf901ebc36345fea669969d2084baebb/simple_websocket-1.1.0-py3-none-any.whl", hash = "sha256:4af6069630a38ed6c561010f0e11a5bc0d4ca569b36306eb257cd9a192497c8c", size = 13842, upload-time = "2024-10-10T22:39:29.645Z" }, -] - -[[package]] -name = "simplejson" -version = "3.20.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f4/a1ac5ed32f7ed9a088d62a59d410d4c204b3b3815722e2ccfb491fa8251b/simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649", size = 85784, upload-time = "2025-09-26T16:29:36.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/09/2bf3761de89ea2d91bdce6cf107dcd858892d0adc22c995684878826cc6b/simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5", size = 94039, upload-time = "2025-09-26T16:27:29.283Z" }, - { url = "https://files.pythonhosted.org/packages/0f/33/c3277db8931f0ae9e54b9292668863365672d90fb0f632f4cf9829cb7d68/simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d", size = 75894, upload-time = "2025-09-26T16:27:30.378Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ea/ae47b04d03c7c8a7b7b1a8b39a6e27c3bd424e52f4988d70aca6293ff5e5/simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1", size = 76116, upload-time = "2025-09-26T16:27:31.42Z" }, - { url = "https://files.pythonhosted.org/packages/4b/42/6c9af551e5a8d0f171d6dce3d9d1260068927f7b80f1f09834e07887c8c4/simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6", size = 138827, upload-time = "2025-09-26T16:27:32.486Z" }, - { url = "https://files.pythonhosted.org/packages/2b/22/5e268bbcbe9f75577491e406ec0a5536f5b2fa91a3b52031fea51cd83e1d/simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01", size = 146772, upload-time = "2025-09-26T16:27:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/71/b4/800f14728e2ad666f420dfdb57697ca128aeae7f991b35759c09356b829a/simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3", size = 134497, upload-time = "2025-09-26T16:27:35.211Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b9/c54eef4226c6ac8e9a389bbe5b21fef116768f97a2dc1a683c716ffe66ef/simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda", size = 138172, upload-time = "2025-09-26T16:27:36.44Z" }, - { url = "https://files.pythonhosted.org/packages/09/36/4e282f5211b34620f1b2e4b51d9ddaab5af82219b9b7b78360a33f7e5387/simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2", size = 140272, upload-time = "2025-09-26T16:27:37.605Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b0/94ad2cf32f477c449e1f63c863d8a513e2408d651c4e58fe4b6a7434e168/simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4", size = 140468, upload-time = "2025-09-26T16:27:39.015Z" }, - { url = "https://files.pythonhosted.org/packages/e5/46/827731e4163be3f987cb8ee90f5d444161db8f540b5e735355faa098d9bc/simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8", size = 148700, upload-time = "2025-09-26T16:27:40.171Z" }, - { url = "https://files.pythonhosted.org/packages/c7/28/c32121064b1ec2fb7b5d872d9a1abda62df064d35e0160eddfa907118343/simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7", size = 141323, upload-time = "2025-09-26T16:27:41.324Z" }, - { url = "https://files.pythonhosted.org/packages/46/b6/c897c54326fe86dd12d101981171a49361949f4728294f418c3b86a1af77/simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53", size = 74377, upload-time = "2025-09-26T16:27:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/ad/87/a6e03d4d80cca99c1fee4e960f3440e2f21be9470e537970f960ca5547f1/simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476", size = 76081, upload-time = "2025-09-26T16:27:43.945Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3e/96898c6c66d9dca3f9bd14d7487bf783b4acc77471b42f979babbb68d4ca/simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f", size = 92633, upload-time = "2025-09-26T16:27:45.028Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a2/cd2e10b880368305d89dd540685b8bdcc136df2b3c76b5ddd72596254539/simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8", size = 75309, upload-time = "2025-09-26T16:27:46.142Z" }, - { url = "https://files.pythonhosted.org/packages/5d/02/290f7282eaa6ebe945d35c47e6534348af97472446951dce0d144e013f4c/simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a", size = 75308, upload-time = "2025-09-26T16:27:47.542Z" }, - { url = "https://files.pythonhosted.org/packages/43/91/43695f17b69e70c4b0b03247aa47fb3989d338a70c4b726bbdc2da184160/simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51", size = 143733, upload-time = "2025-09-26T16:27:48.673Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4b/fdcaf444ac1c3cbf1c52bf00320c499e1cf05d373a58a3731ae627ba5e2d/simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd", size = 153397, upload-time = "2025-09-26T16:27:49.89Z" }, - { url = "https://files.pythonhosted.org/packages/c4/83/21550f81a50cd03599f048a2d588ffb7f4c4d8064ae091511e8e5848eeaa/simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013", size = 141654, upload-time = "2025-09-26T16:27:51.168Z" }, - { url = "https://files.pythonhosted.org/packages/cf/54/d76c0e72ad02450a3e723b65b04f49001d0e73218ef6a220b158a64639cb/simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81", size = 144913, upload-time = "2025-09-26T16:27:52.331Z" }, - { url = "https://files.pythonhosted.org/packages/3f/49/976f59b42a6956d4aeb075ada16ad64448a985704bc69cd427a2245ce835/simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa", size = 144568, upload-time = "2025-09-26T16:27:53.41Z" }, - { url = "https://files.pythonhosted.org/packages/60/c7/30bae30424ace8cd791ca660fed454ed9479233810fe25c3f3eab3d9dc7b/simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb", size = 146239, upload-time = "2025-09-26T16:27:54.502Z" }, - { url = "https://files.pythonhosted.org/packages/79/3e/7f3b7b97351c53746e7b996fcd106986cda1954ab556fd665314756618d2/simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2", size = 154497, upload-time = "2025-09-26T16:27:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/1d/48/7241daa91d0bf19126589f6a8dcbe8287f4ed3d734e76fd4a092708947be/simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413", size = 148069, upload-time = "2025-09-26T16:27:57.039Z" }, - { url = "https://files.pythonhosted.org/packages/e6/f4/ef18d2962fe53e7be5123d3784e623859eec7ed97060c9c8536c69d34836/simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961", size = 74158, upload-time = "2025-09-26T16:27:58.265Z" }, - { url = "https://files.pythonhosted.org/packages/35/fd/3d1158ecdc573fdad81bf3cc78df04522bf3959758bba6597ba4c956c74d/simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c", size = 75911, upload-time = "2025-09-26T16:27:59.292Z" }, - { url = "https://files.pythonhosted.org/packages/9d/9e/1a91e7614db0416885eab4136d49b7303de20528860ffdd798ce04d054db/simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6", size = 93523, upload-time = "2025-09-26T16:28:00.356Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2b/d2413f5218fc25608739e3d63fe321dfa85c5f097aa6648dbe72513a5f12/simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259", size = 75844, upload-time = "2025-09-26T16:28:01.756Z" }, - { url = "https://files.pythonhosted.org/packages/ad/f1/efd09efcc1e26629e120fef59be059ce7841cc6e1f949a4db94f1ae8a918/simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8", size = 75655, upload-time = "2025-09-26T16:28:03.037Z" }, - { url = "https://files.pythonhosted.org/packages/97/ec/5c6db08e42f380f005d03944be1af1a6bd501cc641175429a1cbe7fb23b9/simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9", size = 150335, upload-time = "2025-09-26T16:28:05.027Z" }, - { url = "https://files.pythonhosted.org/packages/81/f5/808a907485876a9242ec67054da7cbebefe0ee1522ef1c0be3bfc90f96f6/simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868", size = 158519, upload-time = "2025-09-26T16:28:06.5Z" }, - { url = "https://files.pythonhosted.org/packages/66/af/b8a158246834645ea890c36136584b0cc1c0e4b83a73b11ebd9c2a12877c/simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b", size = 148571, upload-time = "2025-09-26T16:28:07.715Z" }, - { url = "https://files.pythonhosted.org/packages/20/05/ed9b2571bbf38f1a2425391f18e3ac11cb1e91482c22d644a1640dea9da7/simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e", size = 152367, upload-time = "2025-09-26T16:28:08.921Z" }, - { url = "https://files.pythonhosted.org/packages/81/2c/bad68b05dd43e93f77994b920505634d31ed239418eb6a88997d06599983/simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c", size = 150205, upload-time = "2025-09-26T16:28:10.086Z" }, - { url = "https://files.pythonhosted.org/packages/69/46/90c7fc878061adafcf298ce60cecdee17a027486e9dce507e87396d68255/simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970", size = 151823, upload-time = "2025-09-26T16:28:11.329Z" }, - { url = "https://files.pythonhosted.org/packages/ab/27/b85b03349f825ae0f5d4f780cdde0bbccd4f06c3d8433f6a3882df887481/simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e", size = 158997, upload-time = "2025-09-26T16:28:12.917Z" }, - { url = "https://files.pythonhosted.org/packages/71/ad/d7f3c331fb930638420ac6d236db68e9f4c28dab9c03164c3cd0e7967e15/simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544", size = 154367, upload-time = "2025-09-26T16:28:14.393Z" }, - { url = "https://files.pythonhosted.org/packages/f0/46/5c67324addd40fa2966f6e886cacbbe0407c03a500db94fb8bb40333fcdf/simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54", size = 74285, upload-time = "2025-09-26T16:28:15.931Z" }, - { url = "https://files.pythonhosted.org/packages/fa/c9/5cc2189f4acd3a6e30ffa9775bf09b354302dbebab713ca914d7134d0f29/simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab", size = 75969, upload-time = "2025-09-26T16:28:17.017Z" }, - { url = "https://files.pythonhosted.org/packages/5e/9e/f326d43f6bf47f4e7704a4426c36e044c6bedfd24e072fb8e27589a373a5/simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86", size = 93530, upload-time = "2025-09-26T16:28:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/35/28/5a4b8f3483fbfb68f3f460bc002cef3a5735ef30950e7c4adce9c8da15c7/simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74", size = 75846, upload-time = "2025-09-26T16:28:19.12Z" }, - { url = "https://files.pythonhosted.org/packages/7a/4d/30dfef83b9ac48afae1cf1ab19c2867e27b8d22b5d9f8ca7ce5a0a157d8c/simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726", size = 75661, upload-time = "2025-09-26T16:28:20.219Z" }, - { url = "https://files.pythonhosted.org/packages/09/1d/171009bd35c7099d72ef6afd4bb13527bab469965c968a17d69a203d62a6/simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5", size = 150579, upload-time = "2025-09-26T16:28:21.337Z" }, - { url = "https://files.pythonhosted.org/packages/61/ae/229bbcf90a702adc6bfa476e9f0a37e21d8c58e1059043038797cbe75b8c/simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d", size = 158797, upload-time = "2025-09-26T16:28:22.53Z" }, - { url = "https://files.pythonhosted.org/packages/90/c5/fefc0ac6b86b9108e302e0af1cf57518f46da0baedd60a12170791d56959/simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0", size = 148851, upload-time = "2025-09-26T16:28:23.733Z" }, - { url = "https://files.pythonhosted.org/packages/43/f1/b392952200f3393bb06fbc4dd975fc63a6843261705839355560b7264eb2/simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b", size = 152598, upload-time = "2025-09-26T16:28:24.962Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b4/d6b7279e52a3e9c0fa8c032ce6164e593e8d9cf390698ee981ed0864291b/simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f", size = 150498, upload-time = "2025-09-26T16:28:26.114Z" }, - { url = "https://files.pythonhosted.org/packages/62/22/ec2490dd859224326d10c2fac1353e8ad5c84121be4837a6dd6638ba4345/simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522", size = 152129, upload-time = "2025-09-26T16:28:27.552Z" }, - { url = "https://files.pythonhosted.org/packages/33/ce/b60214d013e93dd9e5a705dcb2b88b6c72bada442a97f79828332217f3eb/simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3", size = 159359, upload-time = "2025-09-26T16:28:28.667Z" }, - { url = "https://files.pythonhosted.org/packages/99/21/603709455827cdf5b9d83abe726343f542491ca8dc6a2528eb08de0cf034/simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769", size = 154717, upload-time = "2025-09-26T16:28:30.288Z" }, - { url = "https://files.pythonhosted.org/packages/3c/f9/dc7f7a4bac16cf7eb55a4df03ad93190e11826d2a8950052949d3dfc11e2/simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661", size = 74289, upload-time = "2025-09-26T16:28:31.809Z" }, - { url = "https://files.pythonhosted.org/packages/87/10/d42ad61230436735c68af1120622b28a782877146a83d714da7b6a2a1c4e/simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608", size = 75972, upload-time = "2025-09-26T16:28:32.883Z" }, - { url = "https://files.pythonhosted.org/packages/05/5b/83e1ff87eb60ca706972f7e02e15c0b33396e7bdbd080069a5d1b53cf0d8/simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017", size = 57309, upload-time = "2025-09-26T16:29:35.312Z" }, -] - -[[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 = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - -[[package]] -name = "sounddevice" -version = "0.5.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" }, - { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" }, - { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" }, - { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" }, - { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" }, -] - -[[package]] -name = "soundfile" -version = "0.13.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/28/e2a36573ccbcf3d57c00626a21fe51989380636e821b341d36ccca0c1c3a/soundfile-0.13.1-py2.py3-none-any.whl", hash = "sha256:a23c717560da2cf4c7b5ae1142514e0fd82d6bbd9dfc93a50423447142f2c445", size = 25751, upload-time = "2025-01-25T09:16:44.235Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/73e97a5b3cc46bba7ff8650a1504348fa1863a6f9d57d7001c6b67c5f20e/soundfile-0.13.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:82dc664d19831933fe59adad199bf3945ad06d84bc111a5b4c0d3089a5b9ec33", size = 1142250, upload-time = "2025-01-25T09:16:47.583Z" }, - { url = "https://files.pythonhosted.org/packages/a0/e5/58fd1a8d7b26fc113af244f966ee3aecf03cb9293cb935daaddc1e455e18/soundfile-0.13.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:743f12c12c4054921e15736c6be09ac26b3b3d603aef6fd69f9dde68748f2593", size = 1101406, upload-time = "2025-01-25T09:16:49.662Z" }, - { url = "https://files.pythonhosted.org/packages/58/ae/c0e4a53d77cf6e9a04179535766b3321b0b9ced5f70522e4caf9329f0046/soundfile-0.13.1-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9c9e855f5a4d06ce4213f31918653ab7de0c5a8d8107cd2427e44b42df547deb", size = 1235729, upload-time = "2025-01-25T09:16:53.018Z" }, - { url = "https://files.pythonhosted.org/packages/57/5e/70bdd9579b35003a489fc850b5047beeda26328053ebadc1fb60f320f7db/soundfile-0.13.1-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:03267c4e493315294834a0870f31dbb3b28a95561b80b134f0bd3cf2d5f0e618", size = 1313646, upload-time = "2025-01-25T09:16:54.872Z" }, - { url = "https://files.pythonhosted.org/packages/fe/df/8c11dc4dfceda14e3003bb81a0d0edcaaf0796dd7b4f826ea3e532146bba/soundfile-0.13.1-py2.py3-none-win32.whl", hash = "sha256:c734564fab7c5ddf8e9be5bf70bab68042cd17e9c214c06e365e20d64f9a69d5", size = 899881, upload-time = "2025-01-25T09:16:56.663Z" }, - { url = "https://files.pythonhosted.org/packages/14/e9/6b761de83277f2f02ded7e7ea6f07828ec78e4b229b80e4ca55dd205b9dc/soundfile-0.13.1-py2.py3-none-win_amd64.whl", hash = "sha256:1e70a05a0626524a69e9f0f4dd2ec174b4e9567f4d8b6c11d38b5c289be36ee9", size = 1019162, upload-time = "2025-01-25T09:16:59.573Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" }, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, -] - -[[package]] -name = "starlette" -version = "0.52.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, -] - -[[package]] -name = "structlog" -version = "25.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/52/9ba0f43b686e7f3ddfeaa78ac3af750292662284b3661e91ad5494f21dbc/structlog-25.5.0.tar.gz", hash = "sha256:098522a3bebed9153d4570c6d0288abf80a031dfdb2048d59a49e9dc2190fc98", size = 1460830, upload-time = "2025-10-27T08:28:23.028Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/45/a132b9074aa18e799b891b91ad72133c98d8042c70f6240e4c5f9dabee2f/structlog-25.5.0-py3-none-any.whl", hash = "sha256:a8453e9b9e636ec59bd9e79bbd4a72f025981b3ba0f5837aebf48f02f37a7f9f", size = 72510, upload-time = "2025-10-27T08:28:21.535Z" }, -] - -[[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 = "tblib" -version = "3.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/8a/14c15ae154895cc131174f858c707790d416c444fc69f93918adfd8c4c0b/tblib-3.2.2.tar.gz", hash = "sha256:e9a652692d91bf4f743d4a15bc174c0b76afc750fe8c7b6d195cc1c1d6d2ccec", size = 35046, upload-time = "2025-11-12T12:21:16.572Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl", hash = "sha256:26bdccf339bcce6a88b2b5432c988b266ebbe63a4e593f6b578b1d2e723d2b76", size = 12893, upload-time = "2025-11-12T12:21:14.407Z" }, -] - -[[package]] -name = "tenacity" -version = "9.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, -] - -[[package]] -name = "tensorboard" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "grpcio" }, - { name = "markdown" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pillow" }, - { name = "protobuf" }, - { name = "setuptools" }, - { name = "tensorboard-data-server" }, - { name = "werkzeug" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, -] - -[[package]] -name = "tensorboard-data-server" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, - { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, - { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, -] - -[[package]] -name = "tensorboardx" -version = "2.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/c5/d4cc6e293fb837aaf9f76dd7745476aeba8ef7ef5146c3b3f9ee375fe7a5/tensorboardx-2.6.4.tar.gz", hash = "sha256:b163ccb7798b31100b9f5fa4d6bc22dad362d7065c2f24b51e50731adde86828", size = 4769801, upload-time = "2025-06-10T22:37:07.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/1d/b5d63f1a6b824282b57f7b581810d20b7a28ca951f2d5b59f1eb0782c12b/tensorboardx-2.6.4-py3-none-any.whl", hash = "sha256:5970cf3a1f0a6a6e8b180ccf46f3fe832b8a25a70b86e5a237048a7c0beb18e2", size = 87201, upload-time = "2025-06-10T22:37:05.44Z" }, -] - -[[package]] -name = "tensorstore" -version = "0.1.78" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version < '3.11' and sys_platform == 'win32'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "ml-dtypes", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/ee/05eb424437f4db63331c90e4605025eedc0f71da3faff97161d5d7b405af/tensorstore-0.1.78.tar.gz", hash = "sha256:e26074ffe462394cf54197eb76d6569b500f347573cd74da3f4dd5f510a4ad7c", size = 6913502, upload-time = "2025-10-06T17:44:29.649Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/1e/77eff7bb320f72a9cb6e9a19eee4d78bee4a6ac1c28ceef60df28b4ab670/tensorstore-0.1.78-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f1bc58164ad964d9cc298d20b62ca704ab6241639a21015e47ce6ea5b5cae27f", size = 15710776, upload-time = "2025-10-06T17:43:47.469Z" }, - { url = "https://files.pythonhosted.org/packages/55/df/f74f8004b246006ae03c90c28e32d71eb8a86a5b325d2d84dda327babdcc/tensorstore-0.1.78-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1910101ea85b6507958da28628ef53712c5311df19a795f449604f82bae6a24b", size = 13771121, upload-time = "2025-10-06T17:43:49.88Z" }, - { url = "https://files.pythonhosted.org/packages/be/b8/ab0d0b2afc53f47fbfd95c10d9ae21d393019aca45c8513657b8d7002f1f/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e92195db0c8c3ca749f24b1e930ab93382ac27430ac4ad2e3f53fc8f739323f", size = 18154513, upload-time = "2025-10-06T17:43:51.694Z" }, - { url = "https://files.pythonhosted.org/packages/f7/ea/c1b4cc6a089a39f63e8d189a55c715e393995628b12b4c8560b3ae4874ba/tensorstore-0.1.78-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90570b867f9100f7405e4116c73910d0bd283a101500ea5680c5a8a881ea05c6", size = 20048971, upload-time = "2025-10-06T17:43:54.358Z" }, - { url = "https://files.pythonhosted.org/packages/58/2a/7167087885b12473f20ae4fddb9a8feeed6bd44ea8d42c73ae29ad3d1591/tensorstore-0.1.78-cp310-cp310-win_amd64.whl", hash = "sha256:4de9d4ee93d712cb665890af0738f4d74cac3b9b9a0492d477a3ee63fbbf445b", size = 12707793, upload-time = "2025-10-06T17:43:56.405Z" }, - { url = "https://files.pythonhosted.org/packages/33/b1/45070c393586306cef44c7bfc47ed2eddfb8930e648aaa847f615e3ae797/tensorstore-0.1.78-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1c91e7ff93561612bd9868f3ee56702b0e4fecb45079a4c152dff9a6aa751913", size = 15712387, upload-time = "2025-10-06T17:43:58.458Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d8/c045da71460301f37704e1ab1eec9e7e480dc711dbd281d86dc3d792c50e/tensorstore-0.1.78-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:781e123d392b2d9115e94b01849797a4540f54cd6d34c6ee32b9491f2f2a399c", size = 13773158, upload-time = "2025-10-06T17:44:00.285Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e8/2b0d48100816649ec516fca31d02ad8028c090324e77b1c309c09a172350/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e650d363ad43754626a828a242785e6359a59fedb171276e9a0c66c0bd963cd4", size = 18154388, upload-time = "2025-10-06T17:44:02.428Z" }, - { url = "https://files.pythonhosted.org/packages/3e/a1/d9be82de18afe764c0fc7fb21b3d3bb0ad12845d202861fff7189afdb99d/tensorstore-0.1.78-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33fed0ffa7a42ad24ce203486cf039f81b211723b45bd54859ba237a9d3aedb9", size = 20050304, upload-time = "2025-10-06T17:44:04.673Z" }, - { url = "https://files.pythonhosted.org/packages/d1/fc/b980958f91a9780e4dbc1038da723d2ad91307dbe30563359606f78926e5/tensorstore-0.1.78-cp311-cp311-win_amd64.whl", hash = "sha256:c02df3d8de4703d9ee42c8f620b2288f41c19a0fd5ffa907b72a736678e22188", size = 12708115, upload-time = "2025-10-06T17:44:06.574Z" }, - { url = "https://files.pythonhosted.org/packages/d0/5f/5853c04bebaed2d3c0ada9245328ffe3fff8b0f0f1c64f4776f67b42033f/tensorstore-0.1.78-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:ce375a8f6621cdb94638b9cdc5266519db16a58353d4c6920e8b9d6bdd419e21", size = 15727539, upload-time = "2025-10-06T17:44:08.631Z" }, - { url = "https://files.pythonhosted.org/packages/a2/e2/f67fcca8f90258c1cf1326aa366fe10f559f4c60102f53fdcc6614159c45/tensorstore-0.1.78-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82f68fa5a3b4c84365a667ea0a7465a53d5d969c4d3909ac990f314d1569ffc3", size = 13780753, upload-time = "2025-10-06T17:44:10.488Z" }, - { url = "https://files.pythonhosted.org/packages/57/de/95013db6ef3b6a14b4237b95184c21becdf56d16605bf42903bb141f729e/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dc0bd6361d73e3f67d70980f96f4e8bcbd8e810b5475a01333ca9c37f0785a5", size = 18157446, upload-time = "2025-10-06T17:44:12.831Z" }, - { url = "https://files.pythonhosted.org/packages/e2/75/6e7cef68cab3a672c6668cc80c399ae6626a498a3ef04b35b3704b41e9cc/tensorstore-0.1.78-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75a17cef99f05fad9cc6fda37f1a1868d5f1502fd577af13174382931481c948", size = 20060211, upload-time = "2025-10-06T17:44:15.189Z" }, - { url = "https://files.pythonhosted.org/packages/1e/46/4ff3e395c44348c7442523c8ddd8ccc72d9ac81838e7a8f6afdd92131c3e/tensorstore-0.1.78-cp312-cp312-win_amd64.whl", hash = "sha256:56271d4652a7cb445879089f620af47801c091765d35a005505d6bfb8d00c535", size = 12711274, upload-time = "2025-10-06T17:44:17.586Z" }, - { url = "https://files.pythonhosted.org/packages/18/36/cfb5a2acf9005896c88f80b93c2aee42f00fab9d0045369fef6e1b297242/tensorstore-0.1.78-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:8a1d0ae7996c80f2e623be5b8cfbc32a307d08dfef3d2dcb455f592908ecd46d", size = 15727334, upload-time = "2025-10-06T17:44:19.93Z" }, - { url = "https://files.pythonhosted.org/packages/54/cd/d1bcc3aab5be4298616dbc060b5aa2012b686270aaa16a9579c7945d0a1c/tensorstore-0.1.78-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:311846cfb2d644cd4a7861005e521a79816093e76d7924c83de5d06ca323067e", size = 13780722, upload-time = "2025-10-06T17:44:21.822Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3b/b0bb4440a9d67859b1abb367e436c62b0a27991dd7109f20be9dabff488f/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630538a66eb9964bd2975c4e09ae83be9984f2e4ebd5f7969983137bfda92071", size = 18157269, upload-time = "2025-10-06T17:44:23.743Z" }, - { url = "https://files.pythonhosted.org/packages/68/d6/d95cde18ca2475bf317051b2be168cc963c5cfcd67e9c59786326ccdca53/tensorstore-0.1.78-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6886bec93b8ba22f83c4dc9e7c1ee20b11025ea9a5a839de21d0cbf7fd7aada2", size = 20060053, upload-time = "2025-10-06T17:44:25.942Z" }, - { url = "https://files.pythonhosted.org/packages/db/a2/dbd1af0e97d5d549051309d72c6e3f2fe81fae636f9db3692d21adc9c731/tensorstore-0.1.78-cp313-cp313-win_amd64.whl", hash = "sha256:e0073de8fa3074bc4cc92ced0210310fd89851899faf42a5ba256f0ba87d095c", size = 12711250, upload-time = "2025-10-06T17:44:27.926Z" }, -] - -[[package]] -name = "tensorstore" -version = "0.1.81" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'darwin'", - "python_full_version == '3.13.*' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and sys_platform == 'win32'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux' and sys_platform != 'win32')", -] -dependencies = [ - { name = "ml-dtypes", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/f6/e2403fc05b97ba74ad408a98a42c288e6e1b8eacc23780c153b0e5166179/tensorstore-0.1.81.tar.gz", hash = "sha256:687546192ea6f6c8ae28d18f13103336f68017d928b9f5a00325e9b0548d9c25", size = 7120819, upload-time = "2026-02-06T18:56:12.535Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/df/f472bd0dee801d7e33c53335ad0fcde9c71e5f9324241faa0a6b4be4270a/tensorstore-0.1.81-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:f64fb510f293079f9e5c63cb227e8a76904655a32912fc107c1e63bd8dc3e187", size = 16501390, upload-time = "2026-02-06T18:55:13.678Z" }, - { url = "https://files.pythonhosted.org/packages/5a/93/5f40c51d7b15d3574b1788a251dd4e3abd0415dab71811e126d2da5e826b/tensorstore-0.1.81-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4282587598885ff447f08369ac9bb681a65e224888cfa8ef8f3dd63544759e6c", size = 14535592, upload-time = "2026-02-06T18:55:16.44Z" }, - { url = "https://files.pythonhosted.org/packages/76/48/b7adcc8eca502ce8050c18cea066ca0c0122df7a686e10da6470e55456b4/tensorstore-0.1.81-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b4ea06038f6912bb6ed8a89db0c31e4e3d1b2404f3365dc756e4bc42bd6a89c", size = 19038732, upload-time = "2026-02-06T18:55:18.924Z" }, - { url = "https://files.pythonhosted.org/packages/40/b0/99294895b030bd7d9ebc06e7ed523d0c09ab65667e031f8a67923f398f86/tensorstore-0.1.81-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51d59f7db9cdae02fce9d347300c0ccfb8265052945757e95592a265eb620b15", size = 21038447, upload-time = "2026-02-06T18:55:21.085Z" }, - { url = "https://files.pythonhosted.org/packages/32/e6/1ce977baf09aa3889f10f04460b588a6c8876ea441e51090c671f0400a6f/tensorstore-0.1.81-cp311-cp311-win_amd64.whl", hash = "sha256:fdb9579a729cccc02127cab5abf26f57a0e27968ba65c9c548ad058f5a45417f", size = 13221673, upload-time = "2026-02-06T18:55:23.195Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/00037db699f74d792efe2696305ddd6932e04306899e3701824a7f7de961/tensorstore-0.1.81-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:7aefa1e3eadca804bce05215184c9cde29205ac2f3b443ca15a4e1846d31af4e", size = 16521245, upload-time = "2026-02-06T18:55:25.559Z" }, - { url = "https://files.pythonhosted.org/packages/86/2e/1deca1b955cb959eec13fd342ffaa2fd84e4770b4e2bcb95a2f541875a52/tensorstore-0.1.81-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7e001d3edc6758eb5dc80556da9e945c1381f0529102fcc0301358ba6b9b70ed", size = 14543561, upload-time = "2026-02-06T18:55:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/b4343eae773f72a8777f82c5328191a06d8a5195e62105c14b7dcc49823f/tensorstore-0.1.81-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c27e07f4e91e6dc6a0878e13e2c5931d1716196b67b0df927f2f571de2576e9", size = 19043982, upload-time = "2026-02-06T18:55:30.076Z" }, - { url = "https://files.pythonhosted.org/packages/31/6c/d8c8508a9f4a83dc910d2365c484ba0debf5e531782065e3657fc8fc9b54/tensorstore-0.1.81-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcb4786c4955e2d88d518b5b5a367427e3ad21d059cba366ad7aebf5fcc2302e", size = 21049171, upload-time = "2026-02-06T18:55:34.383Z" }, - { url = "https://files.pythonhosted.org/packages/44/a9/c1a751e35a0fcff7f795398c4f98b6c8ea0f00fe7d7704f66a1e08d4352f/tensorstore-0.1.81-cp312-cp312-win_amd64.whl", hash = "sha256:b96cbf1ee74d9038762b2d81305ee1589ec89913a440df6cbd514bc5879655d2", size = 13226573, upload-time = "2026-02-06T18:55:36.463Z" }, - { url = "https://files.pythonhosted.org/packages/06/c0/32f7d52bfcf1728f557cccb17ac85f57bcc3fa92f4034368d6e7d7d06406/tensorstore-0.1.81-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:7bb563ad4d4d6c4748d9fe4f01f639ddf4ffef83ac180fc3b6d73f46ad854e62", size = 16521316, upload-time = "2026-02-06T18:55:39.557Z" }, - { url = "https://files.pythonhosted.org/packages/38/b9/06ffc44e38ca18aeb3973f6b709d4d2102e17a8d700c7c3e2af3f2830722/tensorstore-0.1.81-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2ff7e6c457596cf21f31c690e451fe634ac804fc98ff8131188e99d5ef7d29bc", size = 14543212, upload-time = "2026-02-06T18:55:42.246Z" }, - { url = "https://files.pythonhosted.org/packages/00/01/3c27962f7258ad0bb552c3cd324fa2e01f746c8b6e81bd25d468f72204e8/tensorstore-0.1.81-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b218a6fe09c72c002f2c6480fc58b78cdbba8bb9c6f3a0d7dd1f70625cb37995", size = 19044489, upload-time = "2026-02-06T18:55:44.957Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/fe0f14a1da96d6e0aa6c24d6c31f3ce4b203f8e8a1a2e359489e52b33400/tensorstore-0.1.81-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33e7c11035c14dad01aeba012051643110cbb95c239e512106fe1be692c98b6", size = 21052658, upload-time = "2026-02-06T18:55:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e2/cc189d799982f02c200b22405c4d3f28845df6321de2ac3a35ae087758ed/tensorstore-0.1.81-cp313-cp313-win_amd64.whl", hash = "sha256:b55126bcf084cc5fe0151bf465f3a5dedb5b5da0133d01227f75d0e71f9cfae5", size = 13226848, upload-time = "2026-02-06T18:55:49.631Z" }, - { url = "https://files.pythonhosted.org/packages/89/b0/0ca436391f832fad365977623f3c08c4fbbf553fd9a112604aa106646654/tensorstore-0.1.81-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a48c23e4df50681d8f4f365b08a0beb114ab210accbde9f34d37fd7b45c31005", size = 16525537, upload-time = "2026-02-06T18:55:51.708Z" }, - { url = "https://files.pythonhosted.org/packages/8a/02/c10052b86cf8d47b4cf41e5f139b4003c69bb69e506759b0eb87b873d213/tensorstore-0.1.81-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0be0ce646263820f3d4c9ba738d8e9be7da241cbe093ca2fd02e25023344347c", size = 14547490, upload-time = "2026-02-06T18:55:53.899Z" }, - { url = "https://files.pythonhosted.org/packages/01/d1/bd86c46367624522967e896ca45d77ba9085de3f15081fdad6576ba70aa9/tensorstore-0.1.81-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93996e756dce82589f5a19e27b4e7c0b5b40221a7e41ddce46dc13d378dbd157", size = 19050938, upload-time = "2026-02-06T18:55:56.123Z" }, - { url = "https://files.pythonhosted.org/packages/11/a2/59a8e9a33cd9e17461f918bda4a20712ed3c51c52e0e42b2f673441bc90d/tensorstore-0.1.81-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:444c088919a739c20ca1f87935d72de4fd87605eb2c0f093b8d49251b7884aef", size = 21055275, upload-time = "2026-02-06T18:55:58.259Z" }, - { url = "https://files.pythonhosted.org/packages/c5/ec/2988f210729b523975b1bee030cabd64b256943c08463331598f1e03bd4f/tensorstore-0.1.81-cp314-cp314-win_amd64.whl", hash = "sha256:f7aa0a3a470c4d832faff7d77dd688b1d352b718d110c95ceba54ec637ca3ffa", size = 13614713, upload-time = "2026-02-06T18:56:00.291Z" }, - { url = "https://files.pythonhosted.org/packages/ae/5d/60e990df3f1dc57c33644375a0eccb906a79fd8a5e2d81238f856c65ad7f/tensorstore-0.1.81-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6c36d8a827120aa15e50ec5c36dd7e73978d86ba4f46d073fb648d8dda3948e9", size = 16605091, upload-time = "2026-02-06T18:56:02.807Z" }, - { url = "https://files.pythonhosted.org/packages/85/22/f599576815227735d3e34f86f05a8b39d8b15fd979d0029383ebae23978d/tensorstore-0.1.81-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3c31d831707c4ff3c6ecdcba129f7c39e982572837b2f93e02ccb83fc8581bca", size = 14631573, upload-time = "2026-02-06T18:56:04.892Z" }, - { url = "https://files.pythonhosted.org/packages/cb/76/b5d0b424b7af057a3d4de3f312eba9ddf8a3c750a766b42e0b7f6c2ebef0/tensorstore-0.1.81-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fba383f108d7450bf9a03487ac7fa3bb2c3080c91cee9d2da3bb217b560846b", size = 19065251, upload-time = "2026-02-06T18:56:06.972Z" }, - { url = "https://files.pythonhosted.org/packages/54/6c/0f113eae73b1e8eb2f712cf5f1efd269452f0f0045158fae43ce7b4701b4/tensorstore-0.1.81-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88c52f592e2982682045199cabf360462146749d48b7be2969cd640e877c6c3", size = 21066488, upload-time = "2026-02-06T18:56:10.236Z" }, -] - -[[package]] -name = "tensorzero" -version = "2025.7.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5d/63/188bb1f520123008be982f815b7c35234d924d90c1284016ecc6c95886d9/tensorzero-2025.7.5.tar.gz", hash = "sha256:cb366f3c355524e3e0a2a3a2a80e96454d2e5816e2789fb8b93de03874318383", size = 1218364, upload-time = "2025-07-30T16:24:04.804Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/2f/7d57e631f3c14c585dbefb779d11e441c2f22cd03748c75375b0ad08d1ea/tensorzero-2025.7.5-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:40b589770c86cea5942d144300f381d851351defa9efd0986a0d87b8735f7a07", size = 16389069, upload-time = "2025-07-30T16:23:57.282Z" }, - { url = "https://files.pythonhosted.org/packages/c4/73/3673c9f30e81f3107db3a6a600c8846c9b2edd57b9dcb15ea4c03182dd23/tensorzero-2025.7.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a24fed842f2485be39bcbf1c8280a2538e6bfdbd3ab615e2583ae9c86743dd9d", size = 15522191, upload-time = "2025-07-30T16:23:54.692Z" }, - { url = "https://files.pythonhosted.org/packages/94/0d/0d604dbe1089f482767fb8fc227b381d922df72108e6ace87f1884cb4db4/tensorzero-2025.7.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b32b47e1a5f1f6c769eb067698a8ad804f6189f1588e0f4e45445ee9dc329164", size = 16034337, upload-time = "2025-07-30T16:23:47.152Z" }, - { url = "https://files.pythonhosted.org/packages/fa/81/a6ad537839c874c9b03ce5473b4bcc4348f58fa7d6e63baba9f425d98c1c/tensorzero-2025.7.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9338617764a65d0be9482246d84ddc9a76d9c6524abd1e4d10db48f3a2abb180", size = 17233682, upload-time = "2025-07-30T16:23:50.139Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b4/4c43957672ad7bf4d49050c67ddf0ed3b31dfe2ccd990a1d9bc04241e61c/tensorzero-2025.7.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db6fbc8b522f43da219ab9f71c2177295fc6820e9168a98b94facb75317987ab", size = 16112384, upload-time = "2025-07-30T16:23:59.98Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a7/936433b56a6506c1b6ee0476c41e39539fb14dca54aefacb30179bc0b086/tensorzero-2025.7.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d4e17147f9449df8cf6aad0f18c936f1170c0cb59b07760dd09abb47a29b40", size = 17445354, upload-time = "2025-07-30T16:24:02.43Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fd/88f4368b71ae8c4bd1e3ed99c1660467760ca6cfbd31d9167f3a010f9d02/tensorzero-2025.7.5-cp39-abi3-win_amd64.whl", hash = "sha256:a80d9739c61c8d839f8d4f9f61d6333ca13b2bd7ea1bb021ea989dd15a8eb39e", size = 17174978, upload-time = "2025-07-30T16:24:08.122Z" }, -] - -[[package]] -name = "terminaltexteffects" -version = "0.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/92/0eb3f0ad206bf449b7db75f061202dce27d8cb90e598ce3c7d32c0bd80b9/terminaltexteffects-0.12.2.tar.gz", hash = "sha256:4a5eef341d538743e7ac4341cd74d47afc9d0345acdad330ed03fd0a72e41f5f", size = 164321, upload-time = "2025-10-20T20:58:26.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/93/a588ab8b15ceeef23042aa52660fb4891a0e955e92cd3aa97dcafe621720/terminaltexteffects-0.12.2-py3-none-any.whl", hash = "sha256:4b986034094007aa9a31cb1bd16d5d8fcac9755fb6a5da8f74ee7b70c0fa2d63", size = 189344, upload-time = "2025-10-20T20:58:24.425Z" }, -] - -[[package]] -name = "textual" -version = "3.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py", extra = ["linkify", "plugins"] }, - { name = "platformdirs" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/af/83/c99c252c3fad2f7010ceb476a31af042eec71da441ffeef75bb590bc2e9e/textual-3.7.1.tar.gz", hash = "sha256:a76ba0c8a6c194ef24fd5c3681ebfddca55e7127c064a014128c84fbd7f5d271", size = 1604038, upload-time = "2025-07-09T09:04:45.477Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/f1/8929fcce6dc983f7a260d0f3ddd2a69b74ba17383dbe57a7e0a9e085e8be/textual-3.7.1-py3-none-any.whl", hash = "sha256:ab5d153f4f65e77017977fa150d0376409e0acf5f1d2e25e2e4ab9de6c0d61ff", size = 691472, upload-time = "2025-07-09T09:04:43.626Z" }, -] - -[[package]] -name = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - -[[package]] -name = "tiktoken" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/b3/2cb7c17b6c4cf8ca983204255d3f1d95eda7213e247e6947a0ee2c747a2c/tiktoken-0.12.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3de02f5a491cfd179aec916eddb70331814bd6bf764075d39e21d5862e533970", size = 1051991, upload-time = "2025-10-06T20:21:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/df139f1df5f6167194ee5ab24634582ba9a1b62c6b996472b0277ec80f66/tiktoken-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b6cfb6d9b7b54d20af21a912bfe63a2727d9cfa8fbda642fd8322c70340aad16", size = 995798, upload-time = "2025-10-06T20:21:35.579Z" }, - { url = "https://files.pythonhosted.org/packages/ef/5d/26a691f28ab220d5edc09b9b787399b130f24327ef824de15e5d85ef21aa/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:cde24cdb1b8a08368f709124f15b36ab5524aac5fa830cc3fdce9c03d4fb8030", size = 1129865, upload-time = "2025-10-06T20:21:36.675Z" }, - { url = "https://files.pythonhosted.org/packages/b2/94/443fab3d4e5ebecac895712abd3849b8da93b7b7dec61c7db5c9c7ebe40c/tiktoken-0.12.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6de0da39f605992649b9cfa6f84071e3f9ef2cec458d08c5feb1b6f0ff62e134", size = 1152856, upload-time = "2025-10-06T20:21:37.873Z" }, - { url = "https://files.pythonhosted.org/packages/54/35/388f941251b2521c70dd4c5958e598ea6d2c88e28445d2fb8189eecc1dfc/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6faa0534e0eefbcafaccb75927a4a380463a2eaa7e26000f0173b920e98b720a", size = 1195308, upload-time = "2025-10-06T20:21:39.577Z" }, - { url = "https://files.pythonhosted.org/packages/f8/00/c6681c7f833dd410576183715a530437a9873fa910265817081f65f9105f/tiktoken-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:82991e04fc860afb933efb63957affc7ad54f83e2216fe7d319007dab1ba5892", size = 1255697, upload-time = "2025-10-06T20:21:41.154Z" }, - { url = "https://files.pythonhosted.org/packages/5f/d2/82e795a6a9bafa034bf26a58e68fe9a89eeaaa610d51dbeb22106ba04f0a/tiktoken-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:6fb2995b487c2e31acf0a9e17647e3b242235a20832642bb7a9d1a181c0c1bb1", size = 879375, upload-time = "2025-10-06T20:21:43.201Z" }, - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "timm" -version = "1.0.24" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "pyyaml" }, - { name = "safetensors" }, - { name = "torch" }, - { name = "torchvision" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f4/9d/0ea45640be447445c8664ce2b10c74f763b0b0b9ed11620d41a4d4baa10c/timm-1.0.24.tar.gz", hash = "sha256:c7b909f43fe2ef8fe62c505e270cd4f1af230dfbc37f2ee93e3608492b9d9a40", size = 2412239, upload-time = "2026-01-07T00:26:17.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/dd/c1f5b0890f7b5db661bde0864b41cb0275be76851047e5f7e085fe0b455a/timm-1.0.24-py3-none-any.whl", hash = "sha256:8301ac783410c6ad72c73c49326af6d71a9e4d1558238552796e825c2464913f", size = 2560563, upload-time = "2026-01-07T00:26:13.956Z" }, -] - -[[package]] -name = "tokenizers" -version = "0.21.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, - { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, - { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, - { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, -] - -[[package]] -name = "tomli" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, - { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, - { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, - { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, - { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, - { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, - { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, - { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, - { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, - { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, - { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, - { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, - { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" }, - { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" }, - { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" }, - { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" }, - { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" }, - { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" }, - { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" }, - { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" }, - { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" }, - { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" }, - { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" }, - { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" }, - { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" }, - { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" }, - { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" }, - { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" }, - { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" }, - { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" }, - { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" }, - { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" }, - { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" }, - { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, -] - -[[package]] -name = "tomlkit" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, -] - -[[package]] -name = "toolz" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, -] - -[[package]] -name = "torch" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "nvidia-cublas-cu12", version = "12.8.4.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", version = "12.8.90", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, - { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, - { 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/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, - { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, - { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, - { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, - { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, - { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, - { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, - { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, - { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, - { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, - { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, - { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, - { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, - { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, - { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, - { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, - { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, - { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, - { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, -] - -[[package]] -name = "torchreid" -version = "0.2.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/9a/d3d8da1d1a8a189b2b2d6f191b21cd7fbdb91a587a9c992bcd9666895284/torchreid-0.2.5.tar.gz", hash = "sha256:bc1055c6fb8444968798708dd13fdad00148e9d7cf3cb18cf52f4b949857fe08", size = 92656, upload-time = "2022-10-16T12:33:29.693Z" } - -[[package]] -name = "torchvision" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "pillow" }, - { name = "torch" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" }, - { url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" }, - { url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" }, - { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, - { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, - { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, - { 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/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, - { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, - { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, - { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, - { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, - { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, - { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, - { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, - { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, - { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, - { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, - { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, - { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, - { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, -] - -[[package]] -name = "tornado" -version = "6.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, -] - -[[package]] -name = "transformers" -version = "4.49.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/50/46573150944f46df8ec968eda854023165a84470b42f69f67c7d475dabc5/transformers-4.49.0.tar.gz", hash = "sha256:7e40e640b5b8dc3f48743f5f5adbdce3660c82baafbd3afdfc04143cdbd2089e", size = 8610952, upload-time = "2025-02-17T15:19:03.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/37/1f29af63e9c30156a3ed6ebc2754077016577c094f31de7b2631e5d379eb/transformers-4.49.0-py3-none-any.whl", hash = "sha256:6b4fded1c5fee04d384b1014495b4235a2b53c87503d7d592423c06128cbbe03", size = 9970275, upload-time = "2025-02-17T15:18:58.814Z" }, -] - -[package.optional-dependencies] -torch = [ - { name = "accelerate" }, - { name = "torch" }, -] - -[[package]] -name = "treescope" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/2a/d13d3c38862632742d2fe2f7ae307c431db06538fd05ca03020d207b5dcc/treescope-0.1.10.tar.gz", hash = "sha256:20f74656f34ab2d8716715013e8163a0da79bdc2554c16d5023172c50d27ea95", size = 138870, upload-time = "2025-08-08T05:43:48.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl", hash = "sha256:dde52f5314f4c29d22157a6fe4d3bd103f9cae02791c9e672eefa32c9aa1da51", size = 182255, upload-time = "2025-08-08T05:43:46.673Z" }, -] - -[[package]] -name = "trimesh" -version = "4.11.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/41/de14e2fa9b2d99214c60402fc57d2efb201f2925b16d6bee289565901d83/trimesh-4.11.2.tar.gz", hash = "sha256:30fbde5b8dd7c157e7ff4d54286cb35291844fd3f4d0364e8b2727f1b308fb06", size = 835044, upload-time = "2026-02-10T16:00:27.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/b9/da09903ea53b677a58ba770112de6fe8b2acb8b4cd9bffae4ff6cfe7c072/trimesh-4.11.2-py3-none-any.whl", hash = "sha256:25e3ab2620f9eca5c9376168c67aabdd32205dad1c4eea09cd45cd4a3edf775a", size = 740328, upload-time = "2026-02-10T16:00:25.246Z" }, -] - -[[package]] -name = "triton" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, - { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, - { 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" }, - { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, - { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, - { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, - { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, -] - -[[package]] -name = "typeguard" -version = "4.4.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/68/71c1a15b5f65f40e91b65da23b8224dad41349894535a97f63a52e462196/typeguard-4.4.4.tar.gz", hash = "sha256:3a7fd2dffb705d4d0efaed4306a704c89b9dee850b688f060a8b1615a79e5f74", size = 75203, upload-time = "2025-06-18T09:56:07.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/a9/e3aee762739c1d7528da1c3e06d518503f8b6c439c35549b53735ba52ead/typeguard-4.4.4-py3-none-any.whl", hash = "sha256:b5f562281b6bfa1f5492470464730ef001646128b180769880468bd84b68b09e", size = 34874, upload-time = "2025-06-18T09:56:05.999Z" }, -] - -[[package]] -name = "typer" -version = "0.23.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "click" }, - { name = "rich" }, - { name = "shellingham" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, -] - -[[package]] -name = "types-colorama" -version = "0.4.15.20250801" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/99/37/af713e7d73ca44738c68814cbacf7a655aa40ddd2e8513d431ba78ace7b3/types_colorama-0.4.15.20250801.tar.gz", hash = "sha256:02565d13d68963d12237d3f330f5ecd622a3179f7b5b14ee7f16146270c357f5", size = 10437, upload-time = "2025-08-01T03:48:22.605Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3a/44ccbbfef6235aeea84c74041dc6dfee6c17ff3ddba782a0250e41687ec7/types_colorama-0.4.15.20250801-py3-none-any.whl", hash = "sha256:b6e89bd3b250fdad13a8b6a465c933f4a5afe485ea2e2f104d739be50b13eea9", size = 10743, upload-time = "2025-08-01T03:48:21.774Z" }, -] - -[[package]] -name = "types-defusedxml" -version = "0.7.0.20250822" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/4a/5b997ae87bf301d1796f72637baa4e0e10d7db17704a8a71878a9f77f0c0/types_defusedxml-0.7.0.20250822.tar.gz", hash = "sha256:ba6c395105f800c973bba8a25e41b215483e55ec79c8ca82b6fe90ba0bc3f8b2", size = 10590, upload-time = "2025-08-22T03:02:59.547Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/13/73/8a36998cee9d7c9702ed64a31f0866c7f192ecffc22771d44dbcc7878f18/types_defusedxml-0.7.0.20250822-py3-none-any.whl", hash = "sha256:5ee219f8a9a79c184773599ad216123aedc62a969533ec36737ec98601f20dcf", size = 13430, upload-time = "2025-08-22T03:02:58.466Z" }, -] - -[[package]] -name = "types-gevent" -version = "25.9.0.20251228" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-greenlet" }, - { name = "types-psutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/85/c5043c4472f82c8ee3d9e0673eb4093c7d16770a26541a137a53a1d096f6/types_gevent-25.9.0.20251228.tar.gz", hash = "sha256:423ef9891d25c5a3af236c3e9aace4c444c86ff773fe13ef22731bc61d59abef", size = 38063, upload-time = "2025-12-28T03:28:28.651Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/b7/a2d6b652ab5a26318b68cafd58c46fafb9b15c5313d2d76a70b838febb4b/types_gevent-25.9.0.20251228-py3-none-any.whl", hash = "sha256:e2e225af4fface9241c16044983eb2fc3993f2d13d801f55c2932848649b7f2f", size = 55486, upload-time = "2025-12-28T03:28:27.382Z" }, -] - -[[package]] -name = "types-greenlet" -version = "3.3.0.20251206" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/d3/23f4ab29a5ce239935bb3c157defcf50df8648c16c65965fae03980d67f3/types_greenlet-3.3.0.20251206.tar.gz", hash = "sha256:3e1ab312ab7154c08edc2e8110fbf00d9920323edc1144ad459b7b0052063055", size = 8901, upload-time = "2025-12-06T03:01:38.634Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/8f/aabde1b6e49b25a6804c12a707829e44ba0f5520563c09271f05d3196142/types_greenlet-3.3.0.20251206-py3-none-any.whl", hash = "sha256:8d11041c0b0db545619e8c8a1266aa4aaa4ebeae8ae6b4b7049917a6045a5590", size = 8809, upload-time = "2025-12-06T03:01:37.651Z" }, -] - -[[package]] -name = "types-jmespath" -version = "1.1.0.20260124" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/ca/c8d7fc6e450c2f8fc6f510cb194754c43b17f933f2dcabcfc6985cbb97a8/types_jmespath-1.1.0.20260124.tar.gz", hash = "sha256:29d86868e72c0820914577077b27d167dcab08b1fc92157a29d537ff7153fdfe", size = 10709, upload-time = "2026-01-24T03:18:46.557Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/91/915c4a6e6e9bd2bca3ec0c21c1771b175c59e204b85e57f3f572370fe753/types_jmespath-1.1.0.20260124-py3-none-any.whl", hash = "sha256:ec387666d446b15624215aa9cbd2867ffd885b6c74246d357c65e830c7a138b3", size = 11509, upload-time = "2026-01-24T03:18:45.536Z" }, -] - -[[package]] -name = "types-jsonschema" -version = "4.26.0.20260202" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/07/68f63e715eb327ed2f5292e29e8be99785db0f72c7664d2c63bd4dbdc29d/types_jsonschema-4.26.0.20260202.tar.gz", hash = "sha256:29831baa4308865a9aec547a61797a06fc152b0dac8dddd531e002f32265cb07", size = 16168, upload-time = "2026-02-02T04:11:22.585Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/06/962d4f364f779d7389cd31a1bb581907b057f52f0ace2c119a8dd8409db6/types_jsonschema-4.26.0.20260202-py3-none-any.whl", hash = "sha256:41c95343abc4de9264e333a55e95dfb4d401e463856d0164eec9cb182e8746da", size = 15914, upload-time = "2026-02-02T04:11:21.61Z" }, -] - -[[package]] -name = "types-networkx" -version = "3.6.1.20260210" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4f/d9/7ddf6afb27246998ae41f7ad19da410d83e24623b4db065b5a46888d327e/types_networkx-3.6.1.20260210.tar.gz", hash = "sha256:9864affb01ed53d6bf41c1042fbced155ac409ae02ca505e0a3fffe48901b6e1", size = 73702, upload-time = "2026-02-10T04:22:17.641Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/b0/1c45681a8b8d3ccf25cebaa296b06d5240518bd7a7d861cf14a15bf9dd20/types_networkx-3.6.1.20260210-py3-none-any.whl", hash = "sha256:075ccb9f2e2b370c3a9eae9636f2f38890e7c494e6323cb72a0207f104f8225e", size = 162684, upload-time = "2026-02-10T04:22:16.055Z" }, -] - -[[package]] -name = "types-protobuf" -version = "6.32.1.20251210" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c2/59/c743a842911887cd96d56aa8936522b0cd5f7a7f228c96e81b59fced45be/types_protobuf-6.32.1.20251210.tar.gz", hash = "sha256:c698bb3f020274b1a2798ae09dc773728ce3f75209a35187bd11916ebfde6763", size = 63900, upload-time = "2025-12-10T03:14:25.451Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/43/58e75bac4219cbafee83179505ff44cae3153ec279be0e30583a73b8f108/types_protobuf-6.32.1.20251210-py3-none-any.whl", hash = "sha256:2641f78f3696822a048cfb8d0ff42ccd85c25f12f871fbebe86da63793692140", size = 77921, upload-time = "2025-12-10T03:14:24.477Z" }, -] - -[[package]] -name = "types-psutil" -version = "7.2.2.20260130" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/69/14/fc5fb0a6ddfadf68c27e254a02ececd4d5c7fdb0efcb7e7e917a183497fb/types_psutil-7.2.2.20260130.tar.gz", hash = "sha256:15b0ab69c52841cf9ce3c383e8480c620a4d13d6a8e22b16978ebddac5590950", size = 26535, upload-time = "2026-01-30T03:58:14.116Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d7/60974b7e31545d3768d1770c5fe6e093182c3bfd819429b33133ba6b3e89/types_psutil-7.2.2.20260130-py3-none-any.whl", hash = "sha256:15523a3caa7b3ff03ac7f9b78a6470a59f88f48df1d74a39e70e06d2a99107da", size = 32876, upload-time = "2026-01-30T03:58:13.172Z" }, -] - -[[package]] -name = "types-psycopg2" -version = "2.9.21.20251012" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9b/b3/2d09eaf35a084cffd329c584970a3fa07101ca465c13cad1576d7c392587/types_psycopg2-2.9.21.20251012.tar.gz", hash = "sha256:4cdafd38927da0cfde49804f39ab85afd9c6e9c492800e42f1f0c1a1b0312935", size = 26710, upload-time = "2025-10-12T02:55:39.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/0c/05feaf8cb51159f2c0af04b871dab7e98a2f83a3622f5f216331d2dd924c/types_psycopg2-2.9.21.20251012-py3-none-any.whl", hash = "sha256:712bad5c423fe979e357edbf40a07ca40ef775d74043de72bd4544ca328cc57e", size = 24883, upload-time = "2025-10-12T02:55:38.439Z" }, -] - -[[package]] -name = "types-pysocks" -version = "1.7.1.20251001" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/d7/421deaee04ffe69dc1449cbf57dc4d4d92e8f966f4a35b482ea3811b7980/types_pysocks-1.7.1.20251001.tar.gz", hash = "sha256:50a0e737d42527abbec09e891c64f76a9f66f302e673cd149bc112c15764869f", size = 8785, upload-time = "2025-10-01T03:04:13.85Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/07/6a8aafa0fa5fc0880a37c98b41348bf91bc28f76577bdac68f78bcf8a124/types_pysocks-1.7.1.20251001-py3-none-any.whl", hash = "sha256:dd9abcfc7747aeddf1bab270c8daab3a1309c3af9e07c8c2c52038ab8539f06c", size = 9620, upload-time = "2025-10-01T03:04:13.042Z" }, -] - -[[package]] -name = "types-pytz" -version = "2025.2.0.20251108" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/ff/c047ddc68c803b46470a357454ef76f4acd8c1088f5cc4891cdd909bfcf6/types_pytz-2025.2.0.20251108.tar.gz", hash = "sha256:fca87917836ae843f07129567b74c1929f1870610681b4c92cb86a3df5817bdb", size = 10961, upload-time = "2025-11-08T02:55:57.001Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/c1/56ef16bf5dcd255155cc736d276efa6ae0a5c26fd685e28f0412a4013c01/types_pytz-2025.2.0.20251108-py3-none-any.whl", hash = "sha256:0f1c9792cab4eb0e46c52f8845c8f77cf1e313cb3d68bf826aa867fe4717d91c", size = 10116, upload-time = "2025-11-08T02:55:56.194Z" }, -] - -[[package]] -name = "types-pyyaml" -version = "6.0.12.20250915" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, -] - -[[package]] -name = "types-simplejson" -version = "3.20.0.20250822" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/6b/96d43a90cd202bd552cdd871858a11c138fe5ef11aeb4ed8e8dc51389257/types_simplejson-3.20.0.20250822.tar.gz", hash = "sha256:2b0bfd57a6beed3b932fd2c3c7f8e2f48a7df3978c9bba43023a32b3741a95b0", size = 10608, upload-time = "2025-08-22T03:03:35.36Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/9f/8e2c9e6aee9a2ff34f2ffce6ccd9c26edeef6dfd366fde611dc2e2c00ab9/types_simplejson-3.20.0.20250822-py3-none-any.whl", hash = "sha256:b5e63ae220ac7a1b0bb9af43b9cb8652237c947981b2708b0c776d3b5d8fa169", size = 10417, upload-time = "2025-08-22T03:03:34.485Z" }, -] - -[[package]] -name = "types-tabulate" -version = "0.9.0.20241207" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/43/16030404a327e4ff8c692f2273854019ed36718667b2993609dc37d14dd4/types_tabulate-0.9.0.20241207.tar.gz", hash = "sha256:ac1ac174750c0a385dfd248edc6279fa328aaf4ea317915ab879a2ec47833230", size = 8195, upload-time = "2024-12-07T02:54:42.554Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/86/a9ebfd509cbe74471106dffed320e208c72537f9aeb0a55eaa6b1b5e4d17/types_tabulate-0.9.0.20241207-py3-none-any.whl", hash = "sha256:b8dad1343c2a8ba5861c5441370c3e35908edd234ff036d4298708a1d4cf8a85", size = 8307, upload-time = "2024-12-07T02:54:41.031Z" }, -] - -[[package]] -name = "types-tensorflow" -version = "2.18.0.20260121" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "types-protobuf" }, - { name = "types-requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/81/43d17caea48c3454bf64c23cba5f7876fc0cd0f0434f350f61782cc95587/types_tensorflow-2.18.0.20260121.tar.gz", hash = "sha256:7fe9f75fd00be0f53ca97ba3d3b4cf8ab45447f6d3a959ad164cf9ac421a5f89", size = 258281, upload-time = "2026-01-21T03:24:22.488Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/84/6510e7c7b29c6005d93fd6762f7d7d4a413ffd8ec8e04ebc53ac2d8c5372/types_tensorflow-2.18.0.20260121-py3-none-any.whl", hash = "sha256:80d9a9528fa52dc215a914d6ba47f5500f54b421efd2923adf98cff1760b2cce", size = 329562, upload-time = "2026-01-21T03:24:21.147Z" }, -] - -[[package]] -name = "types-tqdm" -version = "4.67.3.20260205" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "types-requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/46/790b9872523a48163bdda87d47849b4466017640e5259d06eed539340afd/types_tqdm-4.67.3.20260205.tar.gz", hash = "sha256:f3023682d4aa3bbbf908c8c6bb35f35692d319460d9bbd3e646e8852f3dd9f85", size = 17597, upload-time = "2026-02-05T04:03:19.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/da/7f761868dbaa328392356fab30c18ab90d14cce86b269e7e63328f29d4a3/types_tqdm-4.67.3.20260205-py3-none-any.whl", hash = "sha256:85c31731e81dc3c5cecc34c6c8b2e5166fafa722468f58840c2b5ac6a8c5c173", size = 23894, upload-time = "2026-02-05T04:03:18.48Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, -] -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 = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] - -[[package]] -name = "uc-micro-py" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, -] - -[[package]] -name = "ujson" -version = "5.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/d9/3f17e3c5773fb4941c68d9a37a47b1a79c9649d6c56aefbed87cc409d18a/ujson-5.11.0.tar.gz", hash = "sha256:e204ae6f909f099ba6b6b942131cee359ddda2b6e4ea39c12eb8b991fe2010e0", size = 7156583, upload-time = "2025-08-20T11:57:02.452Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/86/0c/8bf7a4fabfd01c7eed92d9b290930ce6d14910dec708e73538baa38885d1/ujson-5.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:446e8c11c06048611c9d29ef1237065de0af07cabdd97e6b5b527b957692ec25", size = 55248, upload-time = "2025-08-20T11:55:02.368Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2e/eeab0b8b641817031ede4f790db4c4942df44a12f44d72b3954f39c6a115/ujson-5.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16ccb973b7ada0455201808ff11d48fe9c3f034a6ab5bd93b944443c88299f89", size = 53157, upload-time = "2025-08-20T11:55:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/21/1b/a4e7a41870797633423ea79618526747353fd7be9191f3acfbdee0bf264b/ujson-5.11.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3134b783ab314d2298d58cda7e47e7a0f7f71fc6ade6ac86d5dbeaf4b9770fa6", size = 57657, upload-time = "2025-08-20T11:55:05.169Z" }, - { url = "https://files.pythonhosted.org/packages/94/ae/4e0d91b8f6db7c9b76423b3649612189506d5a06ddd3b6334b6d37f77a01/ujson-5.11.0-cp310-cp310-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:185f93ebccffebc8baf8302c869fac70dd5dd78694f3b875d03a31b03b062cdb", size = 59780, upload-time = "2025-08-20T11:55:06.325Z" }, - { url = "https://files.pythonhosted.org/packages/b3/cc/46b124c2697ca2da7c65c4931ed3cb670646978157aa57a7a60f741c530f/ujson-5.11.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d06e87eded62ff0e5f5178c916337d2262fdbc03b31688142a3433eabb6511db", size = 57307, upload-time = "2025-08-20T11:55:07.493Z" }, - { url = "https://files.pythonhosted.org/packages/39/eb/20dd1282bc85dede2f1c62c45b4040bc4c389c80a05983515ab99771bca7/ujson-5.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:181fb5b15703a8b9370b25345d2a1fd1359f0f18776b3643d24e13ed9c036d4c", size = 1036369, upload-time = "2025-08-20T11:55:09.192Z" }, - { url = "https://files.pythonhosted.org/packages/64/a2/80072439065d493e3a4b1fbeec991724419a1b4c232e2d1147d257cac193/ujson-5.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4df61a6df0a4a8eb5b9b1ffd673429811f50b235539dac586bb7e9e91994138", size = 1195738, upload-time = "2025-08-20T11:55:11.402Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7e/d77f9e9c039d58299c350c978e086a804d1fceae4fd4a1cc6e8d0133f838/ujson-5.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6eff24e1abd79e0ec6d7eae651dd675ddbc41f9e43e29ef81e16b421da896915", size = 1088718, upload-time = "2025-08-20T11:55:13.297Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f1/697559d45acc849cada6b3571d53522951b1a64027400507aabc6a710178/ujson-5.11.0-cp310-cp310-win32.whl", hash = "sha256:30f607c70091483550fbd669a0b37471e5165b317d6c16e75dba2aa967608723", size = 39653, upload-time = "2025-08-20T11:55:14.869Z" }, - { url = "https://files.pythonhosted.org/packages/86/a2/70b73a0f55abe0e6b8046d365d74230c20c5691373e6902a599b2dc79ba1/ujson-5.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:3d2720e9785f84312b8e2cb0c2b87f1a0b1c53aaab3b2af3ab817d54409012e0", size = 43720, upload-time = "2025-08-20T11:55:15.897Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5f/b19104afa455630b43efcad3a24495b9c635d92aa8f2da4f30e375deb1a2/ujson-5.11.0-cp310-cp310-win_arm64.whl", hash = "sha256:85e6796631165f719084a9af00c79195d3ebf108151452fefdcb1c8bb50f0105", size = 38410, upload-time = "2025-08-20T11:55:17.556Z" }, - { url = "https://files.pythonhosted.org/packages/da/ea/80346b826349d60ca4d612a47cdf3533694e49b45e9d1c07071bb867a184/ujson-5.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d7c46cb0fe5e7056b9acb748a4c35aa1b428025853032540bb7e41f46767321f", size = 55248, upload-time = "2025-08-20T11:55:19.033Z" }, - { url = "https://files.pythonhosted.org/packages/57/df/b53e747562c89515e18156513cc7c8ced2e5e3fd6c654acaa8752ffd7cd9/ujson-5.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8951bb7a505ab2a700e26f691bdfacf395bc7e3111e3416d325b513eea03a58", size = 53156, upload-time = "2025-08-20T11:55:20.174Z" }, - { url = "https://files.pythonhosted.org/packages/41/b8/ab67ec8c01b8a3721fd13e5cb9d85ab2a6066a3a5e9148d661a6870d6293/ujson-5.11.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:952c0be400229940248c0f5356514123d428cba1946af6fa2bbd7503395fef26", size = 57657, upload-time = "2025-08-20T11:55:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/fb84f27cd80a2c7e2d3c6012367aecade0da936790429801803fa8d4bffc/ujson-5.11.0-cp311-cp311-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:94fcae844f1e302f6f8095c5d1c45a2f0bfb928cccf9f1b99e3ace634b980a2a", size = 59779, upload-time = "2025-08-20T11:55:22.772Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/48706f7c1e917ecb97ddcfb7b1d756040b86ed38290e28579d63bd3fcc48/ujson-5.11.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e0ec1646db172beb8d3df4c32a9d78015e671d2000af548252769e33079d9a6", size = 57284, upload-time = "2025-08-20T11:55:24.01Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ce/48877c6eb4afddfd6bd1db6be34456538c07ca2d6ed233d3f6c6efc2efe8/ujson-5.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:da473b23e3a54448b008d33f742bcd6d5fb2a897e42d1fc6e7bf306ea5d18b1b", size = 1036395, upload-time = "2025-08-20T11:55:25.725Z" }, - { url = "https://files.pythonhosted.org/packages/8b/7a/2c20dc97ad70cd7c31ad0596ba8e2cf8794d77191ba4d1e0bded69865477/ujson-5.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:aa6b3d4f1c0d3f82930f4cbd7fe46d905a4a9205a7c13279789c1263faf06dba", size = 1195731, upload-time = "2025-08-20T11:55:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/15/f5/ca454f2f6a2c840394b6f162fff2801450803f4ff56c7af8ce37640b8a2a/ujson-5.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4843f3ab4fe1cc596bb7e02228ef4c25d35b4bb0809d6a260852a4bfcab37ba3", size = 1088710, upload-time = "2025-08-20T11:55:29.426Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d3/9ba310e07969bc9906eb7548731e33a0f448b122ad9705fed699c9b29345/ujson-5.11.0-cp311-cp311-win32.whl", hash = "sha256:e979fbc469a7f77f04ec2f4e853ba00c441bf2b06720aa259f0f720561335e34", size = 39648, upload-time = "2025-08-20T11:55:31.194Z" }, - { url = "https://files.pythonhosted.org/packages/57/f7/da05b4a8819f1360be9e71fb20182f0bb3ec611a36c3f213f4d20709e099/ujson-5.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:683f57f0dd3acdd7d9aff1de0528d603aafcb0e6d126e3dc7ce8b020a28f5d01", size = 43717, upload-time = "2025-08-20T11:55:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/9a/cc/f3f9ac0f24f00a623a48d97dc3814df5c2dc368cfb00031aa4141527a24b/ujson-5.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:7855ccea3f8dad5e66d8445d754fc1cf80265a4272b5f8059ebc7ec29b8d0835", size = 38402, upload-time = "2025-08-20T11:55:33.641Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ef/a9cb1fce38f699123ff012161599fb9f2ff3f8d482b4b18c43a2dc35073f/ujson-5.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7895f0d2d53bd6aea11743bd56e3cb82d729980636cd0ed9b89418bf66591702", size = 55434, upload-time = "2025-08-20T11:55:34.987Z" }, - { url = "https://files.pythonhosted.org/packages/b1/05/dba51a00eb30bd947791b173766cbed3492269c150a7771d2750000c965f/ujson-5.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12b5e7e22a1fe01058000d1b317d3b65cc3daf61bd2ea7a2b76721fe160fa74d", size = 53190, upload-time = "2025-08-20T11:55:36.384Z" }, - { url = "https://files.pythonhosted.org/packages/03/3c/fd11a224f73fbffa299fb9644e425f38b38b30231f7923a088dd513aabb4/ujson-5.11.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0180a480a7d099082501cad1fe85252e4d4bf926b40960fb3d9e87a3a6fbbc80", size = 57600, upload-time = "2025-08-20T11:55:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/55/b9/405103cae24899df688a3431c776e00528bd4799e7d68820e7ebcf824f92/ujson-5.11.0-cp312-cp312-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:fa79fdb47701942c2132a9dd2297a1a85941d966d8c87bfd9e29b0cf423f26cc", size = 59791, upload-time = "2025-08-20T11:55:38.877Z" }, - { url = "https://files.pythonhosted.org/packages/17/7b/2dcbc2bbfdbf68f2368fb21ab0f6735e872290bb604c75f6e06b81edcb3f/ujson-5.11.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8254e858437c00f17cb72e7a644fc42dad0ebb21ea981b71df6e84b1072aaa7c", size = 57356, upload-time = "2025-08-20T11:55:40.036Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/fea2ca18986a366c750767b694430d5ded6b20b6985fddca72f74af38a4c/ujson-5.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1aa8a2ab482f09f6c10fba37112af5f957689a79ea598399c85009f2f29898b5", size = 1036313, upload-time = "2025-08-20T11:55:41.408Z" }, - { url = "https://files.pythonhosted.org/packages/a3/bb/d4220bd7532eac6288d8115db51710fa2d7d271250797b0bfba9f1e755af/ujson-5.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a638425d3c6eed0318df663df44480f4a40dc87cc7c6da44d221418312f6413b", size = 1195782, upload-time = "2025-08-20T11:55:43.357Z" }, - { url = "https://files.pythonhosted.org/packages/80/47/226e540aa38878ce1194454385701d82df538ccb5ff8db2cf1641dde849a/ujson-5.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e3cff632c1d78023b15f7e3a81c3745cd3f94c044d1e8fa8efbd6b161997bbc", size = 1088817, upload-time = "2025-08-20T11:55:45.262Z" }, - { url = "https://files.pythonhosted.org/packages/7e/81/546042f0b23c9040d61d46ea5ca76f0cc5e0d399180ddfb2ae976ebff5b5/ujson-5.11.0-cp312-cp312-win32.whl", hash = "sha256:be6b0eaf92cae8cdee4d4c9e074bde43ef1c590ed5ba037ea26c9632fb479c88", size = 39757, upload-time = "2025-08-20T11:55:46.522Z" }, - { url = "https://files.pythonhosted.org/packages/44/1b/27c05dc8c9728f44875d74b5bfa948ce91f6c33349232619279f35c6e817/ujson-5.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b7b136cc6abc7619124fd897ef75f8e63105298b5ca9bdf43ebd0e1fa0ee105f", size = 43859, upload-time = "2025-08-20T11:55:47.987Z" }, - { url = "https://files.pythonhosted.org/packages/22/2d/37b6557c97c3409c202c838aa9c960ca3896843b4295c4b7bb2bbd260664/ujson-5.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:6cd2df62f24c506a0ba322d5e4fe4466d47a9467b57e881ee15a31f7ecf68ff6", size = 38361, upload-time = "2025-08-20T11:55:49.122Z" }, - { url = "https://files.pythonhosted.org/packages/1c/ec/2de9dd371d52c377abc05d2b725645326c4562fc87296a8907c7bcdf2db7/ujson-5.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:109f59885041b14ee9569bf0bb3f98579c3fa0652317b355669939e5fc5ede53", size = 55435, upload-time = "2025-08-20T11:55:50.243Z" }, - { url = "https://files.pythonhosted.org/packages/5b/a4/f611f816eac3a581d8a4372f6967c3ed41eddbae4008d1d77f223f1a4e0a/ujson-5.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a31c6b8004438e8c20fc55ac1c0e07dad42941db24176fe9acf2815971f8e752", size = 53193, upload-time = "2025-08-20T11:55:51.373Z" }, - { url = "https://files.pythonhosted.org/packages/e9/c5/c161940967184de96f5cbbbcce45b562a4bf851d60f4c677704b1770136d/ujson-5.11.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78c684fb21255b9b90320ba7e199780f653e03f6c2528663768965f4126a5b50", size = 57603, upload-time = "2025-08-20T11:55:52.583Z" }, - { url = "https://files.pythonhosted.org/packages/2b/d6/c7b2444238f5b2e2d0e3dab300b9ddc3606e4b1f0e4bed5a48157cebc792/ujson-5.11.0-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:4c9f5d6a27d035dd90a146f7761c2272cf7103de5127c9ab9c4cd39ea61e878a", size = 59794, upload-time = "2025-08-20T11:55:53.69Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a3/292551f936d3d02d9af148f53e1bc04306b00a7cf1fcbb86fa0d1c887242/ujson-5.11.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:837da4d27fed5fdc1b630bd18f519744b23a0b5ada1bbde1a36ba463f2900c03", size = 57363, upload-time = "2025-08-20T11:55:54.843Z" }, - { url = "https://files.pythonhosted.org/packages/90/a6/82cfa70448831b1a9e73f882225980b5c689bf539ec6400b31656a60ea46/ujson-5.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:787aff4a84da301b7f3bac09bc696e2e5670df829c6f8ecf39916b4e7e24e701", size = 1036311, upload-time = "2025-08-20T11:55:56.197Z" }, - { url = "https://files.pythonhosted.org/packages/84/5c/96e2266be50f21e9b27acaee8ca8f23ea0b85cb998c33d4f53147687839b/ujson-5.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6dd703c3e86dc6f7044c5ac0b3ae079ed96bf297974598116aa5fb7f655c3a60", size = 1195783, upload-time = "2025-08-20T11:55:58.081Z" }, - { url = "https://files.pythonhosted.org/packages/8d/20/78abe3d808cf3bb3e76f71fca46cd208317bf461c905d79f0d26b9df20f1/ujson-5.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3772e4fe6b0c1e025ba3c50841a0ca4786825a4894c8411bf8d3afe3a8061328", size = 1088822, upload-time = "2025-08-20T11:55:59.469Z" }, - { url = "https://files.pythonhosted.org/packages/d8/50/8856e24bec5e2fc7f775d867aeb7a3f137359356200ac44658f1f2c834b2/ujson-5.11.0-cp313-cp313-win32.whl", hash = "sha256:8fa2af7c1459204b7a42e98263b069bd535ea0cd978b4d6982f35af5a04a4241", size = 39753, upload-time = "2025-08-20T11:56:01.345Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/1baee0f4179a4d0f5ce086832147b6cc9b7731c24ca08e14a3fdb8d39c32/ujson-5.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:34032aeca4510a7c7102bd5933f59a37f63891f30a0706fb46487ab6f0edf8f0", size = 43866, upload-time = "2025-08-20T11:56:02.552Z" }, - { url = "https://files.pythonhosted.org/packages/a9/8c/6d85ef5be82c6d66adced3ec5ef23353ed710a11f70b0b6a836878396334/ujson-5.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:ce076f2df2e1aa62b685086fbad67f2b1d3048369664b4cdccc50707325401f9", size = 38363, upload-time = "2025-08-20T11:56:03.688Z" }, - { url = "https://files.pythonhosted.org/packages/28/08/4518146f4984d112764b1dfa6fb7bad691c44a401adadaa5e23ccd930053/ujson-5.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65724738c73645db88f70ba1f2e6fb678f913281804d5da2fd02c8c5839af302", size = 55462, upload-time = "2025-08-20T11:56:04.873Z" }, - { url = "https://files.pythonhosted.org/packages/29/37/2107b9a62168867a692654d8766b81bd2fd1e1ba13e2ec90555861e02b0c/ujson-5.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29113c003ca33ab71b1b480bde952fbab2a0b6b03a4ee4c3d71687cdcbd1a29d", size = 53246, upload-time = "2025-08-20T11:56:06.054Z" }, - { url = "https://files.pythonhosted.org/packages/9b/f8/25583c70f83788edbe3ca62ce6c1b79eff465d78dec5eb2b2b56b3e98b33/ujson-5.11.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c44c703842024d796b4c78542a6fcd5c3cb948b9fc2a73ee65b9c86a22ee3638", size = 57631, upload-time = "2025-08-20T11:56:07.374Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ca/19b3a632933a09d696f10dc1b0dfa1d692e65ad507d12340116ce4f67967/ujson-5.11.0-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:e750c436fb90edf85585f5c62a35b35082502383840962c6983403d1bd96a02c", size = 59877, upload-time = "2025-08-20T11:56:08.534Z" }, - { url = "https://files.pythonhosted.org/packages/55/7a/4572af5324ad4b2bfdd2321e898a527050290147b4ea337a79a0e4e87ec7/ujson-5.11.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f278b31a7c52eb0947b2db55a5133fbc46b6f0ef49972cd1a80843b72e135aba", size = 57363, upload-time = "2025-08-20T11:56:09.758Z" }, - { url = "https://files.pythonhosted.org/packages/7b/71/a2b8c19cf4e1efe53cf439cdf7198ac60ae15471d2f1040b490c1f0f831f/ujson-5.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ab2cb8351d976e788669c8281465d44d4e94413718af497b4e7342d7b2f78018", size = 1036394, upload-time = "2025-08-20T11:56:11.168Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3e/7b98668cba3bb3735929c31b999b374ebc02c19dfa98dfebaeeb5c8597ca/ujson-5.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:090b4d11b380ae25453100b722d0609d5051ffe98f80ec52853ccf8249dfd840", size = 1195837, upload-time = "2025-08-20T11:56:12.6Z" }, - { url = "https://files.pythonhosted.org/packages/a1/ea/8870f208c20b43571a5c409ebb2fe9b9dba5f494e9e60f9314ac01ea8f78/ujson-5.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:80017e870d882d5517d28995b62e4e518a894f932f1e242cbc802a2fd64d365c", size = 1088837, upload-time = "2025-08-20T11:56:14.15Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/c0e6607e37fa47929920a685a968c6b990a802dec65e9c5181e97845985d/ujson-5.11.0-cp314-cp314-win32.whl", hash = "sha256:1d663b96eb34c93392e9caae19c099ec4133ba21654b081956613327f0e973ac", size = 41022, upload-time = "2025-08-20T11:56:15.509Z" }, - { url = "https://files.pythonhosted.org/packages/4e/56/f4fe86b4c9000affd63e9219e59b222dc48b01c534533093e798bf617a7e/ujson-5.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:849e65b696f0d242833f1df4182096cedc50d414215d1371fca85c541fbff629", size = 45111, upload-time = "2025-08-20T11:56:16.597Z" }, - { url = "https://files.pythonhosted.org/packages/0a/f3/669437f0280308db4783b12a6d88c00730b394327d8334cc7a32ef218e64/ujson-5.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:e73df8648c9470af2b6a6bf5250d4744ad2cf3d774dcf8c6e31f018bdd04d764", size = 39682, upload-time = "2025-08-20T11:56:17.763Z" }, - { url = "https://files.pythonhosted.org/packages/6e/cd/e9809b064a89fe5c4184649adeb13c1b98652db3f8518980b04227358574/ujson-5.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:de6e88f62796372fba1de973c11138f197d3e0e1d80bcb2b8aae1e826096d433", size = 55759, upload-time = "2025-08-20T11:56:18.882Z" }, - { url = "https://files.pythonhosted.org/packages/1b/be/ae26a6321179ebbb3a2e2685b9007c71bcda41ad7a77bbbe164005e956fc/ujson-5.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e56ef8066f11b80d620985ae36869a3ff7e4b74c3b6129182ec5d1df0255f3", size = 53634, upload-time = "2025-08-20T11:56:20.012Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e9/fb4a220ee6939db099f4cfeeae796ecb91e7584ad4d445d4ca7f994a9135/ujson-5.11.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a325fd2c3a056cf6c8e023f74a0c478dd282a93141356ae7f16d5309f5ff823", size = 58547, upload-time = "2025-08-20T11:56:21.175Z" }, - { url = "https://files.pythonhosted.org/packages/bd/f8/fc4b952b8f5fea09ea3397a0bd0ad019e474b204cabcb947cead5d4d1ffc/ujson-5.11.0-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:a0af6574fc1d9d53f4ff371f58c96673e6d988ed2b5bf666a6143c782fa007e9", size = 60489, upload-time = "2025-08-20T11:56:22.342Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e5/af5491dfda4f8b77e24cf3da68ee0d1552f99a13e5c622f4cef1380925c3/ujson-5.11.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10f29e71ecf4ecd93a6610bd8efa8e7b6467454a363c3d6416db65de883eb076", size = 58035, upload-time = "2025-08-20T11:56:23.92Z" }, - { url = "https://files.pythonhosted.org/packages/c4/09/0945349dd41f25cc8c38d78ace49f14c5052c5bbb7257d2f466fa7bdb533/ujson-5.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a0a9b76a89827a592656fe12e000cf4f12da9692f51a841a4a07aa4c7ecc41c", size = 1037212, upload-time = "2025-08-20T11:56:25.274Z" }, - { url = "https://files.pythonhosted.org/packages/49/44/8e04496acb3d5a1cbee3a54828d9652f67a37523efa3d3b18a347339680a/ujson-5.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b16930f6a0753cdc7d637b33b4e8f10d5e351e1fb83872ba6375f1e87be39746", size = 1196500, upload-time = "2025-08-20T11:56:27.517Z" }, - { url = "https://files.pythonhosted.org/packages/64/ae/4bc825860d679a0f208a19af2f39206dfd804ace2403330fdc3170334a2f/ujson-5.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04c41afc195fd477a59db3a84d5b83a871bd648ef371cf8c6f43072d89144eef", size = 1089487, upload-time = "2025-08-20T11:56:29.07Z" }, - { url = "https://files.pythonhosted.org/packages/30/ed/5a057199fb0a5deabe0957073a1c1c1c02a3e99476cd03daee98ea21fa57/ujson-5.11.0-cp314-cp314t-win32.whl", hash = "sha256:aa6d7a5e09217ff93234e050e3e380da62b084e26b9f2e277d2606406a2fc2e5", size = 41859, upload-time = "2025-08-20T11:56:30.495Z" }, - { url = "https://files.pythonhosted.org/packages/aa/03/b19c6176bdf1dc13ed84b886e99677a52764861b6cc023d5e7b6ebda249d/ujson-5.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:48055e1061c1bb1f79e75b4ac39e821f3f35a9b82de17fce92c3140149009bec", size = 46183, upload-time = "2025-08-20T11:56:31.574Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ca/a0413a3874b2dc1708b8796ca895bf363292f9c70b2e8ca482b7dbc0259d/ujson-5.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1194b943e951092db611011cb8dbdb6cf94a3b816ed07906e14d3bc6ce0e90ab", size = 40264, upload-time = "2025-08-20T11:56:32.773Z" }, - { url = "https://files.pythonhosted.org/packages/50/17/30275aa2933430d8c0c4ead951cc4fdb922f575a349aa0b48a6f35449e97/ujson-5.11.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:abae0fb58cc820092a0e9e8ba0051ac4583958495bfa5262a12f628249e3b362", size = 51206, upload-time = "2025-08-20T11:56:48.797Z" }, - { url = "https://files.pythonhosted.org/packages/c3/15/42b3924258eac2551f8f33fa4e35da20a06a53857ccf3d4deb5e5d7c0b6c/ujson-5.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fac6c0649d6b7c3682a0a6e18d3de6857977378dce8d419f57a0b20e3d775b39", size = 48907, upload-time = "2025-08-20T11:56:50.136Z" }, - { url = "https://files.pythonhosted.org/packages/94/7e/0519ff7955aba581d1fe1fb1ca0e452471250455d182f686db5ac9e46119/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b42c115c7c6012506e8168315150d1e3f76e7ba0f4f95616f4ee599a1372bbc", size = 50319, upload-time = "2025-08-20T11:56:51.63Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/209d90506b7d6c5873f82c5a226d7aad1a1da153364e9ebf61eff0740c33/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:86baf341d90b566d61a394869ce77188cc8668f76d7bb2c311d77a00f4bdf844", size = 56584, upload-time = "2025-08-20T11:56:52.89Z" }, - { url = "https://files.pythonhosted.org/packages/e9/97/bd939bb76943cb0e1d2b692d7e68629f51c711ef60425fa5bb6968037ecd/ujson-5.11.0-pp311-pypy311_pp73-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4598bf3965fc1a936bd84034312bcbe00ba87880ef1ee33e33c1e88f2c398b49", size = 51588, upload-time = "2025-08-20T11:56:54.054Z" }, - { url = "https://files.pythonhosted.org/packages/52/5b/8c5e33228f7f83f05719964db59f3f9f276d272dc43752fa3bbf0df53e7b/ujson-5.11.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:416389ec19ef5f2013592f791486bef712ebce0cd59299bf9df1ba40bb2f6e04", size = 43835, upload-time = "2025-08-20T11:56:55.237Z" }, -] - -[[package]] -name = "ultralytics" -version = "8.4.14" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "matplotlib" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "polars" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch" }, - { name = "torchvision" }, - { name = "ultralytics-thop" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/dc/7947df41679c009bc33b61e10d6274a8ec885206b726ebb6027d5f204b35/ultralytics-8.4.14.tar.gz", hash = "sha256:360dff28ecb6cc7bf561aadf5bfe208c3900380bf1d4b2b190cb8db60e7b7626", size = 1014432, upload-time = "2026-02-10T11:31:51.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/39/3b19ee32a174c285c6b2bdf5cec222155938e5f0cf3fef997df131f98189/ultralytics-8.4.14-py3-none-any.whl", hash = "sha256:0ce8f4081c1e7dd96a7a3ac82a820681443042609c4b48adca85a2289cdaef17", size = 1188742, upload-time = "2026-02-10T11:31:47.44Z" }, -] - -[[package]] -name = "ultralytics-thop" -version = "2.0.18" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1f/63/21a32e1facfeee245dbdfb7b4669faf7a36ff7c00b50987932bdab126f4b/ultralytics_thop-2.0.18.tar.gz", hash = "sha256:21103bcd39cc9928477dc3d9374561749b66a1781b35f46256c8d8c4ac01d9cf", size = 34557, upload-time = "2025-10-29T16:58:13.526Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/c7/fb42228bb05473d248c110218ffb8b1ad2f76728ed8699856e5af21112ad/ultralytics_thop-2.0.18-py3-none-any.whl", hash = "sha256:2bb44851ad224b116c3995b02dd5e474a5ccf00acf237fe0edb9e1506ede04ec", size = 28941, upload-time = "2025-10-29T16:58:12.093Z" }, -] - -[[package]] -name = "unitree-webrtc-connect-leshy" -version = "2.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiortc" }, - { name = "flask-socketio" }, - { name = "lz4" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "opencv-python" }, - { name = "packaging" }, - { name = "pyaudio" }, - { name = "pycryptodome" }, - { name = "pydub" }, - { name = "requests" }, - { name = "sounddevice" }, - { name = "wasmtime" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d8/20/a92ceb094188fcf176da5609878c923273d414e93b8b5bbbb32c4f6ffd7c/unitree_webrtc_connect_leshy-2.0.7.tar.gz", hash = "sha256:9eeddab68e42e286cd9ba1e520303a56fd0920dce2bd4ef0cdec1d21669fda3b", size = 34362, upload-time = "2025-12-31T01:08:12.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/1b/e34448851e1cdad620175e048f58c177ac853564d4d7fdb9aa9cfc21eae7/unitree_webrtc_connect_leshy-2.0.7-py3-none-any.whl", hash = "sha256:82e2b9d842bf58288ec40e0bfd685780e20af9a2d0495aa9330950afde1d8ce4", size = 39989, upload-time = "2025-12-31T01:08:09.787Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, -] - -[[package]] -name = "uuid-utils" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" }, - { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" }, - { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" }, - { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" }, - { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" }, - { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" }, - { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" }, - { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" }, - { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" }, - { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" }, - { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" }, - { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" }, - { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" }, - { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" }, - { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" }, - { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, - { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, - { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, - { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, - { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, - { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, - { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, - { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, -] - -[[package]] -name = "virtualenv" -version = "20.36.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, -] - -[[package]] -name = "wadler-lindig" -version = "0.1.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/67/cbae4bf7683a64755c2c1778c418fea96d00e34395bb91743f08bd951571/wadler_lindig-0.1.7.tar.gz", hash = "sha256:81d14d3fe77d441acf3ebd7f4aefac20c74128bf460e84b512806dccf7b2cd55", size = 15842, upload-time = "2025-06-18T07:00:42.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/96/04e7b441807b26b794da5b11e59ed7f83b2cf8af202bd7eba8ad2fa6046e/wadler_lindig-0.1.7-py3-none-any.whl", hash = "sha256:e3ec83835570fd0a9509f969162aeb9c65618f998b1f42918cfc8d45122fe953", size = 20516, upload-time = "2025-06-18T07:00:41.684Z" }, -] - -[[package]] -name = "warp-lang" -version = "1.11.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/86/507cb6e0534422ff8437f71d676f6366ec907031db54751ad371f07c0b7f/warp_lang-1.11.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1ad11f1fa775269e991a3d55039152c8a504baf86701c849b485cb8e66c49d15", size = 24056749, upload-time = "2026-02-03T21:18:51.64Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/21e9396a963d50171f539f4a4c9411435e7bb9c5131f4480f882d5e51dc6/warp_lang-1.11.1-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8b098f41e71d421d80ee7562e38aa8380ff6b0d3b4c6ee866cfbdef733ac5bdc", size = 134843847, upload-time = "2026-02-03T21:19:14.318Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ff/9ced2d69dc9db6cb6b1d3b80a3d2a81590e11ae368a7864aa5d6089fd820/warp_lang-1.11.1-py3-none-manylinux_2_34_aarch64.whl", hash = "sha256:5d0904b0eefcc81f39ba65375427a3de99006088aa43e24a9011263f07d0cd07", size = 136139429, upload-time = "2026-02-03T21:18:45.854Z" }, - { url = "https://files.pythonhosted.org/packages/25/2f/2713f29bba5800b59835d97e136fa75d65a58b89734ae01de5a5f8f26482/warp_lang-1.11.1-py3-none-win_amd64.whl", hash = "sha256:15dc10aa51fb0fdbe1ca16d52e5fadca35a47ffd9d0c636826506f96bb2e7c41", size = 118951410, upload-time = "2026-02-03T21:19:02.038Z" }, -] - -[[package]] -name = "wasmtime" -version = "41.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/68/6dc0e7156f883afe0129dd89e4031c8d1163131794ba6ce9e454a09168ad/wasmtime-41.0.0.tar.gz", hash = "sha256:fc2aaacf3ba794eac8baeb739939b2f7903e12d6b78edddc0b7f3ac3a9af6dfc", size = 117354, upload-time = "2026-01-20T18:18:00.565Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/f9/f6aef5de536d12652d97cf162f124cbdd642150c7da61ffa7863272cdab7/wasmtime-41.0.0-py3-none-android_26_arm64_v8a.whl", hash = "sha256:f5a6e237b5b94188ef9867926b447f779f540c729c92e4d91cc946f2bee7c282", size = 6837018, upload-time = "2026-01-20T18:17:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/04/b9/42ec977972b2dcc8c61e3a40644d24d229b41fba151410644e44e35e6eb1/wasmtime-41.0.0-py3-none-android_26_x86_64.whl", hash = "sha256:4a3e33d0d3cf49062eaa231f748f54af991e89e9a795c5ab9d4f0eee85736e4c", size = 7654957, upload-time = "2026-01-20T18:17:43.285Z" }, - { url = "https://files.pythonhosted.org/packages/18/ca/6cce49b03c35c7fecb4437fd98990c64694a5e0024f9279bef0ddef000f7/wasmtime-41.0.0-py3-none-any.whl", hash = "sha256:5f6721406a6cd186d11f34e6d4991c4d536387b0c577d09a56bd93b8a3cf10c2", size = 6325757, upload-time = "2026-01-20T18:17:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/a0/16/d91cb80322cc7ae10bfa5db8cea4e0b9bb112f0c100b4486783ab16c1c22/wasmtime-41.0.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:2107360212fce33ed2adcfc33b7e75ed7136380a17d3ed598a5bab376dcf9e1b", size = 7471888, upload-time = "2026-01-20T18:17:46.185Z" }, - { url = "https://files.pythonhosted.org/packages/bc/f0/dcc80973d2ec58a1978b838887ccbd84d56900cf66dec5fb730bec3bd081/wasmtime-41.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f475df32ce9bfec4f6d0e124a49ca4a89e2ee71ccca460677f5237b1c8ee92ae", size = 6507285, upload-time = "2026-01-20T18:17:48.138Z" }, - { url = "https://files.pythonhosted.org/packages/bd/df/0867edd9ec26eb2e5eee7674a55f82c23ec27dd1d38d2d401f0e308eb920/wasmtime-41.0.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ad7e866430313eb2ee07c85811e524344884489d00896f3b2246b65553fe322c", size = 7732024, upload-time = "2026-01-20T18:17:50.207Z" }, - { url = "https://files.pythonhosted.org/packages/bb/48/b748a2e70478feabc5c876d90e90a39f4aba35378f5ee822f607e8f29c69/wasmtime-41.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:e0ea44584f60dcfa620af82d4fc2589248bcf64a93905b54ac3144242113b48a", size = 6800017, upload-time = "2026-01-20T18:17:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/14/29/43656c3a464d437d62421de16f2de2db645647bab0a0153deea30bfdade4/wasmtime-41.0.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dabb20a2751f01b835095013426a76091bd0bdb36ca9bcfc49c910b78347438", size = 6840763, upload-time = "2026-01-20T18:17:53.125Z" }, - { url = "https://files.pythonhosted.org/packages/9f/09/4608b65fa35ce5fc1479e138293a1166b4ea817cfa9a79f019ab6d7013d8/wasmtime-41.0.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d9627dfc5625b4947ea35c819561da358838fe76f65bda8ffe01ce34df8b32b1", size = 7754016, upload-time = "2026-01-20T18:17:55.346Z" }, - { url = "https://files.pythonhosted.org/packages/f4/9d/236bb367270579e4f628fb7b04fe93541151df7953006f3766607fc667c9/wasmtime-41.0.0-py3-none-win_amd64.whl", hash = "sha256:4f29171d73b71f232b6fe86cba77526fee84139f1590071af5facba401b0c9eb", size = 6325764, upload-time = "2026-01-20T18:17:57.034Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/bba9c0368c377250ab24fd005a7a1e9076121778c1e83b1bcc092ab84f86/wasmtime-41.0.0-py3-none-win_arm64.whl", hash = "sha256:0c4bcaba055e78fc161f497b85f39f1d35d475f0341b1e0259fa0a4b49e223e8", size = 5392238, upload-time = "2026-01-20T18:17:59.052Z" }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, - { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, - { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, - { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, - { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, - { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, - { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, - { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, - { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, - { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, - { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, - { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, - { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, - { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, - { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, - { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, - { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, - { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, - { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, - { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, -] - -[[package]] -name = "websocket-client" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, -] - -[[package]] -name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" }, -] - -[[package]] -name = "whatthepatch" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/28/55bc3e107a56fdcf7d5022cb32b8c21d98a9cc2df5cd9f3b93e10419099e/whatthepatch-1.0.7.tar.gz", hash = "sha256:9eefb4ebea5200408e02d413d2b4bc28daea6b78bb4b4d53431af7245f7d7edf", size = 34612, upload-time = "2024-11-16T17:21:22.153Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/93/af1d6ccb69ab6b5a00e03fa0cefa563f9862412667776ea15dd4eece3a90/whatthepatch-1.0.7-py3-none-any.whl", hash = "sha256:1b6f655fd31091c001c209529dfaabbabdbad438f5de14e3951266ea0fc6e7ed", size = 11964, upload-time = "2024-11-16T17:21:20.761Z" }, -] - -[[package]] -name = "wrapt" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" }, - { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" }, - { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" }, - { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" }, - { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" }, - { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" }, - { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" }, - { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" }, - { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" }, - { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" }, - { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" }, - { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" }, - { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" }, - { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" }, - { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" }, - { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" }, - { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" }, - { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" }, - { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" }, - { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" }, - { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" }, - { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" }, - { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" }, - { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" }, - { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" }, - { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" }, - { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" }, - { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" }, - { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" }, - { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" }, - { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" }, - { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" }, - { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" }, - { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" }, - { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" }, - { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" }, - { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" }, - { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" }, - { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" }, - { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" }, - { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" }, - { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" }, - { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" }, - { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" }, - { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, -] - -[[package]] -name = "wsproto" -version = "1.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b8701c2c19a14c913c120b882d50b014ca0d38083c2c/wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294", size = 50116, upload-time = "2025-11-20T18:18:01.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, -] - -[[package]] -name = "xacro" -version = "2.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/62/b2/12fc318d3563481fe01482dd8e925d38e83b62d64291ed16ab0b6d836a91/xacro-2.1.1.tar.gz", hash = "sha256:3e7adf33cdd90d9fbe8ca0d07d9118acf770a2ad8bf575977019a5c8a60d4d1b", size = 104752, upload-time = "2025-08-28T18:21:20.397Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/6b/3fcfd8589d0e319f5fb56f105acbe791198fd36bb54469a91fbe49d828c4/xacro-2.1.1-py3-none-any.whl", hash = "sha256:c3b330ebd984a3bce6d6482e0047eae5c5333fedd49b30b9b6df863a086b35f7", size = 27087, upload-time = "2025-08-28T18:21:19.165Z" }, -] - -[[package]] -name = "xarm-python-sdk" -version = "1.17.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/dd/073cf64fa9e74cfb97c9ded97750ed4652ada1b4921cd0e7d895ff242f7c/xarm_python_sdk-1.17.3.tar.gz", hash = "sha256:e61b988bc3be684c15f8e686958c00e619e14725130ee94148074b8ef5bd9ec3", size = 215842, upload-time = "2025-12-02T03:09:49.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/0a/85b3df0fa6ddd4f1a9d23cd63948aa145797631c9e20d165ada8e13a058c/xarm_python_sdk-1.17.3-py3-none-any.whl", hash = "sha256:3dee2f9819d54f0ba476ea51ff63f2d1eb248e0658da1b1dcab8c519008955bb", size = 186790, upload-time = "2025-12-02T03:09:47.606Z" }, -] - -[[package]] -name = "xformers" -version = "0.0.34" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'aarch64') or (python_full_version < '3.11' and sys_platform != 'linux')" }, - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'aarch64') or (python_full_version >= '3.11' and sys_platform != 'linux')" }, - { name = "torch", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ef/2b/365151a1e2e6aa70c1bd66e0532e3d71915a28a34ebde3d9b068e8849f66/xformers-0.0.34.tar.gz", hash = "sha256:716bd9ffe61f46c2cc0536abf8b8c43ec594bea47a49394ea5cfa417e9de6a6f", size = 14303297, upload-time = "2026-01-23T18:14:31.457Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/44/33/3f4316a70ebbc2cccd3219d85bec9f4c134e5c135afbf8cba2b2be26cb40/xformers-0.0.34-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:381cc47f43e95893e21b7f04f1aa31dc10a81fc95ba92482e4465a5064c77743", size = 110763890, upload-time = "2026-01-23T18:14:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/15/03/5e3cfc5b45d008667e3cb87f1e75144a6fcd87eafa1fabb923f10c4cd9f5/xformers-0.0.34-cp39-abi3-win_amd64.whl", hash = "sha256:941979e890dd18e26f9860daa83acb706e658345d18511a962f909067331cc19", size = 103155172, upload-time = "2026-01-23T18:14:27.798Z" }, -] - -[[package]] -name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/ee/f9f1d656ad168681bb0f6b092372c1e533c4416b8069b1896a175c46e484/xxhash-3.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:87ff03d7e35c61435976554477a7f4cd1704c3596a89a8300d5ce7fc83874a71", size = 32845, upload-time = "2025-10-02T14:33:51.573Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b1/93508d9460b292c74a09b83d16750c52a0ead89c51eea9951cb97a60d959/xxhash-3.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f572dfd3d0e2eb1a57511831cf6341242f5a9f8298a45862d085f5b93394a27d", size = 30807, upload-time = "2025-10-02T14:33:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/07/55/28c93a3662f2d200c70704efe74aab9640e824f8ce330d8d3943bf7c9b3c/xxhash-3.6.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:89952ea539566b9fed2bbd94e589672794b4286f342254fad28b149f9615fef8", size = 193786, upload-time = "2025-10-02T14:33:54.272Z" }, - { url = "https://files.pythonhosted.org/packages/c1/96/fec0be9bb4b8f5d9c57d76380a366f31a1781fb802f76fc7cda6c84893c7/xxhash-3.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e6f2ffb07a50b52465a1032c3cf1f4a5683f944acaca8a134a2f23674c2058", size = 212830, upload-time = "2025-10-02T14:33:55.706Z" }, - { url = "https://files.pythonhosted.org/packages/c4/a0/c706845ba77b9611f81fd2e93fad9859346b026e8445e76f8c6fd057cc6d/xxhash-3.6.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b5b848ad6c16d308c3ac7ad4ba6bede80ed5df2ba8ed382f8932df63158dd4b2", size = 211606, upload-time = "2025-10-02T14:33:57.133Z" }, - { url = "https://files.pythonhosted.org/packages/67/1e/164126a2999e5045f04a69257eea946c0dc3e86541b400d4385d646b53d7/xxhash-3.6.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a034590a727b44dd8ac5914236a7b8504144447a9682586c3327e935f33ec8cc", size = 444872, upload-time = "2025-10-02T14:33:58.446Z" }, - { url = "https://files.pythonhosted.org/packages/2d/4b/55ab404c56cd70a2cf5ecfe484838865d0fea5627365c6c8ca156bd09c8f/xxhash-3.6.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a8f1972e75ebdd161d7896743122834fe87378160c20e97f8b09166213bf8cc", size = 193217, upload-time = "2025-10-02T14:33:59.724Z" }, - { url = "https://files.pythonhosted.org/packages/45/e6/52abf06bac316db33aa269091ae7311bd53cfc6f4b120ae77bac1b348091/xxhash-3.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ee34327b187f002a596d7b167ebc59a1b729e963ce645964bbc050d2f1b73d07", size = 210139, upload-time = "2025-10-02T14:34:02.041Z" }, - { url = "https://files.pythonhosted.org/packages/34/37/db94d490b8691236d356bc249c08819cbcef9273a1a30acf1254ff9ce157/xxhash-3.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:339f518c3c7a850dd033ab416ea25a692759dc7478a71131fe8869010d2b75e4", size = 197669, upload-time = "2025-10-02T14:34:03.664Z" }, - { url = "https://files.pythonhosted.org/packages/b7/36/c4f219ef4a17a4f7a64ed3569bc2b5a9c8311abdb22249ac96093625b1a4/xxhash-3.6.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:bf48889c9630542d4709192578aebbd836177c9f7a4a2778a7d6340107c65f06", size = 210018, upload-time = "2025-10-02T14:34:05.325Z" }, - { url = "https://files.pythonhosted.org/packages/fd/06/bfac889a374fc2fc439a69223d1750eed2e18a7db8514737ab630534fa08/xxhash-3.6.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5576b002a56207f640636056b4160a378fe36a58db73ae5c27a7ec8db35f71d4", size = 413058, upload-time = "2025-10-02T14:34:06.925Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d1/555d8447e0dd32ad0930a249a522bb2e289f0d08b6b16204cfa42c1f5a0c/xxhash-3.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af1f3278bd02814d6dedc5dec397993b549d6f16c19379721e5a1d31e132c49b", size = 190628, upload-time = "2025-10-02T14:34:08.669Z" }, - { url = "https://files.pythonhosted.org/packages/d1/15/8751330b5186cedc4ed4b597989882ea05e0408b53fa47bcb46a6125bfc6/xxhash-3.6.0-cp310-cp310-win32.whl", hash = "sha256:aed058764db109dc9052720da65fafe84873b05eb8b07e5e653597951af57c3b", size = 30577, upload-time = "2025-10-02T14:34:10.234Z" }, - { url = "https://files.pythonhosted.org/packages/bb/cc/53f87e8b5871a6eb2ff7e89c48c66093bda2be52315a8161ddc54ea550c4/xxhash-3.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:e82da5670f2d0d98950317f82a0e4a0197150ff19a6df2ba40399c2a3b9ae5fb", size = 31487, upload-time = "2025-10-02T14:34:11.618Z" }, - { url = "https://files.pythonhosted.org/packages/9f/00/60f9ea3bb697667a14314d7269956f58bf56bb73864f8f8d52a3c2535e9a/xxhash-3.6.0-cp310-cp310-win_arm64.whl", hash = "sha256:4a082ffff8c6ac07707fb6b671caf7c6e020c75226c561830b73d862060f281d", size = 27863, upload-time = "2025-10-02T14:34:12.619Z" }, - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, -] - -[[package]] -name = "xyzservices" -version = "2025.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/022795fc1201e7c29e742a509913badb53ce0b38f64b6db859e2f6339da9/xyzservices-2025.11.0.tar.gz", hash = "sha256:2fc72b49502b25023fd71e8f532fb4beddbbf0aa124d90ea25dba44f545e17ce", size = 1135703, upload-time = "2025-11-22T11:31:51.82Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/5c/2c189d18d495dd0fa3f27ccc60762bbc787eed95b9b0147266e72bb76585/xyzservices-2025.11.0-py3-none-any.whl", hash = "sha256:de66a7599a8d6dad63980b77defd1d8f5a5a9cb5fc8774ea1c6e89ca7c2a3d2f", size = 93916, upload-time = "2025-11-22T11:31:50.525Z" }, -] - -[[package]] -name = "yapf" -version = "0.40.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "platformdirs" }, - { name = "tomli" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/14/c1f0ebd083fddd38a7c832d5ffde343150bd465689d12c549c303fbcd0f5/yapf-0.40.2.tar.gz", hash = "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b", size = 252068, upload-time = "2023-09-22T18:40:46.232Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/66/c9/d4b03b2490107f13ebd68fe9496d41ae41a7de6275ead56d0d4621b11ffd/yapf-0.40.2-py3-none-any.whl", hash = "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b", size = 254707, upload-time = "2023-09-22T18:40:43.297Z" }, -] - -[[package]] -name = "zict" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/ac/3c494dd7ec5122cff8252c1a209b282c0867af029f805ae9befd73ae37eb/zict-3.0.0.tar.gz", hash = "sha256:e321e263b6a97aafc0790c3cfb3c04656b7066e6738c37fffcca95d803c9fba5", size = 33238, upload-time = "2023-04-17T21:41:16.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/80/ab/11a76c1e2126084fde2639514f24e6111b789b0bfa4fc6264a8975c7e1f1/zict-3.0.0-py2.py3-none-any.whl", hash = "sha256:5796e36bd0e0cc8cf0fbc1ace6a68912611c1dbd74750a3f3026b9b9d6a327ae", size = 43332, upload-time = "2023-04-17T21:41:13.444Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] - -[[package]] -name = "zstandard" -version = "0.25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/28efd1d371f1acd037ac64ed1c5e2b41514a6cc937dd6ab6a13ab9f0702f/zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd", size = 795256, upload-time = "2025-09-14T22:15:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/96/34/ef34ef77f1ee38fc8e4f9775217a613b452916e633c4f1d98f31db52c4a5/zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7", size = 640565, upload-time = "2025-09-14T22:15:58.177Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1b/4fdb2c12eb58f31f28c4d28e8dc36611dd7205df8452e63f52fb6261d13e/zstandard-0.25.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:ab85470ab54c2cb96e176f40342d9ed41e58ca5733be6a893b730e7af9c40550", size = 5345306, upload-time = "2025-09-14T22:16:00.165Z" }, - { url = "https://files.pythonhosted.org/packages/73/28/a44bdece01bca027b079f0e00be3b6bd89a4df180071da59a3dd7381665b/zstandard-0.25.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e05ab82ea7753354bb054b92e2f288afb750e6b439ff6ca78af52939ebbc476d", size = 5055561, upload-time = "2025-09-14T22:16:02.22Z" }, - { url = "https://files.pythonhosted.org/packages/e9/74/68341185a4f32b274e0fc3410d5ad0750497e1acc20bd0f5b5f64ce17785/zstandard-0.25.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:78228d8a6a1c177a96b94f7e2e8d012c55f9c760761980da16ae7546a15a8e9b", size = 5402214, upload-time = "2025-09-14T22:16:04.109Z" }, - { url = "https://files.pythonhosted.org/packages/8b/67/f92e64e748fd6aaffe01e2b75a083c0c4fd27abe1c8747fee4555fcee7dd/zstandard-0.25.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b6bd67528ee8b5c5f10255735abc21aa106931f0dbaf297c7be0c886353c3d0", size = 5449703, upload-time = "2025-09-14T22:16:06.312Z" }, - { url = "https://files.pythonhosted.org/packages/fd/e5/6d36f92a197c3c17729a2125e29c169f460538a7d939a27eaaa6dcfcba8e/zstandard-0.25.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4b6d83057e713ff235a12e73916b6d356e3084fd3d14ced499d84240f3eecee0", size = 5556583, upload-time = "2025-09-14T22:16:08.457Z" }, - { url = "https://files.pythonhosted.org/packages/d7/83/41939e60d8d7ebfe2b747be022d0806953799140a702b90ffe214d557638/zstandard-0.25.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9174f4ed06f790a6869b41cba05b43eeb9a35f8993c4422ab853b705e8112bbd", size = 5045332, upload-time = "2025-09-14T22:16:10.444Z" }, - { url = "https://files.pythonhosted.org/packages/b3/87/d3ee185e3d1aa0133399893697ae91f221fda79deb61adbe998a7235c43f/zstandard-0.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:25f8f3cd45087d089aef5ba3848cd9efe3ad41163d3400862fb42f81a3a46701", size = 5572283, upload-time = "2025-09-14T22:16:12.128Z" }, - { url = "https://files.pythonhosted.org/packages/0a/1d/58635ae6104df96671076ac7d4ae7816838ce7debd94aecf83e30b7121b0/zstandard-0.25.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3756b3e9da9b83da1796f8809dd57cb024f838b9eeafde28f3cb472012797ac1", size = 4959754, upload-time = "2025-09-14T22:16:14.225Z" }, - { url = "https://files.pythonhosted.org/packages/75/d6/57e9cb0a9983e9a229dd8fd2e6e96593ef2aa82a3907188436f22b111ccd/zstandard-0.25.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:81dad8d145d8fd981b2962b686b2241d3a1ea07733e76a2f15435dfb7fb60150", size = 5266477, upload-time = "2025-09-14T22:16:16.343Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a9/ee891e5edf33a6ebce0a028726f0bbd8567effe20fe3d5808c42323e8542/zstandard-0.25.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a5a419712cf88862a45a23def0ae063686db3d324cec7edbe40509d1a79a0aab", size = 5440914, upload-time = "2025-09-14T22:16:18.453Z" }, - { url = "https://files.pythonhosted.org/packages/58/08/a8522c28c08031a9521f27abc6f78dbdee7312a7463dd2cfc658b813323b/zstandard-0.25.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e7360eae90809efd19b886e59a09dad07da4ca9ba096752e61a2e03c8aca188e", size = 5819847, upload-time = "2025-09-14T22:16:20.559Z" }, - { url = "https://files.pythonhosted.org/packages/6f/11/4c91411805c3f7b6f31c60e78ce347ca48f6f16d552fc659af6ec3b73202/zstandard-0.25.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75ffc32a569fb049499e63ce68c743155477610532da1eb38e7f24bf7cd29e74", size = 5363131, upload-time = "2025-09-14T22:16:22.206Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d6/8c4bd38a3b24c4c7676a7a3d8de85d6ee7a983602a734b9f9cdefb04a5d6/zstandard-0.25.0-cp310-cp310-win32.whl", hash = "sha256:106281ae350e494f4ac8a80470e66d1fe27e497052c8d9c3b95dc4cf1ade81aa", size = 436469, upload-time = "2025-09-14T22:16:25.002Z" }, - { url = "https://files.pythonhosted.org/packages/93/90/96d50ad417a8ace5f841b3228e93d1bb13e6ad356737f42e2dde30d8bd68/zstandard-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea9d54cc3d8064260114a0bbf3479fc4a98b21dffc89b3459edd506b69262f6e", size = 506100, upload-time = "2025-09-14T22:16:23.569Z" }, - { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, - { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, - { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, - { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, - { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, - { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, - { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, - { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, - { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, - { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, - { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, - { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, - { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, - { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, - { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, - { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, - { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, - { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, - { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, - { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, - { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, - { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, - { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, - { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, - { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, - { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, - { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, - { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, - { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, - { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, - { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, - { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, - { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, - { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, - { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, - { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, -]