From 38bb7d4c1d45ac95b920685df1927c4e62cc72f8 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 20:07:54 -0400 Subject: [PATCH 1/7] =?UTF-8?q?feat(modules):=20RAVEN=20+=20geometric=20ex?= =?UTF-8?q?ploration=20planners=20as=20modules;=20global=5Fplan=E2=86=92Na?= =?UTF-8?q?vigateTask=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two global planners now ship as external modules with pinned reference stacks: - stacks/full_exploration — asm_exploration_planner (frontier-based geometric exploration planner from the construction-site project). The trunk package robot/ros_ws/src/global/planners/exploration moves to that module repo with its history (git filter-repo) plus the bounded-exploration additions from junbin/planning_demo; removed here so the same colcon package never exists in trunk and the module overlay at once. - stacks/full_raven — asm_raven (RAVEN, ICRA 2026): the RayFronts semantic ray-frontier mapper + behavior manager run as a GPU sidecar container declared by the module's compose fragment; raven_bridge relays plans/prompts on the robot side. Trunk support these needed: - robot/ros_ws/src/global/global_plan_navigate_bridge: turns each new Path on global_plan into a NavigateTask goal (cancel-then-resend, identical re-publications deduped) so topic-publishing planners can drive the task-executor local planner droan_gl. Hermetic unit tests registered in tests/colcon_unit_test_packages.yaml. - tools/module_overlay.py: compose fragments may now declare sidecar services with build: sections — build.context / build.dockerfile / env_file relative paths are absolutized like bind sources (+ contract tests). - Isaac follow camera: ISAAC_SIM_FOLLOW_CAM_TOPIC / ISAAC_SIM_FOLLOW_CAM_RES publish the chase camera as a sensor_msgs/Image topic for headless video capture (scene_prep.add_camera_image_publisher). - Docs: stack tables, autonomy modes, global planning page, environment variables, modules.md (sidecars), docs/modules catalog regenerated from the fixture index (raven, exploration_planner, full_raven, full_exploration), mkdocs nav, release notes. VERSION 0.21.0-dev.8 -> 0.21.0-dev.9. Module repos: castacks/asm_raven, castacks/asm_exploration_planner (v0.1.0 pins are placeholders until tagged). Registry: airstack-modules-index PR #2. Co-Authored-By: Claude Fable 5.1 --- .env | 2 +- .vscode/c_cpp_properties.json | 1 - docs/development/stacks.md | 4 + docs/modules/exploration_planner.md | 56 ++ docs/modules/index.md | 4 + docs/modules/raven.md | 56 ++ docs/release_notes/index.md | 25 + docs/robot/autonomy/global/planning/index.md | 21 +- docs/robot/autonomy_modes.md | 2 + .../configuration/environment_variables.md | 2 +- mkdocs.yml | 6 +- .../global_plan_navigate_bridge/README.md | 77 ++ .../config/global_plan_navigate_bridge.yaml | 18 + .../global_plan_navigate_bridge/__init__.py | 3 + .../bridge_node.py | 188 ++++ .../global_plan_navigate_bridge/plan_key.py | 33 + .../global_plan_navigate_bridge.launch.xml | 37 + .../global_plan_navigate_bridge/package.xml | 23 + .../resource/global_plan_navigate_bridge | 0 .../global_plan_navigate_bridge/setup.cfg | 20 + .../global_plan_navigate_bridge/setup.py | 29 + .../test/conftest.py | 6 + .../test/test_plan_key.py | 46 + .../planners/exploration/CMakeLists.txt | 147 --- .../exploration/CMakeModules/FindBlosc.cmake | 394 -------- .../CMakeModules/FindOpenVDB.cmake | 838 ---------------- .../CMakeModules/OpenVDBUtils.cmake | 164 ---- .../src/global/planners/exploration/README.md | 55 -- .../config/exploration_config.yaml | 91 -- .../exploration/exploration_vis_path.png | Bin 21241 -> 0 bytes .../exploration/exploration_vis_vdb.png | Bin 243684 -> 0 bytes .../exploration/include/exploration_logic.hpp | 222 ----- .../exploration/include/exploration_node.hpp | 158 --- .../include/utils/collision_checker.h | 149 --- .../exploration/include/utils/rrt_planner.h | 187 ---- .../exploration/include/utils/utils.hpp | 138 --- .../exploration/include/utils/viewpoint.hpp | 318 ------ .../include/viewpoint_sampling.hpp | 121 --- .../exploration/launch/exploration_launch.xml | 15 - .../global/planners/exploration/package.xml | 34 - .../exploration/src/collision_checker.cpp | 255 ----- .../exploration/src/exploration_logic.cpp | 762 --------------- .../exploration/src/exploration_node.cpp | 916 ------------------ .../exploration/src/exploration_node_run.cpp | 11 - .../planners/exploration/src/rrt_planner.cpp | 784 --------------- .../global/planners/exploration/src/utils.cpp | 772 --------------- .../exploration/src/viewpoint_sampling.cpp | 488 ---------- .../exploration/src/waypoint_routing_node.cpp | 351 ------- .../isaac-sim/docker/docker-compose.yaml | 5 + .../isaac-sim/launch_scripts/pegasus_app.py | 23 + simulation/isaac-sim/utils/scene_prep.py | 66 ++ stacks/full_default/launch/stack.launch.xml | 6 +- stacks/full_droan_cpu/launch/stack.launch.xml | 6 +- stacks/full_exploration/README.md | 49 + stacks/full_exploration/docker-compose.yaml | 11 + .../full_exploration/launch/stack.launch.xml | 157 +++ stacks/full_exploration/modules.repos | 22 + stacks/full_macvo/launch/stack.launch.xml | 6 +- stacks/full_mighty/launch/stack.launch.xml | 6 +- stacks/full_raven/README.md | 66 ++ stacks/full_raven/docker-compose.yaml | 13 + stacks/full_raven/launch/stack.launch.xml | 157 +++ stacks/full_raven/modules.repos | 23 + tests/colcon_unit_test_packages.yaml | 1 + .../modules/exploration_planner.yaml | 22 + .../fixtures/modules_index/modules/raven.yaml | 22 + .../stacks/full_exploration.yaml | 15 + .../modules_index/stacks/full_raven.yaml | 14 + tests/meta/launch_lint_allowlist.txt | 6 +- tests/meta/test_module_overlay_contract.py | 82 ++ tools/module_overlay.py | 83 +- 71 files changed, 1483 insertions(+), 7407 deletions(-) create mode 100644 docs/modules/exploration_planner.md create mode 100644 docs/modules/raven.md create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/README.md create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/config/global_plan_navigate_bridge.yaml create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/__init__.py create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/bridge_node.py create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/plan_key.py create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/launch/global_plan_navigate_bridge.launch.xml create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/package.xml create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/resource/global_plan_navigate_bridge create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/setup.cfg create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/setup.py create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/test/conftest.py create mode 100644 robot/ros_ws/src/global/global_plan_navigate_bridge/test/test_plan_key.py delete mode 100644 robot/ros_ws/src/global/planners/exploration/CMakeLists.txt delete mode 100644 robot/ros_ws/src/global/planners/exploration/CMakeModules/FindBlosc.cmake delete mode 100644 robot/ros_ws/src/global/planners/exploration/CMakeModules/FindOpenVDB.cmake delete mode 100644 robot/ros_ws/src/global/planners/exploration/CMakeModules/OpenVDBUtils.cmake delete mode 100644 robot/ros_ws/src/global/planners/exploration/README.md delete mode 100644 robot/ros_ws/src/global/planners/exploration/config/exploration_config.yaml delete mode 100644 robot/ros_ws/src/global/planners/exploration/exploration_vis_path.png delete mode 100644 robot/ros_ws/src/global/planners/exploration/exploration_vis_vdb.png delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/exploration_logic.hpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/exploration_node.hpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/utils/collision_checker.h delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/utils/rrt_planner.h delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/utils/utils.hpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/utils/viewpoint.hpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/include/viewpoint_sampling.hpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml delete mode 100644 robot/ros_ws/src/global/planners/exploration/package.xml delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/collision_checker.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/exploration_logic.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/exploration_node.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/exploration_node_run.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/rrt_planner.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/utils.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/viewpoint_sampling.cpp delete mode 100644 robot/ros_ws/src/global/planners/exploration/src/waypoint_routing_node.cpp create mode 100644 stacks/full_exploration/README.md create mode 100644 stacks/full_exploration/docker-compose.yaml create mode 100644 stacks/full_exploration/launch/stack.launch.xml create mode 100644 stacks/full_exploration/modules.repos create mode 100644 stacks/full_raven/README.md create mode 100644 stacks/full_raven/docker-compose.yaml create mode 100644 stacks/full_raven/launch/stack.launch.xml create mode 100644 stacks/full_raven/modules.repos create mode 100644 tests/meta/fixtures/modules_index/modules/exploration_planner.yaml create mode 100644 tests/meta/fixtures/modules_index/modules/raven.yaml create mode 100644 tests/meta/fixtures/modules_index/stacks/full_exploration.yaml create mode 100644 tests/meta/fixtures/modules_index/stacks/full_raven.yaml diff --git a/.env b/.env index 42f23cbb0..d29e814e8 100644 --- a/.env +++ b/.env @@ -12,7 +12,7 @@ PROJECT_NAME="airstack" # If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made # to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version. # auto-generated from git commit hash -VERSION="0.21.0-dev.8" +VERSION="0.21.0-dev.9" # Image-tag discriminator ONLY (appears in the image tag suffix, e.g. ..._robot-x86-64_dev). # No Dockerfile consumes it: "prebuilt" does NOT bake the built ros_ws into the image today — # a real prebuilt (workspace-baked) stage is future work. Keep "dev" (mounted code, built live). diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 18da608c4..20fd4108a 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -11,7 +11,6 @@ "/home/andrew/Development/AirStack/robot/ros_ws/src/behavior/behavior_executive/include/**", "/home/andrew/Development/AirStack/robot/ros_ws/src/behavior/behavior_tree/include/**", "/home/andrew/Development/AirStack/robot/ros_ws/src/global/planners/ensemble_planner/include/**", - "/home/andrew/Development/AirStack/robot/ros_ws/src/global/planners/exploration/include/**", "/home/andrew/Development/AirStack/robot/ros_ws/src/global/planners/random_walk/include/**", "/home/andrew/Development/AirStack/robot/ros_ws/src/global/world_models/vdb_mapping/include/**", "/home/andrew/Development/AirStack/robot/ros_ws/src/global/world_models/vdb_mapping_ros2/vdb_mapping_ros2/include/**", diff --git a/docs/development/stacks.md b/docs/development/stacks.md index cfbc9d29b..cc712580a 100644 --- a/docs/development/stacks.md +++ b/docs/development/stacks.md @@ -31,6 +31,8 @@ Anatomy is enforced by a unit test: `tests/meta/test_stack_layout_contract.py` | [`full_droan_cpu`](https://github.com/castacks/AirStack/tree/develop/stacks/full_droan_cpu) | CPU DROAN planner + live `disparity_expansion` (for machines without the GPU planner). | | [`full_macvo`](https://github.com/castacks/AirStack/tree/develop/stacks/full_macvo) | MAC-VO as the planner's disparity source. Requires the `asm_macvo` module (`airstack module add asm_macvo`). | | [`full_mighty`](https://github.com/castacks/AirStack/tree/develop/stacks/full_mighty) | The MIGHTY map-based local planner (+ acl-mapping voxel world model) in place of `droan_gl`. Requires the `asm_mighty` module (pinned in the stack's `modules.repos`). | +| [`full_exploration`](https://github.com/castacks/AirStack/tree/develop/stacks/full_exploration) | The frontier-based geometric exploration planner in place of the random walk (plus the `global_plan_navigate_bridge`). Requires the `asm_exploration_planner` module (pinned in the stack's `modules.repos`). | +| [`full_raven`](https://github.com/castacks/AirStack/tree/develop/stacks/full_raven) | RAVEN open-set semantic navigation as the global planner: RayFronts GPU sidecar container + `raven_bridge` (plus the `global_plan_navigate_bridge`). Requires the `asm_raven` module. | | [`lite_default`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_default) | Onboard-lite topology, unsplit: interface, sensors, perception, flat Local layer, behavior; **no global, no logging**. | | [`lite_offload_global`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_offload_global) | A **split stack**: `onboard.launch.xml` (= lite topology) + `offboard.launch.xml` (global layer only) + `bridge.yaml`. | @@ -285,5 +287,7 @@ ignored. Select the topology with `--stack`: | CPU DROAN topology | `--stack full_droan_cpu` | | MAC-VO disparity topology | `--stack full_macvo` (requires the `asm_macvo` module) | | MIGHTY map-based local planner | `--stack full_mighty` (requires the `asm_mighty` module) | +| Frontier exploration global planner | `--stack full_exploration` (requires the `asm_exploration_planner` module) | +| RAVEN semantic (language-prompted) global planner | `--stack full_raven` (requires the `asm_raven` module; first run builds the sidecar image) | | Onboard-lite, unsplit | `--stack lite_default` | | Split onboard/offboard | `--stack lite_offload_global:onboard` / `:offboard` (+ generate the router config from `bridge.yaml`) | diff --git a/docs/modules/exploration_planner.md b/docs/modules/exploration_planner.md new file mode 100644 index 000000000..1e40d0fc2 --- /dev/null +++ b/docs/modules/exploration_planner.md @@ -0,0 +1,56 @@ +# exploration_planner + + + +> Frontier-based geometric exploration global planner — OpenVDB occupancy map, frontier clustering, bounded viewpoint sampling and an RRT path to the chosen viewpoint, published as global_plan; developed for the CMU AirLab construction-site project + +| | | +|---|---| +| Repository | [castacks/asm_exploration_planner](https://github.com/castacks/asm_exploration_planner) | +| Type | `ros_package` | +| Maintainer | ajong@andrew.cmu.edu | +| License | BSD-3-Clause-Clear | +| Registered ref | [`v0.1.0`](https://github.com/castacks/asm_exploration_planner/tree/v0.1.0) | +| Declared compat | `>=0.21.0-dev.8 <0.22.0` | +| Registry entry | [modules/exploration_planner.yaml](https://github.com/castacks/airstack-modules-index/blob/main/modules/exploration_planner.yaml) | + +## Install + +From an AirStack checkout ([AirStack Modules guide](../development/modules.md)): + +```bash +airstack module add https://github.com/castacks/asm_exploration_planner --version v0.1.0 +airstack up +``` + +`module add` pins the module in `modules.repos` and syncs it into the +gitignored `modules/` overlay; `airstack up` automatically includes the +generated compose override that mounts it into the containers. + +## Compatibility: declared vs verified + +The range `>=0.21.0-dev.8 <0.22.0` is **DECLARED** by the module author +(copied from the module's `module.yaml`). The **VERIFIED** record — rows +stamped exclusively by CI runs of the reusable +[module-system-tests workflow](../development/module_ci.md) — lives in the +registry's [compat/ matrix](https://github.com/castacks/airstack-modules-index/tree/main/compat) +([compat/exploration_planner.yaml](https://github.com/castacks/airstack-modules-index/blob/main/compat/exploration_planner.yaml) once stamped). +A compatibility claim that isn't CI-verified rots: trust the +matrix, read the declaration as intent. + +## Documentation + +- [Module README on GitHub @ `v0.1.0`](https://github.com/castacks/asm_exploration_planner/blob/v0.1.0/README.md) +- *The module repo was not fetched when this page was generated — the* + *links above go to GitHub at the registered ref (failure isolation:* + *an unreachable module repo never fails the docs deploy).* + +## Registered stacks using this module + +- [full_exploration](../../stacks/full_exploration/README.md) + +## Registry notes + +> Extracted from trunk (robot/ros_ws/src/global/planners/exploration, history preserved) together with the bounded-exploration additions from the construction-site demo branch. Publishes a path on global_plan and serves the behavior/global_plan_toggle service; the trunk global_plan_navigate_bridge turns each plan into a NavigateTask goal for droan_gl. Consumed by trunk reference stack full_exploration. diff --git a/docs/modules/index.md b/docs/modules/index.md index db55baf9b..833fdc597 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -19,9 +19,11 @@ directory and is never hand-edited. | Module | Description | Type | Maintainer | Declared compat | Links | |--------|-------------|------|------------|-----------------|-------| | [dfm2_disturbances](dfm2_disturbances.md) | Isaac Sim disturbance library (fan/vent force fields, strobe lights, lens flare) | `isaac_extension` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_dfm2_disturbances) | +| [exploration_planner](exploration_planner.md) | Frontier-based geometric exploration global planner — OpenVDB occupancy map, frontier clustering, bounded viewpoint sampling and an RRT path to the chosen viewpoint, published as global_plan; developed for the CMU AirLab construction-site project | `ros_package` | ajong@andrew.cmu.edu | `>=0.21.0-dev.8 <0.22.0` | [repo](https://github.com/castacks/asm_exploration_planner) · [full_exploration](../../stacks/full_exploration/README.md) | | [macvo](macvo.md) | MAC-VO learned stereo visual odometry (ICRA 2025 best paper) — macvo_ros2 wrapper around the MAC-VO network, publishing odometry, a covariance-aware point cloud, and the disparity image the local planner can consume | `ros_package` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_macvo) · [full_macvo](../../stacks/full_macvo/README.md) | | [mighty](mighty.md) | MIGHTY Hermite-spline local planner (MIT ACL, RA-L 2026) with its acl-mapping voxel world model and a bridge to AirStack's NavigateTask / trajectory_controller seam — a map-based replacement for the DROAN local planner | `ros_package` | ajong@andrew.cmu.edu | `>=0.20.0-alpha.16 <0.21.0` | [repo](https://github.com/castacks/asm_mighty) · [full_mighty](../../stacks/full_mighty/README.md) | | [optitrack](optitrack.md) | OptiTrack NatNet mocap integration — natnet_ros2 client + PX4 external-vision fusion bridges on the robot, and the Motive-compatible NatNet server emulator for Isaac Sim | `ros_package` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_optitrack) | +| [raven](raven.md) | RAVEN semantic aerial navigation (ICRA 2026) — the RayFronts open-set semantic ray-frontier map and behavior manager (voxel, ray, LVLM and frontier search) in a GPU sidecar container publishing global_plan toward a natural-language target, plus the raven_bridge plan/prompt relay on the robot | `ros_package` | ajong@andrew.cmu.edu | `>=0.21.0-dev.8 <0.22.0` | [repo](https://github.com/castacks/asm_raven) · [full_raven](../../stacks/full_raven/README.md) | ## Registered stacks @@ -35,8 +37,10 @@ The stacks below are the ones REGISTERED in the index; the site nav's |-------|-------------|-----------------|--------|----------------| | [full_default](../../stacks/full_default/README.md) | The current full-autonomy topology as a self-contained stack folder — the baseline most users start from and the stack other stacks are copied from | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_default/wiring.md) | [full_default.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_default.yaml) | | [full_droan_cpu](../../stacks/full_droan_cpu/README.md) | Full autonomy with the CPU DROAN local planner (droan_local_planner + live disparity_expansion world model) instead of the GPU droan_gl node | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_droan_cpu/wiring.md) | [full_droan_cpu.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_droan_cpu.yaml) | +| [full_exploration](../../stacks/full_exploration/README.md) | Full autonomy with the frontier-based geometric exploration planner (exploration_planner module) in place of the random walk, driving droan_gl through the global_plan_navigate_bridge; everything else is identical to full_default | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_exploration.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_exploration.yaml) | | [full_macvo](../../stacks/full_macvo/README.md) | Full autonomy with MAC-VO learned stereo visual odometry as the disparity source for the local planner (droan_gl consumes /$ROBOT_NAME/perception/macvo/disparity) | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_macvo/wiring.md) | [full_macvo.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_macvo.yaml) | | [full_mighty](../../stacks/full_mighty/README.md) | Full autonomy with the local planning layer swapped to the MIGHTY module (mighty planner + acl-mapping voxel world model fed by the filtered Ouster cloud + NavigateTask bridge) in place of droan_gl; everything else is identical to full_default | `>=0.20.0-alpha.16 <0.21.0` | *not committed yet* | [full_mighty.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_mighty.yaml) | +| [full_raven](../../stacks/full_raven/README.md) | Full autonomy with RAVEN open-set semantic navigation as the global planner (RayFronts GPU sidecar container + raven_bridge, driving droan_gl through the global_plan_navigate_bridge); everything else is identical to full_default | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_raven.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_raven.yaml) | ## See also diff --git a/docs/modules/raven.md b/docs/modules/raven.md new file mode 100644 index 000000000..1da30da1a --- /dev/null +++ b/docs/modules/raven.md @@ -0,0 +1,56 @@ +# raven + + + +> RAVEN semantic aerial navigation (ICRA 2026) — the RayFronts open-set semantic ray-frontier map and behavior manager (voxel, ray, LVLM and frontier search) in a GPU sidecar container publishing global_plan toward a natural-language target, plus the raven_bridge plan/prompt relay on the robot + +| | | +|---|---| +| Repository | [castacks/asm_raven](https://github.com/castacks/asm_raven) | +| Type | `ros_package` | +| Maintainer | ajong@andrew.cmu.edu | +| License | BSD-3-Clause-Clear | +| Registered ref | [`v0.1.0`](https://github.com/castacks/asm_raven/tree/v0.1.0) | +| Declared compat | `>=0.21.0-dev.8 <0.22.0` | +| Registry entry | [modules/raven.yaml](https://github.com/castacks/airstack-modules-index/blob/main/modules/raven.yaml) | + +## Install + +From an AirStack checkout ([AirStack Modules guide](../development/modules.md)): + +```bash +airstack module add https://github.com/castacks/asm_raven --version v0.1.0 +airstack up +``` + +`module add` pins the module in `modules.repos` and syncs it into the +gitignored `modules/` overlay; `airstack up` automatically includes the +generated compose override that mounts it into the containers. + +## Compatibility: declared vs verified + +The range `>=0.21.0-dev.8 <0.22.0` is **DECLARED** by the module author +(copied from the module's `module.yaml`). The **VERIFIED** record — rows +stamped exclusively by CI runs of the reusable +[module-system-tests workflow](../development/module_ci.md) — lives in the +registry's [compat/ matrix](https://github.com/castacks/airstack-modules-index/tree/main/compat) +([compat/raven.yaml](https://github.com/castacks/airstack-modules-index/blob/main/compat/raven.yaml) once stamped). +A compatibility claim that isn't CI-verified rots: trust the +matrix, read the declaration as intent. + +## Documentation + +- [Module README on GitHub @ `v0.1.0`](https://github.com/castacks/asm_raven/blob/v0.1.0/README.md) +- *The module repo was not fetched when this page was generated — the* + *links above go to GitHub at the registered ref (failure isolation:* + *an unreachable module repo never fails the docs deploy).* + +## Registered stacks using this module + +- [full_raven](../../stacks/full_raven/README.md) + +## Registry notes + +> Wrapper (raven_bridge, sidecar Dockerfile, compose fragment, test_stack) is BSD-3-Clause-Clear; the vendored RayFronts fork (seungchan-kim/RayFronts, branch raven) keeps its own license — see the module's VENDORED.md. The sidecar is declared through the manifest's compose fragment (first module to bring its own container) and is built on first `airstack up` (~20 GB image). Consumed by trunk reference stack full_raven. diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md index 12d996baf..79dd02eb5 100644 --- a/docs/release_notes/index.md +++ b/docs/release_notes/index.md @@ -19,6 +19,31 @@ its own notes. --> ## 0.21.0 (Unreleased) +- **New modules: RAVEN semantic planner and the geometric exploration + planner.** Two global planners now ship as external modules and pinned + reference stacks: [asm_raven](https://github.com/castacks/asm_raven) + (RAVEN, ICRA 2026 — RayFronts open-set semantic ray-frontier map + behavior + manager in a GPU sidecar container, `raven_bridge` on the robot; stack + `full_raven`) and + [asm_exploration_planner](https://github.com/castacks/asm_exploration_planner) + (the frontier-based geometric exploration planner from the construction-site + project, extracted from trunk's `robot/ros_ws/src/global/planners/exploration` + with its bounded viewpoint sampling; stack `full_exploration`). Both plug in + as one `` swap against `full_default`. +- **`global_plan_navigate_bridge`.** New trunk package that turns a + `global_plan` topic into `NavigateTask` goals (cancel-then-resend on each new + plan), so global planners that only publish a path can drive the + task-executor local planner `droan_gl`. Included by `full_exploration` and + `full_raven`; `full_default`'s random walk remains its own action client. +- **Compose fragments may declare sidecar services with a `build:` section.** + `airstack module sync` now rewrites a module compose fragment's relative + `build.context` / `build.dockerfile` / `env_file` paths to absolute, like it + already did for bind-mount sources, so a module can bring its own container + (the RAVEN sidecar is the first). +- **Headless follow-camera video.** `ISAAC_SIM_FOLLOW_CAM_TOPIC` publishes the + Isaac Sim follow camera as a raw `sensor_msgs/Image` topic + (`ISAAC_SIM_FOLLOW_CAM_RES`, default 1280x720), giving headless runs and CI a + third-person video feed. - **Fixed: post-release main→develop sync no longer skips.** The `sync-develop-from-main` workflow skipped whenever the merge result was content-identical to `develop` — exactly the situation right after a diff --git a/docs/robot/autonomy/global/planning/index.md b/docs/robot/autonomy/global/planning/index.md index 2da25a8e2..a9c725309 100644 --- a/docs/robot/autonomy/global/planning/index.md +++ b/docs/robot/autonomy/global/planning/index.md @@ -91,13 +91,20 @@ that generates a plan by randomly selecting a direction to move in, and is useful for testing the robot's ability to follow a plan. It is the reference task-executor implementation, serving `tasks/exploration`. -### Exploration planner - -The [exploration planner](../../../../../robot/ros_ws/src/global/planners/exploration/README.md) -(`robot/ros_ws/src/global/planners/exploration`) is a frontier-based -geometric exploration planner — an alternative to `random_walk`. To use it, -swap the `random_walk_planner.launch.xml` include in your stack's entry -launch file for `exploration_launch.xml`, as described in its README. +### Exploration planner (module) + +The [asm_exploration_planner](https://github.com/castacks/asm_exploration_planner) +module provides a frontier-based geometric exploration planner: it builds its +own OpenVDB occupancy map from the LiDAR cloud, extracts and clusters +frontiers, samples viewpoints around them (optionally confined to a bounding +box), and publishes a shortened, collision-checked RRT path on `global_plan`. +The [`full_exploration`](https://github.com/castacks/AirStack/tree/develop/stacks/full_exploration) +reference stack selects it: `full_default` with the random-walk include +replaced by the module's `exploration_planner.launch.xml`, followed by the +[`global_plan_navigate_bridge`](../../../../../robot/ros_ws/src/global/global_plan_navigate_bridge/README.md) +that turns each published plan into a `NavigateTask` goal for the local +planner. Run it with `airstack module sync` (the stack pins the module) and +`airstack up --stack full_exploration --sim isaac`. ## Writing Your Own Global Planner diff --git a/docs/robot/autonomy_modes.md b/docs/robot/autonomy_modes.md index b1536658f..5396ff8c6 100644 --- a/docs/robot/autonomy_modes.md +++ b/docs/robot/autonomy_modes.md @@ -15,6 +15,8 @@ environment variable is a preflight error.) | **`full_droan_cpu`** | `full_default` with the CPU DROAN local planner (`droan_local_planner` + `disparity_expansion`) instead of the GPU `droan_gl` node | | **`full_macvo`** | `full_default` with MAC-VO as the disparity source — requires `airstack module add asm_macvo` first | | **`full_mighty`** | `full_default` with the MIGHTY map-based local planner (`asm_mighty` module: planner + acl-mapping voxel world model + NavigateTask bridge) in place of `droan_gl` | +| **`full_exploration`** | `full_default` with the frontier-based geometric exploration planner (`asm_exploration_planner` module) in place of the random walk, driving `droan_gl` through the `global_plan_navigate_bridge` | +| **`full_raven`** | `full_default` with RAVEN semantic navigation (`asm_raven` module: RayFronts GPU sidecar + `raven_bridge`) as the global planner — fly toward a natural-language target | Instead of picking a stack per container, `airstack up --fleet ` launches a whole **fleet**: `config/fleets/.yaml` declares who exists, which diff --git a/docs/robot/configuration/environment_variables.md b/docs/robot/configuration/environment_variables.md index 02886d0f4..3fe239ed2 100644 --- a/docs/robot/configuration/environment_variables.md +++ b/docs/robot/configuration/environment_variables.md @@ -78,7 +78,7 @@ These appear commented-out in `.env` (or are read by compose with a built-in def | `MS_AIRSIM_SCENE`, `MS_AIRSIM_ENV_DIR`, `MS_AIRSIM_BINARY_PATH`, `MS_AIRSIM_HEADLESS`, `MS_AIRSIM_PX4_START_DELAY` | UE4 scene fetching/selection, binary override, off-screen rendering, PX4 start delay | [MS AirSim Docker](../../simulation/ms-airsim/docker.md) | | `BAG_STORAGE_PATH` | Host directory mounted at `/bags` on the `l4t` profile (default `/media/airlab/Storage/airstack_collection`) | [Rosbags](../logging/rosbags.md) | | `LOG_CONFIG` | Bag recorder topic-selection file in `logging_bringup/config/` (default `log.yaml`) | [Rosbags](../logging/rosbags.md) | -| `ISAAC_SIM_FOLLOW_CAM`, `ISAAC_SIM_FOLLOW_CAM_OFFSET`, `ISAAC_SIM_FOLLOW_CAM_LIGHT` | Viewport follow-camera: drone domain id to chase, world-frame offset, headlight for unlit interiors | [Isaac Sim](../../simulation/isaac_sim/index.md) (read by `pegasus_app.py`) | +| `ISAAC_SIM_FOLLOW_CAM`, `ISAAC_SIM_FOLLOW_CAM_OFFSET`, `ISAAC_SIM_FOLLOW_CAM_LIGHT`, `ISAAC_SIM_FOLLOW_CAM_TOPIC`, `ISAAC_SIM_FOLLOW_CAM_RES` | Viewport follow-camera: drone domain id to chase, world-frame offset, headlight for unlit interiors; optionally publish the camera as a raw `sensor_msgs/Image` topic (`WxH`, default 1280x720) for headless video capture | [Isaac Sim](../../simulation/isaac_sim/index.md) (read by `pegasus_app.py`) | | `SIM_IP` | Address robot containers use to reach the simulator (default `172.31.0.200`; every sim service binds this fixed address) | `robot/docker/docker-compose.yaml`, `simulation/isaac-sim/docker/docker-compose.yaml` | | `FCU_URL` | MAVROS flight-controller URL on real hardware (default `/dev/ttyTHS4:115200` on `l4t`); desktop/sim derive it from the base ports instead | `robot/docker/docker-compose.yaml` (`robot-l4t`) | | `CACHE_TAG` | Floating Docker-layer-cache image tag (default `cache`) — CI only | [CI/CD](../../development/intermediate/testing/ci_cd.md) | diff --git a/mkdocs.yml b/mkdocs.yml index 99460b788..d14423fab 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -187,17 +187,21 @@ nav: - Reference Stacks: - full_default: stacks/full_default/README.md - full_droan_cpu: stacks/full_droan_cpu/README.md + - full_exploration: stacks/full_exploration/README.md - full_macvo: stacks/full_macvo/README.md - full_mighty: stacks/full_mighty/README.md + - full_raven: stacks/full_raven/README.md - lite_default: stacks/lite_default/README.md - lite_offload_global: stacks/lite_offload_global/README.md - Modules Catalog: - Catalog: docs/modules/index.md - module.yaml Schema: common/module_schema/README.md - dfm2_disturbances: docs/modules/dfm2_disturbances.md + - exploration_planner: docs/modules/exploration_planner.md - macvo: docs/modules/macvo.md - mighty: docs/modules/mighty.md - optitrack: docs/modules/optitrack.md + - raven: docs/modules/raven.md - Autonomy Packages: - Robot Overview: docs/robot/index.md - Autonomy Overview: docs/robot/autonomy/index.md @@ -228,7 +232,7 @@ nav: - VDB Mapping: robot/ros_ws/src/global/world_models/vdb_mapping_ros2/README.md - Planning: docs/robot/autonomy/global/planning/index.md - Random Walk: robot/ros_ws/src/global/planners/random_walk/README.md - - Exploration: robot/ros_ws/src/global/planners/exploration/README.md + - Global Plan Navigate Bridge: robot/ros_ws/src/global/global_plan_navigate_bridge/README.md - Behavior: docs/robot/autonomy/behavior/index.md - Coordination: docs/robot/autonomy/coordination/index.md - RViz Tasks Panel: common/ros_packages/gui/rviz/rviz_tasks_panel/README.md diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/README.md b/robot/ros_ws/src/global/global_plan_navigate_bridge/README.md new file mode 100644 index 000000000..7d264a5c7 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/README.md @@ -0,0 +1,77 @@ +# global_plan_navigate_bridge + +Adapter between a **topic-publishing global planner** and AirStack's +**task-executor local planner**. The trunk local planner (`droan_gl`) plans +only while a `NavigateTask` action goal is active, and it rejects a second goal +while one is running. The random-walk planner drives it as an action client +itself; planners that simply publish a `nav_msgs/Path` on `global_plan` — the +[exploration_planner](https://github.com/castacks/asm_exploration_planner) +and [raven](https://github.com/castacks/asm_raven) modules, or any external +planner following the interface conventions — need this bridge to make the +vehicle move. + +## Architecture + +```mermaid +flowchart LR + GP[global planner
exploration_planner / raven / ...] -->|"global_plan (nav_msgs/Path)"| B[global_plan_navigate_bridge] + B -->|"NavigateTask goal
(cancel active, then send)"| LP[droan_gl
tasks/navigate] + LP -->|trajectory_segment_to_add| TC[trajectory_controller] +``` + +Behaviour: + +1. A new plan whose **pose count** or **final pose** changed (by more than + `replan_min_change_m`) cancels the active goal and, once the cancel is + acknowledged, sends the new plan as a `NavigateTask` goal. `droan_gl` may + still report "task already active" for a moment after the cancel, so the + send is retried (`send_retry_period_s`, `max_send_retries`). +2. Identical re-publications (planners typically re-send their current plan + periodically) are ignored. +3. An empty `Path` cancels the active goal (`cancel_on_empty_plan`). +4. `~/set_enabled` (`std_srvs/SetBool`) pauses/resumes the bridge; disabling + cancels the active goal. + +## Interfaces + +Declared in `launch/global_plan_navigate_bridge.launch.xml` (canonical +defaults; rewire from the stack entry file only): + +| Launch arg | Default | Direction | Type | +|---|---|---|---| +| `global_plan_navigate_bridge_global_plan_topic` | `/$ROBOT_NAME/global_plan` | in | `nav_msgs/Path` | +| `global_plan_navigate_bridge_navigate_task_action` | `/$ROBOT_NAME/tasks/navigate` | client | `task_msgs/action/NavigateTask` | +| `global_plan_navigate_bridge_config` | `config/global_plan_navigate_bridge.yaml` | — | parameter file | + +Service: `/$ROBOT_NAME/global_plan_navigate_bridge/set_enabled` (`std_srvs/SetBool`). + +## Parameters + +| Parameter | Default | Description | +|---|---|---| +| `goal_tolerance_m` | `1.0` | Passed through in every `NavigateTask` goal | +| `replan_min_change_m` | `0.5` | Final-pose displacement below which a re-published plan is treated as unchanged | +| `cancel_on_empty_plan` | `true` | An empty `Path` cancels the active goal | +| `send_retry_period_s` | `0.5` | Retry period after a rejected send | +| `max_send_retries` | `10` | Give up (drop the plan) after this many rejections | +| `enabled` | `true` | Initial state of the `set_enabled` switch | + +## Usage + +Include it in a stack whose global planner publishes plans on a topic, right +after that planner's include (see `stacks/full_exploration` and +`stacks/full_raven`): + +```xml + +``` + +`full_default` does not include it: its random-walk planner is its own +`NavigateTask` client. + +## Testing + +Hermetic unit tests for the plan-change rule live in `test/` and run with +`airstack test -m unit` (listed in `tests/colcon_unit_test_packages.yaml`) or +`colcon test --packages-select global_plan_navigate_bridge` in the robot +container. diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/config/global_plan_navigate_bridge.yaml b/robot/ros_ws/src/global/global_plan_navigate_bridge/config/global_plan_navigate_bridge.yaml new file mode 100644 index 000000000..4abc98c11 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/config/global_plan_navigate_bridge.yaml @@ -0,0 +1,18 @@ +/**: + ros__parameters: + # Passed through in every NavigateTask goal (m). + goal_tolerance_m: 1.0 + # A new plan replaces the active goal only when its final pose moved more + # than this (m) or its pose count changed; identical re-publications are + # ignored so a planner re-sending its plan at 1 Hz does not thrash droan_gl. + replan_min_change_m: 0.5 + # An empty Path cancels the active goal (planners publish an empty plan to + # stop the vehicle). + cancel_on_empty_plan: true + # droan_gl REJECTS a goal while one is still active; after a cancel the + # bridge retries the send at this period, up to max_send_retries times. + send_retry_period_s: 0.5 + max_send_retries: 10 + # Start enabled; toggle at runtime with the ~/set_enabled std_srvs/SetBool + # service (disabling cancels the active goal). + enabled: true diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/__init__.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/__init__.py new file mode 100644 index 000000000..86f74ac2a --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/__init__.py @@ -0,0 +1,3 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""global_plan topic -> NavigateTask goal adapter (see bridge_node.py).""" diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/bridge_node.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/bridge_node.py new file mode 100644 index 000000000..ba1f71392 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/bridge_node.py @@ -0,0 +1,188 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""global_plan (nav_msgs/Path) -> NavigateTask goal adapter. + +The trunk local planner (droan_gl) is a task executor: it only plans while a +NavigateTask goal is active and it REJECTS a new goal while one is running. +The random-walk planner drives it as an action client itself, but planners +that just publish a path on the ``global_plan`` topic (the exploration_planner +and raven modules, any external planner following the interface conventions) +need this adapter: + + global planner --global_plan (Path)--> bridge --NavigateTask--> droan_gl + +Behaviour: + * a new plan whose pose count or final pose changed (``replan_min_change_m``) + cancels the active goal and, once the cancel is acknowledged, sends the new + plan as a goal (with retries: droan_gl briefly still reports "active"); + * identical re-publications are ignored; + * an empty Path cancels the active goal (``cancel_on_empty_plan``); + * ``~/set_enabled`` (std_srvs/SetBool) pauses/resumes; disabling cancels. +""" +import rclpy +from rclpy.action import ActionClient +from rclpy.node import Node +from nav_msgs.msg import Path +from std_srvs.srv import SetBool +from task_msgs.action import NavigateTask + +from global_plan_navigate_bridge.plan_key import plan_changed, plan_key + + +class GlobalPlanNavigateBridge(Node): + def __init__(self): + super().__init__('global_plan_navigate_bridge') + self.declare_parameter('goal_tolerance_m', 1.0) + self.declare_parameter('replan_min_change_m', 0.5) + self.declare_parameter('cancel_on_empty_plan', True) + self.declare_parameter('send_retry_period_s', 0.5) + self.declare_parameter('max_send_retries', 10) + self.declare_parameter('enabled', True) + + self._enabled = bool(self.get_parameter('enabled').value) + self._client = ActionClient(self, NavigateTask, 'navigate_task') + self._sub = self.create_subscription(Path, 'global_plan', self._on_plan, 10) + self._srv = self.create_service(SetBool, '~/set_enabled', self._on_set_enabled) + + self._active_handle = None # ClientGoalHandle of the running goal + self._active_key = None # plan_key of the running/sending goal + self._pending = None # (Path, key) waiting to be sent + self._sending = False + self._retries = 0 + self._retry_timer = None + self.get_logger().info( + 'global_plan -> NavigateTask bridge ready ' + f'(enabled={self._enabled}, goal_tolerance_m=' + f'{self.get_parameter("goal_tolerance_m").value})') + + # ------------------------------------------------------------- inputs + def _on_set_enabled(self, req, resp): + self._enabled = bool(req.data) + if not self._enabled: + self._pending = None + self._cancel_active() + resp.success = True + resp.message = 'enabled' if self._enabled else 'disabled (active goal cancelled)' + self.get_logger().info(f'set_enabled -> {resp.message}') + return resp + + def _on_plan(self, msg: Path): + if not self._enabled: + return + pts = [(p.pose.position.x, p.pose.position.y, p.pose.position.z) for p in msg.poses] + key = plan_key(pts) + min_change = float(self.get_parameter('replan_min_change_m').value) + current = self._pending[1] if self._pending else self._active_key + if not plan_changed(current, key, min_change): + return + if key is None: + if bool(self.get_parameter('cancel_on_empty_plan').value): + self.get_logger().info('empty global_plan -> cancelling active NavigateTask') + self._pending = None + self._cancel_active() + return + self.get_logger().info( + f'new global_plan ({key[0]} poses, goal {tuple(round(v, 2) for v in key[1])}) ' + '-> replacing NavigateTask goal') + self._pending = (msg, key) + self._dispatch() + + # ------------------------------------------------------------ dispatch + def _dispatch(self): + if self._pending is None or self._sending: + return + if self._active_handle is not None: + self._cancel_active() # _send runs from the cancel callback + return + self._send() + + def _cancel_active(self): + handle = self._active_handle + if handle is None: + return + self._active_handle = None + self._active_key = None + fut = handle.cancel_goal_async() + fut.add_done_callback(lambda _f: self._dispatch()) + + def _send(self): + if self._pending is None: + return + if not self._client.server_is_ready(): + self.get_logger().warn('NavigateTask server not available yet; retrying', throttle_duration_sec=5.0) + self._schedule_retry() + return + path, key = self._pending + goal = NavigateTask.Goal() + goal.global_plan = path + goal.goal_tolerance_m = float(self.get_parameter('goal_tolerance_m').value) + self._sending = True + self._active_key = key + fut = self._client.send_goal_async(goal, feedback_callback=self._on_feedback) + fut.add_done_callback(self._on_goal_response) + + def _schedule_retry(self): + max_retries = int(self.get_parameter('max_send_retries').value) + if self._retries >= max_retries: + self.get_logger().error(f'NavigateTask goal rejected {self._retries} times; dropping plan') + self._pending = None + self._retries = 0 + self._active_key = None + return + self._retries += 1 + period = float(self.get_parameter('send_retry_period_s').value) + if self._retry_timer is not None: + self._retry_timer.cancel() + self._retry_timer = self.create_timer(period, self._on_retry) + + def _on_retry(self): + if self._retry_timer is not None: + self._retry_timer.cancel() + self._retry_timer = None + self._send() + + def _on_goal_response(self, fut): + self._sending = False + handle = fut.result() + if handle is None or not handle.accepted: + self.get_logger().warn('NavigateTask goal rejected (local planner still busy?) - retrying') + self._schedule_retry() + return + self._retries = 0 + self._active_handle = handle + self._pending = None + handle.get_result_async().add_done_callback( + lambda f, h=handle: self._on_result(f, h)) + self.get_logger().info('NavigateTask goal accepted') + + def _on_feedback(self, fb): + self.get_logger().debug( + f'navigate: {fb.feedback.status} dist={fb.feedback.distance_to_goal:.2f} m') + + def _on_result(self, fut, handle): + try: + result = fut.result().result + msg = f'success={result.success} "{result.message}"' + except Exception as exc: # noqa: BLE001 - report whatever the server returned + msg = f'error: {exc}' + self.get_logger().info(f'NavigateTask finished: {msg}') + if self._active_handle is handle: + self._active_handle = None + self._active_key = None + self._dispatch() + + +def main(args=None): + rclpy.init(args=args) + node = GlobalPlanNavigateBridge() + try: + rclpy.spin(node) + except KeyboardInterrupt: + pass + finally: + node.destroy_node() + rclpy.try_shutdown() + + +if __name__ == '__main__': + main() diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/plan_key.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/plan_key.py new file mode 100644 index 000000000..c13b78ec2 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/global_plan_navigate_bridge/plan_key.py @@ -0,0 +1,33 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Pure-python plan comparison used by the bridge (hermetic, no ROS import). + +A global planner typically re-publishes its current plan periodically; the +bridge must only replace the active NavigateTask goal when the plan really +changed, otherwise droan_gl would be cancelled and restarted every tick. +""" +import math +from typing import Optional, Sequence, Tuple + +PlanKey = Tuple[int, Tuple[float, float, float]] + + +def plan_key(points: Sequence[Tuple[float, float, float]]) -> Optional[PlanKey]: + """(pose count, final xyz) of a plan, or None for an empty plan.""" + if not points: + return None + x, y, z = points[-1] + return (len(points), (float(x), float(y), float(z))) + + +def plan_changed(prev: Optional[PlanKey], new: Optional[PlanKey], min_change_m: float) -> bool: + """True when ``new`` should replace the goal built from ``prev``. + + Rules: empty<->non-empty is always a change; a different pose count is a + change; otherwise the final pose must have moved more than ``min_change_m``. + """ + if prev is None or new is None: + return prev is not new and (prev is None) != (new is None) + if prev[0] != new[0]: + return True + return math.dist(prev[1], new[1]) > min_change_m diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/launch/global_plan_navigate_bridge.launch.xml b/robot/ros_ws/src/global/global_plan_navigate_bridge/launch/global_plan_navigate_bridge.launch.xml new file mode 100644 index 000000000..8dc83018e --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/launch/global_plan_navigate_bridge.launch.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/package.xml b/robot/ros_ws/src/global/global_plan_navigate_bridge/package.xml new file mode 100644 index 000000000..73e1e955d --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/package.xml @@ -0,0 +1,23 @@ + + + + global_plan_navigate_bridge + 0.1.0 + Adapter from a topic-publishing global planner (nav_msgs/Path on global_plan) to the task-executor local planner: each new plan becomes a NavigateTask goal (cancel-then-resend), so planners that only publish paths (exploration_planner, raven) can drive droan_gl. + Andrew Jong + BSD-3-Clause-Clear + + rclpy + nav_msgs + std_srvs + task_msgs + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/resource/global_plan_navigate_bridge b/robot/ros_ws/src/global/global_plan_navigate_bridge/resource/global_plan_navigate_bridge new file mode 100644 index 000000000..e69de29bb diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.cfg b/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.cfg new file mode 100644 index 000000000..b00240ebd --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.cfg @@ -0,0 +1,20 @@ +[develop] +script_dir=$base/lib/global_plan_navigate_bridge +[install] +install_scripts=$base/lib/global_plan_navigate_bridge + +[tool:pytest] +testpaths = test +python_files = test_*.py +python_classes = Test* +python_functions = test_* +markers = + unit: Hermetic unit tests (no ROS stack required) + linter: ament copyright/flake8/pep257 (run separately, not via colcon test) + copyright: ament_copyright + flake8: ament_flake8 + pep257: ament_pep257 +addopts = + --ignore=test/test_copyright.py + --ignore=test/test_flake8.py + --ignore=test/test_pep257.py diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.py new file mode 100644 index 000000000..17b09b674 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/setup.py @@ -0,0 +1,29 @@ +from setuptools import setup + +package_name = 'global_plan_navigate_bridge' + +setup( + name=package_name, + version='0.1.0', + packages=[package_name], + data_files=[ + ('share/ament_index/resource_index/packages', ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + ('share/' + package_name + '/launch', ['launch/global_plan_navigate_bridge.launch.xml']), + ('share/' + package_name + '/config', ['config/global_plan_navigate_bridge.yaml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Andrew Jong', + maintainer_email='ajong@andrew.cmu.edu', + description='global_plan topic -> NavigateTask goal adapter for task-executor local planners', + license='BSD-3-Clause-Clear', + extras_require={ + 'test': ['pytest'], + }, + entry_points={ + 'console_scripts': [ + 'global_plan_navigate_bridge_node = global_plan_navigate_bridge.bridge_node:main', + ], + }, +) diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/test/conftest.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/test/conftest.py new file mode 100644 index 000000000..019c78b50 --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/test/conftest.py @@ -0,0 +1,6 @@ +# Skip ament linter modules during pytest/colcon test. +collect_ignore = [ + "test_copyright.py", + "test_flake8.py", + "test_pep257.py", +] diff --git a/robot/ros_ws/src/global/global_plan_navigate_bridge/test/test_plan_key.py b/robot/ros_ws/src/global/global_plan_navigate_bridge/test/test_plan_key.py new file mode 100644 index 000000000..f66ba76ee --- /dev/null +++ b/robot/ros_ws/src/global/global_plan_navigate_bridge/test/test_plan_key.py @@ -0,0 +1,46 @@ +# Copyright (c) 2026 Carnegie Mellon University +# SPDX-License-Identifier: BSD-3-Clause-Clear +"""Hermetic tests for the bridge's plan-change rule (no ROS required).""" +import pytest + +from global_plan_navigate_bridge.plan_key import plan_changed, plan_key + +pytestmark = pytest.mark.unit + + +def test_empty_plan_has_no_key(): + assert plan_key([]) is None + + +def test_key_is_count_and_final_pose(): + assert plan_key([(0, 0, 0), (1.0, 2.0, 3.0)]) == (2, (1.0, 2.0, 3.0)) + + +def test_republished_identical_plan_is_not_a_change(): + k = plan_key([(0, 0, 0), (5, 0, 2)]) + assert not plan_changed(k, k, 0.5) + + +def test_small_goal_jitter_is_not_a_change(): + a = plan_key([(0, 0, 0), (5.0, 0.0, 2.0)]) + b = plan_key([(0, 0, 0), (5.2, 0.1, 2.0)]) + assert not plan_changed(a, b, 0.5) + + +def test_moved_goal_is_a_change(): + a = plan_key([(0, 0, 0), (5.0, 0.0, 2.0)]) + b = plan_key([(0, 0, 0), (9.0, 0.0, 2.0)]) + assert plan_changed(a, b, 0.5) + + +def test_pose_count_change_is_a_change(): + a = plan_key([(0, 0, 0), (5.0, 0.0, 2.0)]) + b = plan_key([(0, 0, 0), (2, 0, 2), (5.0, 0.0, 2.0)]) + assert plan_changed(a, b, 0.5) + + +def test_empty_transitions_are_changes(): + k = plan_key([(1, 1, 1)]) + assert plan_changed(k, None, 0.5) + assert plan_changed(None, k, 0.5) + assert not plan_changed(None, None, 0.5) diff --git a/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt b/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt deleted file mode 100644 index e60aa9f17..000000000 --- a/robot/ros_ws/src/global/planners/exploration/CMakeLists.txt +++ /dev/null @@ -1,147 +0,0 @@ -cmake_minimum_required(VERSION 3.5) -set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules" ${CMAKE_MODULE_PATH}) -project(exploration_planner) - -# Default to C++14 -if(NOT CMAKE_CXX_STANDARD) - set(CMAKE_CXX_STANDARD 17) -endif() - -# Find dependencies -find_package(ament_cmake REQUIRED) -find_package(rclcpp REQUIRED) -find_package(rclcpp_action REQUIRED) -find_package(std_msgs REQUIRED) -find_package(std_srvs REQUIRED) -find_package(geometry_msgs REQUIRED) -find_package(nav_msgs REQUIRED) -find_package(visualization_msgs REQUIRED) -find_package(tf2 REQUIRED) -find_package(tf2_ros REQUIRED) -find_package(tf2_eigen REQUIRED) -find_package(tf2_geometry_msgs REQUIRED) - -# set(OpenVDB_INCLUDE_DIR "/usr/local/include/openvdb" CACHE STRING "Path to OpenVDB installation") -find_package(OpenVDB REQUIRED) -# find_package(vdb_mapping REQUIRED) -find_package(Eigen3 REQUIRED) -find_package(PCL REQUIRED) -find_package(pcl_conversions REQUIRED) -find_package(message_filters REQUIRED) - -########### -## Build ## -########### - -# Specify additional locations of header files - -# Declare a C++ executable -add_executable(exploration_planner - src/exploration_node_run.cpp - src/exploration_node.cpp - src/exploration_logic.cpp - src/viewpoint_sampling.cpp - src/collision_checker.cpp - src/rrt_planner.cpp - src/utils.cpp) - -target_include_directories(exploration_planner - PRIVATE - ${PCL_INCLUDE_DIRS} - ${EIGEN3_INCLUDE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/include) - -target_link_libraries(exploration_planner - # vdb_mapping::vdb_mapping - OpenVDB::openvdb - ${EIGEN3_LIBRARIES} - ${PCL_LIBRARIES}) - -add_executable(waypoint_routing_planner - src/waypoint_routing_node.cpp - src/exploration_node.cpp - src/exploration_logic.cpp - src/viewpoint_sampling.cpp - src/collision_checker.cpp - src/rrt_planner.cpp - src/utils.cpp) - -target_include_directories(waypoint_routing_planner - PRIVATE - ${PCL_INCLUDE_DIRS} - ${EIGEN3_INCLUDE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/include) - -target_link_libraries(waypoint_routing_planner - # vdb_mapping::vdb_mapping - OpenVDB::openvdb - ${EIGEN3_LIBRARIES} - ${PCL_LIBRARIES}) - -# Link libraries -ament_target_dependencies(exploration_planner - rclcpp - rclcpp_action - std_msgs - std_srvs - geometry_msgs - nav_msgs - visualization_msgs - tf2 - tf2_ros - pcl_conversions - tf2_eigen - tf2_geometry_msgs -) - -ament_target_dependencies(waypoint_routing_planner - rclcpp - rclcpp_action - std_msgs - std_srvs - geometry_msgs - nav_msgs - visualization_msgs - tf2 - tf2_ros - pcl_conversions - tf2_eigen - tf2_geometry_msgs -) - -# Install executable -install(TARGETS - exploration_planner - waypoint_routing_planner - DESTINATION lib/${PROJECT_NAME} -) - -install(DIRECTORY - launch - DESTINATION share/${PROJECT_NAME}) - -install(DIRECTORY - config - DESTINATION share/${PROJECT_NAME}) - -install(DIRECTORY - src - DESTINATION share/${PROJECT_NAME}) - - -############# -## Testing ## -############# - -# Add gtest based cpp test target and link libraries -# ament_add_gtest(${PROJECT_NAME}-test test/test_exploration_planner.cpp) -# if(TARGET ${PROJECT_NAME}-test) -# target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME}_node) -# endif() - -################################### -## ament specific configuration ## -################################### - -# ament_package() must be the last call in the file (ament requirement). -ament_package() \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindBlosc.cmake b/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindBlosc.cmake deleted file mode 100644 index 44a0f3f54..000000000 --- a/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindBlosc.cmake +++ /dev/null @@ -1,394 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: MPL-2.0 -# -#[=======================================================================[.rst: - -FindBlosc ---------- - -Find Blosc include dirs and libraries - -Use this module by invoking find_package with the form:: - - find_package(Blosc - [version] [EXACT] # Minimum or EXACT version e.g. 1.5.0 - [REQUIRED] # Fail with error if Blosc is not found - ) - -IMPORTED Targets -^^^^^^^^^^^^^^^^ - -``Blosc::blosc`` - This module defines IMPORTED target Blosc::Blosc, if Blosc has been found. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``Blosc_FOUND`` - True if the system has the Blosc library. -``Blosc_VERSION`` - The version of the Blosc library which was found. -``Blosc_INCLUDE_DIRS`` - Include directories needed to use Blosc. -``Blosc_RELEASE_LIBRARIES`` - Libraries needed to link to the release version of Blosc. -``Blosc_RELEASE_LIBRARY_DIRS`` - Blosc release library directories. -``Blosc_DEBUG_LIBRARIES`` - Libraries needed to link to the debug version of Blosc. -``Blosc_DEBUG_LIBRARY_DIRS`` - Blosc debug library directories. - -Deprecated - use [RELEASE|DEBUG] variants: - -``Blosc_LIBRARIES`` - Libraries needed to link to Blosc. -``Blosc_LIBRARY_DIRS`` - Blosc library directories. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``Blosc_INCLUDE_DIR`` - The directory containing ``blosc.h``. -``Blosc_LIBRARY`` - The path to the Blosc library. may include target_link_libraries() debug/optimized keywords -``Blosc_LIBRARY_RELEASE`` - The path to the Blosc release library. -``Blosc_LIBRARY_DEBUG`` - The path to the Blosc debug library. - -Hints -^^^^^ - -Instead of explicitly setting the cache variables, the following variables -may be provided to tell this module where to look. - -``Blosc_ROOT`` - Preferred installation prefix. -``BLOSC_INCLUDEDIR`` - Preferred include directory e.g. /include -``BLOSC_LIBRARYDIR`` - Preferred library directory e.g. /lib -``BLOSC_DEBUG_SUFFIX`` - Suffix of the debug version of blosc. Defaults to "_d", OR the empty string for VCPKG_TOOLCHAIN -``SYSTEM_LIBRARY_PATHS`` - Global list of library paths intended to be searched by and find_xxx call -``BLOSC_USE_STATIC_LIBS`` - Only search for static blosc libraries -``BLOSC_USE_EXTERNAL_SOURCES`` - Set to ON if Blosc has been built using external sources for LZ4, snappy, - zlib and zstd. Default is OFF. -``DISABLE_CMAKE_SEARCH_PATHS`` - Disable CMakes default search paths for find_xxx calls in this module - -#]=======================================================================] - -cmake_minimum_required(VERSION 3.15) -include(GNUInstallDirs) - -mark_as_advanced( - Blosc_INCLUDE_DIR - Blosc_LIBRARY -) - -set(_FIND_BLOSC_ADDITIONAL_OPTIONS "") -if(DISABLE_CMAKE_SEARCH_PATHS) - set(_FIND_BLOSC_ADDITIONAL_OPTIONS NO_DEFAULT_PATH) -endif() - -# Set _BLOSC_ROOT based on a user provided root var. Xxx_ROOT and ENV{Xxx_ROOT} -# are prioritised over the legacy capitalized XXX_ROOT variables for matching -# CMake 3.12 behaviour -# @todo deprecate -D and ENV BLOSC_ROOT from CMake 3.12 -if(Blosc_ROOT) - set(_BLOSC_ROOT ${Blosc_ROOT}) -elseif(DEFINED ENV{Blosc_ROOT}) - set(_BLOSC_ROOT $ENV{Blosc_ROOT}) -elseif(BLOSC_ROOT) - set(_BLOSC_ROOT ${BLOSC_ROOT}) -elseif(DEFINED ENV{BLOSC_ROOT}) - set(_BLOSC_ROOT $ENV{BLOSC_ROOT}) -endif() - -# Additionally try and use pkconfig to find blosc -if(USE_PKGCONFIG) - if(NOT DEFINED PKG_CONFIG_FOUND) - find_package(PkgConfig) - endif() - if(PKG_CONFIG_FOUND) - pkg_check_modules(PC_Blosc QUIET blosc) - endif() -endif() - -# ------------------------------------------------------------------------ -# Search for blosc include DIR -# ------------------------------------------------------------------------ - -set(_BLOSC_INCLUDE_SEARCH_DIRS "") -list(APPEND _BLOSC_INCLUDE_SEARCH_DIRS - ${BLOSC_INCLUDEDIR} - ${_BLOSC_ROOT} - ${PC_Blosc_INCLUDE_DIRS} - ${SYSTEM_LIBRARY_PATHS} -) - -# Look for a standard blosc header file. -find_path(Blosc_INCLUDE_DIR blosc.h - ${_FIND_BLOSC_ADDITIONAL_OPTIONS} - PATHS ${_BLOSC_INCLUDE_SEARCH_DIRS} - PATH_SUFFIXES ${CMAKE_INSTALL_INCLUDEDIR} include -) - -if(EXISTS "${Blosc_INCLUDE_DIR}/blosc.h") - file(STRINGS "${Blosc_INCLUDE_DIR}/blosc.h" - _blosc_version_major_string REGEX "#define BLOSC_VERSION_MAJOR +[0-9]+ " - ) - string(REGEX REPLACE "#define BLOSC_VERSION_MAJOR +([0-9]+).*$" "\\1" - _blosc_version_major_string "${_blosc_version_major_string}" - ) - string(STRIP "${_blosc_version_major_string}" Blosc_VERSION_MAJOR) - - file(STRINGS "${Blosc_INCLUDE_DIR}/blosc.h" - _blosc_version_minor_string REGEX "#define BLOSC_VERSION_MINOR +[0-9]+ " - ) - string(REGEX REPLACE "#define BLOSC_VERSION_MINOR +([0-9]+).*$" "\\1" - _blosc_version_minor_string "${_blosc_version_minor_string}" - ) - string(STRIP "${_blosc_version_minor_string}" Blosc_VERSION_MINOR) - - file(STRINGS "${Blosc_INCLUDE_DIR}/blosc.h" - _blosc_version_release_string REGEX "#define BLOSC_VERSION_RELEASE +[0-9]+ " - ) - string(REGEX REPLACE "#define BLOSC_VERSION_RELEASE +([0-9]+).*$" "\\1" - _blosc_version_release_string "${_blosc_version_release_string}" - ) - string(STRIP "${_blosc_version_release_string}" Blosc_VERSION_RELEASE) - - unset(_blosc_version_major_string) - unset(_blosc_version_minor_string) - unset(_blosc_version_release_string) - - set(Blosc_VERSION ${Blosc_VERSION_MAJOR}.${Blosc_VERSION_MINOR}.${Blosc_VERSION_RELEASE}) -endif() - -# ------------------------------------------------------------------------ -# Search for blosc lib DIR -# ------------------------------------------------------------------------ - -set(_BLOSC_LIBRARYDIR_SEARCH_DIRS "") -list(APPEND _BLOSC_LIBRARYDIR_SEARCH_DIRS - ${BLOSC_LIBRARYDIR} - ${_BLOSC_ROOT} - ${PC_Blosc_LIBRARY_DIRS} - ${SYSTEM_LIBRARY_PATHS} -) - -# Library suffix handling - -set(_BLOSC_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) - -if(WIN32) - if(BLOSC_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib") - endif() -else() - if(BLOSC_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") - endif() -endif() - -set(Blosc_LIB_COMPONENTS "") -# NOTE: Search for debug version first (see vcpkg hack) -list(APPEND BLOSC_BUILD_TYPES DEBUG RELEASE) - -foreach(BUILD_TYPE ${BLOSC_BUILD_TYPES}) - set(_BLOSC_LIB_NAME blosc) - - set(_BLOSC_CMAKE_IGNORE_PATH ${CMAKE_IGNORE_PATH}) - if(VCPKG_TOOLCHAIN) - # Blosc is installed very strangely in VCPKG (debug/release libs have the - # same name, static build uses external deps, dll doesn't) and blosc itself - # comes with almost zero downstream CMake support for us to detect settings. - # We should not support external package managers in our own modules like - # this, but there doesn't seem to be a work around - if(NOT DEFINED BLOSC_DEBUG_SUFFIX) - set(BLOSC_DEBUG_SUFFIX "") - endif() - if(BUILD_TYPE STREQUAL RELEASE) - if(EXISTS ${Blosc_LIBRARY_DEBUG}) - get_filename_component(_BLOSC_DEBUG_DIR ${Blosc_LIBRARY_DEBUG} DIRECTORY) - list(APPEND CMAKE_IGNORE_PATH ${_BLOSC_DEBUG_DIR}) - endif() - endif() - endif() - - if(BUILD_TYPE STREQUAL DEBUG) - if(NOT DEFINED BLOSC_DEBUG_SUFFIX) - set(BLOSC_DEBUG_SUFFIX _d) - endif() - set(_BLOSC_LIB_NAME "${_BLOSC_LIB_NAME}${BLOSC_DEBUG_SUFFIX}") - endif() - - find_library(Blosc_LIBRARY_${BUILD_TYPE} ${_BLOSC_LIB_NAME} - ${_FIND_BLOSC_ADDITIONAL_OPTIONS} - PATHS ${_BLOSC_LIBRARYDIR_SEARCH_DIRS} - PATH_SUFFIXES ${CMAKE_INSTALL_LIBDIR} lib64 lib - ) - - list(APPEND Blosc_LIB_COMPONENTS ${Blosc_LIBRARY_${BUILD_TYPE}}) - set(CMAKE_IGNORE_PATH ${_BLOSC_CMAKE_IGNORE_PATH}) -endforeach() - -# Reset library suffix - -set(CMAKE_FIND_LIBRARY_SUFFIXES ${_BLOSC_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) -unset(_BLOSC_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES) - -if(Blosc_LIBRARY_DEBUG AND Blosc_LIBRARY_RELEASE) - # if the generator is multi-config or if CMAKE_BUILD_TYPE is set for - # single-config generators, set optimized and debug libraries - get_property(_isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) - if(_isMultiConfig OR CMAKE_BUILD_TYPE) - set(Blosc_LIBRARY optimized ${Blosc_LIBRARY_RELEASE} debug ${Blosc_LIBRARY_DEBUG}) - else() - # For single-config generators where CMAKE_BUILD_TYPE has no value, - # just use the release libraries - set(Blosc_LIBRARY ${Blosc_LIBRARY_RELEASE}) - endif() - # FIXME: This probably should be set for both cases - set(Blosc_LIBRARIES optimized ${Blosc_LIBRARY_RELEASE} debug ${Blosc_LIBRARY_DEBUG}) -endif() - -# if only the release version was found, set the debug variable also to the release version -if(Blosc_LIBRARY_RELEASE AND NOT Blosc_LIBRARY_DEBUG) - set(Blosc_LIBRARY_DEBUG ${Blosc_LIBRARY_RELEASE}) - set(Blosc_LIBRARY ${Blosc_LIBRARY_RELEASE}) - set(Blosc_LIBRARIES ${Blosc_LIBRARY_RELEASE}) -endif() - -# if only the debug version was found, set the release variable also to the debug version -if(Blosc_LIBRARY_DEBUG AND NOT Blosc_LIBRARY_RELEASE) - set(Blosc_LIBRARY_RELEASE ${Blosc_LIBRARY_DEBUG}) - set(Blosc_LIBRARY ${Blosc_LIBRARY_DEBUG}) - set(Blosc_LIBRARIES ${Blosc_LIBRARY_DEBUG}) -endif() - -# If the debug & release library ends up being the same, omit the keywords -if("${Blosc_LIBRARY_RELEASE}" STREQUAL "${Blosc_LIBRARY_DEBUG}") - set(Blosc_LIBRARY ${Blosc_LIBRARY_RELEASE} ) - set(Blosc_LIBRARIES ${Blosc_LIBRARY_RELEASE} ) -endif() - -if(Blosc_LIBRARY) - set(Blosc_FOUND TRUE) -else() - set(Blosc_FOUND FALSE) -endif() - -# ------------------------------------------------------------------------ -# Cache and set Blosc_FOUND -# ------------------------------------------------------------------------ - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Blosc - FOUND_VAR Blosc_FOUND - REQUIRED_VARS - Blosc_LIBRARY - Blosc_INCLUDE_DIR - VERSION_VAR Blosc_VERSION -) - -if(NOT Blosc_FOUND) - if(Blosc_FIND_REQUIRED) - message(FATAL_ERROR "Unable to find Blosc") - endif() - return() -endif() - -# Partition release/debug lib vars - -set(Blosc_RELEASE_LIBRARIES ${Blosc_LIBRARY_RELEASE}) -get_filename_component(Blosc_RELEASE_LIBRARY_DIRS ${Blosc_RELEASE_LIBRARIES} DIRECTORY) -set(Blosc_DEBUG_LIBRARIES ${Blosc_LIBRARY_DEBUG}) -get_filename_component(Blosc_DEBUG_LIBRARY_DIRS ${Blosc_DEBUG_LIBRARIES} DIRECTORY) -set(Blosc_LIBRARIES ${Blosc_RELEASE_LIBRARIES}) -set(Blosc_LIBRARY_DIRS ${Blosc_RELEASE_LIBRARY_DIRS}) -set(Blosc_INCLUDE_DIRS ${Blosc_INCLUDE_DIR}) -set(Blosc_INCLUDE_DIRS ${Blosc_INCLUDE_DIR}) - -# Configure lib type. If XXX_USE_STATIC_LIBS, we always assume a static -# lib is in use. If win32, we can't mark the import .libs as shared, so -# these are always marked as UNKNOWN. Otherwise, infer from extension. -set(BLOSC_LIB_TYPE UNKNOWN) -if(BLOSC_USE_STATIC_LIBS) - set(BLOSC_LIB_TYPE STATIC) -elseif(UNIX) - get_filename_component(_BLOSC_EXT ${Blosc_LIBRARY_RELEASE} EXT) - if(_BLOSC_EXT STREQUAL ".a") - set(BLOSC_LIB_TYPE STATIC) - elseif(_BLOSC_EXT STREQUAL ".so" OR - _BLOSC_EXT STREQUAL ".dylib") - set(BLOSC_LIB_TYPE SHARED) - endif() -endif() - -get_filename_component(Blosc_LIBRARY_DIRS ${Blosc_LIBRARY_RELEASE} DIRECTORY) - -if(NOT TARGET Blosc::blosc) - add_library(Blosc::blosc ${BLOSC_LIB_TYPE} IMPORTED) - set_target_properties(Blosc::blosc PROPERTIES - INTERFACE_COMPILE_OPTIONS "${PC_Blosc_CFLAGS_OTHER}" - INTERFACE_INCLUDE_DIRECTORIES "${Blosc_INCLUDE_DIRS}") - - # Standard location - set_target_properties(Blosc::blosc PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" - IMPORTED_LOCATION "${Blosc_LIBRARY}") - - # Release location - if(EXISTS "${Blosc_LIBRARY_RELEASE}") - set_property(TARGET Blosc::blosc APPEND PROPERTY - IMPORTED_CONFIGURATIONS RELEASE) - set_target_properties(Blosc::blosc PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES_RELEASE "CXX" - IMPORTED_LOCATION_RELEASE "${Blosc_LIBRARY_RELEASE}") - endif() - - # Debug location - if(EXISTS "${Blosc_LIBRARY_DEBUG}") - set_property(TARGET Blosc::blosc APPEND PROPERTY - IMPORTED_CONFIGURATIONS DEBUG) - set_target_properties(Blosc::blosc PROPERTIES - IMPORTED_LINK_INTERFACE_LANGUAGES_DEBUG "CXX" - IMPORTED_LOCATION_DEBUG "${Blosc_LIBRARY_DEBUG}") - endif() - - # Blosc may optionally be compiled with external sources for - # lz4, snappy and zlib . Add them as interface libs if requested - # (there doesn't seem to be a way to figure this out automatically). - # We assume they live along side blosc - if(BLOSC_USE_EXTERNAL_SOURCES) - set_target_properties(Blosc::blosc PROPERTIES - INTERFACE_LINK_DIRECTORIES - "\$<\$:${Blosc_RELEASE_LIBRARY_DIRS}>;\$<\$:${Blosc_DEBUG_LIBRARY_DIRS}>") - target_link_libraries(Blosc::blosc INTERFACE - $<$:lz4;snappy;zlib> - $<$:lz4d;snappyd;zlibd>) - - if(BLOSC_USE_STATIC_LIBS) - target_link_libraries(Blosc::blosc INTERFACE - $<$:zstd_static> - $<$:zstd_staticd>) - else() - target_link_libraries(Blosc::blosc INTERFACE - $<$:zstd> - $<$:zstdd>) - endif() - endif() -endif() - diff --git a/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindOpenVDB.cmake b/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindOpenVDB.cmake deleted file mode 100644 index 0dac1de40..000000000 --- a/robot/ros_ws/src/global/planners/exploration/CMakeModules/FindOpenVDB.cmake +++ /dev/null @@ -1,838 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: MPL-2.0 -# -#[=======================================================================[.rst: - -FindOpenVDB ------------ - -Find OpenVDB include dirs, libraries and settings - -Use this module by invoking find_package with the form:: - - find_package(OpenVDB - [version] [EXACT] # Minimum or EXACT version - [REQUIRED] # Fail with error if OpenVDB is not found - [COMPONENTS ...] # OpenVDB libraries by their canonical name - # e.g. "openvdb" for "libopenvdb", - # "pyopenvdb" for the python plugin - # "openvdb_ax" for the OpenVDB AX extension - # "openvdb_houdini" for the houdini plugin - # "nanovdb" for the nanovdb extension - ) - -IMPORTED Targets -^^^^^^^^^^^^^^^^ - -``OpenVDB::openvdb`` - The core openvdb library target. -``OpenVDB::openvdb_je`` - The core openvdb library target with jemalloc. -``OpenVDB::pyopenvdb`` - The openvdb python library target. -``OpenVDB::openvdb_houdini`` - The openvdb houdini library target. -``OpenVDB::openvdb_ax`` - The openvdb_ax library target. -``OpenVDB::nanovdb`` - The nanovdb library target. - -Result Variables -^^^^^^^^^^^^^^^^ - -This will define the following variables: - -``OpenVDB_FOUND`` - True if the system has the OpenVDB library. -``OpenVDB_VERSION`` - The version of the OpenVDB library which was found. -``OpenVDB_INCLUDE_DIRS`` - Include directories needed to use OpenVDB. -``OpenVDB_LIBRARIES`` - Libraries needed to link to OpenVDB. -``OpenVDB_LIBRARY_DIRS`` - OpenVDB library directories. -``OpenVDB_DEFINITIONS`` - Definitions to use when compiling code that uses OpenVDB. -``OpenVDB_${COMPONENT}_FOUND`` - True if the system has the named OpenVDB component. -``OpenVDB_USES_BLOSC`` - True if the OpenVDB Library has been built with blosc support -``OpenVDB_USES_ZLIB`` - True if the OpenVDB Library has been built with zlib support -``OpenVDB_USES_LOG4CPLUS`` - True if the OpenVDB Library has been built with log4cplus support -``OpenVDB_USES_IMATH_HALF`` - True if the OpenVDB Library has been built with Imath half support -``OpenVDB_ABI`` - Set if this module was able to determine the ABI number the located - OpenVDB Library was built against. Unset otherwise. - -Cache Variables -^^^^^^^^^^^^^^^ - -The following cache variables may also be set: - -``OpenVDB_INCLUDE_DIR`` - The directory containing ``openvdb/version.h``. -``OpenVDB_${COMPONENT}_INCLUDE_DIR`` - Individual component include directories for OpenVDB -``OpenVDB_${COMPONENT}_LIBRARY`` - Individual component libraries for OpenVDB - -Hints -^^^^^ - -Instead of explicitly setting the cache variables, the following variables -may be provided to tell this module where to look. - -``OpenVDB_ROOT`` - Preferred installation prefix. -``OPENVDB_INCLUDEDIR`` - Preferred include directory e.g. /include -``OPENVDB_LIBRARYDIR`` - Preferred library directory e.g. /lib -``OPENVDB_${COMPONENT}_ROOT`` - Preferred installation prefix of a specific component. -``OPENVDB_${COMPONENT}_INCLUDEDIR`` - Preferred include directory of a specific component e.g. /include -``OPENVDB_${COMPONENT}_LIBRARYDIR`` - Preferred library directory of a specific component e.g. /lib -``SYSTEM_LIBRARY_PATHS`` - Global list of library paths intended to be searched by and find_xxx call -``OPENVDB_USE_STATIC_LIBS`` - Only search for static openvdb libraries -``DISABLE_CMAKE_SEARCH_PATHS`` - Disable CMakes default search paths for find_xxx calls in this module - -#]=======================================================================] - -cmake_minimum_required(VERSION 3.15) -include(GNUInstallDirs) - - -# Include utility functions for version information -include(${CMAKE_CURRENT_LIST_DIR}/OpenVDBUtils.cmake) - -mark_as_advanced( - OpenVDB_INCLUDE_DIR - OpenVDB_LIBRARY -) - -set(_FIND_OPENVDB_ADDITIONAL_OPTIONS "") -if(DISABLE_CMAKE_SEARCH_PATHS) - set(_FIND_OPENVDB_ADDITIONAL_OPTIONS NO_DEFAULT_PATH) -endif() - -set(_OPENVDB_COMPONENT_LIST - openvdb - openvdb_je - pyopenvdb - openvdb_ax - openvdb_houdini - nanovdb -) - -if(OpenVDB_FIND_COMPONENTS) - set(OPENVDB_COMPONENTS_PROVIDED TRUE) - set(_IGNORED_COMPONENTS "") - foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) - if(NOT ${COMPONENT} IN_LIST _OPENVDB_COMPONENT_LIST) - list(APPEND _IGNORED_COMPONENTS ${COMPONENT}) - endif() - endforeach() - - if(_IGNORED_COMPONENTS) - message(STATUS "Ignoring unknown components of OpenVDB:") - foreach(COMPONENT ${_IGNORED_COMPONENTS}) - message(STATUS " ${COMPONENT}") - endforeach() - list(REMOVE_ITEM OpenVDB_FIND_COMPONENTS ${_IGNORED_COMPONENTS}) - endif() -else() - set(OPENVDB_COMPONENTS_PROVIDED FALSE) - set(OpenVDB_FIND_COMPONENTS openvdb) -endif() - -# always make sure openvdb is picked up as a component i.e. -# find_package(OpenVDB COMPONENTS pyopenvdb) results in both -# openvdb and pyopenvdb targets. Also make sure it appears -# first in the component lists. -list(INSERT OpenVDB_FIND_COMPONENTS 0 openvdb) -list(REMOVE_DUPLICATES OpenVDB_FIND_COMPONENTS) - -# Set _OPENVDB_ROOT based on a user provided root var. Xxx_ROOT and ENV{Xxx_ROOT} -# are prioritised over the legacy capitalized XXX_ROOT variables for matching -# CMake 3.12 behaviour -# @todo deprecate -D and ENV OPENVDB_ROOT from CMake 3.12 -if(OpenVDB_ROOT) - set(_OPENVDB_ROOT ${OpenVDB_ROOT}) -elseif(DEFINED ENV{OpenVDB_ROOT}) - set(_OPENVDB_ROOT $ENV{OpenVDB_ROOT}) -elseif(OPENVDB_ROOT) - set(_OPENVDB_ROOT ${OPENVDB_ROOT}) -elseif(DEFINED ENV{OPENVDB_ROOT}) - set(_OPENVDB_ROOT $ENV{OPENVDB_ROOT}) -endif() - -# Additionally try and use pkconfig to find OpenVDB -if(USE_PKGCONFIG) - if(NOT DEFINED PKG_CONFIG_FOUND) - find_package(PkgConfig) - endif() - pkg_check_modules(PC_OpenVDB QUIET OpenVDB) -endif() - -# This CMake module supports being called from external packages AND from -# within the OpenVDB repository for building openvdb components with the -# core library build disabled. Determine where we are being called from: -# -# (repo structure = /cmake/FindOpenVDB.cmake) -# (inst structure = /lib/cmake/OpenVDB/FindOpenVDB.cmake) - -get_filename_component(_DIR_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) - -if(${_DIR_NAME} STREQUAL "cmake") - # Called from root repo for openvdb components -elseif(${_DIR_NAME} STREQUAL "OpenVDB") - # Set the install variable to track directories if this is being called from - # an installed location and from another package. The expected installation - # directory structure is: - # /lib/cmake/OpenVDB/FindOpenVDB.cmake - # /include - # /bin - get_filename_component(_IMPORT_PREFIX ${CMAKE_CURRENT_LIST_DIR} DIRECTORY) - get_filename_component(_IMPORT_PREFIX ${_IMPORT_PREFIX} DIRECTORY) - get_filename_component(_IMPORT_PREFIX ${_IMPORT_PREFIX} DIRECTORY) - set(_OPENVDB_INSTALL ${_IMPORT_PREFIX}) - list(APPEND _OPENVDB_ROOT ${_OPENVDB_INSTALL}) -endif() - -unset(_DIR_NAME) -unset(_IMPORT_PREFIX) - -# ------------------------------------------------------------------------ -# Search for OpenVDB include DIR -# ------------------------------------------------------------------------ - -set(_OPENVDB_INCLUDE_SEARCH_DIRS "") -list(APPEND _OPENVDB_INCLUDE_SEARCH_DIRS - ${OPENVDB_INCLUDEDIR} - ${_OPENVDB_ROOT} - ${PC_OpenVDB_INCLUDE_DIRS} - ${SYSTEM_LIBRARY_PATHS} -) - -foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) - # Add in extra component paths - set(_VDB_COMPONENT_SEARCH_DIRS ${_OPENVDB_INCLUDE_SEARCH_DIRS}) - list(APPEND _VDB_COMPONENT_SEARCH_DIRS - ${OPENVDB_${COMPONENT}_ROOT} - ${OPENVDB_${COMPONENT}_INCLUDEDIR} - ) - if(_VDB_COMPONENT_SEARCH_DIRS) - list(REMOVE_DUPLICATES _VDB_COMPONENT_SEARCH_DIRS) - endif() - - # Look for a standard header files. - if(${COMPONENT} STREQUAL "openvdb") - # Look for a standard OpenVDB header file. - find_path(OpenVDB_${COMPONENT}_INCLUDE_DIR openvdb/version.h - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES - ${CMAKE_INSTALL_INCLUDEDIR} - include - ) - elseif(${COMPONENT} STREQUAL "pyopenvdb") - find_path(OpenVDB_${COMPONENT}_INCLUDE_DIR pyopenvdb.h - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES - ${CMAKE_INSTALL_INCLUDEDIR}/openvdb/python - ${CMAKE_INSTALL_INCLUDEDIR}/openvdb - ${CMAKE_INSTALL_INCLUDEDIR} - include - ) - elseif(${COMPONENT} STREQUAL "openvdb_ax") - # Look for a standard OpenVDB header file. - find_path(OpenVDB_${COMPONENT}_INCLUDE_DIR compiler/Compiler.h - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES - ${CMAKE_INSTALL_INCLUDEDIR}/openvdb/openvdb_ax - ${CMAKE_INSTALL_INCLUDEDIR}/openvdb_ax - ${CMAKE_INSTALL_INCLUDEDIR} - include - ) - elseif(${COMPONENT} STREQUAL "openvdb_houdini") - # @note Expects both houdini_utils and openvdb_houdini folders - # to be located in the same place - find_path(OpenVDB_${COMPONENT}_INCLUDE_DIR openvdb_houdini/SOP_NodeVDB.h - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES - ${CMAKE_INSTALL_INCLUDEDIR}/openvdb - ${CMAKE_INSTALL_INCLUDEDIR} - include - ) - elseif(${COMPONENT} STREQUAL "nanovdb") - # Look for NanoVDB.h - find_path(OpenVDB_${COMPONENT}_INCLUDE_DIR NanoVDB.h - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES - ${CMAKE_INSTALL_INCLUDEDIR}/nanovdb - ${CMAKE_INSTALL_INCLUDEDIR} - include - ) - endif() - unset(_VDB_COMPONENT_SEARCH_DIRS) -endforeach() - -set(OpenVDB_INCLUDE_DIR ${OpenVDB_openvdb_INCLUDE_DIR} - CACHE PATH "The OpenVDB core include directory") - -set(_OPENVDB_VERSION_HEADER "${OpenVDB_INCLUDE_DIR}/openvdb/version.h") -OPENVDB_VERSION_FROM_HEADER("${_OPENVDB_VERSION_HEADER}" - VERSION OpenVDB_VERSION - MAJOR OpenVDB_MAJOR_VERSION - MINOR OpenVDB_MINOR_VERSION - PATCH OpenVDB_PATCH_VERSION - ABI OpenVDB_ABI_FROM_HEADER # will be OpenVDB_MAJOR_VERSION prior to 8.1.0 -) - -if(OpenVDB_VERSION VERSION_LESS 8.1.0) - set(_OPENVDB_HAS_NEW_VERSION_HEADER FALSE) - # ABI gets computed later -else() - set(_OPENVDB_HAS_NEW_VERSION_HEADER TRUE) - set(OpenVDB_ABI ${OpenVDB_ABI_FROM_HEADER}) -endif() -unset(OpenVDB_ABI_FROM_HEADER) - -# ------------------------------------------------------------------------ -# Search for OPENVDB lib DIR -# ------------------------------------------------------------------------ - -set(_OPENVDB_LIBRARYDIR_SEARCH_DIRS "") - -# Append to _OPENVDB_LIBRARYDIR_SEARCH_DIRS in priority order - -list(APPEND _OPENVDB_LIBRARYDIR_SEARCH_DIRS - ${OPENVDB_LIBRARYDIR} - ${_OPENVDB_ROOT} - ${PC_OpenVDB_LIBRARY_DIRS} - ${SYSTEM_LIBRARY_PATHS} -) - -# Library suffix handling - -set(_OPENVDB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) - -set(OPENVDB_PYTHON_PATH_SUFFIXES - lib64/python - lib64/python2.7 - lib64/python3 - lib/python - lib/python2.7 - lib/python3 -) - -# Recurse through all the site-packages and dist-packages on the file system -file(GLOB PYTHON_SITE_PACKAGES ${CMAKE_INSTALL_FULL_LIBDIR}/python**/*) -foreach(_site_package_full_dir ${PYTHON_SITE_PACKAGES}) - string(REPLACE ${CMAKE_INSTALL_FULL_LIBDIR} "${CMAKE_INSTALL_LIBDIR}" - _site_package_dir ${_site_package_full_dir}) - list(APPEND OPENVDB_PYTHON_PATH_SUFFIXES ${_site_package_dir}) -endforeach() - -set(OPENVDB_LIB_PATH_SUFFIXES - ${CMAKE_INSTALL_LIBDIR} - lib64 - lib -) - -list(REMOVE_DUPLICATES OPENVDB_PYTHON_PATH_SUFFIXES) -list(REMOVE_DUPLICATES OPENVDB_LIB_PATH_SUFFIXES) - -# Static library setup -if(WIN32) - if(OPENVDB_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib") - endif() -else() - if(OPENVDB_USE_STATIC_LIBS) - set(CMAKE_FIND_LIBRARY_SUFFIXES ".a") - endif() -endif() - -set(OpenVDB_LIB_COMPONENTS "") - -foreach(COMPONENT ${OpenVDB_FIND_COMPONENTS}) - message("COMPONENT = " ${COMPONENT}) - set(LIB_NAME ${COMPONENT}) - - # Add in extra component paths - set(_VDB_COMPONENT_SEARCH_DIRS ${_OPENVDB_LIBRARYDIR_SEARCH_DIRS}) - list(APPEND _VDB_COMPONENT_SEARCH_DIRS - ${OPENVDB_${COMPONENT}_ROOT} - ${OPENVDB_${COMPONENT}_LIBRARYDIR} - ) - - if(${COMPONENT} STREQUAL "pyopenvdb") - set(_OPENVDB_ORIG_CMAKE_FIND_LIBRARY_PREFIXES ${CMAKE_FIND_LIBRARY_PREFIXES}) - set(CMAKE_FIND_LIBRARY_PREFIXES ";lib") # find non-prefixed - find_library(OpenVDB_${COMPONENT}_LIBRARY ${LIB_NAME} - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES ${OPENVDB_PYTHON_PATH_SUFFIXES} - ) - set(CMAKE_FIND_LIBRARY_PREFIXES ${_OPENVDB_ORIG_CMAKE_FIND_LIBRARY_PREFIXES}) - elseif(${COMPONENT} STREQUAL "openvdb_je") - # alias to the result of openvdb which should be handled first - set(OpenVDB_${COMPONENT}_LIBRARY ${OpenVDB_openvdb_LIBRARY}) - else() - find_library(OpenVDB_${COMPONENT}_LIBRARY ${LIB_NAME} - ${_FIND_OPENVDB_ADDITIONAL_OPTIONS} - PATHS ${_VDB_COMPONENT_SEARCH_DIRS} - PATH_SUFFIXES ${OPENVDB_LIB_PATH_SUFFIXES} - ) - endif() - - list(APPEND OpenVDB_LIB_COMPONENTS ${OpenVDB_${COMPONENT}_LIBRARY}) - if(${COMPONENT} STREQUAL "nanovdb") - # nanovdb is headers-only, no lib component - if(OpenVDB_${COMPONENT}_INCLUDE_DIR) - set(OpenVDB_${COMPONENT}_FOUND TRUE) - else() - set(OpenVDB_${COMPONENT}_FOUND FALSE) - endif() - else() - if(OpenVDB_${COMPONENT}_LIBRARY) - set(OpenVDB_${COMPONENT}_FOUND TRUE) - else() - set(OpenVDB_${COMPONENT}_FOUND FALSE) - endif() - endif() - unset(_VDB_COMPONENT_SEARCH_DIRS) -endforeach() - -unset(OPENVDB_PYTHON_PATH_SUFFIXES) -unset(OPENVDB_LIB_PATH_SUFFIXES) - -# Reset library suffix - -set(CMAKE_FIND_LIBRARY_SUFFIXES ${_OPENVDB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES}) -unset(_OPENVDB_ORIG_CMAKE_FIND_LIBRARY_SUFFIXES) - -# ------------------------------------------------------------------------ -# Cache and set OPENVDB_FOUND -# ------------------------------------------------------------------------ -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(OpenVDB - FOUND_VAR OpenVDB_FOUND - REQUIRED_VARS - OpenVDB_INCLUDE_DIR - OpenVDB_LIB_COMPONENTS - VERSION_VAR OpenVDB_VERSION - HANDLE_COMPONENTS -) - -# ------------------------------------------------------------------------ -# Determine ABI number -# ------------------------------------------------------------------------ - -# Set the ABI number the library was built against. The old system, -# which didn't define the ABI in the build config, uses vdb_print - -if(NOT _OPENVDB_HAS_NEW_VERSION_HEADER) - if(_OPENVDB_INSTALL) - OPENVDB_ABI_VERSION_FROM_PRINT( - "${_OPENVDB_INSTALL}/bin/vdb_print" - ABI OpenVDB_ABI - ) - else() - # Try and find vdb_print from the include path - OPENVDB_ABI_VERSION_FROM_PRINT( - "${OpenVDB_INCLUDE_DIR}/../bin/vdb_print" - ABI OpenVDB_ABI - ) - endif() -endif() - -if(NOT OpenVDB_FIND_QUIET) - if(NOT OpenVDB_ABI) - message(WARNING "Unable to determine OpenVDB ABI version from OpenVDB " - "installation. The library major version \"${OpenVDB_MAJOR_VERSION}\" " - "will be inferred. If this is not correct, use " - "add_definitions(-DOPENVDB_ABI_VERSION_NUMBER=N)" - ) - else() - message(STATUS "OpenVDB ABI Version: ${OpenVDB_ABI}") - endif() -endif() - -# ------------------------------------------------------------------------ -# Handle OpenVDB dependencies and interface settings -# ------------------------------------------------------------------------ - -# Handle openvdb_houdini first to configure search paths - -if(openvdb_houdini IN_LIST OpenVDB_FIND_COMPONENTS) - include(OpenVDBHoudiniSetup) -endif() - -# Add standard dependencies - -find_package(TBB REQUIRED COMPONENTS tbb) - -if(NOT OPENVDB_USE_STATIC_LIBS AND NOT Boost_USE_STATIC_LIBS) - # @note Both of these must be set for Boost 1.70 (VFX2020) to link against - # boost shared libraries (more specifically libraries built with -fPIC). - # http://boost.2283326.n4.nabble.com/CMake-config-scripts-broken-in-1-70-td4708957.html - # https://github.com/boostorg/boost_install/commit/160c7cb2b2c720e74463865ef0454d4c4cd9ae7c - set(BUILD_SHARED_LIBS ON) - set(Boost_USE_STATIC_LIBS OFF) -endif() - -find_package(Boost REQUIRED COMPONENTS iostreams system) - -# Add deps for pyopenvdb -# @todo track for numpy - -if(pyopenvdb IN_LIST OpenVDB_FIND_COMPONENTS) - find_package(PythonLibs REQUIRED) - - # Boost python handling - try and find both python and pythonXx (version suffixed). - # Prioritize the version suffixed library, failing if neither exist. - - find_package(Boost ${MINIMUM_BOOST_VERSION} - QUIET COMPONENTS python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR} - ) - - if(TARGET Boost::python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}) - set(BOOST_PYTHON_LIB "python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") - message(STATUS "Found boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}") - else() - find_package(Boost ${MINIMUM_BOOST_VERSION} QUIET COMPONENTS python) - if(TARGET Boost::python) - set(BOOST_PYTHON_LIB "python") - message(STATUS "Found non-suffixed boost_python, assuming to be python version " - "\"${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}\" compatible" - ) - else() - message(FATAL_ERROR "Unable to find boost_python or " - "boost_python${PYTHON_VERSION_MAJOR}${PYTHON_VERSION_MINOR}." - ) - endif() - endif() -endif() - -# Add deps for openvdb_ax - -if(openvdb_ax IN_LIST OpenVDB_FIND_COMPONENTS) - find_package(LLVM REQUIRED) - find_library(found_LLVM LLVM HINTS ${LLVM_LIBRARY_DIRS}) - - if(found_LLVM) - set(LLVM_LIBS "LLVM") - else() - llvm_map_components_to_libnames(_llvm_libs - native core executionengine support mcjit passes objcarcopts) - set(LLVM_LIBS "${_llvm_libs}") - endif() - - if(NOT OpenVDB_FIND_QUIET) - message(STATUS "Found LLVM: ${LLVM_DIR} (found version \"${LLVM_PACKAGE_VERSION}\")") - endif() -endif() - -# As the way we resolve optional libraries relies on library file names, use -# the configuration options from the main CMakeLists.txt to allow users -# to manually identify the requirements of OpenVDB builds if they know them. -set(OpenVDB_USES_BLOSC ${USE_BLOSC}) -set(OpenVDB_USES_ZLIB ${USE_ZLIB}) -set(OpenVDB_USES_LOG4CPLUS ${USE_LOG4CPLUS}) -set(OpenVDB_USES_IMATH_HALF ${USE_IMATH_HALF}) -set(OpenVDB_DEFINITIONS) - -if(WIN32) - if(OPENVDB_USE_STATIC_LIBS) - list(APPEND OpenVDB_DEFINITIONS OPENVDB_STATICLIB) - else() - list(APPEND OpenVDB_DEFINITIONS OPENVDB_DLL) - endif() - # Newer version of OpenVDB define these in Platform.h, but they are also - # provided here to maintain backwards compatibility with header include - # others - list(APPEND OpenVDB_DEFINITIONS _WIN32) - list(APPEND OpenVDB_DEFINITIONS NOMINMAX) -endif() - -if(OpenVDB_ABI) - # Newer version of OpenVDB defines this in version.h, but it is are also - # provided here to maintain backwards compatibility with header include - # others - list(APPEND OpenVDB_DEFINITIONS OPENVDB_ABI_VERSION_NUMBER=${OpenVDB_ABI}) -endif() - -# Configure deps - -if(_OPENVDB_HAS_NEW_VERSION_HEADER) - OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_IMATH_HALF" OpenVDB_USES_IMATH_HALF) - OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_BLOSC" OpenVDB_USES_BLOSC) - OPENVDB_GET_VERSION_DEFINE(${_OPENVDB_VERSION_HEADER} "OPENVDB_USE_ZLIB" OpenVDB_USES_ZLIB) -elseif(NOT OPENVDB_USE_STATIC_LIBS) - # Use GetPrerequisites to see which libraries this OpenVDB lib has linked to - # which we can query for optional deps. This basically runs ldd/otoll/objdump - # etc to track deps. We could use a vdb_config binary tools here to improve - # this process - include(GetPrerequisites) - - set(_EXCLUDE_SYSTEM_PREREQUISITES 1) - set(_RECURSE_PREREQUISITES 0) - set(_OPENVDB_PREREQUISITE_LIST) - - get_prerequisites(${OpenVDB_openvdb_LIBRARY} - _OPENVDB_PREREQUISITE_LIST - ${_EXCLUDE_SYSTEM_PREREQUISITES} - ${_RECURSE_PREREQUISITES} - "" - "${SYSTEM_LIBRARY_PATHS}" - ) - - unset(_EXCLUDE_SYSTEM_PREREQUISITES) - unset(_RECURSE_PREREQUISITES) - - # Search for optional dependencies - foreach(PREREQUISITE ${_OPENVDB_PREREQUISITE_LIST}) - set(_HAS_DEP) - get_filename_component(PREREQUISITE ${PREREQUISITE} NAME) - - string(FIND ${PREREQUISITE} "blosc" _HAS_DEP) - if(NOT ${_HAS_DEP} EQUAL -1) - set(OpenVDB_USES_BLOSC ON) - endif() - - string(FIND ${PREREQUISITE} "zlib" _HAS_DEP) - if(NOT ${_HAS_DEP} EQUAL -1) - set(OpenVDB_USES_ZLIB ON) - endif() - - string(FIND ${PREREQUISITE} "log4cplus" _HAS_DEP) - if(NOT ${_HAS_DEP} EQUAL -1) - set(OpenVDB_USES_LOG4CPLUS ON) - endif() - - string(FIND ${PREREQUISITE} "Half" _HAS_DEP) - if(NOT ${_HAS_DEP} EQUAL -1) - set(OpenVDB_USES_IMATH_HALF ON) - endif() - endforeach() - - unset(_OPENVDB_PREREQUISITE_LIST) -endif() - -if(OpenVDB_USES_BLOSC) - find_package(Blosc REQUIRED) -endif() - -if(OpenVDB_USES_ZLIB) - find_package(ZLIB REQUIRED) -endif() - -if(OpenVDB_USES_LOG4CPLUS) - find_package(Log4cplus REQUIRED) -endif() - -if(OpenVDB_USES_IMATH_HALF) - find_package(Imath CONFIG) - if (NOT TARGET Imath::Imath) - find_package(IlmBase REQUIRED COMPONENTS Half) - endif() - - if(WIN32) - # @note OPENVDB_OPENEXR_STATICLIB is old functionality and should be removed - if(OPENEXR_USE_STATIC_LIBS OR - (${ILMBASE_LIB_TYPE} STREQUAL STATIC_LIBRARY) OR - (${IMATH_LIB_TYPE} STREQUAL STATIC_LIBRARY)) - list(APPEND OpenVDB_DEFINITIONS OPENVDB_OPENEXR_STATICLIB) - endif() - endif() -endif() - -if(UNIX) - find_package(Threads REQUIRED) -endif() - -# Set deps. Note that the order here is important. If we're building against -# Houdini 17.5 we must include IlmBase deps first to ensure the users chosen -# namespaced headers are correctly prioritized. Otherwise other include paths -# from shared installs (including houdini) may pull in the wrong headers - -set(_OPENVDB_VISIBLE_DEPENDENCIES - Boost::iostreams - Boost::system -) - -if(OpenVDB_USES_IMATH_HALF) - list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES $ $) -endif() - -if(OpenVDB_USES_LOG4CPLUS) - list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES Log4cplus::log4cplus) - list(APPEND OpenVDB_DEFINITIONS OPENVDB_USE_LOG4CPLUS) -endif() - -list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES - TBB::tbb -) -if(UNIX) - list(APPEND _OPENVDB_VISIBLE_DEPENDENCIES - Threads::Threads - ) -endif() - -set(_OPENVDB_HIDDEN_DEPENDENCIES) - -if(NOT OPENVDB_USE_STATIC_LIBS) - if(OpenVDB_USES_BLOSC) - list(APPEND _OPENVDB_HIDDEN_DEPENDENCIES Blosc::blosc) - endif() - if(OpenVDB_USES_ZLIB) - list(APPEND _OPENVDB_HIDDEN_DEPENDENCIES ZLIB::ZLIB) - endif() -endif() - -if(openvdb_je IN_LIST OpenVDB_FIND_COMPONENTS) - find_package(Jemalloc REQUIRED) -endif() - -# ------------------------------------------------------------------------ -# Configure imported targets -# ------------------------------------------------------------------------ - -set(OpenVDB_LIBRARIES ${OpenVDB_LIB_COMPONENTS}) -set(OpenVDB_INCLUDE_DIRS ${OpenVDB_INCLUDE_DIR}) - -set(OpenVDB_LIBRARY_DIRS "") -foreach(LIB ${OpenVDB_LIB_COMPONENTS}) - get_filename_component(_OPENVDB_LIBDIR ${LIB} DIRECTORY) - list(APPEND OpenVDB_LIBRARY_DIRS ${_OPENVDB_LIBDIR}) -endforeach() -list(REMOVE_DUPLICATES OpenVDB_LIBRARY_DIRS) - -# OpenVDB::openvdb - -if(NOT TARGET OpenVDB::openvdb) - set(OPENVDB_openvdb_LIB_TYPE UNKNOWN) - if(OPENVDB_USE_STATIC_LIBS) - set(OPENVDB_openvdb_LIB_TYPE STATIC) - elseif(UNIX) - get_filename_component(_OPENVDB_openvdb_EXT - ${OpenVDB_openvdb_LIBRARY} EXT) - if(_OPENVDB_openvdb_EXT STREQUAL ".a") - set(OPENVDB_openvdb_LIB_TYPE STATIC) - elseif(_OPENVDB_openvdb_EXT STREQUAL ".so" OR - _OPENVDB_openvdb_EXT STREQUAL ".dylib") - set(OPENVDB_openvdb_LIB_TYPE SHARED) - endif() - endif() - - add_library(OpenVDB::openvdb ${OPENVDB_openvdb_LIB_TYPE} IMPORTED) - set_target_properties(OpenVDB::openvdb PROPERTIES - IMPORTED_LOCATION "${OpenVDB_openvdb_LIBRARY}" - INTERFACE_COMPILE_OPTIONS "${PC_OpenVDB_CFLAGS_OTHER}" - INTERFACE_COMPILE_DEFINITIONS "${OpenVDB_DEFINITIONS}" - INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_INCLUDE_DIR}" - IMPORTED_LINK_DEPENDENT_LIBRARIES "${_OPENVDB_HIDDEN_DEPENDENCIES}" # non visible deps - INTERFACE_LINK_LIBRARIES "${_OPENVDB_VISIBLE_DEPENDENCIES}" # visible deps (headers) - INTERFACE_COMPILE_FEATURES cxx_std_14 - ) -endif() - -# OpenVDB::openvdb_je - -if(OpenVDB_openvdb_je_LIBRARY) - if(NOT TARGET OpenVDB::openvdb_je) - add_library(OpenVDB::openvdb_je INTERFACE IMPORTED) - target_link_libraries(OpenVDB::openvdb_je INTERFACE OpenVDB::openvdb) - target_link_libraries(OpenVDB::openvdb_je INTERFACE Jemalloc::jemalloc) - endif() -endif() - -# OpenVDB::pyopenvdb - -if(OpenVDB_pyopenvdb_LIBRARY) - if(NOT TARGET OpenVDB::pyopenvdb) - add_library(OpenVDB::pyopenvdb MODULE IMPORTED) - set_target_properties(OpenVDB::pyopenvdb PROPERTIES - IMPORTED_LOCATION "${OpenVDB_pyopenvdb_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_pyopenvdb_INCLUDE_DIR};${PYTHON_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;Boost::${BOOST_PYTHON_LIB};${PYTHON_LIBRARIES}" - INTERFACE_COMPILE_FEATURES cxx_std_14 - ) - endif() -endif() - -# OpenVDB::openvdb_houdini - -if(OpenVDB_openvdb_houdini_LIBRARY) - if(NOT TARGET OpenVDB::openvdb_houdini) - add_library(OpenVDB::openvdb_houdini SHARED IMPORTED) - set_target_properties(OpenVDB::openvdb_houdini PROPERTIES - IMPORTED_LOCATION "${OpenVDB_openvdb_houdini_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_openvdb_houdini_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;Houdini" - INTERFACE_COMPILE_FEATURES cxx_std_14 - ) - endif() -endif() - -# OpenVDB::openvdb_ax - -if(OpenVDB_openvdb_ax_LIBRARY) - set(OPENVDB_openvdb_ax_LIB_TYPE UNKNOWN) - if(OPENVDB_USE_STATIC_LIBS) - set(OPENVDB_openvdb_ax_LIB_TYPE STATIC) - elseif(UNIX) - get_filename_component(_OPENVDB_openvdb_ax_EXT - ${OpenVDB_openvdb_ax_LIBRARY} EXT) - if(_OPENVDB_openvdb_ax_EXT STREQUAL ".a") - set(OPENVDB_openvdb_ax_LIB_TYPE STATIC) - elseif(_OPENVDB_openvdb_ax_EXT STREQUAL ".so" OR - _OPENVDB_openvdb_ax_EXT STREQUAL ".dylib") - set(OPENVDB_openvdb_ax_LIB_TYPE SHARED) - endif() - endif() - - - if(NOT TARGET OpenVDB::openvdb_ax) - add_library(OpenVDB::openvdb_ax UNKNOWN IMPORTED) - set_target_properties(OpenVDB::openvdb_ax PROPERTIES - IMPORTED_LOCATION "${OpenVDB_openvdb_ax_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_openvdb_ax_INCLUDE_DIR}" - INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${LLVM_INCLUDE_DIRS}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;${LLVM_LIBS}" - INTERFACE_COMPILE_FEATURES cxx_std_14 - ) - endif() -endif() - -# OpenVDB::nanovdb - -if(OpenVDB_nanovdb_LIBRARY) - if(NOT TARGET OpenVDB::nanovdb) - add_library(OpenVDB::nanovdb INTERFACE IMPORTED) - set_target_properties(OpenVDB::nanovdb PROPERTIES - IMPORTED_LOCATION "${OpenVDB_nanovdb_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${OpenVDB_nanovdb_INCLUDE_DIR}" - INTERFACE_LINK_LIBRARIES "OpenVDB::openvdb;" - INTERFACE_COMPILE_FEATURES cxx_std_14 - ) - endif() -endif() - -unset(_OPENVDB_VISIBLE_DEPENDENCIES) -unset(_OPENVDB_HIDDEN_DEPENDENCIES) diff --git a/robot/ros_ws/src/global/planners/exploration/CMakeModules/OpenVDBUtils.cmake b/robot/ros_ws/src/global/planners/exploration/CMakeModules/OpenVDBUtils.cmake deleted file mode 100644 index 084f27d71..000000000 --- a/robot/ros_ws/src/global/planners/exploration/CMakeModules/OpenVDBUtils.cmake +++ /dev/null @@ -1,164 +0,0 @@ -# Copyright Contributors to the OpenVDB Project -# SPDX-License-Identifier: MPL-2.0 -# -#[=======================================================================[.rst: - -OpenVDBUtils.cmake ------------------- - -A utility CMake file which provides helper functions for configuring an -OpenVDB installation. - -Use this module by invoking include with the form:: - - include ( OpenVDBUtils ) - - -The following functions are provided: - -``OPENVDB_GET_VERSION_DEFINE`` - - OPENVDB_GET_VERSION_DEFINE ( ) - - Parse the provided version file to retrieve the a given OpenVDB define - specified by and store the result in . If the define has a - value, the value is stored. If the define has no value but exists, ON is - stored. Else, OFF is stored. - - If the file does not exist, is unmodified. - -``OPENVDB_VERSION_FROM_HEADER`` - - OPENVDB_VERSION_FROM_HEADER ( - VERSION [] - MAJOR [] - MINOR [] - PATCH [] ) - - Parse the provided version file to retrieve the current OpenVDB - version information. The file is expected to be a version.h file - as found in the following path of an OpenVDB repository: - openvdb/version.h - - If the file does not exist, variables are unmodified. - -``OPENVDB_ABI_VERSION_FROM_PRINT`` - - OPENVDB_ABI_VERSION_FROM_PRINT ( - [QUIET] - ABI [] ) - - Retrieve the ABI version that an installation of OpenVDB was compiled - for using the provided vdb_print binary. Parses the result of: - vdb_print --version - - If the binary does not exist or fails to launch, variables are - unmodified. - -#]=======================================================================] - -cmake_minimum_required(VERSION 3.15) - - -function(OPENVDB_GET_VERSION_DEFINE HEADER KEY VALUE) - if(NOT EXISTS ${HEADER}) - return() - endif() - - file(STRINGS "${HEADER}" details REGEX "^#define[\t ]+${KEY}[\t ]+.*") - if(details) - # define has a value, extract it and return - string(REGEX REPLACE "^.*${KEY}[\t ]+([0-9]*).*$" "\\1" details "${details}") - set(${VALUE} ${details} PARENT_SCOPE) - return() - endif() - - # try re-searching for single defines - file(STRINGS "${HEADER}" details REGEX "^#define[\t ]+${KEY}.*") - if(details) - set(${VALUE} ON PARENT_SCOPE) - else() - set(${VALUE} OFF PARENT_SCOPE) - endif() -endfunction() - - -######################################################################## -######################################################################## - - -function(OPENVDB_VERSION_FROM_HEADER OPENVDB_VERSION_FILE) - cmake_parse_arguments(_VDB "" "VERSION;MAJOR;MINOR;PATCH;ABI" "" ${ARGN}) - - if(NOT EXISTS ${OPENVDB_VERSION_FILE}) - return() - endif() - - OPENVDB_GET_VERSION_DEFINE(${OPENVDB_VERSION_FILE} "OPENVDB_LIBRARY_MAJOR_VERSION_NUMBER" _OpenVDB_MAJOR_VERSION) - OPENVDB_GET_VERSION_DEFINE(${OPENVDB_VERSION_FILE} "OPENVDB_LIBRARY_MINOR_VERSION_NUMBER" _OpenVDB_MINOR_VERSION) - OPENVDB_GET_VERSION_DEFINE(${OPENVDB_VERSION_FILE} "OPENVDB_LIBRARY_PATCH_VERSION_NUMBER" _OpenVDB_PATCH_VERSION) - OPENVDB_GET_VERSION_DEFINE(${OPENVDB_VERSION_FILE} "OPENVDB_ABI_VERSION_NUMBER" _OpenVDB_ABI_VERSION) - - if(_VDB_VERSION) - set(${_VDB_VERSION} - ${_OpenVDB_MAJOR_VERSION}.${_OpenVDB_MINOR_VERSION}.${_OpenVDB_PATCH_VERSION} - PARENT_SCOPE - ) - endif() - if(_VDB_MAJOR) - set(${_VDB_MAJOR} ${_OpenVDB_MAJOR_VERSION} PARENT_SCOPE) - endif() - if(_VDB_MINOR) - set(${_VDB_MINOR} ${_OpenVDB_MINOR_VERSION} PARENT_SCOPE) - endif() - if(_VDB_PATCH) - set(${_VDB_PATCH} ${_OpenVDB_PATCH_VERSION} PARENT_SCOPE) - endif() - if(_VDB_ABI) - set(${_VDB_ABI} ${_OpenVDB_ABI_VERSION} PARENT_SCOPE) - endif() -endfunction() - - -######################################################################## -######################################################################## - - -function(OPENVDB_ABI_VERSION_FROM_PRINT OPENVDB_PRINT) - cmake_parse_arguments(_VDB "QUIET" "ABI" "" ${ARGN}) - - if(NOT EXISTS ${OPENVDB_PRINT}) - return() - endif() - - set(_VDB_PRINT_VERSION_STRING "") - set(_VDB_PRINT_RETURN_STATUS "") - - if(${_VDB_QUIET}) - execute_process(COMMAND ${OPENVDB_PRINT} "--version" - RESULT_VARIABLE _VDB_PRINT_RETURN_STATUS - OUTPUT_VARIABLE _VDB_PRINT_VERSION_STRING - ERROR_QUIET - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - else() - execute_process(COMMAND ${OPENVDB_PRINT} "--version" - RESULT_VARIABLE _VDB_PRINT_RETURN_STATUS - OUTPUT_VARIABLE _VDB_PRINT_VERSION_STRING - OUTPUT_STRIP_TRAILING_WHITESPACE - ) - endif() - - if(${_VDB_PRINT_RETURN_STATUS}) - return() - endif() - - set(_OpenVDB_ABI) - string(REGEX REPLACE ".*abi([0-9]*).*" "\\1" _OpenVDB_ABI ${_VDB_PRINT_VERSION_STRING}) - unset(_VDB_PRINT_RETURN_STATUS) - unset(_VDB_PRINT_VERSION_STRING) - - if(_VDB_ABI) - set(${_VDB_ABI} ${_OpenVDB_ABI} PARENT_SCOPE) - endif() -endfunction() diff --git a/robot/ros_ws/src/global/planners/exploration/README.md b/robot/ros_ws/src/global/planners/exploration/README.md deleted file mode 100644 index 0e609baa8..000000000 --- a/robot/ros_ws/src/global/planners/exploration/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Exploration Planner - -The Exploration Planner is an optional global planner for autonomous flight. It maintains an OpenVDB-based voxel occupancy grid map, extracts frontiers from the map, and generates trajectories that let the drone explore unvisited areas. Because it maintains its own map, you can turn off the `vdb_mapping` node in the launch XML; the planner will generate and publish multiple linked straight-line trajectories as a shortened, collision-checked RRT path to a selected viewpoint. - -## Functionality - -In your stack's entry launch file (e.g. a copy of -`stacks/full_default/launch/stack.launch.xml`), replace the -`random_walk_planner.launch.xml` include (and optionally the vdb_mapping -include) with: - -``: - -Then when running the robot stack, after taking off, toggle the global plan from the rviz Tasks Panel or the GCS (the `~/global_plan_toggle` service) and the exploration planner runs in place of the random walk planner. The working process is: - -1. Create and maintain a voxel grid map with odometry and laser scan, to visualize, check topic `"~/vdb_viz"`, where `~` is the namespace. -2. Extract frontier and select viewpoints for exploration. -3. Generate RRT path, shorten and publish as global plan. -4. Continuously monitor the robot's progress along the published path. -5. Once the robot completes the current path, a new exploration path will be generated. - -This loop continues, enabling the drone to keep exploring the entire space. - -The planner is derived from the random walk planner and shares several of its parameters; parameter documentation is still being expanded. - -## Parameters -|
Parameter
| Description -|----------------------------|--------------------------------------------------------------- -| `num_paths_to_generate` | Number of straight-line paths to concatenate into a complete trajectory.| -| `max_start_to_goal_dist_m` | Maximum distance (in meters) from the start point to the goal point for each straight-line segment.| -| `checking_point_cnt` | Number of points along each straight-line segment to check for collisions.| -| `max_z_change_m` | Maximum allowed change in height (z-axis) between the start and goal points.| -| `collision_padding_m` | Extra padding (in meters) added around a voxel's dimensions when checking for collisions.| -| `path_end_threshold_m` | Distance threshold (in meters) for considering the current path completed and generating a new one.| -| `max_yaw_change_degrees` | Maximum allowed change in angle (in radians) between consecutive straight-line segments to ensure a relatively consistent direction.| -| `robot_frame_id` | The frame name for the robot's base frame to look up the transform from the robot position to the world.| - -## Services -|
Parameter
| Type | Description -|----------------------------|----------------------------------------|-----------------------| -| `~/global_plan_toggle` | std_srvs/Trigger | A toggle switch to turn on and off the exploration planner.| - -## Subscriptions -|
Parameter
| Type | Description -|----------------------------|----------------------------------------|-----------------------| -| `~/sub_map_topic` | visualization_msgs/Marker | Stores the map representation that is output from the world or local map topic; currently using vdb local map.| -| `~/tf` | geometry_msgs/TransformStamped | Stores the transform from the robot to the world.| - -## Publications -|
Parameter
| Type | Description -|----------------------------|----------------------------------------|-----------------------| -| `~/pub_global_plan_topic` | nav_msgs/Path | Outputs the global plan generated by the exploration planner.| - - - diff --git a/robot/ros_ws/src/global/planners/exploration/config/exploration_config.yaml b/robot/ros_ws/src/global/planners/exploration/config/exploration_config.yaml deleted file mode 100644 index 4b3bbb0b7..000000000 --- a/robot/ros_ws/src/global/planners/exploration/config/exploration_config.yaml +++ /dev/null @@ -1,91 +0,0 @@ -/**: - ros__parameters: - robot_frame_id: "base_link" - lidar_frame_id: "ouster" - map_frame_id: "map" - pub_global_plan_topic: "~/global_plan" - pub_goal_point_viz_topic: "~/goal_point_viz" - pub_trajectory_viz_topic: "~/traj_viz" - pub_grid_viz_topic: "~/vdb_viz" - pub_frontier_viz_topic: "~/frontier_viz" - pub_clustered_frontier_viz_topic: "~/clustered_frontier_viz" - sub_lidar_topic: "sensor_pointcloud" - sub_map_topic: "vdb_map_visualization" - sub_vdb_topic: "vdb_grid" - sub_robot_tf_topic: "/tf" - sub_target_path_topic: "/perspective_goals" - srv_exploration_toggle_topic: "~/global_plan_toggle" - - publish_visualizations: false # should trajectory visualizations be published - num_paths_to_generate: 5 # how many paths to string together - - # Planner Parameters - max_start_to_goal_dist_m: 10.0 # how max distance the goal can be from the current robot position - checking_point_cnt: 100 # how many points to collision check between the start and goal points - max_z_change_m: 2.0 # max height that the goal can deviate from the current height - collision_padding_m: 0.5 # how much space should the path have to any obstacles - path_end_threshold_m: 0.2 # how close to the goal will the path be considered complete - max_yaw_change_degrees: 180.0 # how much the goal can deviate from the current orientation - - # Mapping Parameters - map_voxel_resolution: 0.25 # voxel grid size - l_occ: 1.0 - cluster_cube_dim: 5.0 - voxel_cluster_count_thresh: 50 - - # Stall detection parameters - position_change_threshold: 0.2 # Minimum distance (meters) to consider as movement - stall_timeout_seconds: 20.0 # Time without movement before clearing plan - - # view sampling - bbox_length : 1.5 - bbox_breadth : 1.5 - bbox_height : 1.5 - viewing_distance : 1.5 # not used - num_viewpts_per_cluster : 6 - kViewingDistanceInnerR : 1.5 - kViewingDistanceOuterR : 2.0 - kViewingDistanceZOffset : 1.0 - viewp_bbox_unknown_frac_thresh : 1.0 # In a box around a voxel, all unknown as free, all are allowed to be unknown - viewp_bbox_known_occ_thresh : 0.0 # No occupation allowed - coll_check_endpoint_offset : 0.4 - robot_model_bbox_fraction : 0.1 - - # Traj Planning - too_close_distance: 4.0 - too_close_penalty: 5.0 - odometry_match_distance: 3.0 - odometry_match_angle: 3.14159 - odometry_match_penalty: 1000.0 - momentum_collision_check_distance: 15.0 - momentum_collision_check_distance_min: 4.0 - momentum_change_max_reward: 6.0 - momentum_change_collision_reward: 2.0 - viewpoint_neighborhood_too_close_radius: 10.0 - viewpoint_neighborhood_sphere_radius: 6.0 - viewpoint_neighborhood_count: 5 - momentum_minimum_distance: 0.5 - momentum_time: 6.0 - momentum_collision_check_step_size: 0.4 - - # Voxxel Planner RRT params - planner_norm_limit: 1.0 - dense_step: 0.1 - max_explore_dist: 100.0 - planner_max_iter: 20000 - traj_smooth_horizon: 50 - max_connection_iter: 1000 - - # RRT force adding nodes around the start and goal - rrt_region_nodes_count: 16 - rrt_region_nodes_radius: 0.4 - rrt_region_nodes_z_layers_count_up: 1 - rrt_region_nodes_z_layers_step: 0.4 - - priority_level_thred: 10 - - # Collision checker (Safe) - robot_model_safer_cube_dim: 1.8 - - # Collision checker (Risky) - robot_model_cube_dim: 1.4 #0.8 \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/exploration_vis_path.png b/robot/ros_ws/src/global/planners/exploration/exploration_vis_path.png deleted file mode 100644 index 029e9323ffacb267afa1e11d8f487ce951b21535..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21241 zcmX6_1z3~c+aBE@l9GbR5Evy&Gb!nmZbYQJYk-uLpp>+fbT^|B5Ky{dG}16~qrdm} z|JZfGwRbP)ea<;g-S_jHMQNzXlMvDofq+cw4wxfvlaJ9jzelmTp#7PVP3&9>-XnlE6)n|8A0T zv$F88b#{8HZR=1^5JBJ>?S+<>MC>6nHA7`czI9Rzv` zQk0d__Q^a%1SK)(S%9ydFRI3WKl?EJ#NadeBMlen*y$m1-%wHlwfBOzpGj!Rd4;j5 z`BFj-=H+Xhec6tGWi~1Qo)1qvdmY>!A^ti>eI}-wQLH~Zir`=B%Z!UWuRpF#vTxp4 z^4nC$jw?L#RQGO&mO5V??v(FTP1-dI#A8my2pg)R(y?dL})i?9s2XPk;6Y5$0lCdXS$2&DG& zZ@$>KEVjD*lSFhp$PWx7AHOVe{!kqMd?=cbX0kr)j?rSvamh03H}kM@EA1_@o@@wz z$v3e3rmN`}|NE<}(C-iM4o3BsBknU1H*;W6aDO5NF_JY3#sWxG#@G+Z0Fs<6XXOo_vl?n$D995NJ_z!s2 zH|sI{8;M9VGiQeo7EZze5%R6!DFGo(q-6~5m4SiC*XAx{fD+BHjmR%s2xZ;9>hTEc z(6MIoPv2@PCT`rVY)j0k5l`r`dP_|#d#W6f#0puSxdBxWdLs?@8J=e{*p zxFzAo@~-_Prn3zVd=PP=7esPR)Q@+|Ust+ePH}UTYbfZPwhZbLX(<|1W++-e#QSr9 zC9r#Q3ojCIc&~DjR4A=TVFWqtc2E4UF>~{w<=Lbh-Nts4V8m_)t$a6Oc&*@I7)5r5 z?BM3S(x{t~+-HfFUjMvwVft_9+CO7BMU3;@j^mt*<7ekqy8h3OVJJL$fnf)=i7|{S{H-Z}J3t+CdveS6G=Sg! z#5Z$5OWffkMlo5ge0!UW+Emzw9GvKi#?|B(Zsapsz=lS|YL1bIX1c!Vh6M*3r`ntn z$e^B@llj&MqsTSX(SV=a3S7a%Z8{vaKAhLY&4;c@(0Y_SLXrqi@GO{Uk(!TrcdxCF z6Z~6H(dkc;^GQC2Zs|#KAyLdWF3yY7C4BpncaB~DPfl7b8s{J|!LA;QpS5AqtW-4( z*~PR^6mcNr*^$MX+KFO87Yq(JgfDBniaXhKSe3N7x z2ce?Gb0+M!RFf54e#KyXalBhx} zGuNo2K%a(QRhzl_I6|*nw%FktsaH%iR0rEIHy{hj4mT%c&V^4@UX%9MB=~7UC z74^ArhM#IS3KONNd>tw`jp%XHD5TO&-q0X0;#9D*;;3^SdP-CGlhvvtQ+a~W*2zho zIx@ddz*z8wm_rE%uc_G7qehxCbkNkmW7X0z&)c5X$}ddqb;zEs81`fgql3u-d%V7G z0R>+=LRhIuxjyaZ=wuII|A&6q&GO|by3*nms_tfq_3%+XE`cY!;$q7?0IogHbwm>N zYJ8W}G1T&5@G}pW)oye3%w0)u*JM)ma}Ukoq%xQp$X#SAFKy$MwO#-FM6)I(m6@Qz zOrP|x)rS}}t)=1DUd zmuAiSm*eC}9V!FTnEN~5QjERZ?w?GNN8a*3J9x#vxG~C`3R$yHIOcY{e;c6=A+{`LcAN^bxk`UQ9SBH=26T$AaWhY3}C!mF>g&9d5l zw6}u&8VcItUMA#J@fG3Y(?gQWT~iz8fHE?1J&Z$12wkFB4fKhQy&7)Hra1rn`f{RaLn8^}Q3m+HkKc*35-L1wW0 zgCG5M;@e2$<9JmHE`H9JI`E1-jWycO69T(oi9MmrL~PFs_q!GpYyKqIMISV>=7n%v zykF~9rz+L{2}ndBobtj*T<-;EiX7`Jj`4MQJArmB^#w<{J^nl6S6NZ}1vqOhN}T4% zXN6R*xy1X8@At7aVMC&sp@0v;g)ZME@Z@Ed{cKRv5O2%g; zd*$vu_G{Ngq<_92mPi|EDiex_v*3uy?;p}y22Wu&7T4`_6|QL)1B}kdqbCypw}!1z5n?trHdZF+)jyr!(rXfV_c79Le0I?oqa%nFq42 z#m2r=I_x?y)##qtu;5Mm>o5KX1(K6amJ7A!2GpkK=nV=9y`(PiCiSg8NZ#r${Z~ow zCPn9ORMhq$M+f^qj(sgn*-}EINg52(&=4RdAI%a?FnT-!_ zb6EX&k|7Y-^DT+NEs7jSxa`%&2MX2;pJCmyhJzdTiWz|~48B3D8xgR}IsV%o?13af zBtan&%t7MGR-oz}XZKmyRQ@n^lI%~r2E*JHW}z9O_!EE4Bp&qmhdEA7Q{vuyZC}-9 zoadTexFJ<^SNv}CLv9gHk6W;zIy_%E`7d6r0ncmLG+|e5Z`2eakv3!Ok^`CIuZoQ` zWxQX7U;RU!Lo?bOYWm{cC<@E$thRnBSZV)L3QOj&is}_T?RG0KlG<6s1fQEsJ=~^Z z+}@3i4ke0BELdPA7@;TVL(NK82YQNlymU0Rxd*NkM9VGz7ZGHVxWMNIpvE4803!?) ztnZj(AS6AA6XYP6u|HxdKoHP7(QLq_gUr_y)y_MRWYbL)Y052XxTtsK+^ww;)3FJGUxCYA2)r>G|{;wlq<7r@;6zq1{?Wd=rB?x5Ein zLNvH7irw~ck%hSR3kY#|pCPoG6wNEAXBRpq6t^}eH2t6#N^yMW6=~U5 z*x0s`Q@!GeMqY$FtjK74*{Z!>dj*{XBq(WEN?V;Vf?`;=WqNQ&#jaw1$ zO1L6`#-iE238TrL#u_BHyh$>RnUi3P+z8in3}i~fZ{Owe zJF$~#tLEnu(^9$E?Zr=0I}|2xldgNe&21eCBtb*1P&mcbbEmCmt)=>}@z8$7sskwb z0SykkTGtBaWhD6HMZax#MnSTAAX#N|8{_8qF8gB`egJ2rgh2(d z>YAu&>`>Toe9#k$s+d8mL?v{-Pa?8uT7COyd97l$ z(ZVON0~dGWD$nz9Z=!X3;BZg%rZO_QWdVJ^lfOevkj*8VzfOFZ08W?nEl)Kon^#cI zX5;wf3-Uw**G3URrJZ3otIUwj(Zuq@%#R>8YzH1oUo~ZB~RWIO&DRMG7Ey^Sr zPgU!9a1^Zbe=k#nska?#PSoy-DeGNdUx#@95f`PPo%qO<)m^zIRhH%Rr5Z^iIK#`0 zUpm{qZ&E6^?5FB+XbL)bAV-U{V@q1B4>B$_f<;Y#yig|&m$i#Fd402SdNl7%19|{8 z`%Q8JBe_!B;8sW5tSMtRZ{U5wq9qA(+o6EcdrruIO_aG!wfj1|O|fy+1NU;&pX=<5 zx*LNc3DQRPbaK}%=JJY%JSu;^$S@{z)J_|rN>^EOe|}(X7QYYbBJs}K_xd;fgnQ+m zxEglHkD1ONGw^nyqrQ{HyWSj>_10*9{oWShl%_*Frj1D6W!B040S4mZ1O~_M|$DmtJNQ+sSSGG(h}D7Rm&+-i41?; zh9nO*ySgo(1d@5CEQP0e-_7sLb%vi+^`A9DnSGe~9(JTq`$v}K4EJp;>V0FgqF%(+ z+`jItjXZ_J{#5Q`*+W*6%sc&llIN zH~Qp+i5CiE*UAm&Mpr{jXzYB$+i&kVxN*4%qouR;*Jn$iN3OE1 zv^1x;9V^R+Dy#a?>LZ@PT2z0#eqSm5zyF>dmq-Hs?wR=p{K*1XjiM?E641!#dFYSo zjb%oCxW(VQ7C|x8J6MQemfxufpEUkv`4|0n`OXVrcCo0w&vJN&$Cq$eUlH4HQqK zzddb!`nJO3-@nWMApflNvms60+ysl53F^iz!=y*}#Gld`Ksn71QMom~)7v^BkWlJ3 zw=du44a+8y4cbtFT4+%5`A{H_*@g_eMxxcT{^EG2>}OB*$^l$TS4-X{!jc z`^Vg%&QlsNNLb3#(SksoDTDEPzR8dpi1gGbE+hUqXT4>=zq<>LuSe1MYHkH|w?0w~ zzu&`|@@kcC7PKx?q_slLW-E4Tld%$4s2z`3M`)e05}{B)J%zBdJ1zy0bI48NmKd5~ z`qfhF(z!Md3hNB(8rLh_kH_Ep{_~H^{4tZW+5P_tGlWs!+>fM3|I>Bg^O5$=%(ED0 zq6hfHZ4b@?`dW`4VYAw*9*Czm*OivP(*1E?z<6;XYWt4BwI$T(ICz6>cof7oUm z6`Biw=Cs3Q>BQgIr*2j5!SPv%h9&vSbM=j(q7}A8*Z60IFQ-apnKQt6V$P4w8w&=Z zM8rKgHschCR)nJo%F?+t^14JcxLa5X^!dr#D)98gJ* zCQl0?hfM!L5%K%v0e=4h?gjYbe^Wd}IC@8=K24pn^FeLIZxDha%?J;$UHEx=y@$7C26`CBgxao*6#f5EQm9XJ)~S}pnxVEtt2aHRohJ0nvV8!}+f55t}_ z(xmCo&FD`XB*g6v1)^bjrUOtUCuJr+o4<>$$E4ah+x0sb!P5=ZzPs!Pr6k(HL-WyM zYNJkb#hDe&Iaq-7u8qFby0g-UFTkRoK|XtC*`KBx%^R||&zVMJoSfF=K-nbb)Ll>v z^rHy*?Ln3*Lh#UFKCUB3V%vaUJbM(=h4k-7ACI3J3nr?I+~HzV|v8g$5X=j zcbUS#(*A>euYt(hTQRcLMYMbyK5yvGLLXUY^uMx!<7RU0e%W^YpQmYtNXudl>RG90 zT1EzjXD>QjiI|61Q2Wx!y+58BUiwX{iOth@oV;DSn;6C7_3X)4~dLZ?2dY~}{83rv$pFLzcz#9aT)t?#N+R*+d)FPYz)5O-> z$`+{#e~UGBrrvGG@&7h*C6ZsEHC$sE5xl!&dRTrL~^iVQ$ zpn}lCZJRvR^l;k@w>=C0a#s4WSIJ) ziJ;M5#WYXQC|SU+mW3h52P9XEA}526x*cm`n5X6-bG{@G5$6TE*n`_O?9h|zxSyr} z0E2dS4+j?#82nOG<~DQMi&ft$kT0dGuB@}e6q+@!L+@NR=Yx@H(2&|g;QJo6Sb;n) zr9w1(ij(5X#`n!DyTr^4_9pET%?w5f1{Z-cv5^uWGYn+i-!yR?K)ZUqV^a5x5QKD_#M)X7f*{Cx295J!s zH4tr%S_8a#Bii4?p_7Q2zI$uWlVMOcUJ;dg>;N1;beH0kl5kvyUXV$h^#5fE@FMMc zBFu6un3>WB#t5MAN_DXAaN3fl(i;&Y5`J(Pi*Y0DB3onYySV3_!!+j_yN-tl1_jj~ zE!X!!a!y^!xrc2yyVVP0a+L<&i+ZV(m&D|1m{}k?q(UCiI+9~&_I;;mJV#2T@xp6!=XRrs9xv^hQGi0?`>#e=1n)d5v0S`{3VAXs$;tyFJWyX$+=(A-ry0?B4_ zNKoCF!|U;DDf=-590Oc31Nsve*sS$Q7dIjDW1p`BgTqO_N?6wSBB#khSL z{6MgZC-Gu4?w^GIdyX|fE1TH$<|rPt3~{UfXt(W0?q;|UkwJ;V|-uE zi-8^ug#sQPb4DJw80wx>XM)q0_@<(ADy@$I8yC;kX9jf12*(Ap%Mz5G&}uiZ)!rPU z{zxnsG$PzrJ!`|k7X%Ng&Vx5$f&V%%yJtQhpoR)4O&pZ~5`&mfa_8!l)ix3n=SO)fR-T8!eaC;tuefR zuwQ}8tRVPSFaqw++qJuxjcfRzPV_g(3g&&xMtl3)M-g5$q`y}Dw(wEuM9ue~#JPM9 zS(0fC0l&7T65nhRJXskq$N1nYz8t^yp&$qo*;h$5Cjg$Cq0A&*<==&T=y^-&h3N0! zq7Q}aUgx!`b=3LGc^}v1Gs_t8qcuyCb9-3Ml3s+dn-HmP&4Pz`7*ADZe>g6@&UR?t zxTRj>c1N)VgFykuQ1PfDCqEt_zVA5guP+?8F;ySYc??H;X5Z|NH~#t)OVr!>yDEJ2 zrjRy7UA$22VHhBm2F{y)jxQtLZhXKRaE3B9ziFtlUM*NE;`@%^YW0UMOP{qL z9Fg=;LwidorFBG86!`%bS-zvcSmUiVQx?W0$vtx`Bx8z@mh$o-Cjcv$Vzs)@>!S1m}+$B4`N3MjHmbM5r02YR2<{| zIg0zvI2_#_uk>R0(U9lZ=cyEHj@XG$7b~KS0d2FrKOHxYN(J4Hr)qL@>Q)^(8!tP_ z8cQAlIXRCZdJ_qrql&dQdb4Ff%YC{Y)=}SMY8B}6=n!%EHAR&?_*ug(W8_OX$`&x> zeNZ+|y^~F@FP_oeEgzbEi=)_nZ)aR=UQ3}MxL(>~?r-5zD~fut-zAAz~T5Z##)-f~P^{u;v9!5ZBRYAsw1+ z`lU`$nIUWcRRy#Dz%o6mwcE#dKW7oa^%0dFI*BmCb1`^5;QYyOVkqLaX1##B_z~e* zf32)2f!&D_qA9v;KXR#RxWcgWW}0_Hwe#w^L+GTr=bhD!YvW>Hs`Ih4lq+ia>yxgR z@gu9}4Mm7bWAgj*#x1Y2%b%PsqUW#qmumE|%2kDTciw}X@sLl#a5{1cSYXoB&R(8t zM-<>srTPP8v<^~WYS+t3Gg>;(*3q)Ni*8;C)*NpAGD7+cS_hxysxK2d$nytiqYx@m z-WfadJ_AxK;&5L}gS-MSL?hA4FDM#z+&X&?;dJ?Db6W*B3$JT8xLrBEXQ}jANj@4a z_tHhUy5w1@)T1%fY(#-X8x*{pG}r5`{IJnC5_#7Dc@Sto`2SvjvMy@!%WSgM(t8y2 zQx|{0UG;0%9W0Ic;x4DTqs_H|=U_HdcFo)?yMVfan1TYV z5^Z90XcH+f@1C~#q)>2;y#L;eOZW8!?7hK`)8RUD^q`k{8K0rz+0v|Tq}HX0E%yo?@+vim6aZB}gf6&u4Q?RDWT*%KX{eYb3VC@?bB{mrrkUs-(Q^0d z@}QKFX|I*mR7&*otysZ!V(-cqfA6p_CqzWS|Jhp5r3cC>baJohWzR{n2798|sY8hQ zb2-_8JCm-hRbeO!}ji5KS*@KFEk!@7f*f?7?f<2+L&*=HW}99=nQeC$=P zv!P9XnADp3KQGi!jEMJYxr7A$^>&ih!K$9N6xnsw*%_Y5V3-0qKzDU#rbvR%3EY~$ zVrguA=uO*k*~E`9Lh>;nCC}y6ez)Qb7cv&X zo!8;;tnI|J3=(UvXz)SH26o|!y^zDU{iV4;jLXEfrl#Dt7+ zZgcfWaqbL12B^xGib%Irip^JeAyU7oLevk`4@?Hsvk~-r3_z$4!u`*vm>-Y=Sjo?Fbhyf~}JCG+G>D#%2TWlRFUbJRSSHt|#TpNZYov%Y{MV@Cfs@5} z%fkqg^N->tH72Wh&c=e2rJ&WRPVucU(a!Cr%)@EFNeS%{T@8tMeTdHUtL_5Q;j5OR zc|yOJ5=Dg?<+ET|V4v-kQK|UOk>Gnz$Pa;qbudHcU9&Tq9_-?EOqt0av^gP)R%JcZ zC_a^oRIrv?>8f#Mt`_!SrfVCupUcP)ib9HF-9ApHS=3Mh6E&aJAedx?G`3>SNXR~n0g)qNgoXm-btVOqI;pwn&Bui zBb$3MFBB$!sF?vzaI3n&i{YCBu@3b_E>X(I;a^i7Fd}-HzLfWTVrSXe$>*b&b`f=V)kUgP&G`_7Pm@F5;@N( z^v2L+`()i4)J600fey4Z4_8jc3keK0Q2_`j%w@bkLD#G1TEjQNSMp@7CZk__R&X?v?>|i@hHA0{7?kSZ~NNW=i43O^Wy0MYGe`^V^O! z$Z?xh!@NV@MLUpgzI18z&%ptsYv8kmj>va44PjJ&IjB~^bUKZw7e}%bCk=UK`L*rp zt$zQ~Bgc;-J1hqb(T_55^qmNl+}-AgY!GLL08f|pe9>8l-gfKkQLcKJxX}4@q|6GC zf671t5srxN*1$W#gr!#bG0v4Y+(VpL%z%K0e zUqY<;w{yC7kJ>?Cv?GpTQ~g+5p~yK}AO-9gnxmALDGoRkzg1a^^j`}--|7OE)>%MZ z_jjuGUl@sCZxo1f%vQx|bjNi&r*Red-k^~0y~;|@0-kG`*<7NG>7TCcKi7=0 z+jXtfoYq_b<5ch5RC)XT$hSyYS7ic86bNJ&%|^up8L3XK&={sA*MN0v0RL8t&ivMb zyvMCVx3$CLfRszWkFydzliWoV47enwy`$uKk(ukj<=K%%gg-UlR2XR&zG*rAr5V!D zonJ3%Zl;RUe~A5js?(@Z?DmHD6MmpN>;rYv`bO`{`DIwN;$tSM5HgofTGrMBzCJ>2 z$?D=GaN}tD>(ULG9EMF!>nw!mUYRFqKQ1wDoZ#u>tCn92AyNaoB!1Ep$Vdv>Yy_7# z>i&iri8m5|mDS6hqQeUbW#$zo?VNqn_S#C=*R$Wf*?5SWaQNeY)FbSdwR1K8a1H1_ zgp5{Y6@U_=mS?Qkk|)9jKsX)q)ARgzhS_z%IKrRPad{4Or`*!Q?XXCiH_)4J!0I6K z2O3%B9BWt#?npcMqZjq1zXns0~F-;W6-=>QVM74-}byU4T-Z!pYw z<`IZU4;&Ma8=-DLkLmfBM)VK9HE*g^346|;s9R5o3BWOckd!yv+?;7!NTUN*-HDGf z_Yg2W`F^DD1Fuh$Wui5D1gP>X?1AsIg_n+&-_af&WnsD~mKV|GNw=p*D0IhD`S3BC z(Z5qORf_zw?PB1P7u6bomE(Qb9u}g`G>Z3Biz_Dw*gxSZe<}bd&uZ8&0e}UI8K5;h zgR0sjGVlY}eKCj~AO6V)tk7RqlRYy-MNc>ETjPuzn6|*Z3A+Txt`n)#A6_H+G81RY zHkdMS8i5uMHgFYbGI1|ST7-~GA1ysPI0}<_6}#;6s8Q^m#|iW;H2=g z!~V5VUjI$6y4)95i`! zt;c`rXCrbG6qs96XUkJR5F`pBG4gY(0G|PrED$O1Gjv$;RNM~CCT;^0X5A|!mj z$v^6>fk`3S*U$cB{<85trUp%RRXE>crw{Lr98HJlcE{tdIK>DWSQ8c8jSlgE(%}F=xpK?wnJf-YJ_fENtOA*?eJLM_|(g>MU2$it7@)Cbq^3vm+0? z*XjTfN!pKMgvzhu%f}{K+(xqV3avY*Uo%#SK>BU}ECfN^<$;eh9I|=}Ac3cr?L?e6 z4KssM)jNYvO0LM?TBFo@cTd|1yMlNRe!CT^0!pUu`i`vzr3+_OuQ*T+Sh6@$Bb6Nph>Ihz;KUS1BD*3C=!>3RB{B-) z9gbqKZ8%VWd_F@~j+J#%5eQphmS@7+5t`I{_4nf&Z)e7?5nv{IAtvqR=AX~M7JN#} zSWKK=`_)fR8%TL#8i&w=^nV%-8h5g^FT?~1FYd3s;`n&!#MCSCA5!-zuG_qa78B|9 zC8Rv*JX#h7(nD1J*lpNVoxf%L3D?aWo36(7s{a-~Jc>&u?YV?iP2qy|qT`>L{W-&u zu13eq!wY2iO=Zs_-M#-5_47l&*KwYx+iZDdXhdDmv8`|g?DW9&>?Lz>64?6#IiZ2? ze&}&BYzX#DeoxW=Hv1sYj>?FON(en4ym(y{Q*7l0&~K(1zAUB>bDmw||5fe-m^Xyn zXFmti1S8rdyLroo#B6b7WOrkhDznoi+EG%y~TA5|-rww9b#F#bOL;v3&2UM@8o*ObECVnZ-tg+MAxqX72*CBRL*+%I_876iYy_dH1Wx4nBl(Q9P~)F# z>>k(ncWFRog_jw`A-_tIL<-v=B)4XI_b8dS%j##ZEJe21JYiNY{o`wPf`Bgb`IPGk z%w-32E;C&fM})J}YyO}gnO1=u=rp(Pmlv`+X)m@(sUo*#j4e|w$p!EO-Za`@BtJoM z&L&PkU#Q~=m*e|s(*ebs= zXFD%sLxh?LgN%$ID%}Q;D+#yNm$6-#MoL+VKn%7i)|WO5{mIbRJ+k=@F*z@W z_|Cpd#i-;fwdk}s;6zA_Cv#CgLf6vG>Nz(ci9p$8+oPv9+XH(KWgwrRvhaBRTPs$z zXR+cQ%F_ZVRJfg=R0R5^wuXWgtQAuefTqz?VMMGWV%YZ9G@)GDtZcf67h{J-966sI zpR#TCYC1o!xXns~XW!}t9B3oN_Qiv!39wAW@8(u}9?%*t^KwpPaea^oZ6X4r2Osk4 z(FD}`yQRO+Yxi}qruNk|4t=yA&#YUo6DWP;QZ|_Nt#V5qI37IJZ+vb$N5Ra+;T)4w zX@&Tu@jHVo^t;ceJ++3IEV}`jy(67U1uUeDx@B zJNdP_#0?lm*{EXds>n1A3g#DnyxTWT5TDtjfWS7SS5)4tQW#cPq0T%Z&M5YPhtJVeuPB$V2jk=KgWrZYyI%IbfoCmZ{*^LmPmo;UXFvCC zFdQZh+8oNi`3`|4Dd@-K3|Z2D9&+-%`ii9PU})0L`1NvDJl5+=Zq>ZQxDY|a_JXq*DHkp37eRxEsGP59`)bN!Wn#|Kys)H?_?PANUZt#(b zV#}7}+)5C}TcryVIfnp|ZI}g`G3SpFfN_t7-W+uVIV+&_>ZapQyQrZP(n%?}$FdX= z(wuc;Rt9biovD2!h=X;Or-k8Fk%l*6CbWl_q}K`sPjI0V>Bdq%Mo&eu6NWgelhtbo z=v`aC&{#ubaf1(0Cdr3V7+qiP2dY(lq6GU^<@?v!C^++rbJYuf_xcxOIH$}o1n5O7KKGi$jON(30;=(aW_!1TR02q)Rm;C zb?E6|mA}?tM{5dCe<~gvNPNp7cy+;YAxjVte4p!_$!0a{9cz~V;Ofxi#HjXas5I%k zyDpg{qL?aXiKMUVl~Tjg(uI>>{1XjjbHW`36Q+&NXKsw5u0u(LD*|7EMsbj#&5?3R zVz|(}BjM^r|r27kK~XP52nPcQ3qdQIBK$F(VzvUYd4Tc9U*;s zYEs&lpey<2>lhwr`%pQDKPWi{C$JrqQbY;>plG1DPAtg zm`O^|iGwmxRX^gQZ!9%GM=1v{9C~u)-AUB8O0$YBA#$q&x>b7ENDT~%v)M9Axy)AH z`1!>us|W3MQCdwdME-oEsd0Wh-@7$O-j}$fkULlX3G^tap3kRU1{%!|V?`Qd$9G1o z2_0dk|II$G`lt-Gg3Qo%a;qs!AE{`IG35!CxV#$>pa#5;`cGN=6-^MBpYFOx@}Wm4 zWcK#)TbZ2O-q*oQ>8idU3vtDiL^_l(XU-J6HycSf<(Fi(t=vBq#GI-8LgzRR?H{FA zxmshI<{e0R*KZk#tzUNrA+Hv|9T;ufsr(GrVNB_N&(iK0IggDh-JWpe8-KirXkP;& z=&`@nDVrt*k;_$*2qkq?ZWX8s1Nn;B&!PRH$Il%ClD}tazObTo+S<>Xci2YD$0}(> z7{LDOpxt=5xWEDGe7YQzIH5K*-?Ta0-TiAt8J=NhPZ9^F*}l#g0fQ4IU^^1_P6h57 zBAOOxysfgaRT^E}y5ta#Ur$9J>E>1wN75UVRWV;j;|CaIPI87zo(_o48HYEapMRYu z7*xto$=?(CTUkUKaJfAFL?-9HIWuXwo)~7KQmUb^Q5>ss?03ENkKHaM{H&d6*|;k5 zp&Do76LT8GW@qN~^jUwB1>yk5;h9BQJJ3T9z2tkw ze#e>;xBBKsbSgHz$q@Vz_!H6pe42AFB|KaY8W!V7e^D47mHkeK8`Zb|P3wdoZS`RS zW3TP|BPwga7o%_j>kx1A$6X%a)t6GJGgqt2Yd^*y*cv9V$}6^lpj}_k!x%V2FyT4D zmzX%*1)_19r`aarVT{w9JSEe}Q<<1zQ1e4bnX6+19m*w&z2|x1E!E@OAqG1SgniAQ zdT72pr^i+&xI=?+-pMobM=34$Evvl z_IhyIxQ15Ll0pT%nd|!0Srjh~1=rZvhLn~!*0m~B2#xDS6sBf(mYryTJ}Vo2YSG5H zBFBu}_<9RJAO?jpL7x4ra->crJ$^zGbCZo2-!=Ddh$alK1GGl_tkYQa@zJSxe7_F+ zkpPeGYWx<@?!J5LqNmaIf8)SSf=${#fGDI{LUTT?8#|~96GYt_8&-&RkJchf^&~%M zJ8sz82XIcf{wucKLLX+=#1IEwt*N@YdZZ!K{E-`qY0Q(H6v-!TYO_JMO`27+noZmc zK3k#B;><-O;Q9ql%UcP|mLlvEp-S`$)qPb3>tQ`DEfV^`8PE$B@|NWpo7H<$vUiPf z@+s6VjkCRWMWK2oXD)U?^r$`>`4>0;w@jG`i~Ne~Jyz5N9uV>FOOaw%&7*myD#BH> zuZ6}N9lc{$jOa;(&MP3?PgeGXb_+9%_nlEEmmI86rF!|3RoZ8E=G$9`u3 zZ9UMlY(N&)8!uz3N;LlIUq&$`P7M&lqQ@_KekySkxbd@qNFfWx$c{Ftio5kQAk)lS zk1dOLCxNv_(4`X(uxQR$t9Stl`@U!E=?Z{zmEO=^O zO#G!E5kc_5KOM{g+;?f!ed^@I0~@>NbgtsL9c?c|>hvko5p5?_I`_RpwQYqaNdu2c zg1h56XJN3TPrax6XP)lh-*W;Z!WU%EOxiOMvLA4%ILUQ-TyDc)%6vTuxOq!ytVZ5t| z*SIUmO2nk?&J$y6zRL~d0rz!mUaM*eg8mqPl{EFN>D1pJ)GrY+1Wq1g+`wcpB8)ok z)_w3<-Zdb5M4L^_%8R#A`@_MWIW1JkXA^v8&|WTk-aGS3V(X-%&+!5{7@4@W#xW{J z@3SqBk%_IQ2<<)n2}Z04Mf99z}YD>Lh7*5#9}<)twY8f>J~tPU4@^ zt=2mJsv5tmpbiL=?OvZ5ecwZ{Z|ceMg-f6&(Ij6XZP6Y0sf-)BKuiz69mXTy0$Y0W%lS3~7$I7;;>F3 z#kmuvj9Uh&Amwkf9DA1Q;!U@0%L6#=Oq$C?Cv6s0MTb|gPXzQioe#4$WxBn-!#!7~ zWEoZ0^h@RjXl7+1;occ2=%9ihgWho}*CfX5vD7<6-)s-lyVr8Y=Iv6>AMWa|LYsdY z_Wh?oVnsh)_E4F;$Ko&U{x}JnsdMS8h;9fRoY?;TM@%HB804I}t#ayJAdxtI8K82F zW?RWwb+-M#60SR*>i_>7S$a3xGxq)}w$}7-}G>Y&qo%l4rlM%RFwmiZ?G0Ce)YTIlqR@gx2=OOC*r%&Eu z<Se5kq5wT^IEwiwT_b*AN1y$kMj!1A~tQS_4vt8fi)`h`k3 zRu_!pLm*;C_xE1kMs$qs8C=Ua9R~}9{BS+{+ki)+r)U2lJkXWC%l~|Ht~GFLQ7A*} z;q1+T7cY96f?&<_h0BU<~G3-e??l9JCA z3ik5;DUK8^C1ge&Dk*;Dig9spH}~jb8(Keg2RDczxKH(rmb8-Z1CXZjO7?dtyPYwO zy$%6D^l;QZEJFO!KYkG`EUYUs*LsIExv`uY6IVl4${zax1pP>dZ#6k|N2?hKjNdE= zH45c>dyhT}tAHA5+MvFbpJ|gI$^`zJFY*^Ne0Mg(3JKw4wYLB9FLg+76^v%xt-B@I z=hIrkD@ExHh*|DvSM~%Nm==c-@J3%o)@d^me4Khx4&2%cUEdw)F=DI%eV=% zn?-oX&^Il27YT7hRdFb<6uVHHEn2s5q|+8SFjuH=SjApty%}Q^ep@tTqfwJUXv)9* z&N=_7+tk}&@3FCv?%yGHZAu7GY+N8iH?yu?EjOzobn*6N_1fMr+87yEHN5PUAf+l| z+DoN;-E3|N^IcE*n@{uu#uQlsqrn~L`Iyrz@IPW%+V)ArI08Zg1hP)o&ccNjPAhpx zMqSAM^=?KyN*D-4;T2{Cs5gTdKuVr_YJ{aIhabbamck(d@j;9#IwEsfFW3zs*UOgP z_m^JhM%xv&^eH!fg)s$^)&a;c#I8R{ScUvMU;!>bbJOoPR@rB7D(qXoYZrKRP)M4h zj%)M06fL?Wued+@^-ZHF zZ4vF2xNx9Pu)XpXKxK{fh%h5LhZ>Y^pr$&-;>}J9?2o2C4e!i={aUmJ7l`wN+2nVcr%!$f=6d#oL$qg7l{mlz_ZVqspL{V55DNle8EqFI`V zQ_Am-R5#$!;dewAj=$?XMjDnAlv0_P?nXw1{mX-?cMfJ_bQ=zMC?UTL!uHS$A6VDb zx7Go;j@u5ud&1~O^aTxg!Wa0M}0_>R|rOJJjfijvsuJ5%!Uncz^VhFM6>w6j@u>?NA zJ2s?;=h;x~Lt`bf6-z3!0wZ^L%jqKl!Xfugj@`2qgURZ;?W{n!srbwGHEHHrl<@{8 zk;n=B^i|-Jvh#f!{3{E(c=*R@C-s&#)sLSJNd#tCv4dFrq5ZIEZ%P#@Go>d!XsL&%?(3{j0i_+2T2|)#(|^{n zn)0u^Bw7zv>`STgJuqL=y}u*e{Cv`6poNm>Nh}kq$Y^x~_nS#3+k|n(3RuE7PXI38 zvRTxs9SW$N9#Vtf+r!`8Gj2*<3f7{0jug$bIVFKUY-D8VzMnUt&n?2!F&6+VE1U0< zT*G~qf%buh=Qj;8$;$B;?3%s5McbMu63r2Bdlv%qhOzJX1Xb+@cgB^sR?A;~^I<9B z4Kum)`u%KL*uYLowFY3Vc6VOi+ZuHHouCABos0aD=j>v@0CIkOq-O4oi*jpw(8dFa zx@s4VZ`}$gKy(%4RSgMFDnfzp(98i+e{llu0lvGXC+!2Qb2{Br39ySKd=ybA5{or% zf0Yg}ls_>>D2WAunG~6^5*v?Uf5I0TXr<<4p2SigtJ*&DHlXNLsNa`5z0iInq}34G zIVn72y#BXCYc0;Gqjz>d_CVh=GJvo9cy^35W&KS=n7w=cOD38VI}djX$+fuSQ005Z zVF$Qr`<@H+7Qdn1=|!%iPqrU$&~&FE0E@bKgmvOHGTyv5GjAq5`+@aEM4ZC-C_7*I9}q zZ}SZF(0CH#@Q&>1xH{gw^GTloHrD)fvJcpN`&m9P-E}=S#od+H`lH#DXiD87kH>CsFT#8iL)TMJA~}k`IH976#LwERfDL!Kp^BXZ>?0LDmjHRU-4>01! zrVNQ*9{AHKZ3ov_FN7{kde`x8k6t$Jn{RIc+X(JqpHfS%l>j7V;C?sHy^RZ2ik02HE&<; z>y8)=w{Q~a^Z-8$&-D*_9h$Al2UKP}3l7v(p&TubK>>xJ63pEmve3cy@!9<63tIn?FJI}vv8zyKv$K7P{UvgwfW zq*P~wmpvY2Z(n>aV{BU@)9D*k)mk;-R%0Qcy;rk4Y;Ieh<)eo?z-FiFadg_c@P({w zg$Qe*HizzhNvX66Aa0;4U8@|hY2Y@SToIB`FPZmF$3lg_jwg*J7yvt1&vt1ha^DzcVs=T za&Bxb>ZT7V2pPj+CZ?2J{j$Sj_$Qm>7`Gc;OI;({e6>$l(2e)u_)s~hlCDlu3??}S zK_WN5B_ho=5qnQtPKyZ_*GgtZ4gRX35f@T?vhz&;OTQ?H~$35mbPM>Y#XPm zX|5K-vk)V(b7op+fw$l{+~>B8&@K-c#CU;#n>_Ba8*xQY7hTG)FdAkK=BJ0nE?d5e-lFU z?|rs;3K!@$8G1cu{>uXgA(+eZfsyD>E;yJ-WgyU=%4}F4{IEHNGrnJ**tpg|Q8tj) zNUc}HbUoaWvJsF#Gg+Mi{4QYV+ zDGoQD{XS9gBf-OSavNM{00p_dMDIeEJn?_y?eVh3Wxv?%{la~twZ3~&d#4UqKwXDb z-oZ|dRR8~=V&x`Z&oo&H-8knS6`;ygJRz0D%I z5>gIT{FLg?Kda~(+>>-!0CP1}K0)mKfh+w!ou3>+DQ%_ftew{=jE(blk%zD@pqT?C zsbqJ(ElCNbaD-Mtgw)>Qdmg=*{i{?!PpYCTyv*{-FJKbX9lg<4SyvPj+>C8PJu7#s z7AlC)Rs_@gA2!2QRLqtam~&hJRR+PWv^jW|Ena_tZ8tb+>dD6oUXDNcBq6Lbo1Z^c z4yv*(Ua3jLN+L}?LlDNj&NJ!b{V_)3LoB_L$c|F)zRH#zgYWO)Gx#o4nS|ApwAxmv z9Mprt0j37~1_u*II?Sc=im!ieA}7>{CB-nm1*oAPL^;W85_`p2)@}xzHUc9}Cv;&N z?kgA|yBWmT;|<@eLO0XRKl{2Mu1&$MU@_`PEbt7Wi{hcNlWGphk7|c4V%YT4xB*I( zPc~vJu$W3@i!ey`VraQ7yEd3-B@XE8zJ*Z*UQ$M~`1y<{u@rml5&uwkIg&N6ot6a` z*zTaj#=o{{*Adp3AWBE6!`(jFB$fJs@#h0CtZ_-&MGH(qpTuTJJlHEOS0Qv{{5eyp zfatD(A9S?zjrzmLIS$b`DVhRqTtoKSYBe$L)eU;&vTjCLOlkOxdNR(m(sO(-Sle6H z93l3bofm7F4|fKi*fl@d>)0K1G^^ZIi>ud=5*ZS@hH3`EY!|(M-zd6GFUkXcc4meD!)o(ua)$sVR?qN3-9Yykr14 zRSQH)Jt>&Un0i{p(*3l4PVEvBadz$zryY-0~^gJIqO*iokf~&groG_Ok zMf%r9i@FZqm8^Fkc&|Jg>Nk0213U(-Q$2HFp7i>p`E;=m9oGZMLq%(9QgCVCF zcw!B%LCZM{e2CBRpb%4Km=6(zmBad9Ii zHd>F2n5s3&fcNICh6QQN=YPtgpO`1q%HrA5>D=Rdj`-T^ZMk*_9SZJnDj$^fsM$7G zg20~(IYUy24E2PGEu!#J!W#1tP)iB2wzvvw IF!N6OKbF!KjQ{`u diff --git a/robot/ros_ws/src/global/planners/exploration/exploration_vis_vdb.png b/robot/ros_ws/src/global/planners/exploration/exploration_vis_vdb.png deleted file mode 100644 index 023090bb8971e6dd20963f2635105adfb31b70ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 243684 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

|DIIfdt!9db8@2OEeFwHfpz8_bDz2raxNI>!c< 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%0DXVbr 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 z z^V3NPo$>q$-lyqG8xSDeOUx|l?Bq0*Rsyrk=SfSqcd7ie;$^6Kxo7&uncf;~t7Grf z@i-qI4?j}*|L46!PvywZi{OBqyP_m19y|p%j5pq%ByY#+^<7dUBK}tUyyGz3s1u}7 z)}JuQb6;w+ehq7X-7{Ze(J$cXpJR!itdKwGyy{my2i?JN@Jm>HGc3u5y}M!IV&UIB zRa5Cpa>1zsaN;!SYp6n*wE8wL{qHAO7#JbO; zekSP6`-ffbNV1w_Yca#1fJdZ3C%j)Md`s3;707vI1-qbMfG5HEveI4gQNiPrJv$Ft z?po8V{~}jU%QUE94siW2xY!1PPAI5_#D1`HHw+Ag`$j>qszY$%qFbZw5zUrYo;7hI zMFp?Be_Xpv&j`^v(Q#jp?t{^~*@mvP8fzaUpw`$ zjBHM#yAvjrK-nn$l7$xoY-+GXqN$PlpdjNT#=)8+-Yy{utZsws zdG_|<&`EuwZTD?goAn&5eU7y|VWWIGkGLK}BH_1xTLu5M(s=iyI(fkQ`O%4}Go z@>jiZ-ne*+!wHE=qRrQMZFjUo(N^IDT~(!W-%#QAFGBoa;fj(v3i|ib(>`_ zcKWxRd()-9%jDF!coE|InC$R>2Kgy-Eurd8=n zN1P7fzE|1;Wmimyc0ot)DqC4yvtH@klGrI#Q(NhkzUH zrri18_Z|dkdMqbChotMRR$y;CAT-fga>10N1+W$9YR~7yL3n%;oWBGERpXY5K6~Um zd)q153`sUe!y8IGiFR^>yC_6Q1Z3)T)|XlgU3VB%>GRmfbNaeyyZBDF{hxfoNFm{< zI13J$FK9_NK#)djZ;%&lIPJ3vxGC9uUjpTAWGBPw8K`!3=k(-tG^fxAT_`4E^=8(abQ(PA}BP3XdN5o?U0i~W#x6Oq^Ww2?#Y9Heu=Xy8CDA*=f%mi+n0Tbj7ty&)PUB1h` zGtEZ?T3U@=U)AWdDoMtKR9H5~dwr+tz!@fwbXhpAe7py;XZfbOvJ34xPgMgl(GHPE z%zpaKr=dO~kB0%Ml~wQ!&a7ESk_|HxWsJmtAe=0b?|C77te zSj}&t6m{Epeihtsp$ zRxXT^K7m%_^jTPvXWWA>e6EO9Z^QH{FeFxxQZ2eA_-F^DCqPsLJSm&jOk6Jrc0d3W z-jc-17odAY4i_&&v`NzOh8=LXfq904u{yr;S`6?Zc&c|Ut&*>YM0rU2x65yW#CZ1u_^W_o zyA(4}w8>Mwzdu_KCyL?JNq5sCA_7K^f{aY}`uvGOek?EF)$J~u?hiCKLxjom*42$= zG_s$S{ogf$GOA%GyRSF6CPdq=pxppkBCSQR4==qDVWI)#Rl)KYsB467$%JTYiK+^L z0}LJ^lYC^%R}jT-tf1ap2Qbpd2zWQ{BusTu;#CM}aEYQc08R*)0uEJlGtA(c&(ui- z)kE2K`?8Hw+r2FSL7!yDEGS}}L9wyMlW^h_PmlN`Ys5YWnsipL8c0fq{#rwAnZVzom z#&W4e>`Q#4aGC%y5?j**S2_TTM9AnqK95dl3YwbSMB5Nz7%$MGV(HBI zB*K6L4T41em>QS_-Gq`GI*<|9i@~)e3yWB@7b@!Ag-whi+Ij^1j57sP37OjK{Y(QS zYYs`PaW1WakrquQ>WF$5_e|;1HhHVRE2!{91Pe`>muT{<9TU7QqX@fP*zK zTGK&T^=D{qh0&?7cs5koaDrPf+4_E6Y=d7;wb3CSq%(p!f0AelUaS)PbEOU5-wNlI z;#UsHo(-ccDtNZZ9pm(&p4EJfj`3ep;NfcVIdPJkTmw9reG{aoLbi?Gh`-Tl;VJEK zz~BVY=4rxAogHxIjF6&Ko`J%Hq5=GMwR^s}I6=TirP=V4Hh@1h-{5vM@&{U5y*hRt zxNsi&Sv=2T{3x~Y1zqxmd@Up%aBT>@p5)0Fgjhr~g8eYM5a2K%+U4C~a-Rnn5l3I` z_Fi|v4{RKWdbO{+;87nB{%?|ZDk?yjw|};)3cAyPY^2wp5|)AMPQcRP-Xz3=v}nC5N)a9{fs?02(_%n`5dDts z!;na?(91#kv+f@w1Q$=4NZp!b{`hP-knH|_7FUQb7M-pE2W(7E!s&z@nLn>kk<`)} zc;X$;>r;<_|8lli#;Zc0jDp?y{77~oF*=Q`}m zOg@AS8zFKi)XF9=dym0xdGOHa@35QYsCQk3Dz=F!h6oBB_?@Mpxm(;~uJUkj-g zL_6BgQX9Kymv8QXYmmqe*G7`k-AAGwYDKjDlW-cSo0)d39a0Q7P$GdrLYmL$P1L_P zMI4$Wh}StD7oho_okPJPkNoWV2ZU?x>0_HANrzy1p%-D>F`5VXvGowpoljC~4o-)N zti)#yCLS1aXOS(NfG_?o$a_GrEJXqCkRR}OC`q7K;5)0~_7_B9o|-}!eKS0u>e#Nf zu4OA~I!!`pa&j zZMF;3hCszdk+`_G5dx0D&t}5PErKwQPW0Trbou#hbebaNcgfP>-rre~?4C%rf{7MA zzwj`~8Y7w-5^%N=j+H>E?4i4Lw`dBS4j7s!swkyc=rKVgO-YguS@AY*mh7jzr)}!= zbU88^-Z=6RL=Ex&>XYOo%A_22ZOP~D)S4dRt$e4(x_#EWxgevcC!&=~K8;NxN_w@; zEk78PCM*c`km|!IjhP#Xg6lPx$=UzPWMH6p?4R%I;=!+P;!1yE}CjgVl2L(Q}i zG+8_YhUxGh2_RJ5I;R?-v>sAa9q@`8Si4o&7UnJ!DlWfzMfg+CTi=JmGe%^2VRp`SN^y>$yBi&LIf4YtU#F%Jw{f%=CLceQ<{{KhPk#?C}`O`j8jcZZb zy{|+&{40oP1A%`3{}rmx_s?FhuQh*0o_4vxn`kdwU_+k` zJx|kQn-aT*6W-Gvn+OG);5zwb($xri_llS6oi=}NZF1w$0^mE|^Zhjo^6rJ(hat{y zx$!!uCX2sDyKq(0fm)g=JhGim_@pU|Ph_2Z;XNYS77+r=Wg?#~M-U}M(TewXLVJfG z6@U3ylsS&p=r`wDM3&Vk*ag{>JojHO5z+`@GRLeA$>QB6cvr4;xw#%@R1W49)%CJ+ zi`d4pbV$~b0S?y5!Fl)#)xD@BAes?uz=u}V%4--S(W6~jRkXTi4QX}DuL783=aU<6sGia^-;(~%07#PU(n>Hd+pE_AqmSj01q^+8&hqD)8z#!w(FlMA^ zd2iuSA=o9s`p-o)Ja-vor z7f2i-KF1~XUGmYU*gsw)E~o2**GDJ`oV`Vu#qgSyw8SO?S*R18O6YxA?eBy#%v;T&4wBpubGe1pT*1{}?FW2lb^;Um~Ur83%)s#b>*= zdKG&h#bzSp(e?WJY`sXVFaFuu!SPZkJ_UtGMN{zTV}eY#T!E9P;CM0A)Iv=Se5zSbSePK+qca5= zS4Zd$Ys!iC0m{8)m6)#JljNkHYkZMRy4%OXm9!vYKUv6sy1EE+I=Sj{ai_{NTrc5J zfpUg(t{0!DjTTM4-Bj{v?}WnE93Cur*`s1%H?MiD?`pQVcse>fi#Dk7VPGeps(wbt z%|STj}{N_3F zqdoaFJo1?Fwj>FD_G9<|0Rb>%sIVp+kzyWA^{4BIzaY7w?hI6yxg*V)Q{cg$z!1$t zrlb(wec5x`fPpY{q-fD`_C`M1EHwIk#kGiNOD=mgBaC1b(GKyKbVQ4F9q_6Hrs!DhdX1;y z=uxCbQ^hAG6?rZYh)iM;rY#0iXcd=K7rMLY2aM23`qf@hR=(dLJgF0G#UNM4az&EnICEI?{{5IXHl{p|DJj9yud28A)-^=tg6+=lb+6^QAsWIRGBF;n#k* zMNuII+0#YR<>Xjskw@wJg}|!oAafd=tc72{3$Mz@_s4B8LM8_{nVcVh1J!-=`UaSI z+|3NEbl6poIcV*KfAxo!4%pmbz$!9q^k(#+J$Vi1VX}`W&-6KG@TCJth!^d~d7j_3 zbwGK!AmNo&Q1CHio7Cd&RnmqVN+G7$8*I#c6=HWRBoGL2IGB3VM$1LCy661ukfTYC z)+X~EXv|@9thr3YB?-q6W`ZP-27S5@j+Vpb6YlF|L>O#44q>r5%*+6ugr-%+LNWB4 zVkBUxMb$`v6b+y2%j@k18*(};?&APPB|zT0?wyH^hP!U?EYM@LpUUml+NLijQZH9{ z5^k^{+S?97STw|sFdi}V8C{z9u%!9FsuHGJ7_$2UN%C(onDaKvn=3gWLla>UuyGU2 znjspXTP5?C&)b)x+@2$v7QgG~6;N@?Z4EYd z3Ow}(2-Wni%S+&ccVM~AW7Xn8osj|st5-p*>V>Y9z&=w7gNOMq0-vvvrgBY@YiL z&GYHZK6H;cECiZbAT`mg1y2ftt%o5#BZox;@+s>t9#n~0P^OBgr?&L{rD7G3-CP6< zEb_kowh0ExojE0ObNfkXk`t+a@*Wg~!KeZ7<{hwVJv6sL$yqni26Frghz;G{Z2e>*tu7b0IGrf{uMN+bkItN~HQx?ENu}s0nqk8pPtRt6h0{b+oKk8U zAvzH`GOoH$|0Mntoq)|>2y!@66(~*7Nid9yvabFL)=nudNvIR75k zllvcl5hL{HQRAnoYMoA}7(K`s4Jj$UC1f@?!O0UM5fVhydfGf#`9IdnrliA<4M_HD z{|lF{_MU_>&%~?iXPizx2}gko6cud2RunOK>TRYwAhjdgHqv~oW0+6 z8A$BAK08(uq5VKbvbDd;(bJwo$P-73RxW|$fE4*fotP;S6dNx=?sR$xxPsz(2$VZq zU1jbUwwZ086^hEeiMICaoxkiR+6wlcaZ6aT<%G=AGI-%;IB){0`)5)3b@6+y+6j3( zV8KY3VDYZ04(Z*ocoA$JZCUN(;>`T15Wa5_MVm4hrclJgF;V_2wHN7QnPn|w8fIZS z>BL_2sqzsGS*LO5is*#d1Lf`GSm!c|8mmj}QH8{lFS)03^GBYNx8{k^W&B={tc@ce&Ny0rt4ukfel zyW7ft(x6My)C4qn&blfOp8jv^Z7#Y=G+N>LmxQ?g8}~xW2ynH+1K)+=7Fo0kjHD<# zw!^xQ-2aD$3LSW+?22-x49ZUlZ*hREsqnkkjkojsDwsGQrrI2~Rq)-P7=QQjDi}Q% zrl?5B<%{r+oTNT+4oqDjBsbrF(sS9!82FLFv2XY%eD<#Ue}_Z(eh(c5LsP}FTDK|1 z86@x;`E&OcU6_?@6zHvVL=!#q+|1@q(9y$rK>LI^E5QJRM~guPNkB+RvK*pmabAK? zB{;2l^h7yeu4L^4r{Hblc^ziOpd{mKBF?6@;mOZo=oEYN0*u!<6DX>xv&C{0TqS`y zS^yafMI}fxSXJg#|GH{YU$6_7j`Nxw5CO+;%!h|xhJqS^eAaydX^}821}ZPO$!&Rf zPET^>j5zlLTcPWB29A8vIPJp^vaCX5X1B%H03sgtBiSy<{Sa7U} z7gYU+mFq>-$T$NIsw6_1!HQ%n1Y1FyP1mA_$9&H}BSPmbsudYQyX{H6?@)Q5;hZ4Z z0Jq!-<)a|?ZTI(iZE#Bv9C6aVr;8`^#1HvwZkyMtIrmq9x`nC9nJ@h-o1){S3y1+PMElKd~ zf5Q)Y5wX7UHrTWlj_rVZABNb$Fd!Npc>-Q}$&-Nlf+hEhrU$JQB`_G`hX@%8K%k#H zg$iCh2QMVSO4%jK>45Q}Fr%M&vSX)>r);pp_aK%F&v!gpqMhDl&d&6@-DT)o=K(?K z`g_=#=YSO^J37z-%fRCXe9;BpazLR3e{_NRUUUFs9m0{sP6BZqB5_j#*Kr^lw!8|_ z8cpJ%4KTwXiqltwnYatZXNm^E6Ga?dFFtZ z7kh0;5PZUcL*j2=BjY2I{jd_?&=Jr7Q-;IKcf;fFxu-dS2?3(XcoI46d|85>T~I7e z*oCJwNo2sE4`gvLa4^4I?DdrwW{(pMJS995u=z_kbW-Sj^WK4Rslx6}@62AOy4;M; zd{B^YeJ7)N(`*CIMFoQOFI7ml$1be3B+9zJn(Zk zIuBxWa*~uiyl%3=-dcI#UsK3?6#7*U?;BiUbu*zL1eR|zUi6i>AYiR{m#^L;oYAT; zLwKop#{*panUJAb!AOd-`wQ5(-J67O+yDpmi;l_n9~aZCi2AQq`9_)@4tUC7{!iDz zzAxeNr{UxG;nZ<4|6@-94#>D=^x8PJo-=rv7TsjBu{RzS&;JrV(r-${2 z#AFyQTNCyr+Wm}7=3X#J46g2k-^sF_eomO)55|Wt^-P1p;>;xPliuU>?i580b{*K069 z-hrW$MSFds=grix!TtH$g+;uYEviJundHl@*Hr2Lp+RYPBdAaDjUz>?tAbnP=i8Un zg1{=teD_Yu_p1DTqEhH8<5F{&KGN+*BuU_i2eSmDW|j0PTjyR&QAMJyF1tyC^)K0h zVetLQ@Vou6VUA}MeoMa;`l*f&?CG#;9(NehgXLM~I@Vp)qDT%y?Ou@QO9_`!7e!xz@eM>=3hkZ3?rZUyAEk+s>zOIL%#%n7UEr;f_>k26-PlMS8QPv?r-X#1l=b*k*JhUH6g=mwgsDOzD{6FKbRgV zqTrgBfqU)SI^9W)cJY=3qYRe03IR*?zv4b0xAUn4-?Gp_IDjz$qV+(IZe5b2TO~?3 zvW@OSaE=9s&S*xw&OMni3xYGfr|Y5ikZ>8XgFSD+{9JqUw%fIZlJWY)EEZ)Hk$b)V z_sS#i#2HWjY^T%QnsQIE=l^t=Z>Al|cVWKI7dfjeOsOHoCkRB(J zGE);EXh0@qcHHwW2f+Mk?&C|; zv+Pq%%AM{Y*TO6y!lHwpri}1Tw-9pMMYjv4`a|~Nrx&%6H?%^sd%2mSp@+z-nqWO6 z?o;;JJ{V_0v_WXQwVusU{b9MPLtl3u1`ibLJyr7+{r0ry$Tu zWNEcG1~_#R=H1YXbtCpEIYl%Dzsuw1IYQ3!SrKI42-$Wj41ZOy>k#5I^nPo-p>Z4M zWT-{7y;APc9=ZU3Du;+5m=+IX4Q}4-+d(OLFG(Vrd$JrR%9_uK6X4ccL=XAtUwALy zGfgKFe*G=**;cC~63844&uYkMcQ^dzS@F4lEPU%x@%hn}Si+33Kfekd`nm5ij>qM7 zUTJ|hWXV)gGAw%lUjA>m<43)VgpZ#knjYSGPUy-N1O&jKL>M+owkC9UinCMPqZqH& zK!Hlh`S5~F7^87e`JOr(-oPydZNK3J4APEkU9I-7g{%sk(t<-Eg{Fc2DU?)2Z!6SjQo`hw! zOm5dzK$?ME$4oa1?#hsQ%jtSzt0vV?n1JiL!>o;l6OT18dWbfKconuz{q8$5o8_&hmHG%c!XAXRhj8!x~Ip9yJ7?lS1?h7(s|>mKOr zG)_#6Hp@ft0cBP8f~}oh_FPFKEaZI>!U1jFb~t%PvCI=5-N{Lis_uxy`O%KfV<#k0 z`rFU=LRLhbJne^-X@%^4!C60-Uzc3)lSd7{>}4-`h=yn{uk`#sDI6vyL8S|_lT}|B z5OfDn@R|5LWjs8u=R%_DiT>&(xaD?xc_^Q+N0)#fJr3`!_mr8`)xzs?F{&S}5SD`h z0oGEKXIH_aPx|HnVno6Ze+g^Xxuq>_SB0yI1Po7w+hkpKzy5C0t-weQvv;dV9POL0?zZ)3A)tIu{B!F7d&K?)iHN_)ghHxj=n_&1gU`8`7aeL@3W2RqH9 zalKV^zK?K9Xp~{rTt6u#G5!0kK0jRmPZI1Zf`ez^=`*mcDvu-26|&+vVL|o4JQ$#O zk96yuJ3D=Lrj%rupQK#G&u{uO6Ma9?+Dpzqf4daSjE(UTm~w3iS)TPU46=0#MGsJL zU_D(`q6p0s*TIzGaOxsNjnO0Dx}9Q~ey+#%2aee%ELq`klapRq#uDa!|8MGzNVh=6 zlqpnocax6D)yN+U)3!=UnB_n@Y`VLUhi}l8P<60XuWGqm(Qyk~B4k&$|8a+k04=0b zx5WY)!f_;f931};Cg=J}2YL`m+n+PtI6)kHU&I(tQ0hO$IRgAD!Klp3= z6+uLt2c*Nf3OK2=^=UKVSH6Tyh71w@&*QR#FDqUf=JM;Ut-i9i;eZCD=uwWP<>IG_ ziGe%&Ae~z*y+GZ3{T~_0m5N~?x2`A+4G`G{zgP+XqH9%04HO#j)lb3kV{pC7(^mm2 zB3@eq&%FTOUgf-v&Q5qnFN!lM$LGj2Al|qAH9vuupM{5?baz|^=FAgKg=bfT7B4DY zy1|b1PYN5oxgSqxu7&jTd zS~C=ur9zVF8qSWD$hqP=*m4cpw?^m1`M)U zXJU{ya~rV8%2K2o@Q4oZ3=inp@k2{Aib|kpt%xe;-YNXDt>9GQ(Li@Q%>AmnWskgO zzfToz9xoaQSwqcHSUwl7w7`r1ElMUA{8U(l6<-jAY8Jbwe;^kRbd#u*E)n|K6Rnps zAV=7rSmaKx>G8m=Q($x~tb11!tNGE4^?dG+kgpGCkI`?;Qxy<3Z3ze4hcZXnMpk4k z81fdyK3cmV!4%Q%CD|4_?A~O@TSU%IgF}54c{3AJMUywXu}DZ@vXi8V#)@u!3>8k5 z__k#gx^;s*r(qnsdtaJYRpoOWnXJ%jHE7PZOlDkGe~HiB?!0PlW{~?*!1Yrv%6Zv0 zcfgZ2&zSi)i1zY-L)~TH^aZma&(s$hKN?~T<2 z3x|#f&E01|4?i_|9IGIEKu1Nv$dN*#Fe+2+XIx(&z6!G-zC&^d)xEr4SpIid+6)P8 z(4xydqdjmf8U{o`z8>kh)`yCY7aCxMG%TCm4eS0L4wnWN?aI3uu5*ZDt6Wjmy&@v! zGp>VGy5q?$b462b!^Rb5^;7WupV=RSfuZT}vsM1ny1U`uel9+HW8j-G(+{9Fz<}`zsTmW z0apF1D26(GINY~L+@1!0n_f%ZGqoHo6e$))j3jvwy!zXz))gi1GP0okuZ1WI^zh*T z4Q6}X7vgwdv$okgq~WoO0M8BLX4ZgLGuq%&a>h9S%Bma(GaS{sMkj3dUbx$ZWsK#PC7izy#CC#2XNQf9LPzpIm~)+I zGQ3{l4c^ow{F)E%5e?v}XRNp9wj6n4B8(j;ysHy}^w76MeI1MmfoQk2!Kp|=WNsNK zMx6m#ypUib*8Tfn7gto}PqYoxTn?O8xJxulQl0wC@IyT+{gbERpzhoh6$zt;3#X*v z>2UWAaPojiI6qwqjgsVH$)`lZr4v^E03`uGcOG7R&6jBD^@@s@08^&IJ$j~dRFpd@ z$``%`NkNqHwj${@Rq*1AfwGdyv+%~B;d`$Ku9Mcm%!#G$_iaLCB4pePbvqz+9^4~I zuiGxb#dn}nmwXrf7*>7XH-GiNiN5UqD4+r&q9HyD$Q1LxDRHmaFN@!KYXRJ>c*b68 zf&|k@e-x7vprRXI@xm*JY{|54NYdF-x<@3`3^P6Mim|r!>#y}WzyOGC)M@rT?O^xQ z66C8*;b%M~3%WZYFCRW!Cmd=3N?w4r7AT(NU;Qz4obG#VlG;d?td58<1@0?804(H* zH@Wos1}L#euuM*ZZ|NHQwXeeFlj8HD2d-m3;o!%Xi~z^CHYlWy_8Nt?BVVb9vjM4asUiZ2VI zU6~jdA-UZmk<7Sacj>y$-r_&$Y0%a#NOE6r^%`97U~U`|g1I@RWzUogA6zpw?zHw9 zcG^{ix3|De?(@ELf3_SlOk8cnbK<>p|IN_9pGazS!X(q-st)G?#VD$IxuJudNqx49 zr0kY9Yp2uP(V}&A!`9Ol3jvcShz9VtH{lcah|k^KP_Pxs&%*LYe9PY+WW5b59615g zMg>j}cygzu5@26s3!AfAaSoy-QF}y?`AT-UbO{ne^rw!9fE0b*C%)o8ukxh0S5BRV zsu~gH0a3llid3n}k-xbC`t=hh*pQ-lJdl{_B8=Q0D;OvskY=2ts6oD8*$GKLRo(cQ=ECm#o!`5N3)dQ>vCy$kYR&f0w4W zmO;Zd=*fZN68N%-2dipGMKL-GMk|J3u6iN1UA&Pv)TPS(uM@7N?iV@4_o`nY& z!Z1^zDD!uA8hqGw0H!O5c6b%jCS0t8;S$jvXd(6t1yz-h*@r?f7wgp1bWcDVL*S#90?iekS~$w^OZ1d3fv$GBD=a^|2rZtpyIrbgoFKvh>U&- zfvzZ;2EX_>*#ACktP)9r>-66|E&|3xgLgo1&+6tNi&UuKRTJ${;4yh&8|XJyGy{4% zEthvfL@YG!6Mf##N&L`SJ z>8Ra2HCuojn*}q1eB)l#;5Lilrp#{TLH&B2`qh|c5m`%71mXbReI9OF4(ST1SA8Rt zZiPm@NJ=b_HXg>`4v{fXx?R0Zw=jQ$4@!k>1Ymkj;QaFTC5(x4E>yEl=adU4!p7rJ z)GXS<`97_8TL-+f2QD?j@?j!64siWsxLy(PsJr4L+B3%aj^9XxG#_i4^gcAtd-i=8 z=bw|njVa=9r;F#dk*uh!>L~4O{}()rNP@Ko;4ah1ubFuFj%w7FJ0E}v*@}_aylFm- zP~nlyfeXkaJasTk%obz({Y<@M*})(bMl;AD5@Do6-QD5|wfB9vQST9+^TO;9w3*qF zknPhnSyl6#_QSv%bknLrWo|)hhbZTL$*_y`2$@N)f+X%eE(+e*^<&D~moPTf+ArR~ zpaIT=z51f#W+<)z8UP3NMOPQS8f`laBrwr&)i0sB1;%E=;subM1y8>O%N|i}LBO$c z_eTM!=COt)OW%6SezXjdlf@7714-@`iWKEn zPYc%!0|N$#WXwhz4^=zV)j_N3;T-O&g3-|310_{(wpK)zXX@;!s6yCYWsmR)cQ0Lq z!B!3x4#yUf1tlVAVq`sQz|$}I6KgZ-_2{|Zp+&>kF`}7q?iiGu5goGHI(nZf@`%U_ z&x_yW$RT|tkNAxb`vcQFAV)b9r0fHL+lPp=H;hUo1SbP;?%R zvh3SV!;$Cwwe~BX17cyHo=>n`F+v7VdI83!I$!hmEp#4uNN^d0h>mgI-@7|up+(ZD zd~W`>Cy&4Ff%|SKWaZ;x+A3qXb^zpm5%Tk4xyiiX>NQyZCS1892>7xG-TKvhgfZ@s zir!MFsI!PjyG2!XX!oQF2I-r7BaAbVFE_-?jQ^)dl6vhg5NBmQ=)joj8$=-y^KTpN z;`=vs`xT}W+iQa%h~%PERZx9dKZGdQauDKTVA3cUnJUh)rver!TtCiK+7RuJ0d9W& z%U6Pv_#`F@t3tC~p{Bu+_u!{%M1pX7I;2}f7QH=?lnBE|!WhNS{K=(|R|-X^g`DWD z24@ij9Q@0sCqyVKb_1?&^`1_MbYmLfVtc9@E0)k8!bV_WO|+-l^ul7)(O-Z_=|=Jl zfk-_H*HPku$0OjR2J5_m9^vCWB2iKvv9n34BUakKTQ+8uL}Vq3wAlaA&4IF!MaFe# z@dIYNkx6OLSOs}f5r>Q!kfEqaa`Gw^Z-R5XAYUp>HgsU`rYvE|56${))jB&r^p}pv zFurOUpsu#k##1L z4I*hU6j)Yul#n-V8q5hyeSZ0J9xrbdpSN9v7xc*D=DQ1t?t#na;Zh3$WOzYRA!GEQ za9N5~LS~2re3rO4H$Ny5K>fbV_-?fz-A1C08H1ByLJo`{BOa($$PK>Yq7oJ7;AZ`T zHiuEy>ecHSHG3x_Gt7Az(yEzrMB8p5hjg<4(-8N^HmN|*JwStH=G$VRl~K5p%H08{PlaBrMuj#9t05;b z&7}sNK=nz}<&RYJ@R$aP2{3)CXy(H^`GL3)O3w(Z1_R&wDLnftXlW3mTH|wIMh@KO zBEe6E#zr{TMeoSDf!QvkJck?LRuj=Iyaers#pmP%xFH7yCBlbQFkPYJ%dkk`ykrxh zxAU=flOY@~baaHr``zR5J?rCLuvW`X6~YrdZ@OqQ&Mgxi(7YljErDy-;3^!t3Nv#Z zwCOhRwnJneQ++TTyT>-qM317gZDHj zGaczV4Q>P`c>FKeFGx3{+Y8yk;){(I`s}wf_(b4jl|CDUKHKuN*&U;FSjS%z5)&uH z4}{iBkdqIOOVdBm-+p&jq{QXHCaYDIv%mm}TNUhueEoiDlsZa(I|A8yl)T^=EOr=* zuDWD@I^tu+2&W-aSv(Bhs)MKBfZ~ggF%Vw(9$dTv&22EuB5G}*@GRt|*)P+(tSX8v zA#a)sQM(1=BHzsgXXX0t4?^Xuc(+X2a!efzuy!&_cWYcGD-Qm+cql za|d~4mw$5M$uiiwm)>hIxj$TMfq$P5D}Dz36GbU=vt$$?-KF6}j>{rkA2(*`eU-e) zqWQt-4~|kr0Kz~uKtOf(CKCBL>0vq0rs{`HDpE z@gvmR$hMencbd~bqM}J=6rY1^n*>vhJ{B>OI;gE}IF&x{MO-M%)&r?&qRo}Asb7Uo zvH`kVAuhSP5Fl@_f!caJ&zXoEa(AM_sS z1r06&(>-uRgWq_eU0Ob`2d<9_OvJQ8ttMW?DohBEK6{{rtZ&O&2l5U;cN}$3=|a&KQ#wl+U?V4KeAA*&(4bTX`$1k9G-lsQz{GnWy`Ldv3>8r zd+@J3S)RCrg{R&0Wi17XP|VVrYL3P2bnyWkAuTU@)!D7N&y z`a-Z5Bo#Wo4nLu&1gRksI{$hME;d1F!+Org5M=q^54VaT{RXI)1zwInTnm5aFkpN5_ zBbto0F9D6PMwef_xEa3mkZ=A9-D+25*%^Q^78bN~2r3+L7H&)8E9V6v$;VkrantoK zcA6z8#8{IdQ3G2U&2vTnNc%(+a+$hL2JxKn$1IAhO0>%WP&n^m_X6Uo0e>!j;Kys$* z%@eJWq5{rUeP~M6mKvWaPWHJm%oGj`^3B-eG67TVO=q8 zg=Auylmt2Bg#1LaU~Dhw;eM-*d;~BhQZzHxwksV+dJzPFF`uR`mx+6Bc9G@aTmrD| zXdcrZ_Mi5je-VF=yj;kdCt=lZe72kCs^HDH1(#fABH!)p@bq(V`+T2`p9(5d2{U$o zpq^g1v=Sow!~Xd&ejE%NM#g`a`pXac{k1m%u6k)*fMaFGx)J)k3uL_cl*kS8dWGu( z&}RaAu?}X0&=U#@eLTjDFzJI^6<*AJ!NodQC+peu-doM*)nq#&0@nPidRwdYLOfsf?M7W5kUm%wQhTB_Q z!axrk2X+JH8YD{HlB3;_6$wewhC4NN@ZREibaa!AKHCBl74j1mI=crhn_!S>Xvh3jnxu1o z_M(sodw@kQi``87Kq7&$@QEA|%`LqGZBhr1xWHl;f2iVz58I_LEryF2E+aNaNDiU`#y9e za9ZKxl?dYX9hi+Ei!<_Ip2={#^KmGzfRm@m`27XRcH~va9th)r{J~@}!sn`RrVQ>i z9$D zFlhq(;+OF7tG?+=fu$`#0yM|K#6hBrdww5=USh&k20qLq9PYAxeQ0fS9)-vm8$!~o z#r4zN?9SqHT#n+n`bPBIYG9&8?(I=sr4*eELvMq4)1Yg2GvOoJ!9=tTqOBV8L`Ad< zT7|8j70iWeBt$Bb_5jHGn1p_<`C z;1)gdwf0r`@fXDBtIfj7>{<&Hm&5e=LM#2-T`*j_ADw>bKfV)K>e;-`@3#7)>))6T zWv2N%iyd}ifpN*O{5r^ZR`DSC2nhB+6SiFvBy7tj-?SrNSr2?QJX)XHdSAhY#00ow z5u`{-09B<>RSU;UL`GkiK3tMCNH1TkW98T3sb9kfhaqvAAlWjm3q5)gwHTfP(7;;^ z-rb;G6Z5_?zOdIh#K3~__LVvTDqR{%$fsEK8ai}g3A09omh5WA;}2RprN}5L#n0OB z#yO&ykFTs2awVsu9xuGL6^iuHz2!@B_ii1Xsh%DaEu!7y$KijbzA2m(ii+WnCBl{j zDPec5Zbz4x2-#!Bh)7y$@MPoMd5D)Dra{Rn9KLu{6QqP8s;a7jp@hSCudeCyq4=Sr z!+PFs)!ub9$M(;r7hfqz@4!L2nX-o{G&I*Em<~Fz3B>#Q{2h!XE z0|%<-Ic}qGFO(Z4l6`nJjFJAIYT+^oJ`&z_4btX|N|Aj*FiDkKSFjrG$Zjl$Ti)t| zJ1zEfS_3PWhx>y(@rN{j&uus8!LoxOlWg#jY=ArR#gI%y6T~J#?}IZR7AT@$g_|I2 zmT(?vZh*#Gs7?@-hg5LXXG_q6=m@qpSQBj-Ka|-2bML??J#p}B3S)~hlYe%)wEcmJ z@MXpMXf1>R*>F&|JY4$*+&Nu1CWVUPDj(4{PRS0*X7F^k`{?6>7#~>*AlRm4M=O~~ zU*Yt#jx{B7nmq^_^>^8xitrs1z_b_g;66ptS1=MU10PCd(+l2#{5k&1MtB5iHWO!n zGsmI$ESxA6WsL!HhKnKOjXPoSt^WHACSW3sxJDR85*~;cFPeep0VqUk0qLx=OJbPIKyor+bNHyOg_J)0u#S!;W;h&oxQRAET7Nj$3hiE?1F~a< zCvr|Kob80sHd53@RytH0Txo}7m&A9ZL#Di2WcA@Ubh^ph;98UDM1EXoX$`_EFpO?E zz>%NBG+o9&LlN0NyjuKi*I?o*-*hVye(Gn?byWmMQ*MAkv#_<7_I5%?HN=g?wSkeh z3!-cy;DJQ@ZkwKSr^4S-N8}kBq8%*W(_nd!$r>+dy_RL6AXarvnTFGAiXcyanaq7e zvA7qCcf;93VD$6dkm2541ktyMHTBktH^JQwf!Jf`eMCDhCgAG<{?i`1-PtF8;cS>V z8g5opaJ+O4Uf&_SxN}m4J4mJr_Yb)!PI=-I#x$>I!}&mcwj129k8t#oZ2iCw=T2aT zb_#&0->c{MZ{EU}?6ToQ#DXN}=;FPm`^LQdiw7ZlFdX-WCU3CCbcG%#lz>D@IgH5^ zpL1M@jb-$UhiMZ;lkv<;kdz=2Asy^9EwR^^Xy9@2-tZ(q>5x0wR?D6?5A9f>t=)N<%{ySG)d-|?3|qJ90bUp9 zee-L+^U&D^nR_LYlQS+%L%Nz^{~#jdqcZ~+X=)bX`qO$vz3Lix*HrZ@E>0vW-OM7Mz(mm>4d?Bpw1Bu7TUaSh>>;hCAs0tPmY#fBM|s6OB!f`Y{si)>cSzyT9-% z7QWO4tb&4_f%SO~e^*Vu{h+~B>5wM;i+Zu95@;%hf#YC=3(0P=9x;zeg_PSMR*D&E zo#3$frb8@phS2gctb z+W-6j#$FGDZinKHP%p_HA|hbaTo^XPw-0YO%>0te;{uXQn%-~eB)i~U$d~-~<;SP& z8e(;(jOl-S4?b~=#X>rIynb+1P=M1*NU*3b=Ct*wikR;CA-Lqi4#VDUf zXe1lprkABNAoV6@AV_w{j2T?(>0$k-(@^V<$Oe-L3BtB-R5IK@Q8=Q=D8B+(4)2WO zdO@_qc}%^SoNK8oQ^M+f++#{K499KX*g$#tWM=Rivak$f}NJRJs_R2nH>!Fw9M2Ww1j>3arpG zw;vh`Gy1_AMfBP!$KgUVj4(y7+Z0*)XBuFvOPPpQQ%5wrd#rOFBSUm)>66`_gvM(w zEE&Uy_V&QyKOZ*~&yb|A!16Dv7hk2z+s?cyiu;ZEGz>Hav1BPqQa0RFV7>bbA3}eL zGv!!B%v*}#qAmnToCt$%fe6#j9>umc7!nETQE;*avO=?J@mT3u=7sQZ4doEZ@#(W= z6!<%#3|2S*l@-7w6ZtMKP)kSLVytdY6%_|Li$yb|{xp=m3*Ay-oPi@?+(L-8N%$?@ zYCTUT*m z>o<93h)IA-HxcQ*QT?Qcy6qD<7ZJgZ;U{?BCCLsJ%S?G+USPvXID6SQ?cRy>7ORnA z{C1%h-f=?Vf;kGCJqNhCos+`sLA-94_#28mTmQ|YOKkk-ED)8Cp(#;f!FA3W?oHd_ zp4)^6_LnRy_YNF|p;=4l=~*98z*B}>hp&5F7MF1|f;Mh4OgBfq6~n1JcEF@BxZ7{= zFn#fs78sKico0sA0ET6VcDtmhTl);$JRRQKD2R7ewa>3mu8KG+)4CM-Pgg^;;T!FN z^b8oE3zsjemv@UQJE9&65!PU+Dmtq=mwoIlFE=#4?NHMW-5L~j!hv>Z(~mvd;o)}l z5{xkS92E?#sLnmnK3nG-ee;`M3*&+`mTzf;1RFAKE{Ps08k-=YFWJ&TUMqIm`+p$p zs@bp7tHeM39xVG3qA21a$lnYzek|tMMZ@>5_D{kbF&(PjfU|#q(T~Bf`^5B;XCUpH zzU2~ftVwv;YUpl)C~k>|D_*GWh8T}1uWp3|4Tb{g9+7Nl+=rVqu(EXvu;p{WMhzYf zviH~mEVR&P=lfq^DT@0#R}r!K+Pc7L={bSZR4CDl+?SMq-QdfRT+smbNLudBW;ppb zxKiO;HZmHr=0VzIxOg(q+OMb_az^_<8W)f9-Cl1?6589s3L?5i@}MoSU(@nmkZ+iM#)gVyDvBr+)J`$a;^WZe$vxhNqxxtQ*1mF)Ta9k z4@h4<|UyE;&HQIWfE0~UUP_k%~O!6g7gQ5d)x)_S}3U&sbddBzpM~XN2 znNrxlzYoU<)Ljx@)8Rz8tO7L+mTy;nsV^{h9H=^fJL;*u0U`Py1wlRe*X@y5T zunSSIz8=s!@C-#*XBqH-ClR_cLg*0}Yp^)T_5SFEQK9#~BVD`$YKkCF5tUYhbyt)l5~?12HE; ze)T@*<8%sKdVmXcKBB$YMiUC+7=sz)-*i(U+s{GaMcCGv$L~8j$c%|0#n`zCRXWKw zW?lUlI_*wZ<*f>-*0fi3Le&I+_$ZynJf7ffrc@{`hY^yNX-W`&kXER?80fSxGX$bN zN_5&i4)%#=XnG)V(1JoPZ&MT8d+!L3LZ-9JXTvC4^ci^9IdJZWk4dcOhZRfx|IgTz z0*Bhcke|8symskeFmiRz?U1U{%lHAB661IA10D>*fJ9)weSn3Rs&3uUNw2X%7O9JeSNrNHPFx8yRw+M{=Kug807*naRH>KqU7ed+VUf)b zGRFghJ+MS^*xk@`NP`2KFU#K`;qPj2gU5XZ$57ysFa@R0HoXs&`fQ7LM#FhQv}Zo% zeqWPqBp5Q%r$J*Sj5mFclUJa03;g*gFbHUZ|B;5OHDLQ;C@h7g{{cB8U_vG=oD+Bv zp1TO6Txg<~x;U2$X4=pH&3mF+E6nuCx&}J=InWXeS zjdUob9-VlvC341OxW#mz zmz09GFnrfyz;{gWz%5ep!B|D_zAqTyvY3VwYHLGZH#%BWHIyrv{^8H?!|zzHBNu9l zj~C}61R&;!r2vW08iJCz3Hcu8N;(z(i38qT35pi3_XU1Iu24VS4oWvvrf^cGzZTk!)%{xis?x{3P%_D_Es-M)4;5y$vHQ=psIC%+*FN-2J^AwfF ztq@Go;q{~J&%vcI7ikL`vuvaY!91C2>%#Z)Hc)l}#*Oly7A)5|C^?P{!5u3$cBSyg zPR)kP+gxt~fQi}8%bgEnzT4k-Qj*~px&}2mO88O_83IF6g!lWIGI;d!LXuQe zEIi{4EPvF0Uv8d#5ytrrW2fvNfc#s<-+iYA$(Av&zj)T9r@|ATx8BCu-}ycd83ar2 zg2XTdd)Li_g9>T!8wD^*Qg+<95~{XA?H-wE=iBHfI@$?3oOD=&Y>zlA1AqY@Ff`63 zF;EBmziyZs$+%S=l&;lpo+#jbV3sFvG)VPqt<(GozJ#1qjn-<2PXkXRBr5LNi3)AG zN}p}625E%~-66_JpDm-st%H*WXWoIKE-NG+`W*}y2??p9L2rRvjCXOq&P6KfVV*8e zENK=b+Y4;@0E*7}wmCfqZk+~4N_<4y>XrA>Rv4rRJQeJReBH-e9m=B4BP547YhM?bj>8enf`|&abpPCFdTiWwWyc8UCm-?H>E8l9IA)53N@idpe@e+`3qqnp3=5eL++n{4QgKV zpSHFRmZw6xuHpXH*Zt>rcEZ1|5zZ#Bz3!VH9Rs6Bi75EU5!hzWn$e=_*}EZS7B^4r zMNrtfig{fGCjrn!`yoZcx& zd32ZyNdeA+BNMtze!5kMV5}tNI8gxFT~HpggrWoMdo|p%^{7?^u;_ZxRQT2Z_^Kr* z_JarJ`IlF}bk#?+3wHYavDYbz<;d96>P$QbJDguThlY<^x?y!F#g_!p{zss2K^Wv6 zDx?ny{#kGLgRI*HM8L0sZ%8lQYnt%Le$6y=8wJdWf>}|Zlf&o|Z_vjSVSaXLFzhf4 z)H^kXj}2Qy=WSZOm z0@19|_CwA%DFPkr-DmpY3=i#~eo&9~ej6^spuXzI`tU?CN52iMy9ZYO(0<*K! z`Tid`5XNW2_#8-4e5XsQh)y4lz0RgAYNr;$ru$GqGg{GEx_B*c8o-*%&;MMo`pkSM}*=m0$Y1*ors^XL5|@Zb4b2*)07_}|k*6qoP-i-A)& zms(5|JfJA5)lvf`g|Oi*L890EhyRG58Ec<~;tKTvRFp$nx=@A1#=?kEFnO9mwBK?l zyZ-qg?-&jH?U~^LCP#{9M!g1`B4A&)An%5r@rnj>be6YK6QSB6z)+9SC`UoI$#b`A zJ#=1%rb>t!DP#+6Ky;5dh6``N&>2tx9M`i{V}SFAVbZM-n*dYpg26VUpchL8(VpeP z0Nub?mv&qL@ouZ40)s~T|KD@f|Npn{@?D_W(h%TH4iR$=Q6W#Ist-**dpz~T#{ zB`3nXDgNsNoNMq+D>w%EQb#@HD6NhpMaNOTBFnA@&JPBg?-oR>wF_Q723J~PTa&tj zV09;aRJMH|E1FwHNY=nii^QN{plOhu=|Am8(@VI&2kwd%&2L?xxCdU=!wx0-Gop{Z ztBU>blP?4O2JJE7iCuSP2|EwMHy;*B#4CR08>UR?2kW=M;}6?zHf$(da5H(^c7=Vr z3oQcCk&&=yv1r@3fmeC~Ubp}hv_e!Dgej2Si;DAN=dR%5NB8!?$S{@Z4kwnr2{msj zW;8*%r~$x$0oGEK)lZ4??;s=vBZga`-Uxm^V(Q~e^1ER(M zRzp_?LXvPJSv)P!fD1q>B9ai-s@i*UfT# zy77V{`1ual+m_dB*Eek#%n$K8kF^Z>lD&Kl-uV->T!79* ze|3u;8dN^c7V92)+2^(Gw9e@bLK5Q!*Cb-$rkKt1Z>X!=$*8P{;U-x`?kLe@7*!+B z*TTtCQJibR;6hqZ^tyE{>gOZ0s#8_l0z)0F2UYnr;l`A7*jw(i>)TcYYl{4r8QxLo z(=>gY=+v`@ICct#>o?4~GKjdj@5grdWMCnkP!tDorj?RBaA&k=K&#h=;&xcnz94EfD6CyFcj$#t1G_YoR1<3?t|{W5f>+kDEs)D(=?$`4ky`7 zPjkOLyTkA5wDy&j;U)A3kkjd_)=79n4>Lay7xAH`SupRnK6}D>x2V^Zqu?HoAl?(k z!`QKcNQL`Y&B)P-c2gHrHbZrbFjFuvGabG>9WoN(k0r4B4vP)xl9AQBV5J`AU3oiT zHH;|{{2l*-C9ytS2Q#E$*C%cl%kDZ3JC4HAo9w<_AY%Hcsn$ZchJy98kBER_!$n(Q z8hU^388JUS9V#o~*iq4Bcz_|PqI&AMZ0K(j;OTa@S2R1ms?I=;O)yJN+73Gfu-Kp= z+6Oxrn_S{Bk~?y^2;%hX2M_5w=w-|OudNfu z;hlG2+q(|eiBoheWf0srM>GkA-}oAwI}ayMi@&;Vvk>^L{T3}TykM}9ju_v6_%P|2 z7lw!5=*+%vLq;&()^>+qe+d*hc>%cC1;av+arEFm=5@xWzs;{HhjDj+qS|jkdKMxC zg*utffNV<`8><%#0`|6u^6KVJ?J^hVbW@ctnPx@`%t?fW4tlza`hFhGMoEZ|UWBRo zi*M!4>SX}o&|tuVfUho~*C_nSyI@ADdV4z$KvOHMEf6|^++i@)h09LAe)Q>n2zJ$R zIl(2*I-#dWxToCzNz)8XR75Q)7E*w^I?;BUI`z@fFme>+=EBI#K3f=S7M~&TRCV`2 zbQn(sSofWUbfmT&+f2xZgZwqG0C!ug6Rd0m;O#EGx|;&~+SRg_E?nhR)NFyQ5H*CF zqVm<%Qo!uBjqkpv9xnb4Qgl+gPuBt+e^r!Sk8>C~3uZK?rOuydn{5nVO!ZxiSESLP zuifvw?KGE?3nz~A&Ou)>n_UOtW|O>ktE5UWNH&P#Gyw0O2;8P&`}a`}4=tT-zx@M> zXy`vH@}?=WL)b&%eG6daa#-;b-{ir`kee-{;7N&rtA*;`V0AL{^?k36?oFJwh5RVk z-3?!h4gO`6AA?{tfd^%5>hN)Qn;JaW#dYYgz@*8dT{o$axz|Hg4Q$vX>X>ZXVL2?gca$Hv~R2&9uxruu>ssccJ{ef9j7LxRH(+=N#EGD=(`=7i%Ifof`#ha|&5yx$0BXceGvsLr+&ovS~1$=Bh^Ip2J_?0QdE5Cw36QKnHW z^RYg?8E&5p31eWkX{6cdb^X~p0_}9n$Wcgj&D)g=amggRJaGwQn%A?TT5bPl2ZMxf z+v7idK};c05%9oxsOV!3k1Ekp#-6jnO{Cy7{}@?F7?8JN8*JF7n6pKXg6nPv#br?6 z08=MGPNu2;t;>go(_%3pyd2s?a6Qlu{b!5SRnfv}rqa7jm)FNly7SMlba>)R;`8z+ zMW-_3zm3jP7+#{9*GG&84um<^i6&uq_Y-iTQbfVeRsfwmt>-WWknl^F;5vt@RrUwu z`)EWzqe7?8KxBwOoO@1itfX`SZG1;J`1Kj3q*7d*u|9m7@(EG~ypT7+J z9BNGV3rSG0QfELN8vNT;@e{2|hNrFoUU3<|rNQ-aFu}O}f z%JJAnr^gI{)w)x{*QY{?L;zPG718aLvw<#HDPPZxzHNXmF|pXsyVY=;9&vwYBFaek zEBoPdx;A{cjsJN^he&4q-wrFyw-q9zMD%)OX5hTDWkg1<<-R)uXMreE^P9hlIDk9| zmA?>bF2aV5{(Wb=H&J^BB)B;A$RHjyQ5IA>DjK3lvUxb?bO$eLx2xRw1)H~u)nVA_ zvn?XP(bLthM6*TT2t_nIS@9r?QxGdNaaG~;7MSHS2{|?iCaOBdny}?N`yMnDLyGQ2 zK6o6Axn12WyLilbfAf|s;lw$JZ_4Fz$`U+XaNxc5@6*o7;P6mgHm64bH@5 zl0%O*SdwiAgMhykqUs|UiAJU#@$aiFp;#p2T&;prTcBNX2YXqPL50)zI^9i?JDWa3#?EJ9 zOqjhy$Bx1FH5C;lw$z2Mt{mXV7|*y;Sh)P;$1|{^J~i4Z5R!t>$$`0M3u;n zl@<^675Z(%hdT%Zwd>n|E0 zN=0`_1luN3-YX%on7?WltkPfNw`Hi8`{gb%`71+U0kGv2s^i`yBjXnq`$SwTBS%=z z|K9>-L7))fAb(34Xl)Rbo(7Mb0+Ghn_8)>hANVJtx3&ojb9p%x%&9_PiX};~lD|hs zLlgz3%wZ2Af|THa*&aP{!{$iLu+y{m_XPg{uu7gh)dLpE*AZ@q*Az&d7C0@-Mfv8& zs=$YonPeZ?3O7heQTs~3F@3@V!)6MiZH}}L(%-M`jc{r!T&#qc1n6vml(F`Gr0FT^ znVte=?MoPwCG5#!`YoZfO&GafZSpOf8N0sM@~=!Xgs{3m|LYz2Y!H@(GV+H(ne$@* z)QAbQ_A8uNFuIWCOGI$+&HYeT4cpF!&>_gs8De}aOv;5xrkhdI;9{L1;2Vz!Lwf^h zF)(QyWRHe^rlH3(ib{QXiW6$(@zfXW?uLWi49tfcq=E+@{66?iFSyBFkM5p(oR`_N z7jCvH3;jy=s{DWSX~>81p3)@fU9f%Z$z}iNwnMB@z~_twpgSCIo(RLX9}YIw61oV zFI@bd?dB9xt@3;mN zZ1mZ|J|AE06og-evJUmWu3{PKmQlSC#yCW8EuoOo)P4v<9SDAk>3&Qe?S7Z$Ws5iZ zhWqY8k*f;rYedxKYeF<4{>wBl)GIBj5;ziDceDRjIFQKem`1_}g zg;#Ws@%UJ{W9)hoO&4QrgY(7z$4xLqA3ZJD3;EOi=gS3W3XTzUk+F{wYj;Cj6nuIr z3=ZRSCRl`&2&rGU5gxct&x@072?5q^g+rxK*{LSrg$r(WzLyQxf}v@mw3dvvHc@b) zpiq>ZHbMlk9fJ4_9_XKytJ;@pntaiP5(K}dL4F{aX}bzF^`aIJ!1J%V>zKIFXD7HY zd^xZm=GqbM^r3F{ro)4!7O=hDw@9Di5y_PXA|qkc2+_=FZibRGaJmR;>!7|KcA2za zF)_Yz-0tr1$VaYR6{XJo&__Rl7O#lfI^knTi0`}%Q`f-J72^NcR2X_Y#12(2-&O^Q zF68v@EQQ!b;H`(A3(#GMNNX@qpkIp~U_ck7N5C&@^>?}cvdD;pjD9dR7HTewX*J!j zryb7gJman*SQ6#Y{jKM%s)bcKV*a*z*wX+@+hOfhsvgwW842j`vGVh<@m-N1x9vb4 zg-7PvpMVwmwHfJw+(9ra6$VKGg9e>w+o9odU^&A7jR-T*HbZ`@HCZ^K+1*OUsJq!# z_3&}1sDwA)fi9!4SrpuJ2aJ+FVQh@)N`3ZLm|(KzD%0;ei`JMq!#wI|Jt}NGjx72TZYenf;^jnOmKgNq3d(+6Sf(9}kl! ziuT(V1#$ZNR|Vm&y(psKrDtHRR48KkpWSc&j2C7r9)%$er$kp*Cz;9n~#BD1I!uP$P}Gm-?B#hRe5U| zBs_U0^t8cHg$*UZ4yp2|ClWJqjL>69l^i3GdP1 zSSRf35FACu&?vYm7Dh??`3g{@2M;FDJMvxqpqiME842I~A}swrZ2ny#9P%a9zr71i z=s~Kr8MB4HdyU07^wUR#B_V)fWZVxuJ?e@qp+M6_RmiM9SgnPMkC(X? zl4emty$w;Vv9KGw`opD3e|TTg+u=?y$mNg2?4NBl`BwFGvp##+ZGp=SQbhJ+74K^| z`s@=0?)BMD1mO5<5N*0AGlQt0ZI+OzvU7j;rn4PiL!&>*1|JE}k?!%v7Pu5CqTp4R zgh#f4=xgh_|7M@ti1K)W6V*PV{l5tY2|uV%K>RacXtF!ig;g{g5X8vRKq*Kz+tLD4 zr_d`zPEQkUfhh{!`enWBaFJeiSbyQl1l%};on7!n#WKia>f2tNE*9;^ z>tT*XF5uDQ?ufRhBgmcG!shoWDGh!dKbTH%Wi|Bc1DS~Mh=j7?8F0sZSo>?J)=QQz z12Xmpt|Jrh=x9;GdSb5l8?AVl_m;R0lkU6h6727Qqn*BO{y++m{p&^XcWs^6O>UeZ z;R9R@*)4ak#h>49mnq5-VE3d{9j>nlC*eh0kX;=fLW2d2cpoLJX-_?)c3EB%g} zMFUvIe!BMUCpromu87L7O)cWRSy&8t({Lu(GJ;Jcv_j8EEGe=>e0Yhroyo%0Yl?{> zqS?l9$OGIuN3<6=Kw7H*{0mj^HC+#WQK}-@+#(V(FV;bFI(%O@?AftH6#*}}2KhF< zP@_zGJTNaxG#MI#An)mhB#D6kvK`VQgtzS|7Z-jz*81#SU0_ic!(%ZTQ@s%)Vj(&S zq6dIB5Mug6WW0}v4w(|T-WUZT4>;6o5O0#vm`QfoVVEREwEgti4$qp#sz6WdtQ(z= z3*h9&z-d-~ld4yZJnf||_GDZGE?yS)fBn&;;KT9DlrL{z!i8>#7%U>-jm^-iiB9>c zgX?LGh7Ec$=dM!7?1Nl1T$6v5Yybct07*naROo9t_ib2!jR* z$B|=4VTzqB1h~O|2q%?^WsJ8J0RE$p90`m1i6%n>N;=`S z7D2`V90Ez;eC1cyLgsMcbh`92zUg^VXZpZV*nI$BkkJa^^0W?jh0b6Qj0Zhndmpv8 z*`EKa*T54dUf<%<*;6I6w{itdOn}cx65cO82D9}?R3Opz*qBgX?13*i3Uy?a+P+ zIvYi!3A)>Kk8B-}{3n#`fJ3kQrWq$KB1$A5_B4WbD)bu&u?Y~NxPMwrA+h)%+?{$% z5v@xl>g80pSnMwwIZ*(fFX>5;l0JKMhI=Wj6+FOyy=_m%$Hc9t<}!FMiN0FAM_@?`JYQl@_z}N4??z+h1g`fA<7U)TB_a9g~Rqd z8e|L=ZT2kKxeKz#!LLTcsgtnIrOiyecme!uo&SqK>w%)@`+mJmyC-lP^A{8{WvVb* zdHXFmCHcmur^CJXiILgs?Fx$d4c+#UAh);mkr*rB+C$s!*j;?DCdtX;3$H`X>w(KG z2mbVcASMY3@GsvL(`5o486~U@CguuPm0l|Whhw;Q)iyWibITi{Mhqr*ToLb%Xgw(O zO1hK`xF!*5dII%Zt7~AXOE1VC%{Hf{{)Ap(*yL{5=`z1bh8Bz!`JQh7?EmAPkRK-b zgc%+nCq^_KT9vb}s}&72e*Mx47-jK%G?tk)RVUhZ$QkFNbLSEtMbR*~u*v|mgw1zu zw;#H@)&v7WB--ZR)$WwZ%c>;CZ7xr@d};!T_F|iFa-ahq3sM47ET?rJu1Kl0aP%j$)pS?E^kE1%%zJIOVS}n`AWLaJ$ z*^>8tHyGOj%qAcPk|7~LGQ@*~gj{C@fnv_UV?b0L&yYl)D0d}f`!H+JDD++1Y*?%kkt zq+%D8s-wPw-mTQ7ZYH*AE7QjXkEd?dX6}@d&%X$KTn||((0gVpzpDQKiMJD!&;+ZRrQ!ik_bB-$USg|T%1uFA+tJW80Oj#px0;HH~J z)1#>oR?ktR;F<85QW&cSLUVULL#XsG3RWydGws)LgDH7L7HQWVx1u;JZ*GSNUkSc$ z`>l|0JT?YixX-vUqD;cm(*;4ARV<3;$V927%`iYc0%_ID#~^oo@cn&lb>Zu`35S&t z68;1Axd5X;T)(W@1{|c(QC`_dMxse z^^Rp7I(>D6FY-y4QBK|%Atl-VZfUSdW6%yO)e#|#l3b~;FT6In5u!!xWw<8V78n|& z`~}e;-nvnkQpxD;55L9y{>ajojX1s0C1sRz?UbaO6YW+nSa_^cY-N&dg8v+b$Be26 zNr(eh#EGWisX_R83N!}9Nce*Kzk3uQ0frH+1N8;?b3OPE1D!CpM4{^-?F&;Vd(y!gfEMUW#m_#SS*@`9nXSy5GE{ykpx)}3`paJ-Zq##2gg@N{RiR8#%hFD zzYl|b7TP3G1HAMO+;3tMFXK)J`zo-yepFPQ3li;8Y4%quR8xKg9=ksHbqag8hVA2@ z--XfzkUDEKtHy{AHo$ny@ZEr7JEt3Whu;pcFdpZ2GGGNlF@Z~@J`=AX%OsvR|a+r(O zc`B4>Tj9R8I*yma0^L^@PNHxd>u!p_DdXb7j;WZ7hH=6$+ zb}A5ZjqAu@aeUGx6tm4p7eR~Sspg1_HH<%Z(+`)hrSm! zC)$SxAlKr=-5G&JAGqJkh=K88|L1mpN;3pi)nW?Y5-vKAjt)Vz zWno>Jj_3^n2Ym43!B9&w(8lYb*l-;zSxDt~zM(o`oEAx$()?khB;1LG^Ul1DbE{~UpV&^tUGRa%;SWR&23CScAKYXK0B$9}=)iz!B zK|;4BzpYR(m7wi}qKMF|&nIL{LuyCk4@Aeuy}Jj->uo9xU{+BjBB&=jHkPPOFL;MJ zerPMlRog{$`snmDDAmpS)i*-fH-hg6hv4-$p;@Z;F>elR(#h|9R`p(NU_{c5&VV`5 zzSXK6d%t;dq8a=MX@^_9G!*`T)FagcH^z$wVx<48As8<)>m_mEQ{Zhcto6g7!l7D^ z$oLol&cn5(3ch}X{6r7k+57M(s%*sItX}h&@nITOe5~IE;|k!^R=83hTpkk(`Riel z#B;KA7I{@~s)?3%IuGE)8{zuw@`pr1qU@iYoCN>yTj=bC>`WmMDP1e3a5P-FVt+e5 z^?RYuwmP&P6HT97sOAwHvM%E}1CkBqWO~)AHKruoXq*49CPFUE7fk^kqmpO-1n(XR zCDA4Mm1L&EJ=a3&7}#SFxC_HWvLBMwz3rg$9U6j1AGX<#!(IElI@!Yjl>S@zdP%^# zIMHPMZdjC!meK>h|I|03Og&#Q9>|+4nmphf8m98rn*{-HIv3&qU`4>iHE1tud2gr1 zpr`t*`s9higYKu#t9tKI{-zp!@f#>CaJNkuq+@yeU2~#+au60o;NTFzm6Hd%2jM0M z`T{-vy^G_zL0xwZq+8gZYUrwl`QLH=uX-K09c`2XLOIg4EW1;sN{M_j$?mCuI1|!6 zRN)DoJucNfRF~;w1`93h4$YSTss|p?7x1vzM&0h$DZ;+G?1Au)PgKD?7wHLBb4WFf zu<_+v05$x&OudKW;$iVZShG^Nb$ss!!HqLAg0iIXS?**WAMdxf_B&e*-!_N@Mv7;j z7PB}}lcOkVWL~_h*cDEy51xcl-K?y4I9#srSp(bTKv(#o+VxCk6(9?yISWNhmZY}9 zr;UPjc5`vSeV_UA5PT^y`lC}L9P)>fY*394Jut&3^x?YGajjY}zI|t?eGx!y1*|jb zP)2!O)$f9Q)wyiWXN=crIRuTnMUd&TE-IZ{3?IJ;b8dzV8_jf*ZmLz*XUktBZ=z@b zrJBWI8&v3`X+QZRY`zY1EppfF^inVU$ylGQp?3&UZDe1+*b5UQoJO`H%|--#K|v-P ze3cw%5bysSlQB#3D{Yf_yapq9EzGl+G?`H8(fA$9UJZ1`$4| zyS=PHnukTr3;BZ@)%(k%4z=Bh2|~W1ug<6_RNnsu;qg~p4P8NRszp0LBst}$4iC-( znVCe-A!G>VSd|5gpc9`_i2d=o2x(4=7e31WOU>e&o&al7;P?P+h@eAg@q(Kw*k^{} zixK=EzY>&wAQD|6)Ms~}>SsN67UXV(IFs3V*J3!lgzr_VLA7XtEv4B~(n0 zK+4b=fH9HTusrw$D1U~qLVJ~;z|Q^f&OT!jZUn2Vx~>sgfAE&HB=|aanPfw#dBW@Q z!+#K@6GXIY@?@Ah3knJZx&3p1&p$frPCXQbpJ$Xuw5gLG(BpxY3-H=IL4EeW{{n8= zAZ#6t;InW|bld9&uk<$@*!sEM`z1vbtZ@Sc;X>l}e|_jR_uQUsy|Y3CLj9%Z!ap#C^hu%H8uhQgcoQZ% za6plrMEnZ8u?K3T#E_-Mkn5nVshK{zR?}zihB6yV#m@f7iMADtNcg>T3y4{?nL8zJ z{YCX}|1f;GeJjV*^YCdE*3n1VCyq6zD%x*u>m>4N4ayt(XY zN4u|l>GJ<_in}R*7a}m;PyCR&g@&i!sg@z&M4Zz=Ii_bM{yrdXd zo(zk}h(L=OP7J^l7vHNp07;|2UNip*?$;5zA3qs$DsSl2eba z088!$m4q85?$PX8|--$PrQ?-Rn4H2BX#h9nph;OT{;09(8RGa&x} zVcJ($kZ;9E>>gw|EF|+kfij&2|Huh=|6}p`sTUw44L08l;|!k9b_tUj^mkY5`zah~ z+RTcnTSGpd{NevIQmrx!N7aYDVlbm8BRvD&G$^W(kpVMiLh&q^I1yJrXtWCMfFsd# z#wfZI!$e2v^)<^xlhM%)8yCaYx1s$KtX(G(F7Gctv6?1Gv~RXJX@`bj zi_6e-hXREWj9423!WqCVZZKifZ+ndkP^g6|yge|+!4G(5JLH-qFD~C=yxa)m^6l5r zpg#*&YGJZdTUy|ocZc81nm&6Z(kW@4D)t1WBU=wbkuD;xha5+Hb$6)$env8sX~{Ky zKpnl8&WHDp!;u7(E0+}T7md{$_aZRPR6#0jEZ7i#fS`RKz?S1$_loNIy3PI&8W(KN)w3bH(B z78K+=k2qxdfeLxZsP<-Bpl%0aWL;eNX>;^D(N0G9`ZLqa<~x_ z03Bx`(?Jrx6AZosIIsZxb3p4Bdk<|ZESV?Txi*+0Jr@Uug&d*feCYa-h7aU2<0GHf zHG~V6WmIf~TWqRq$r|nME~q&RRcD~R9S$E7O-4)%fo*Sp{hZBUjtiZSExAs%d15k;M<>4 zv|yLdd`fSD52H)^+EQ^VT-*iePW9O)(3dzj!};Sd$3UoWhAf?Iq~la5(bkuFKd~KF zNjVX2^Gu$XDqZzRneo-V;p^)6Q`aJCulK-h^9?rvVEqC?v^D8S>13!)g+iV7TT$SR z6Ht92xNL194EZ2F7LE^IU8%1cvbSRnuvuCLu(9&lY+SM0PBF2qQ z%8R-{T>}_dQ~K58wL9$bv2ce(6rSpY$zJFQz@b6-3rSq8S>#QOg|$hLuY2N;@^ZL* z6#lUcx`AiQVPZD4Ux2%Bhnc!(uw9^!(kmZ53@uHT-(+Ye?7XsGK zcX|x0PJ+4d*xvgQ{07=4u_p2Zd6!T036Sv0nXuBY{5em?~K#ufV9 z!a;S*@kT`3SUElT-s2Gwa3s=RzC&oZ ztJ~rFiiRFYm*tj{aP@5Aa42y64B`k`HcSNq{fm4s`A@W)-SFacI3LbCfPcu#%k`+M#S zzV4Qme{vLtfPX9(_GMPiY*P&GZ8-{_A?d%jJq{n0L&g-Cxd}E&C8O(2Br3h1fPcR6 zXQA&E>Aiwh(7jJ9Q~4c$PWWL0{6LL{KB+yobw1EP><|MXfTDO5+9s1t zW@+VJwzNXNN#0Vt3r<;-4@G|&zT6*xZSRYSD1_!e85T#JXhXCkfUV=K#=bALVNFDV0B2y3d+YoIH zPA9czkFww0w zsUvZt0q$)&Li<`d#KuG0379l5^t0G#T2o4BI~JY@sYj-T>&SEm(=6{D4kgHDyd)*b z9cTGQsr^s5Op4xUiIA_=gc5Bl6itN3O2U^9`QY!jRpRx7*Dt)!eF3P8b5FjPPXnYR zakrOcvdOifoy&gRAYLEQF1e+WwX>mi_U4fU%qz-lF1DPeAhUoWFLc&cvil@lYvJxx z6orbOl_l$>r_3lLpBGiq5-n?}hDI2tt5o20*XnPu=sBYiaFmYk)EWDAY#~W>s;a}6 z8DUHk%$*}z$gUI-#XfZg&Z=7QZ#^m)n`I?LhlkqSqDH-#wfLC9Ko9_^>2l`-J z1+=xmowtbb)i|5J{=qhQ_9yV@*Wp3k_xQC$JX;OrkXojg5iZN<`CR!?LYB`>G(^BLqBJ zm3TwovTqxc40Gp;2Jr9Yu<0h4JqIpc6f%^WvqFl}-VPt>a!L{sVA?bxMVUGc;&mrd zN3>&%bj}83Ey+P%=g@G3MBAa?eT;+gb{rd0VZI-xONXKWHD1^=2$xkoTMy8qo}ANa zQ|<9cQ=e8T9#|bG*z2_sq={$rUkz9S?kIA7ol2 z!rBGlUU&}*v*Ecz#)E*h>aiDkWTEUo((=hfss(esA%$IXj60Q+KlNrxttDGIe5~y=eGqTJbtY%SqxT2jm%o6$o6FTGt?mS4RNwsx6Ggj5H={N@3{4F}iqhB!XU~eJ z$JntjE{iRaal@N?;k`ZLt7$DaTXY3a{%iOy9{-*=l+VaH9k3sxO-td<25BO$d_g?Pq}~$jJ1#*ei&3 z#c}8Rmv1u%jz*^NsN4Hmw^FemnssuSRja;6AMkHSmk9RrinpiMn5Z8sQ@#r=YSLnsN?ri-&Ds)sik zk}Msw*>2qKzCRGGE$#$;3ddEexAcN=caKX)Aaj=bMm^k-s*MH&`}fka=}syUDv<5K zpIQkBat+R-uCq)k{ibF^Y7UIozL%8fxB7lTz%QPG#;p+hfspnp0T@=L7wYI`!DNuAuUmY`ruybB@5^9gbldVv;1yWP( z@0gw*;r{5_qsprL=pS^} zy*Mej8OIFV_5WG1P?{n}>$jPN7c08Lt4Iz3@B4_Y=p(HZI;+)QVNwiSqZ06f2-wgW z6WIuWMOD)H49L3+j{gnh>Z0DQ6);t|todCi)OAdK)#3W=wo0h~Kuo)7$ea!x4bHXP z?FJS4^!n_s8bBi2*CsO(z4yr4B5721q6+3o9mQ=r4R>b`#E%oxley|Coo3@a0-@g6 z_4UAFYiHjJ9YLb~{N`Xq$oZ1Q!RP`%)dq8Hj_283kgrRstck$idey2meB;HvfLgwM zys$)BQvj>g2c%gdBtXezV>>4Kd^mM=F~cvnLr!Km$p-tvJe4JX327P7(+Mqg&{PAL z+d@2@D+^WxH`2gSOb`G7AOJ~3K~!$%mO*&x5%IczKxF*v{}5hlCxoR>m>}%yie`q3 z>jnp5sZ9cb8w8{Q^&8vaHUsZT(``+b>*W@vaM*dynPtC@8#oyOyFu9)alzGj;rSkz zs6L6xYv9Q_@VI)H<|M%CbjTkg2snfj%JOq?`#&$HK34#byT1!-=0j?s^z!kpQm8g;r+-y15DRj2u^Vs5fw6 z@(yEJ$0E*b+TG%u#w9`Vt)c-Om69O?!?5UU;`M|UXm|$(JH-2BL=m28*F$Cz94d$3 z7(5SmZHL=$F#g|x2m%u|)o|~<;4m-O3?uJ?S)-C0a*O?MSONs`15mYy776veywN#-~B6? zsq4CnW-?NwsJ|axdlS6sj;wyC{sCv*OQHk$N*oyx*czQ0hU+X&@*49Wb&N_5#}-L! z@E8nMz+iC_l&cTTF&FN-s{$&K7bCM^mr zp|0Vhaya@D^hy5I3CU1+LwK3>>TUM2k%hM!FMs1zb0?s@shfddL|cCmWv0Pb45T&+ zu>Bx>B6a4or^C%^gjDv%UNXaY-!hC6^IqHo4@vtm+Wzg6FxQ5hdM&CsjRrd}EmHqN zNsH(NdMu`v9&}Mg-UwQfqZPXQ1krYjpY@J`G&T7sEfZ!f6-|bB2re|kxwFvP2m?|& ze91#FTUvkQVLk}ukBIrbmNqz91;71B46-$7o%8tdg4D-Gd8$Dp+3)*=*4FM>P~{() ziVndHC*el0k6o#l>ZSyt1Gr8kbr75J0EO{zO)OmS!9(f;yzLT{Wk8!3_V&T6sxPNZ zz~eozC>55ep~<$D#*&Fi`^8_GgpVB?)PiS?lVm%JC{Ut#Xv;V`3=1L<%$^tqmt(xc z`$Pb)_wWcaEYGmsze zk3J4t-xPVo5Y$Td_E`szw9m4}nn$`dG`7H+r3N2Fg7Fp+MGc9GM7_E~H0vPfEI!5> zBt9A~)Bkrfm6^79OfuYikMXwp$hTo)ssWr7>x|=p2wcFo^@~hkGh~@F<2Vxn86N|K zWzrE30Q-IL%%E^EkugV=p`?4lBj*}E9)wbZA6WIz259;n#2b8lqsTc_x6quO{4EC3le~C;Kg!?ix;-Pld|BJJ4AAz zM!5i5!zZISNCj9HRua{QRG^eYeL_Tb1-S+iG|76!arSW z{CR&rqc4Ob!K<(2Cw>2^CIgs+1h`Wg1by@_95@6dSRa=5*|a?S!*9QAvw{jE9d{Tq zZIZwIK2_!sW%)?F11mit~)0xX;ZC2P(1;p9;f zXIY}w3{_rpqP=4P%t~li3>zmW0zETz))q;?b#bEUaoP{B42$GRFm_7GO@M6*Ow`?J zxlr+hhtek$2HT8@wo@cH?B@r&%!}H@#6#irqG>qr9ISXm=-_I1f^QHm9EZ&O&}~1z z6fQT5%Fag6=Z5(@g3;_O2hKaa{pN&FuXzGEcOG7QPZV;~2>7q}F+#vW5OBFrO|1`3 z?FYQVx^K=TxM{BOMZMHTBFekR;KnG=u3KSjGHg*RG-}DDM;lwJp+A1A9M-LMp6qzFFndM#vV}I$?8_FGuX=4SBw;o8-rY0xg8!$;xxN$BW+#~ujox?rmP zB_Tez(U|PV-sy9{JhJ z@Kwovv$hG|*a1E@@Eacox8GQ+=`mm~3`;^kyp*sk&iSxoPONC`!0J;|N^)mC&E1br%$_4_&Rc0v0>G(GULu);{KJ-Tuq4 zOpTOJm?fGXgPm~p9r%Nq6fms&4wp3>!m03*8acUNHxvHA&GsG$F){AOt5b5|8*0%T zpC7h;Bz-7xE-e!K!4Z*=S-2$lxi0{>{ytQDQ!>7i>Fqw&13Meg*guv7JD=PZYOT~=!#09P(R za}6}tL9#Tuox3JT!g~ipZ&&U0#X*r!SSd~WfGCuZlqlR#&YuVGfS4k(yDyV~^&Zhy zCkcs(AHILYFS(FJ$r0t$X@~o8_&?56+piM_i!14DO(@YmI0R$k%nN{7;YdGBk;rda z4BU_$E(K74>S5U11C^bsWm~8COe7()z`q1mCm=>xGDHrvvM8{ zrZ`Y@UAO>~O@eW=ib7LOw7YCdy0EMNaF2Lz6qJ%-^^;6-kRxS_jt*z?5~m3GVP>(h zb~aKt&}k$mMLi}Dn9G5@I-iZgsqifWIn;=BL=j_&X=WZh7 z>vtJ1FMl;S`ZL3`zlQ>qQ2YHsQ;v#V!HSUA8Mur*cQRbyuIZyIbWSi@g5tWF!t?r9 zd&8w8&54mELYgHLnk6~PuuF1WSuC0cad}j7r3YU52>J$~e5)vFf9D#QYS2f%r*h7S z%BmN-$l7v>j!WY2WCLw;Obm>lB-$d~oN<2-v^7H0S!i!oGq_a9QSf8?VD=*D>VVcd z$eskN<_iLzlqlR!dQ{POe~b#B$^%z*TOT;gkJ9J*1)pWq3g|X z_?#IY1p{e53Rx;)%(_a?H9a0xpK$@dNPs-84o3dgA|NFPP@SHazcqW7Dv;=4%D=7_ zCYyAzR_eB8OGNwkXF-v)#r^&8+8bCVsmho#85nK7t1Fj<-Ac3&rpJi_hV6-Vd?aSr z5OHNlpTm5w5if6#=%Qm0*Xr7pqCq4?`_!q$I-^J{#f^4~Rs(8%tpO`yybbMZvSjGkT)k*%@7; z?VyUIYd9#u9_gJuJRDidrqh9>nZ2L9xqG5*rO&SDH%>qV^Wrf;S5^czF~1vzFOE_Z z-~ozbpx9tPmlgQP2Q})jrpk};>GJW3^DD49@6k1Clt(Bv?>9c`x%fW|1#amU6% z_7u_19EH{Dyzv;|{WstiNo+b-of=6^fkg{Mli~M^y2_`{z_a5+6K}LA_!H8AZ?c_1rZ0h zb`30-qTqXWz!Kfev0eNzQR@369jEwQN6PGxn+IQhB=}y_WGew-+xM)LOOc)-1|wMx z6%uu|u+*R^jUJaS8Q%)$+aWJXgDws8=7=W4_EGEgg2yI2J~;U2PqcMW@@?-64Rm6H zNH!dk1lKHwOatc;JM{M3uQM8$McU=-#vRf2D(2qV03I)H_1TIa*c3s8{9?e-EM>&V z^1vPOKs@}{u(5^UAs;NWdeFh4=V9fe2CEn#;Dg0Bl8i{9=%P*f0ywVrHTuKh%>!_U zYEc*sk`0K2kMI7Cuv9r)EwpJGs;j76W{@2OEIM?+aCs#7#V*LUNHonhs12oEL00Eg zma}+(4U0t!MJ3G_Ve1EQx)BBN-jTQEKMizTrT~J*E zFXfBE2>@kxL%9?}OGp$Y<>t;7Wy;+;h-6Qc-i+q%QLmIqIam|yQJ^#(&PMnkEsLN- zcA##N(bo)9(}ap>+F6(|87{N{yG6^%gZIDM#kYQjI2|XUXt|K(zg(^cbY$yTt1)Ls z==nT$z!}l*Q9ZYvV1dD4!V2*TaQ*F}%esM*QbE2S*#eyeZ3drK4R{W!e%C%!si6nb zra{U$$dUTrbQk3DN-LzOxiF2}q4*wW+j)BlNurc7*1>u6#0Z-2pOK4#h9RaWO`C!=!IC7Xp0ehjAdz6Bl zb^~}|qE?t^2Bho4Ej_BnJS0pwicql@i8q~*a7n&>et7Kl@Kp*T95A`%`Mm*1gY+5p zt7^D(7?xirUMHtQ>0cPH_pOca@q6lz`u{=`48V&W0LQ@|tOpG*F@!};I=U@gdnyw@LS0O~~;5oba_RwoPZ+s;) zropbg@YYU|PdR-GRG$nl)2@|h-?$DIy9n$#>a88D6z8hn?+Ta`Z7<>G&KgepMfkoG z5_BWh!Qu#HBLl#M2=0_^-cXkkr!XsuVR%|jg_3N5Pfi+d6DdwPl)o4iPyr~HY$wKyfwAh; zMj#M@@3jF&tODJ@qJwX31KBdO!!Oof7QsW=k9Bm|2%ik&T>#mu?N`;{>xUTzNz{7G zT?}pK!lS>rRgjef7taG9i1rXy_~O^#8$X5TRHQ78*CipAJsC>0kE^c>^I%CfoSzi@TqEIqeK2M`BssC5lh9F( zDbb$kkg(5mNXSR3e|uofb)sp|^zfH2!mIx!6cJziEad41HAemI>|0&S8xlg}@nsWlC7@Ce)@RrppE z`0n>bova5e&T}M37pl=NS8bKX$HLkqQ5npvZ?3k`XTNQs&+b>JBFBbUDhov7WA@u2 z_;TXtzr9mfIH~AS(jTDf9Ar!p|7-5%2>zb~40PD9qv4bP7737MIJHeYZ*P_iA@t?h zHN?l+*AwWHDu&5=;{@r_(Gnp~o`bm#inG|Ea(zi;(uJ5<(X-}Fff8x?3qOPP8)3~_ zAw{{^9=vuw+$EytCU`jJ%V3~VDZBVf+rzPo>VM%z`vG*3mEU-NluLnws2~ZDeHAL` zG@XTO4SK-Om%vYcE(Sr-GD194B?_ci?ar$1fEfYrdli1BGB{5Rx33sLu@Rp+XRIUh`kKE;M}48*jFr-p+KHV1lI|&^TKDr^ht<=Z9x~*i^sr! zSs46Wu7Zol;hNPjV-t)?6(szLGWfP8M^PtQj{I22A2p=p!1PUmjLYaTv*uHT{h@|A zkEVzUJK7~&9M7pTLHBbVb~RHSoVTnAaQodN8V*4Mjsgo-!9P4N-hNN2g`otT&rLa5 zZ>obLH-_lxwOV+9?uk_$dO&;)2D$TbVJuh^7ZGD43MsROwC|Fw14_~BY@T)KJLL*VU))31xlS(mGX zWs=7u+(7bHm>~HS=}46>3;oemg!o)n?0`}eqP=kz&<)%6!_6v*E`L>& zoxSUNNK1j`QnW%*M5*w$OVD;fSl21QoTYH;2s8&4@!@E*s;>t6`e4>$ub$1J*ZMC~45e)DT48d1-K#Dq8$!R!$zjNkP%L`1A7;AjlzRhQKJJcaU*~XK)!!#7Hae+PF@Kp@ zZ^1I-$Z>ZE?D&&-tr7951+e}WbH@QIG&hA;aRl>YZ9NCMQ_MfN!%Z%8aoR7y5_Ohg z#1%zBFwkze>keT}$aJwzT$0dzFZovZzJ|M?`VY|EX#amZu81PC!2{Q6{;R#rQB=pI z;Q;I&f@8yiYrqHzAAtfX4s!qvZKgf2Vy$Q(wC{D5@cw(CB#J76i4pw%MwpOg@!>~% z)r`O~YD*ID6B9%vSW&FASZ5YTXGTrK)re?E5l*_VN#hZG6!_sUA$~l}m@ecedk(?2 zT~JjgDnn|>&4xQS!I%i*d2x^cT|Q_S6oT|g`jL+U$JHUL$192mBmzk>km3<7Ee4jy ziR8q14}|@u>JVQa1gubCauhEBD}3UI>mnG{)tl(EtA?P+_$Q4|hS_((N99l~b#lG! z(D+91{W*1dV_X6FhlMw`Q%rGDb!Hr?t6x`QOWy%YAI zf{awrW~tfj6?=k(YMwjf^jp|qoXh!3Cs<~Zsg#f)<|Jq6o|cLtBHvYKpysSdw9_y+ z2&ZeIuNNk%qs&=i*gSc+fd*&=aJGg;9o?qB-d4}u=idDk6Cqn_uim~p+Cz!fTJJMG6E;5p|Men# z?jh*|1&;26!_uu87Y~m*Onwi8$uS&12)RzG;zns=r=|+VPuDG!j&0fuc;FlGzbb;u z%&I~rWI)l~q00`H!!wA;`MD27jj7+1iDH8*>~zoCaU{pUhB2b4n4=lM*Lh*jpzwsp zfPm{TJmhdPRDnf{MAM+fRDS&{!GT*^;O2EOSDiR?IuJ7rID0(M*#%0c`NrqZ2gT<` zm{|OYHEPmt;4x z)w4sV0#hR(=kIpg z+hMYfFUHtB4N@>1Gm+* zJRX>sE!wr$g?{$xAB8?VH7yujs5=Q|-xR~>x&~22uvtpfT(XGDo?a>ggRnxJwA{DE z-_~1xY4}{OFs`#BFq+cqRu~?Fln8v6B7sJ-H9LR_QP3U9`24?^f7Yi!vd2B4@hk4h zdD(mL%tiRRzR!9c%1;O1jvXs%NzW>R@ma3@+3qtSAupG!{atoEtVw}o@jOh(akkIJ`rw8K7Oxgf#X2?7-4E4C5kDq)k{T8VKUe8rTmEcSFN=!VSz)Oa`lZ>GxEBs_16qsx~ z;Ki($PAnRP3K1kBU@?g$a~SHmU* z-AOWLd^}8>3Ug+`v}uswe2{ftS+Gcqe-OT<9>c0~HJdr!1B;Vk*_crKhA90M(SWfd zNjv}mAOJ~3K~z&xdFUg!`EL950A@JM^&K}}_+#JmrSQ+X75K~-q4EIa7Yiv$LZXnN zOf@K~>INr{SbzS`8*tO@*6Zkz=5TT&V-y;4JrdHP=q}h$jLTXD8nOUB*jId$xsxiJ_u*3 zq3Ud?-R(ac5O9FMemwX(DM@^>;@KjHNwYN_9Q?Dm*TtO}@#TE~4Ox zYR#6K?V|qm{^FT7^IRU`nLH^6O4f&d*3%1TYv6Ph zTxdtzQTegukUrlq8Iu`yrPm{vd)7+lPM;mY_BJU^4*|nl+MQAi! zz96FDJyHPHiNlJ9CL0Bp)3W7S#2ui>#2rMBxG1{EnPO_3`!1%%4>r;ECWqHRLoKW} zv4+cWFe}p62Wf~%=cAGY>7#vuWGf2n*agQ=NE;Q}%-9vru&z3rl?7{8izefIm2fdS zbs8F*f(2t@5uT2J`~6TG(I5RVR95E!4(^AUMKJa%TESA3q>_i1DH#rAZvXiuIzyrBUMbo3F6;A#E`maFEF6gX>Wa)mbJq#^} z;RkAgffW+}%5)HXd+ycnD6~`b4g>=X&4ER>zR&=#yl>;A5pDGM!|bbIRVSmf18!Y! z!!_j(bx)Y|R9L!5G=TD_Vbk^S%}1fBNkqXL8iVt|7vdpjn$1B)FoJDS32LTRmoJJi zbsV`Ko_j$EARYfpn-x!-76Q9b?#2+Qkq+91z~izAi~$i@y#XGaFG%&H{{j;hz<7rh z4lf;LglOAgpH7DI(lfAk+afAU>cxT>4=hN4m5C4o4G%9O1~{=(d>kmL1P@GD4moSW z0~PK2U`>=I`A%_UuS24(K)%Dtosh&oAS+v^0asCT8usmi6H;N@q$JpI8%&mN=~34q zP$3u5nd!6}bW4CE(UVV(bASJ}n%J;P=(S19&~U!hoI2q2c;JQ& zqG>4Kb(N~}#m9?$kC{>^Ur}hS5~lXKW4;@@{@;BUhD}COauhrv0j5nCMgmi(iV8~( z@zzt71Oo7zKfx8XE7^I>Ttb>*qE@s@6<2^k7XeSB%T&MbM2q}*ZQUTzwt5(}4*(`7 z`Mp6f%Nnm3HYL;XMaGBbe+T%Y#obgK3%BW5Xj=ew`(UfCr%CdNX!{3I)@R>pf4BEt z2q)Tl=t*{^q|sixC47Ai3%_9gS>+orX%%=Ah55_W^)TEMZmT$9DZFSD#7mFZJ~jkz zer&w_@<~q7_TQomY^77@>Q^vGfRYw1eY#CmoAy*O6w@96qum62;yU*Fkdto zfA!qeDZQ>ajzmksA)~PhQcdQr`}>2X*Q0@!R`JkzLhVD+H1{PBtd4`Zv5*si#{GgH zRzz5ptuT`0Z7)a?1iv4OrGa$vYyhD^Ucbn4k^bN|Sy6^I##^*gx)!&auOG8h9N6U@ zFjGnZ4wM83g7cOpptumz*p4*Zf&Kj*gjL_HsMFvD$ zQ^Ea@LmH{Iz8~zf{_elO#^Us!0`TxZ8(8~&|J%mdu)qE#oIVwNKQ0T3ibNDVD+{BG zASOnXS^v_V;(Zyxd%w9IUfU@&?K_VxV#jVV;LI8e6SH7krg#k5Nk;}877;8iaCTWI zS~y~B5KoSZ%TQ`D_p`xbRMj_xspQ1t!|V`ZOs28ohQVhQJ-;mqO)M{4~jIoKze*gL4>$pVhhe-w~ zP^(kV{(SgniVx0x2<>V;m8M;gIw|;hN}BNh{z{{;Z2GCxDaija-2rbn(-l&k!_th3)Ad75ERokc)jV04oxmmzLW* z2&ECYc=QHfRRr4Z14AK1`{3?yg60{eD$41Y>d?;UIN!ub`^kEwlH@elq?=JYdlvS* zEsRl~Ixk%be&5y0 ziFS%q2eL!M+cz$P!MsAD&1Ab) z1q@2Mf+*E)IpA<}W2AVXB+j^mx)*q%8_LwDj-~_8QDrEnRe~M>dKI`FfQt%rC_*Q$ zgRIZ4{~-KOE**m$H}VIQ@42lUu8A<@?1_V^8$|=i)J5F&!>I!oHnV5f))9GeS_<5~ z9=_rrQ+}pYU2DChYttt}y7@Ufy2F=Mt}hYEUeUradnt=+RGoo*qih>Hl5Hj((Vex* zPwbB&{VM6`0s+XhDft;K$RryecM?&RtCT(|{^rENT{<>W-U*MS!=WK~u_wF_$cqyN zzw&h3)p;W$2#5M%eFTiC#Yg95q4n)li^iQeEXd}18(U8!Ja5p~%r~#q^r>L!y6|OF zX2M_Sa#?>{4k>x?`m4~a&faN5ml<>5+8d%<#{=MknY=*1UJGP9A)LVMQI1zGG>aXi zlveVmZE$#xkem$kt4_U1u<16)bJ2}H@J*PeN+IjFL7`KxZUr(OR2OEEZjd=jGJ5F( zY`jf04cg=6x;x<~KN0CB6EooEJ46v1r>L)YMzlMwxH{xdA45a#!af@vXv+Td4m|s` za4PBRgWg^-{nFPbrU-oAk^H6HKZ(xfdjsWRy}HqHX$9~+!1b#{18A#}zZ3ZS;Fm91 z%TUbVMavA8e|EpYZmb2}~iI8BO^Y>mkEOU5%I?h^u-uMx$ zxN9rp?+Eq!oA)n`!iTe^g`Z!4m})gSQ7h4|K_B%R9EMX(kl_=BYT7!WWM$>ZSu^|9 zcS+UEf4|S{Mu{TXI#wC|d1s@V=u@X+Ul!GgLJZ*_WoHDNkOb$OVEiOu_8U#)M^VCI zgxwWzqja-eQM2d6LaPzZ4k$oZH$0IAKd>lioZx{qNuudd?}c3hLMjps93Oz|B1`=n zn+112C|Vyyu%NBwu>S)0QFTuGE-6t6DNhP zJ597O5lXOthO_1~4O0u@{;vn$cXtXB?(@Qj?*yaZ zW<6}P!=&B_tBiko7#QHy1Dly$0JFz$wOlpMO$;D0Ni?gZ$+s%t%MU?%I)8uZe*XS! zHJq)6E5Xr8>+PXlpk|hQ8nn~>u$qfjMrFtoV((Gdr@FFL>=1urJdlRRG_y_-czIi16(48tbe9ZjXSVmDq1{ zx!PKv9q_}X=}@x|dM`0@MZ_UGw{_n$-*$_J5jVT4r+fKuCmc* zM_U^_FaUEug^a7^PO{M)ez$=ESZp&|ztjdhUI|?mpD4_8hNI`O1XnOpq56V@$ zym${R9s|pg?WHH)$lR6gy|atQeh%JVAq~q}3I~4#|M~=6*#W~Y_ZbL;4({u!punMf z#s4(M*j&zq^F;e?IXwJCaQQ$Fyz+Z!SAD59g7xuxLWVP1=?y@W1Ctwo-fkB<=}{s! z7S`P)8i+yKy=tyda|68pCJd?5&9zm*?Zy?uC%=U0H<}l&(a=>36CEbvhM>bm&2s}d z^&(f4pp|r7e0UUGQ*BP?1tv8i&`2 z-dReL2VhIW^Wt(*SHF9M{pp`zS2291lX*y&SQ^Dd$nGwvtq}-caR+_wxErKh@QfQ^g$Ov{YFta(#UO%TOlawW2ES6bNsV+(^3 zMtOGYmHyzaPW%{h*1`BiSYCepZ4j6MA8do#Gr{Etcf<9i!X?8DO7wEjMlgGwJ6YF! z7u9oRj4{xF1GF{3fg^DEL-EsSQSh`3xc+v?x(W)alSg5tL)Vw9iM;9XMXBOtljL#T zQ7xW5-ae>#9Xe}5x6N4wldlmjG970lB-+K1&BAF;IDHPCMY^tt86i7NEr3VW`)lb2 z5dm*)7JB$Vzo-s)`yI|~|Nd;y*M~tjTx>}SMy(F?r^%3w$kdy$Qy-oWsp{S~Zx84utkS4^#?Ki+w14&pMSr$~s zkMLDAgHr8IYVxRla#9NAC+4Uc(`s|yHWE%?n{^4-x zh#nIx$hA|%xo`r~YwGgX>sFAzW^5cxohDic6u5j@O!1tp7OypYxC@qqiaDPjy9L^T ziCf^pd7gh-DrL58q4~jTh8-(lOVckoa9)buE{|d&q*f4SoP(a$DUMabO}B(Dn^h3K zNW4}>GShBEf(VTjEfh_LcUXk)&YgwU2Eib%w2O9JXCE^rRYbq1&I~$^Tn#jyffR#L z<#)ddZ@&vOqnP1VQwvk_fK<_I9<7Flu7Ma;qM|4e8^Q0T!0mJTNVSk5$i-m*j+euN zGD0MBFKhnPF&>z&+T*3U>$^0c<0nm|Wsj@Rr1V0VTmtcFaDF$$eHJ#{DjJ9==cO*F zcwLZ}ewDyvs=ob8*N6&=CyodSh!L!0Ayz1KGts0#k)3V4OC0|gHr)%e4W3`!SKxOq z!4P8hKsz6cSHsfPMEqQ<`otO04!BS(nN>H880vS6Pk|YC7_V~qBs9Gb{vlC<9Uv(S z@^6F`BPmd;Ulgc`aybJ#G&eXS+D6FDgD*b8~LegL@e3#K;}4Dxk@yEvsEZq6YbHSxuSybv>_0W!c8+Koj3Qt|Log=v=6inXmqC+~Bxk~eD29=a z9kpM_Elzx5FhBukQz;Iv(X%#HD!|@mAU**m=ZhvoQK0iuFzS7!9R_+u>G-;n!S!A) z7yn~IE=-vLx%p~Ixl!C>SK32~_RyfHUHl(ggOxkW)#_%MV}q5^vc|a|#Ei_)B*4)# zaP4eCYBikn!Xg(HVl~uw`9IXXcX%AvdG`HV^bQcLXavCmfW3fCof6eZiIVNumTbwk z?D(ZECys3~pPan;l8b)h6zdBZm)MFZeQ}&fvE?p9qAEp76gxqJ9gP4<5CqssEb3y* z`^Phbot>STJ+oLy$$eet!eZu3KXc|e&$*xb>D&&p|Cjqsjj&3Su4BXqpPUSH)4(Cw z)ZaPu9Z%c}H=D<-pSG^+@xUi6eyh;8)uwNv>(gTqloSgv zcmR^E&;^g5E9HPGj>zYQmfylVsRld58-n~HXw^On|5OZrenkS3Gd-{@L*B>K#DBxj zw!ypv2K$bm0AKoJ`Ms$Y%I0XFx)AK#348Yf>9FN(xNmhFdD0R&v)9{)M>DiTguhTZ zSsy}E??Dyf{ zS6iX=9q{!*`*tYNUU;t;&i@$Z{*nE0-mQhDPZ|?R2E4TN~g2hxWs5b^`8k;>cln zODy4_`r(8ji!bHG2{=9=H~T-R4-Oo*F&ogT&?zM2CPvT05A?xr{uMH^#GU3Fl3>ej zi`1QYg-|kGjPUVO)I1mFOq!<}M1;M+PX=f=4BA9Dy5V#;y!k%l8v+&5(x76Rq#>VL z=J0HRy4RuW46Oeca22*4f;%)Av*Dp1*lVCy;OiG5Csj$xrx7s}9GyPER?sC#{VW8N7I& z2c|z@bN&{~hOS2%+Gp#a%5V+B&vuHCwb`*94(O2v9%mZL)vM6lq(nnu@v_8@=0W6! z=GnP>68L)7IFS@x9;d4tN@GMI%}~sv>;@Sc7h%aAFx`+oF5s8I%Z^sb8tJ_*^XffU z;pBnHeWYi=e)myW=`BXF=D z?$m>8JTPSv%&rb+M(a8U>pu-mO;D8scTR- z{u_~%z^Fu#y()bbq`w*n%lnmk;5J~A_J2HAX_t1r%8w=a%gK=ctusH{4o*Qq&CS-fyBQe; zFgzGd8wmxV{e3up0OpT^Qc~qGP)G=l9TBlsM|gxAz4B&j8cKVcL>qUW3)4_e9*6H6 zcvyvn5_DKusU@6=qtTcR`q5yZUxG8tfshYvGE_=H=Tfs6+s6C!v+$X3j{X>CT+_J4 zr6!VEatcf?ld|;A@bXYdjPA}hF~YBO2_58YvlJsdNuoG%rb(icX3mrR#UNy3D#4mXmg?{UPFUd0sgEMoCQpm@G&lhpd@pc)p zwo%=03WYXr>2WViw;3&+_QN!rJvR79Xd-4LTYAkSQ81F4Jp}v2``^`^;hvwp$8Vo# zr(z6(E^RQ$H1*Sa2{N@A>3N?T-M&4VSG(d4p^o5zq7_hb6OKV7=0r77$a9c3i3k3` z=zJnQanzqV;Z`Zc1@`xE!F@k6y9B40pu-#*(Q(v(2!)9>}V1C#$Z2` zYc8nz?ecfFgAtvE%(XkMw_10x8xYXXXTtWix1^H`Rw}C$XVe_36!jv zkSw$DcV$Y^uu_luYZ(@5*cEBqUq;OUKX)2%DNZ#`aX?a{O;DDuU*kAi&b8|>%}o|O zyG~4d$QB7}+HQP;>G{8nzZvAjLAgr=d_G=!{}bGPCp2`y!Xjr~@OYsr3}x?@p64)M zaI_Dm*{tvI!6NG;r`R+ae;t+$%HIf712@G%5!mU#&;EEE%_feSV=HfV=N_1-U2B1h zcVv7Fkp*Ldn-2`xBj42=zWEhUem7jvgd&FfB@(=^J5nXxlMH+%PF*Y~EQ&|d$p%+7 zrIT3}BZkEb01rI+ z!n$?g9q7&>55Pd=smF5*>#pEgAW9-Sk~5(AP8c^Qy88KTP~}3R(-JmK<-$QmZD=3b zZMjj z2r3l!>E<5Yv<{$wj%K+33$Xh&INU7vrT@Gnq)AP4PES87{q9(UQuBIqP~$#7d?`i( zE-TEkm})7GA#z-qDn-K!6!5}XKkOTXyd(*Pw(x>q>UMv6rHX1JWvZ$qmoCG6H-!5b zotSSpszxvv_Bl9371<)k<;XWt@cDS`_TRGUXYly#l3l*;J?Kvj>w!-ygc)TpJuiME zJP?rBlvuE@QsJ<5jl;V8+<%cW>|(WA5JUT90Ct9v>>k86Z)bL@kM zo!5f|B8~<6y|J@V#*3>Lq!A}2NhZq)MbXh@eLW&Y*wG@=ghq1Z*=E_M(#FVcJOooF zM<&9ldmYwU%(`3;idW2xSYI1|i2K%um{>#9;~DOSGq1pn*67UwLy)X^TW8&AvCyU| zgo2Eka?!941{Ix+m(M4jlA&Y1h1Y-95-CQ!Tnk_RX83&Ge>GB1dFo6Z$O}dQ03ZNK zL_t&uDNsIc0+mETns0HOamp(2cp!g*7~5uqLU42k{N@$;f4~cqHJfPWY{)GTsmAyY zY`-cg&0_s+CVL`Yhjz7vp^}la9a2>I1K||xPM-wpR`|9T?r~_K#SZRWJ4d)f&|s>Kw;^|(MjLs` z;>*(=f}O)qXL>8sJ+LepmZiYa0GtnkPrC(OA;|MUt+vf1jR!t0M$>sY1T!r7**^+7 z-rXSw?x89_8=!3)6e~J}1H-WBbBa60M(QsSsn2xm-5tA*TQoJo3=3CG^>i&nI!@u9 zr}ezV+Ow|x0j!({-#5@uGBPw8%4`Wjbeqn6@j?VnL6I_4p3^xUdr@ zT2KR0bD-Q};C!SOzG&#`bZzVOLMXilvQ06slN_Y{%=o(v&~rtM&1;v#%O{sgFyJqL z2{WqUwtMZHw3{R}`?tZG$JFb`!h=tSgZWD0prlMFww5IP?t0k$2DBQ~D$_IJ)-{nv zaNiB_q$d)`_m8ciJ=tN#)Z>8#w@azp438KbGMCT8p4Z^L-C}^J5t*L;>3@KVHB$Hd z?&zEhPO!`znQeNRs*)3@y*4QhX^^e}EVdc#dVuOQDH`UW(NMMxu&?GKc{VcvMT)_t z*+$0htFH?Wd*TF`G)bgPb?ReZpX3XVMF~+xh^$O_)Szy*`|aN{U$Z@q9EW$dOFyvT z6P7!OMs@-@6lEJ`^ozihuS3%`0D2S62@At!kc{qluOdA73TXzFiByi@# zjB-Vp05ek4pm>@TBSImeo`9rvkAOJ{XaL@7gJvyL(!w~x?)MK7bm1+9=wvch$z^U_ z7BA(W*M`^sm7V~k5T@M&IW{6RdE?~18RzD~SMnVdK zivXA7m|Bm8Q^%orF3PX?mlfiA%B$d}`=yxC(E>Z)i2P<(ErMltI{PA2d!X(P#foua zrXt&<-y8Y=gkpHmLi;@!fS-R~dYgeOkU1U(E=Sk1MFq@|2=cS%p?Ewz{4f-lIN8|>0wKPk@I>Lc&96tTFD>eWb)O6``1S9hq(~H$W=>D6 z3JEZ2V)*xhxl#a%H>_1-24s9h!H~nl5qjB9&1uM{?W7q@;9HhTF{8Z$e!T^*874Ju zs)3tLj!|oCdac95@(y9^wjLhF?vhd-u-+uF70Uh+Gy&fAhOc$&YrsFl>ZPF(8tKI*xX}euO+psQ zxlm@NK|8dB)J*u^LklDgQEu$1n0LP;f4{x)MaO#GR@tSi$G|XKF$SD(E95wh?p0uT z2=xldYiwyKz%xIRzn@wItPvW?Uu_h_TE~uUu)H#ycm<@RoH+w#S3z-+7OL)WO$+qt zAe~8PsQdJ@@c2{C+q;HG0vAD)MPkC`vGuwAvIJ&+1Q|eDWd}(y_Ob?pyYoE!a2z~z zN$c}8J@j0SqLQ5?85$R)S!a#xcNh$gLNjXT8?g5ANF&@X5zViD0u2LV6n^|$_WQip z35!<2Bbp9>S3B%|1BM2~_{Iw_JTJX|Rxa#%LwL`z@*t_e4Rxb979G_}4sH}En+1<* zuj_w(A3m$mP=*Fz=N9NZ9j=>OAiZzw)F&&z=ksm5Ndy!aCc zY28XXaNBLLY`Og|E?tHaoBIcFt}7chi(07&WQj3e`up$0eRsp3|7EyxPL9ZzR#!pM zl(?K|MngL;BwB)36~kdFVXUhjut^G)reJ!er^AL(WqI&XG*0>3=xI^g03>p;k*OCXdWkM@z{V)QDBf>?avjYfC~ID8cD zzALI402{t&z2Dj&g?}d}OPcUG)sloo-#9DFup_H$Pqb`6D4cp-NzKIN@jEo)a&ErD zzFi*gO1F5kcht$O)x!=hnFWRL&=sgL<*W8;5++{Y6~7b+8{i&bPK+LU6p!S}1@*ei z?}AS$2HTYww2+IY!^bSfkV6CT>Sj1`T*`pK6Izff!E?7gDD-$$9N({AHzwk^uajd) z(CaWJwoT83yKIt&H8;TaEpW6UtcbGc7LDH>=X39Op$Wd=_V8~<*+0X%oNGe)+{$Oe zxvLUKP__!LHJdU$-hk#`L(iGWV->BC^C=oDpG&7jUvVx@9s6J?zGr22+#w34-!ct^ zdw}h4LF>`*vPqNRp@-%3=45E=IM*4eM0@UI--NDjMya#?^xxpGAM1-Wz>|`oq*%P* zGp0k9z1MkMf$ggQ-thY)9-)(Oqkj-fWGKYS=-zOvFV*rzQozd4u5(Dc4soe(IPtTS zZCC{({Ok_{6-jU{8D=LN3~NLLf0thjZ-DLo$n`Umu=MU<4#AmWm;z`o&Ffv-^_uCttjfq1)0|-HBk(B#P(g9a*Kw}Fuwu;=3j)TWwmW@lJW&z*ZsF9EAfy-Cn z%sF1U?H>4M0^h77N3PW<_d9CBo$x1&?T9>%S9?(~V=d?=vm(_uh3QS;NB6 zz1jWX55ma-7?*?X`#r&Qe%>^wzYaB7uqOmHsie6uihEjt1Um5t`=DIW$evUQi|!!i zxF$VQ*h;$R+lZD1XxI+F|1UAX&o;u3HinJOg*U^3l|(((P?6BXg2V&Br!1LvEwVLcT8e@{pN<**-4HVff;#ZoN=r8P#y zL>cyjCxMwO-M}9?Rp!*9`fSoBC>h#A&eRnB@kf7&J;Ou_z8O&0Z|}8sQ1eJBv~F#| zv%pjc`50;j{%EQ4LZUAT8@?eN0`sqVMiHVjW?L~b|EBl~(lyC3=o#s?hm!#@tg zoK!e53`Q^Y1P@HGX<+rfh`Qg~Xmda7;oE=EXefCOk;R4iaPv|rMhpx>>uEUA44vKK zH-7srDAGnZ$;nVW8CEWV`&Pk87nXqv>h~*1&$-0eGuGbh>giheubT%SNNu6}Cgw~S#G45O%)``*y+RzlSQV?<6xl-qitHyBXl^k4n<;+&8zZ(t=lUOx?(;o3AQ7}51zd2_>^-E=kFOO1rXZBBWdnC zi~as+ezq!N9jn&J?M{LkZOBt&GxYI=Al39FYU~W~01yCu+I1d$@1-PeAqcm}LFdTK zfce!@bo32C(y+9*F!~*8(-A(9r{BHf^AL0H#><#Pwbv zK=z%G>q3;p*MnLXK1ETTvn>`2owif-eyVOC0_NQ;fnE1~UX~d>;h_*TyeGzHu)=%3H&(V!~s}r^$NNdYZyV}A;>F>UT%fT#Zq!TntG+F{-yJ< z<5lsB1C}1`l`C&H8Q=y>U-6la|?QYFH4X z{f|`6%awAn8NTqC%#Z>l!LP≦!i)t!>b6{GPPJYj23W;N*$IeaX*@On=$eFA=jJ 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

    S@lV0djThyVZp diff --git a/robot/ros_ws/src/global/planners/exploration/include/exploration_logic.hpp b/robot/ros_ws/src/global/planners/exploration/include/exploration_logic.hpp deleted file mode 100644 index 962357d93..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/exploration_logic.hpp +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// 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. - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "utils/utils.hpp" -#include "utils/rrt_planner.h" -#include "viewpoint_sampling.hpp" -#include "utils/viewpoint.hpp" -#include "utils/collision_checker.h" -#include "rclcpp/rclcpp.hpp" - -typedef std::vector> Path; // x, y, z, yaw -using GridT = openvdb::Grid::Type>; - -struct TimedViewPoint -{ - ViewPoint point; - rclcpp::Time time; -}; - -struct init_params -{ - float max_start_to_goal_dist_m; - int checking_point_cnt; - float max_z_change_m; - float collision_padding_m; - float path_end_threshold_m; - float max_yaw_change_degrees; - - std::tuple voxel_size_m; - - // viewpoint sampling params - double kLOcc_; - double kVoxSize_; - double kViewingDistance_; - double kBboxLength_, kBboxBreadth_, kBboxHeight_; - int kNumViewpointsPerCluster_; - double kViewingDistanceInnerR_, kViewingDistanceOuterR_, kViewingDistanceZOffset_; - double kCollCheckEndpointOffset_; - double kRobotModelBBoxFraction_; - double kViewpBBoxUnknownFracThresh_, kViewpBBoxOccupiedFracThresh_; - - // in init_params - double too_close_distance, too_close_penalty; - double odometry_match_distance, odometry_match_angle, odometry_match_penalty; - double momentum_collision_check_distance, momentum_collision_check_distance_min; - double momentum_change_max_reward, momentum_change_collision_reward; - double viewpoint_neighborhood_too_close_radius, viewpoint_neighborhood_sphere_radius; - int viewpoint_neighborhood_count; - double momentum_minimum_distance; - double momentum_time; - double momentum_collision_check_step_size; - - // RRT - double planner_norm_limit_; - double max_explore_dist_; - int planner_max_iter_; - int max_connection_iter_; - int traj_smooth_horizon_; - int rrt_region_nodes_count_; - double rrt_region_nodes_radius_; - int rrt_region_nodes_z_layers_count_up_; - double rrt_region_nodes_z_layers_step_; - - int priority_level_thred_; - double dense_step_; -}; - -class ExplorationPlanner -{ -public: - ExplorationPlanner() = default; - ExplorationPlanner(init_params params); - ~ExplorationPlanner() = default; - - std::optional generate_straight_rand_path( - std::tuple start_point, - float timeout_duration); // x, y, z, yaw - - std::optional select_viewpoint_and_plan(const ViewPoint& start_point, float timeout_duration); - - std::optional plan_to_given_waypoint(const ViewPoint& start_point, const ViewPoint& goal_point); - - float path_end_threshold_m; - - std::vector> voxel_points; - - bool first_grid_received = false; - bool grid_ready = false; - - GridT::Ptr vdb_grid; - - openvdb::FloatGrid::Ptr occ_grid = openvdb::FloatGrid::create(0.0); - openvdb::FloatGrid::Ptr prev_grid = openvdb::FloatGrid::create(0.0); // to save time for frontier extraction, only check diff for new free voxels - openvdb::BoolGrid::Ptr frontier_grid = openvdb::BoolGrid::create(false); - - std::vector> clustered_points_; - - std::shared_ptr viewp_sample_; - - collision_checker_ns::CollisionChecker collision_checker_; - - std::vector executed_trajectory_; - std::vector momentum_executed_trajectory_; - - RRT_Planner rrt_planner_; - double dense_step_; - -private: - // Numerical constants - float max_start_to_goal_dist_m_; - int checking_point_cnt; - float max_z_change_m_; - float collision_padding_m; - float max_yaw_change_degrees; - - double kLOcc_; - double kVoxSize_; - double kViewingDistance_; - double kBboxLength_, kBboxBreadth_, kBboxHeight_; - int kNumViewpointsPerCluster_; - double kViewingDistanceInnerR_, kViewingDistanceOuterR_, kViewingDistanceZOffset_; - double kCollCheckEndpointOffset_; - double kRobotModelBBoxFraction_; - double kViewpBBoxUnknownFracThresh_, kViewpBBoxOccupiedFracThresh_; - - double too_close_distance_, too_close_penalty_; - double odometry_match_distance_, odometry_match_angle_, odometry_match_penalty_; - double momentum_collision_check_distance_, momentum_collision_check_distance_min_; - double momentum_change_max_reward_, momentum_change_collision_reward_; - double viewpoint_neighborhood_too_close_radius_, viewpoint_neighborhood_sphere_radius_; - int viewpoint_neighborhood_count_; - double momentum_minimum_distance_; - double momentum_time_; - double momentum_collision_check_step_size_; - - double planner_norm_limit_; - double max_explore_dist_; - int planner_max_iter_; - int max_connection_iter_; - int traj_smooth_horizon_; - int rrt_region_nodes_count_; - double rrt_region_nodes_radius_; - int rrt_region_nodes_z_layers_count_up_; - double rrt_region_nodes_z_layers_step_; - - int priority_level_thred_; - - ViewPoint robot_pos_, explore_goal_; - - bool explore_goal_cleared_by_other_robot_; - - // Variables - std::tuple voxel_size_m; - - std::mutex mutex; - - std::vector sampled_viewpoints; - - // Functions - bool check_if_collided_single_voxel(const std::tuple &point, - const std::tuple &voxel_center); - - bool check_if_collided(const std::tuple &point); - - double angleBetweenHeadingAndMomentum(const ViewPoint &point); - - bool computeMomentumVector(double &momentum_direction_x, double &momentum_direction_y, double &momentum_direction_z); - - float getCylinderReward(const int &viewpoint_index, const ViewPoint &planning_point); - - bool extractCylinderViewpointDistances(const int &viewpoint_index, const ViewPoint &planning_point, std::vector &viewpoint_distances); - - bool doesPointMatchTrajectory(const ViewPoint &point); - - void goalSelector(int priority_level); - - static bool compareFrontier(const ViewPoint &p1, const ViewPoint &p2); - - std::tuple generate_goal_point(std::tuple start_point); -}; - -double get_point_distance(const std::tuple &point1, - const std::tuple &point2); - -double deg2rad(double deg); - -double rad2deg(double rad); - -bool lineSegDistances(const ViewPoint &point, - const ViewPoint &line_segment_start, - const ViewPoint &line_segment_end, - double ¶llel_distance, - double &perpendicular_distance); \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/include/exploration_node.hpp b/robot/ros_ws/src/global/planners/exploration/include/exploration_node.hpp deleted file mode 100644 index 7b1f0c16e..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/exploration_node.hpp +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// 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. - -#pragma once - -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "exploration_logic.hpp" -#include "utils/utils.hpp" -#include "rclcpp/rclcpp.hpp" -#include - -class ExplorationNode : public rclcpp::Node -{ -protected: - // Planner - // ExplorationPlanner exploration_planner; - std::unique_ptr exploration_planner; - - // String constants - std::string world_frame_id_; - std::string robot_frame_id_; - std::string lidar_frame_id_; - std::string map_frame_id_; - std::string pub_global_plan_topic_; - std::string pub_goal_point_viz_topic_; - std::string pub_trajectory_viz_topic_; - std::string pub_grid_viz_topic_; - std::string pub_frontier_viz_topic_; - std::string pub_clustered_frontier_viz_topic_; - std::string sub_lidar_topic_; - std::string sub_map_topic_; - std::string sub_vdb_topic_; - std::string sub_robot_tf_topic_; - std::string srv_exploration_toggle_topic_; - - // Variables - init_params params; - // nav_msgs::msg::Path generated_path; - double momentum_time_; - int num_paths_to_generate_; - std::vector generated_paths; - bool publish_visualizations = false; - bool received_first_map = false; - bool received_first_robot_tf = false; - bool enable_exploration = false; - bool is_path_executing = false; - - geometry_msgs::msg::Transform current_location; // x, y, z, yaw - geometry_msgs::msg::Transform current_goal_location; // x, y, z, yaw - geometry_msgs::msg::Transform last_location; // Last recorded position - rclcpp::Time last_position_change; // Time of last position change - double position_change_threshold = 0.1; // Minimum distance (meters) to consider as movement - double stall_timeout_seconds = 5.0; // Time without movement before clearing plan - - double cluster_cube_dim; - int voxel_cluster_count_thresh; - - double map_voxel_resolution; // map voxel grid size - - // Callbacks - // void lidarCallback(const sensor_msgs::msg::PointCloud2::SharedPtr msg); - - void lidarCallback(const sensor_msgs::msg::PointCloud2::SharedPtr msg); - - void mapCallback(const visualization_msgs::msg::Marker::SharedPtr msg); - - void tfCallback(const tf2_msgs::msg::TFMessage::SharedPtr msg); - - void vdbCallback(const std_msgs::msg::ByteMultiArray::SharedPtr msg); - - void ExplorationToggleCallback(const std_srvs::srv::Trigger::Request::SharedPtr request, - std_srvs::srv::Trigger::Response::SharedPtr response); - - virtual void timerCallback(); - - virtual void generate_plan(); - - void publish_plan(); - - // Other functions - std::optional readParameters(); - -public: - // explicit ExplorationNode(const rclcpp::NodeOptions & options = rclcpp::NodeOptions()); - ExplorationNode(); - ~ExplorationNode() = default; - - virtual void initialize(); - // TF buffer - std::shared_ptr tf_buffer; - std::shared_ptr tf_listener; - - std::shared_ptr> tf_filter_; - std::shared_ptr> lidar_filter_sub_; - - // ROS subscribers - rclcpp::Subscription::SharedPtr sub_lidar; - rclcpp::Subscription::SharedPtr sub_map; - rclcpp::Subscription::SharedPtr sub_vdb; - // rclcpp::Subscription::SharedPtr sub_robot_tf; - - // ROS publishers - rclcpp::Publisher::SharedPtr pub_global_plan; - rclcpp::Publisher::SharedPtr pub_goal_point; - rclcpp::Publisher::SharedPtr pub_trajectory_lines; - rclcpp::Publisher::SharedPtr pub_vdb; - rclcpp::Publisher::SharedPtr pub_frontier_vis; - rclcpp::Publisher::SharedPtr pub_clustered_frontier_vis; - rclcpp::Publisher::SharedPtr planning_debug_vis; - - // ROS services - rclcpp::Service::SharedPtr srv_exploration_toggle; - - // ROS timers - rclcpp::TimerBase::SharedPtr timer; -}; diff --git a/robot/ros_ws/src/global/planners/exploration/include/utils/collision_checker.h b/robot/ros_ws/src/global/planners/exploration/include/utils/collision_checker.h deleted file mode 100644 index aff0bb6f7..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/utils/collision_checker.h +++ /dev/null @@ -1,149 +0,0 @@ - -#ifndef COLLISION_CHECKER_H -#define COLLISION_CHECKER_H - -#include "utils/utils.hpp" -#include "utils/viewpoint.hpp" -#include "rclcpp/rclcpp.hpp" - -#include -#include - -// #include -// #include -// #include - -namespace collision_checker_ns -{ - // struct VirtualObstacle - // { - // ViewPoint point; - // float radius; - // rclcpp::Time expiry_time; - // }; - - class CollisionChecker - { - private: - // Constants - float kVoxSize_; - float kBboxLength_, kBboxBreadth_, kBboxHeight_; - float kL_occ_, kUnknown_fraction_thresh_, kOccupied_fraction_thresh_; - // bool kConsider_unknown_vox_occupied_; - // float kVirtual_obstacle_radius_, kVirtual_obstacle_expiry_time_; - // float kRobot_pos_clear_radius_; - // bool kEnable_gate_obstacle_; - // float kGate_cube_size_, kGate_wall_size_; - // std::string shared_frame_tf_topic_; - - // Stats counters - int count_hits_ = 0, count_misses_ = 0; - - openvdb::BoolGrid::Ptr collision_grid_ = NULL; - openvdb::FloatGrid::Ptr grid_map_ = NULL; - - // ros::Publisher debug_map_pub_, debug_virtual_obstacles_pub_, debug_virtual_gate_inner_pub_, debug_virtual_gate_outer_pub_; - // pcl::PointCloud::Ptr debug_map_; - // pcl::PointCloud::Ptr debug_virtual_obstacles_; - // pcl::PointCloud::Ptr debug_gate_obstacles_; - - // openvdb::math::Transform::Ptr darpa_vdb_tf_; - // tf::Transform darpa_tf_; - // bool darpa_tf_set_ = false; - - // openvdb::BoolGrid::Ptr gate_obstacle_inner_ = NULL; - // openvdb::BoolGrid::Ptr gate_obstacle_outer_ = NULL; - // bool inner_gate_obstacle_enabled_; - - // std::vector virtual_obstacles_; - - // ViewPoint robot_pos_; - // bool robot_pos_set_ = false; - - void initGrids(); - openvdb::Vec3d point2vdb(const ViewPoint &p_in); - void clearCache(); - // bool isNearOdometry(const ViewPoint &p); - // bool isInGateObstacle(const ViewPoint &p); - // void checkOdomInInnerGateObstacle(); - - static inline ViewPoint subtract(const ViewPoint &p1, const ViewPoint &p2) - { - ViewPoint p_return; - p_return.x = p1.x - p2.x; - p_return.y = p1.y - p2.y; - p_return.z = p1.z - p2.z; - return p_return; - } - - static inline ViewPoint addPoint(const ViewPoint &p1, const ViewPoint &p2) - { - ViewPoint p_return; - p_return.x = p1.x + p2.x; - p_return.y = p1.y + p2.y; - p_return.z = p1.z + p2.z; - return p_return; - } - - static inline ViewPoint divide(const ViewPoint &p, double factor) - { - ViewPoint p_return; - p_return.x = p.x / factor; - p_return.y = p.y / factor; - p_return.z = p.z / factor; - return p_return; - } - - static inline double norm(const ViewPoint &p) - { - return sqrt(p.x * p.x + p.y * p.y + p.z * p.z); - } - - static inline ViewPoint multiply(const ViewPoint &p, double factor) - { - ViewPoint p_return; - p_return.x = p.x * factor; - p_return.y = p.y * factor; - p_return.z = p.z * factor; - return p_return; - } - - public: - CollisionChecker(); - - void init(const float kVoxSize, - const float length, - const float breadth, - const float height, - const float l_occ, - const float unknown_fraction_thresh, - const float occupied_fraction_thresh); - - // void callbackDarpaTF(const nav_msgs::Odometry::ConstPtr &odom_msg); - // void markStartGate(); - - void updateGridPtr(const openvdb::FloatGrid::Ptr &grid); - void setOdometryForVirtualObstacles(const ViewPoint &robot_pos); - - bool estimateSurfaceNormalAtHit(const openvdb::Vec3d &hit_point_world, double inflation_factor, openvdb::Vec3d &normal_out); - bool collisionCheckRayStrict(const ViewPoint &p_start, const ViewPoint &p_end, openvdb::Vec3d& hit_point); - bool collisionCheckInFreeSpace(const ViewPoint &p); - bool collisionCheckInFreeSpaceVector(const ViewPoint &p_start, const ViewPoint &p_end); - bool collisionCheckInFreeSpaceVectorStepSize(const ViewPoint &p_start, const ViewPoint &p_end, const float step_size); - - // void publishCollisionCheckerViz(); - // void publishGateOuterObstacleViz(); - // void publishGateInnerObstacleViz(); - - // void addVirtualObstacles(const PointSet &obstacle_locations); - // void clearExpiredVirtualObstacles(); - // bool isInVirtualObstacle(const ViewPoint &p); - // void publishVirtualObstacleViz(); - - // bool gate_obstacle_created_ = false; - - }; // class CollisionChecker - -} // namespace collision_checker_ns - -#endif \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/include/utils/rrt_planner.h b/robot/ros_ws/src/global/planners/exploration/include/utils/rrt_planner.h deleted file mode 100644 index 4a6362363..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/utils/rrt_planner.h +++ /dev/null @@ -1,187 +0,0 @@ - -#ifndef RRT_PLANNER_H -#define RRT_PLANNER_H - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "utils/utils.hpp" -#include "utils/viewpoint.hpp" -#include "utils/collision_checker.h" - -// Define a Tree -struct Tree -{ - PointSet vertices; - std::map edges; // point, parent - ViewPoint goal; - - void clear() - { - vertices.clear(); - edges.clear(); - } - void insertPoint(const ViewPoint &new_point, const ViewPoint &parent) - { - vertices.push_back(new_point); - edges[new_point] = parent; - } -}; - -class RRT_Planner -{ -public: - RRT_Planner(); - bool RRT_Init( // const float robot_model_cube_dim, - // const openvdb::BoolGrid::Ptr& grid_map, - // const float l_occ, - // const float voxel_dim, - const float norm_limit, - const float max_explore_dist, - const int max_iter, - const int max_connection_iter, - const int smooth_horizon, - // const bool consider_unknown_vox_occupied, - const int rrt_region_nodes_count, - const float rrt_region_nodes_radius, - const int rrt_region_nodes_z_layers_count_up, - const float rrt_region_nodes_z_layers_step, - const float dense_step); - - bool build_RRT(const ViewPoint &start_point, const ViewPoint &end_point, - collision_checker_ns::CollisionChecker &collision_checker, const bool reset_trees = true); - PointSet getPath(); - PointSet getCoarsePath(); - // void updateGridPtr(const openvdb::BoolGrid::Ptr& grid); - bool visualizeTrees(); - // void setRobotModelCubeDim(const float robot_model_cube_dim){kRobot_model_cube_dim_ = robot_model_cube_dim;} - - // Tree Structure - Tree start_tree_; - Tree end_tree_; - - /* ---------------------------------------------------------------------------- */ -private: - // Debugging publishers - // ros::Publisher viz_tree_start_pub_; - // ros::Publisher viz_tree_end_pub_; - - // RRT constants - float dense_step_; - float kNorm_limit_, kNorm_limit_sq_; - float kMax_explore_dist_; - // float kRobot_model_cube_dim_; - // float kVoxel_dim_; - int kMax_iter_; - int kMax_connection_iter_; - int kSmooth_horizon_; - bool kConsider_unknown_vox_occupied_; - int kRRT_region_nodes_count_; - float kRRT_region_nodes_radius_; - int kRRT_region_nodes_z_layers_count_up_; - float kRRT_region_nodes_z_layers_step_; - - // Random number generator - std::uniform_real_distribution distribution_uniform_; - std::default_random_engine generator_; - - // RRT variables - ViewPoint start_point_, end_point_; - ViewPoint last_point_; - bool last_point_found_ = false; - - // Solution - PointSet path_; - PointSet coarse_path_; - - // Mapping variables - // openvdb::BoolGrid::Ptr grid_map_ = NULL; - // float kL_occ_; - - // Main functions - ViewPoint randomPoint(ViewPoint goal); - void interpolate(const ViewPoint &p1, const ViewPoint &p2, PointSet &path); - void pathSmooth(PointSet &path, collision_checker_ns::CollisionChecker &collision_checker); - ViewPoint nearestPoint(const ViewPoint &p_rand, const PointSet &vertices); - void extend(const ViewPoint &p_rand, const ViewPoint &p_near, ViewPoint &p_new); - bool validEdge(const ViewPoint &p_new, const ViewPoint &p_near, collision_checker_ns::CollisionChecker &collision_checker); - bool extractPathFromGraph(collision_checker_ns::CollisionChecker &collision_checker); - bool nearGoal(const ViewPoint &p_new, const ViewPoint &goal); - void addRegionVertices(Tree &tree); - // bool collisionCheckInFreeSpace(const ViewPoint& p); - void swapTreePointers(Tree **a, Tree **b); - void reversePath(PointSet &path); - - // ViewPoint auxillary functions - static inline ViewPoint subtract(const ViewPoint &p1, const ViewPoint &p2) - { - ViewPoint p_return; - p_return.x = p1.x - p2.x; - p_return.y = p1.y - p2.y; - p_return.z = p1.z - p2.z; - return p_return; - } - - static inline ViewPoint addPoint(const ViewPoint &p1, const ViewPoint &p2) - { - ViewPoint p_return; - p_return.x = p1.x + p2.x; - p_return.y = p1.y + p2.y; - p_return.z = p1.z + p2.z; - return p_return; - } - - static inline ViewPoint divide(const ViewPoint &p, double factor) - { - ViewPoint p_return; - p_return.x = p.x / factor; - p_return.y = p.y / factor; - p_return.z = p.z / factor; - return p_return; - } - - static inline double norm(const ViewPoint &p) - { - return sqrt(p.x * p.x + p.y * p.y + p.z * p.z); - } - - static inline double normSq(const ViewPoint &p) - { - // use instead of norm for efficiency and when you don't need the sqrt - return (p.x * p.x + p.y * p.y + p.z * p.z); - } - - static inline ViewPoint multiply(const ViewPoint &p, double factor) - { - ViewPoint p_return; - p_return.x = p.x * factor; - p_return.y = p.y * factor; - p_return.z = p.z * factor; - return p_return; - } - - static inline bool equal(const ViewPoint &p1, const ViewPoint &p2) - { - if (p1.x != p2.x) - { - return false; - } - if (p1.y != p2.y) - { - return false; - } - if (p1.z != p2.z) - { - return false; - } - return true; - } -}; - -#endif \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/include/utils/utils.hpp b/robot/ros_ws/src/global/planners/exploration/include/utils/utils.hpp deleted file mode 100644 index 66ff4406b..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/utils/utils.hpp +++ /dev/null @@ -1,138 +0,0 @@ -#ifndef _OLE_3D_UTIL_H_ -#define _OLE_3D_UTIL_H_ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include -#include -#include - -using GridT = openvdb::Grid::Type>; - -// void generateVDBMarker(const GridT::Ptr& grid, -// const std::string& frame_id, -// visualization_msgs::msg::Marker& marker_msg); - -void generateVDBMarker(const openvdb::FloatGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg); - -void generateFrontierMarker(const openvdb::BoolGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg); - -void generateClusterMarker(const openvdb::FloatGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg); - -bool surface_edge_frontier(openvdb::FloatGrid::Accessor accessor, openvdb::Coord ijk); - -void create_clustering_coordbbox(const openvdb::BoolGrid::Ptr grid, - openvdb::math::CoordBBox &coord_bbox_out, - const openvdb::Vec3d ¢er, - const float frontier_count_cube_dim); - -void point_clustering_vis(openvdb::BoolGrid::Ptr grid, - const float cluster_cube_dim, - pcl::PointCloud::Ptr cluster_cloud, - const int voxel_cluster_count_thresh, - std::vector> &clustered_points, - openvdb::FloatGrid::Ptr visualized_clusters); - -void mark_voxels_bool(const std::vector &point_list, - openvdb::BoolGrid::Ptr grid, - const bool voxel_value); - -class VDBUtil -{ -public: - static void updateOccMapFromNdArray(openvdb::FloatGrid::Ptr grid_logocc, - pcl::PointCloud::Ptr xyz, - const tf2::Vector3 &origin, - float l_free = -0.5f, // - a value if not hit - float l_occ = 1.0f, // + a value if hit - float l_min = -10.0f, // lower bound of free - float l_max = 10.0f, // upper bound of occ - int hit_thickness = 1, - double max_range = 50, - float visited_cleared_logocc_min_thresh = 8.0f); - - static void setVoxelSize(openvdb::GridBase &grid, double vs); - - static Eigen::Vector3d computeCentroid(const std::vector &point_list); - - static bool collCheckPointInPartialFreeSpace(const openvdb::FloatGrid::Ptr grid, - const Eigen::Vector3d &point_xyz, - const float length, - const float breadth, - const float height, - const float l_occ, - const float unknown_fraction_thresh, - const float occupied_fraction_thresh); - - static bool checkCollisionAlongRay(const openvdb::FloatGrid::Ptr& grid, - const openvdb::Vec3d &p1_xyz, - const openvdb::Vec3d &p2_xyz, - const float bbox_length, - const float bbox_breadth, - const float bbox_height, - const float l_occ, - openvdb::Vec3d &hit_point, - float dist_offset_from_cluster, - bool consider_unknown_occupied); - - static long int countVoxelsInsideBbox(const openvdb::FloatGrid::Ptr &grid, - const Eigen::Vector3d &bbox_center, - const float length, - const float breadth, - const float height); - - static void createCoordBBox(const openvdb::FloatGrid::Ptr grid, - openvdb::math::CoordBBox &coord_bbox_out, - const openvdb::Vec3d ¢er, - const float bbox_length, - const float bbox_breadth, - const float bbox_height); - - static bool checkIntersectWithCoordBBox(const openvdb::math::CoordBBox &search_bbox, - const openvdb::FloatGrid::Ptr grid, - float occupancy_threshold, - bool consider_unknown_occupied); - - static openvdb::Vec3d convertEigenVecToVec3D(const Eigen::Vector3d &point); - -}; // class VDBUtil -#endif \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/include/utils/viewpoint.hpp b/robot/ros_ws/src/global/planners/exploration/include/utils/viewpoint.hpp deleted file mode 100644 index 15041d4ab..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/utils/viewpoint.hpp +++ /dev/null @@ -1,318 +0,0 @@ -// Modified from cmusubt drone_traj_planner - -#ifndef CUSTOM_POINT_H -#define CUSTOM_POINT_H - -#include -#include -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#define PI 3.14159265359 -#define TWO_PI 6.28318530718 - -// Define a Point -struct ViewPoint -{ - // Position - double x; - double y; - double z; - - bool orientation_set = false; - geometry_msgs::msg::Quaternion orientation; - double orientation_yaw; // equivalent to the yaw within orientation - - // Scoring - bool score_initialized = false; - double score; - bool cleared_by_other_robot = false; - - // Viewpoint score (from map processor) - bool viewpoint_score_initialized = false; - double viewpoint_score; -}; - -// Define a set of Points -typedef std::vector PointSet; - -// Define operators (for map to work) -static inline bool operator>(const ViewPoint &i, const ViewPoint &j) -{ - if (i.x > j.x) - { - return true; - } - else if (i.x == j.x) - { - if (i.y > j.y) - { - return true; - } - else if (i.y == j.y) - { - if (i.z > j.z) - { - return true; - } - else - { - return false; - } - } - else - { - return false; - } - } - else - { - return false; - } -} - -static inline bool operator<(const ViewPoint &i, const ViewPoint &j) -{ - if (i.x < j.x) - { - return true; - } - else if (i.x == j.x) - { - if (i.y < j.y) - { - return true; - } - else if (i.y == j.y) - { - if (i.z < j.z) - { - return true; - } - else - { - return false; - } - } - else - { - return false; - } - } - else - { - return false; - } -} - -static inline void printPoint(const ViewPoint &p) -{ - std::cout << "(" << p.x << ", " << p.y << ", " << p.z << ") {" << p.orientation.x << ", " << p.orientation.y << ", " << p.orientation.z << ", " << p.orientation.w << "}" << std::endl; -} - -static inline double distancePoint2Point(const ViewPoint &p1, const ViewPoint &p2) -{ - // Euclidean distance - double x_diff = p1.x - p2.x; - double y_diff = p1.y - p2.y; - double z_diff = p1.z - p2.z; - - return sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff); -} - -static inline double angleDiffPoint2Point(const ViewPoint &p1, const ViewPoint &p2) -{ - if (!p1.orientation_set || !p2.orientation_set) - { - return 0.0; - } - else - { - double angle_diff = p2.orientation_yaw - p1.orientation_yaw; - - // wrap around - if (angle_diff > PI) - { - angle_diff -= TWO_PI; - } - else if (angle_diff < -PI) - { - angle_diff += TWO_PI; - } - - return angle_diff; - } -} - -///////////////////// -// PCL -typedef pcl::PointXYZI PCLPointType; -typedef pcl::PointCloud PCLCloudType; - -static inline Eigen::Vector3d point2eigen(const ViewPoint& p) -{ - return Eigen::Vector3d(p.x, p.y, p.z); -} - -static inline openvdb::Vec3d point2vdb(const ViewPoint& p) -{ - return openvdb::Vec3d(p.x, p.y, p.z); -} - -static inline PCLPointType point2PCL(const ViewPoint &p) -{ - PCLPointType point; - point.x = p.x; - point.y = p.y; - point.z = p.z; - // point.intensity = p.score; - point.intensity = 0.0; - return point; -} - -static inline ViewPoint pcl2Point(const PCLPointType &pcl_point) -{ - ViewPoint point; - point.x = pcl_point.x; - point.y = pcl_point.y; - point.z = pcl_point.z; - return point; -} - -static inline PCLCloudType pointSet2PCL(const PointSet &ps) -{ - PCLCloudType pcl_cloud; - for (const auto &p : ps) - { - PCLPointType pcl_point = point2PCL(p); - pcl_cloud.push_back(pcl_point); - } - return pcl_cloud; -} - -///////////////////// -// Geometry msgs -static inline ViewPoint geometryMsgs2Point(const geometry_msgs::msg::Point &geom_point) -{ - ViewPoint point; - point.x = geom_point.x; - point.y = geom_point.y; - point.z = geom_point.z; - return point; -} - -static inline ViewPoint geometryMsgs2Point(const geometry_msgs::msg::Pose& geom_pose) -{ - ViewPoint point; - point.x = geom_pose.position.x; - point.y = geom_pose.position.y; - point.z = geom_pose.position.z; - - // tf2 (ROS 2-friendly) - double roll, pitch, yaw; - tf2::Quaternion quat_tf; - tf2::fromMsg(geom_pose.orientation, quat_tf); - tf2::Matrix3x3(quat_tf).getRPY(roll, pitch, yaw); - - point.orientation = geom_pose.orientation; - point.orientation_set = true; - point.orientation_yaw = yaw; - return point; -} - -static inline geometry_msgs::msg::Point point2GeometryMsgs(const ViewPoint &point) -{ - geometry_msgs::msg::Point geom_point; - geom_point.x = point.x; - geom_point.y = point.y; - geom_point.z = point.z; - return geom_point; -} - -static inline geometry_msgs::msg::Pose point2GeometryMsgsPose(const ViewPoint &point) -{ - geometry_msgs::msg::Pose geom_pose; - geom_pose.position.x = point.x; - geom_pose.position.y = point.y; - geom_pose.position.z = point.z; - if (point.orientation_set) - { - geom_pose.orientation = point.orientation; - } - else - { - geom_pose.orientation.x = 0.0; - geom_pose.orientation.y = 0.0; - geom_pose.orientation.z = 0.0; - geom_pose.orientation.w = 1.0; - } - return geom_pose; -} - -static inline geometry_msgs::msg::PoseArray pointSet2GeometryMsgsPoseArray(const PointSet &point_set) -{ - geometry_msgs::msg::PoseArray msg; - for (const auto &point : point_set) - { - msg.poses.push_back(point2GeometryMsgsPose(point)); - } - return msg; -} - -static inline nav_msgs::msg::Path pointSet2NavMsgsPath(const PointSet &point_set, const std_msgs::msg::Header &header) -{ - nav_msgs::msg::Path msg; - msg.header = header; - for (const auto &point : point_set) - { - geometry_msgs::msg::PoseStamped pose_stamped; - pose_stamped.header = header; - pose_stamped.pose = point2GeometryMsgsPose(point); - msg.poses.push_back(pose_stamped); - } - return msg; -} - -/////////////////// -// Viewpoints -static inline ViewPoint pclHSVtoPoint(const pcl::PointXYZHSV &pcl_point) -{ - ViewPoint point; - - // Position - point.x = pcl_point.x; - point.y = pcl_point.y; - point.z = pcl_point.z; - - // Score (from HSV 's' field) - point.viewpoint_score = pcl_point.s; - point.viewpoint_score_initialized = true; - - // Orientation from heading (HSV 'h' field) - tf2::Quaternion q; - q.setRPY(0.0, 0.0, pcl_point.h); // roll=0, pitch=0, yaw=heading - point.orientation = tf2::toMsg(q); - point.orientation_set = true; - - // Store yaw separately - point.orientation_yaw = pcl_point.h; - - return point; -} - -#endif \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/include/viewpoint_sampling.hpp b/robot/ros_ws/src/global/planners/exploration/include/viewpoint_sampling.hpp deleted file mode 100644 index 97a03b3a0..000000000 --- a/robot/ros_ws/src/global/planners/exploration/include/viewpoint_sampling.hpp +++ /dev/null @@ -1,121 +0,0 @@ -#ifndef _VIEWPOINT_SAMPLING_H_ -#define _VIEWPOINT_SAMPLING_H_ - -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include -#include "utils/utils.hpp" - -namespace viewpoint_sampling_ns -{ - class ViewpointSampling; -} // namespace viewpoint_sampling_ns - -class viewpoint_sampling_ns::ViewpointSampling -{ - -public: - using ListOfClustersT = std::vector>; - pcl::PointCloud::Ptr viewpoint_list_; - -private: - int cluster_idx_; - - // params - double viewing_distance_; - float bbox_length_, bbox_breadth_, bbox_height_; - float l_occ_, vox_size_; - uint16_t num_viewp_to_sample_; - double viewing_distance_inner_r_, viewing_distance_outer_r_, viewing_distance_z_offset_; - - // offset the endpoint for collision checking along the ray connecting the viewp to cluster centroid - // fraction of the robot model bbox so that the collision checking above is done for a small bbox - double coll_check_endpoint_offset_, bbox_fraction_viewp_centroid_coll_check_; - - // flags - uint8_t solver_type_; - - // viewpoint attributes - double current_cluster_score_, heading_; - double viewp_bbox_unknown_frac_thresh_, viewp_bbox_occ_frac_thresh_; - - // lists - std::shared_ptr cluster_list_; - std::shared_ptr> cluster_score_list_; - std::vector xy_points_; - pcl::PointCloud::Ptr centroid_list_; - std::vector line_y_points_, line_x_points_; - std::vector cluster_x_points_, cluster_y_points_; - - // current geometric entities - Eigen::Vector3d current_cluster_centroid_; - Eigen::Matrix opposing_viewpoints_; - Eigen::Vector3d direction_vector_; - Eigen::Vector2d fitted_line_; - Eigen::MatrixXd A_mat_; - Eigen::VectorXd b_vec_; - Eigen::Vector3d selected_viewpoint_; - openvdb::FloatGrid::Ptr grid_map_; - -public: - ViewpointSampling(ListOfClustersT &cluster_list, - double viewing_distance, - float bbox_length, - float bbox_breadth, - float bbox_height, - float l_occ, - float vox_size, - uint16_t num_viewp_sample_per_cluster, - double viewing_distance_inner_r, - double viewing_distance_outer_r, - double viewing_distance_z_offset, - double coll_check_endpoint_offset, - double bbox_fraction_viewp_centroid_coll_check, - double viewp_bbox_unknown_frac_thresh, - double viewp_bbox_occ_frac_thresh); - - // data handling - void updateGridPtr(const openvdb::FloatGrid::Ptr &grid); - void updateCentroidListPtr(const pcl::PointCloud::Ptr ¢roid_list); - void reset(ListOfClustersT &cluster_list); - - // utility methods - void sampleViewpoints(); - void projectToXYPlane(); - void computePointsPerpToLine(); - void selectOnePoint(); - float computeYawFromDirVector(); - bool fitLineToPoints(int solver_flag); - void useSVDDecomposition(); - void useQRDecomposition(); - void useNormalEquations(); - void createAMatrix(); - void createBMatrix(); - Eigen::Vector3d computeMean(); - pcl::PointXYZHSV constructViewpoint(); - void sampleViewpointsAroundCentroid(); - int sampleViewpointsAroundCentroidParamaterized(const double viewing_distance, - const double z_offset, - const double angle_spacing); - - // logging and plotting - std::string setupFilePath(int file_index, std::string file_name); - void writeToCSVfile(std::string file_name, Eigen::MatrixXd &matrix); - void writeVecToCSVfile(std::string file_name, - std::vector &points); - void logDataToFile(); - void plotPointsAndLine(); - void drawPointsAlongLine(float resolution, float length); - void toXVecYVec(); - -}; // class ViewpointSampling -#endif \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml b/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml deleted file mode 100644 index 24e9ee2ca..000000000 --- a/robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/package.xml b/robot/ros_ws/src/global/planners/exploration/package.xml deleted file mode 100644 index b313f8ecc..000000000 --- a/robot/ros_ws/src/global/planners/exploration/package.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - exploration_planner - 0.0.0 - Frontier-based geometric exploration planner — kept as the intended alternative to random_walk for planner selection from the RViz Tasks Panel (future) - - Andrew Jong - - BSD-3-Clause-Clear - - ament_cmake - - rclcpp - rclcpp_action - std_msgs - std_srvs - geometry_msgs - nav_msgs - visualization_msgs - tf2 - tf2_ros - tf2_eigen - tf2_geometry_msgs - pcl_conversions - message_filters - - eigen - libpcl-all-dev - libopenvdb-dev - - - ament_cmake - - diff --git a/robot/ros_ws/src/global/planners/exploration/src/collision_checker.cpp b/robot/ros_ws/src/global/planners/exploration/src/collision_checker.cpp deleted file mode 100644 index 440b85690..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/collision_checker.cpp +++ /dev/null @@ -1,255 +0,0 @@ - -#include "utils/collision_checker.h" - -namespace collision_checker_ns -{ - - void CollisionChecker::init(const float kVoxSize, - const float length, - const float breadth, - const float height, - const float l_occ, - const float unknown_fraction_thresh, - const float occupied_fraction_thresh) - - { - kVoxSize_ = kVoxSize; - kBboxLength_ = length; - kBboxBreadth_ = breadth; - kBboxHeight_ = height; - kL_occ_ = l_occ; - kUnknown_fraction_thresh_ = unknown_fraction_thresh; - kOccupied_fraction_thresh_ = occupied_fraction_thresh; - - initGrids(); - } - - void CollisionChecker::initGrids() - { - // Grid used to cache collisions - collision_grid_ = openvdb::BoolGrid::create(false); - VDBUtil::setVoxelSize(*collision_grid_, kVoxSize_); - } - - inline openvdb::Vec3d CollisionChecker::point2vdb(const ViewPoint &p_in) - { - openvdb::Vec3d p_out(p_in.x, p_in.y, p_in.z); - return p_out; - } - - void CollisionChecker::updateGridPtr(const openvdb::FloatGrid::Ptr &grid) - { - grid_map_ = grid; - // clearExpiredVirtualObstacles(); - clearCache(); - } - - void CollisionChecker::clearCache() - { - collision_grid_->clear(); - // ROS_INFO_STREAM("hits: " << count_hits_ << " misses: " << count_misses_); - } - - bool CollisionChecker::collisionCheckRayStrict(const ViewPoint &p_start, const ViewPoint &p_end, openvdb::Vec3d &hit_point) - { - openvdb::Vec3d vec_start = point2vdb(p_start); - openvdb::Vec3d vec_end = point2vdb(p_end); - - return VDBUtil::checkCollisionAlongRay(grid_map_, vec_start, vec_end, - kBboxLength_, kBboxBreadth_, kBboxHeight_, - kL_occ_, hit_point, 0, false); - } - - bool CollisionChecker::collisionCheckInFreeSpace(const ViewPoint &p) - { - - // Convert to PCL point - Eigen::Vector3d query_pt = point2eigen(p); - - // Map does not exist - if (grid_map_ == NULL) - { - return false; - } - - // Setup cache accessor - openvdb::BoolGrid::Accessor collision_grid_acc = collision_grid_->getAccessor(); - const openvdb::math::Transform &collision_grid_tf(collision_grid_->transform()); - - openvdb::Vec3d cache_xyz = point2vdb(p); - openvdb::Coord cache_ijk = collision_grid_tf.worldToIndexCellCentered(cache_xyz); - - // Check cache - bool cache_hit = false; - bool cache_value; // only valid if cache_hit=true - if (!collision_grid_->empty()) - { - cache_hit = collision_grid_acc.probeValue(cache_ijk, cache_value); - } - - // Do collision checking using cache or explicit check - bool is_in_collision; - - if (cache_hit) - { - // Cache hit -- use stored value - count_hits_++; - is_in_collision = cache_value; - } - else - { - - // Cache miss -- do collision check - count_misses_++; - is_in_collision = !VDBUtil::collCheckPointInPartialFreeSpace(grid_map_, query_pt, - kBboxLength_, kBboxBreadth_, kBboxHeight_, - kL_occ_, kUnknown_fraction_thresh_, kOccupied_fraction_thresh_); - - // Add result to cache for next time - collision_grid_acc.setValueOn(cache_ijk, is_in_collision); - } - - // Ship it! - return is_in_collision; - } - - bool CollisionChecker::collisionCheckInFreeSpaceVector(const ViewPoint &p_start, const ViewPoint &p_end) - { - - // Map does not exist - if (grid_map_ == NULL) - { - return false; - } - - // Setup vector - ViewPoint check_point = p_start; - ViewPoint direction = subtract(p_end, p_start); - double diff = norm(direction); - direction = divide(direction, diff); - ViewPoint step = multiply(direction, double(kVoxSize_)); - - int num_steps = ceil(diff / kVoxSize_); - - // Step along the vector - for (int i = 0; i < num_steps; i++) - { - // Get next point on vector - check_point = addPoint(check_point, step); - - // Collision check - if (collisionCheckInFreeSpace(check_point)) - { - // Collision found - return true; - } - } - return false; - } - - // same as collisionCheckInFreeSpaceVector but with a custom step size - bool CollisionChecker::collisionCheckInFreeSpaceVectorStepSize(const ViewPoint &p_start, const ViewPoint &p_end, const float step_size) - { - - // Map does not exist - if (grid_map_ == NULL) - { - return false; - } - - // Setup vector - ViewPoint check_point = p_start; - ViewPoint direction = subtract(p_end, p_start); - double diff = norm(direction); - direction = divide(direction, diff); - ViewPoint step = multiply(direction, double(step_size)); - - int num_steps = ceil(diff / step_size); - - // Step along the vector - for (int i = 0; i < num_steps; i++) - { - - // Get next point on vector - check_point = addPoint(check_point, step); - - // Collision check - if (collisionCheckInFreeSpace(check_point)) - { - - // Collision found - return true; - } - } - return false; - } - - bool CollisionChecker::estimateSurfaceNormalAtHit(const openvdb::Vec3d &hit_point_world, - double inflation_factor, - openvdb::Vec3d &normal_out) - { - if (!grid_map_ || grid_map_->empty()) - { - return false; - } - - // Clamp inflation to non-negative. - const double infl = std::max(0.0, inflation_factor); - - // Build an inflated world-space AABB around the hit using your helper. - // We inflate the robot bbox uniformly by (1 + infl). - const float len = std::max(kVoxSize_, kBboxLength_ * static_cast(1.0 + infl)); - const float brd = std::max(kVoxSize_, kBboxBreadth_ * static_cast(1.0 + infl)); - const float hgt = std::max(kVoxSize_, kBboxHeight_ * static_cast(1.0 + infl)); - - openvdb::math::CoordBBox search_bbox; - VDBUtil::createCoordBBox(grid_map_, search_bbox, hit_point_world, len, brd, hgt); - - // Accessor & transform - openvdb::FloatGrid::Accessor acc = grid_map_->getAccessor(); - const openvdb::math::Transform &tf = grid_map_->transform(); - - // Accumulate centroid of OCCUPIED voxels (value > kL_occ_) inside the bbox. - openvdb::Vec3d sum_world(0.0, 0.0, 0.0); - std::size_t count = 0; - - for (auto it = search_bbox.beginXYZ(); it; ++it) - { - const openvdb::Coord ijk = *it; - - float v = 0.0f; - const bool active = acc.probeValue(ijk, v); - if (active && v > kL_occ_) - { - // Use voxel center in world space - sum_world += tf.indexToWorld(ijk); - ++count; - } - } - - if (count == 0) - { - // No occupied mass found in the (inflated) neighborhood — cannot infer a normal. - return false; - } - - const openvdb::Vec3d centroid_world = sum_world / static_cast(count); - - // Outward direction: from obstacle mass centroid -> hit point. - openvdb::Vec3d n = hit_point_world - centroid_world; - const double nlen = n.length(); - if (nlen <= 1e-9) - { - // Degenerate (e.g., centroid == hit); no stable direction. - return false; - } - - normal_out = n / nlen; // unit vector, world coordinates - return true; - } - - CollisionChecker::CollisionChecker() - { - } - -} // namespace collision_checker_ns \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/src/exploration_logic.cpp b/robot/ros_ws/src/global/planners/exploration/src/exploration_logic.cpp deleted file mode 100644 index 95633b0b9..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/exploration_logic.cpp +++ /dev/null @@ -1,762 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// 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. - -#include "../include/exploration_logic.hpp" - -ExplorationPlanner::ExplorationPlanner(init_params params) -{ - this->max_start_to_goal_dist_m_ = params.max_start_to_goal_dist_m; - this->checking_point_cnt = params.checking_point_cnt; - this->max_z_change_m_ = params.max_z_change_m; - this->voxel_size_m = params.voxel_size_m; - this->collision_padding_m = params.collision_padding_m; - this->path_end_threshold_m = params.path_end_threshold_m; - this->max_yaw_change_degrees = params.max_yaw_change_degrees; - - this->kLOcc_ = params.kLOcc_; - this->kVoxSize_ = params.kVoxSize_; - this->kViewingDistance_ = params.kViewingDistance_; - this->kBboxLength_ = params.kBboxLength_; - this->kBboxBreadth_ = params.kBboxBreadth_; - this->kBboxHeight_ = params.kBboxHeight_; - this->kNumViewpointsPerCluster_ = params.kNumViewpointsPerCluster_; - this->kViewingDistanceInnerR_ = params.kViewingDistanceInnerR_; - this->kViewingDistanceOuterR_ = params.kViewingDistanceOuterR_; - this->kViewingDistanceZOffset_ = params.kViewingDistanceZOffset_; - this->kCollCheckEndpointOffset_ = params.kCollCheckEndpointOffset_; - this->kRobotModelBBoxFraction_ = params.kRobotModelBBoxFraction_; - this->kViewpBBoxUnknownFracThresh_ = params.kViewpBBoxUnknownFracThresh_; - this->kViewpBBoxOccupiedFracThresh_ = params.kViewpBBoxOccupiedFracThresh_; - - this->too_close_distance_ = params.too_close_distance; - this->too_close_penalty_ = params.too_close_penalty; - this->odometry_match_distance_ = params.odometry_match_distance; - this->odometry_match_angle_ = params.odometry_match_angle; - this->odometry_match_penalty_ = params.odometry_match_penalty; - this->momentum_collision_check_distance_ = params.momentum_collision_check_distance; - this->momentum_collision_check_distance_min_ = params.momentum_collision_check_distance_min; - this->momentum_change_max_reward_ = params.momentum_change_max_reward; - this->momentum_change_collision_reward_ = params.momentum_change_collision_reward; - this->viewpoint_neighborhood_too_close_radius_ = params.viewpoint_neighborhood_too_close_radius; - this->viewpoint_neighborhood_sphere_radius_ = params.viewpoint_neighborhood_sphere_radius; - this->viewpoint_neighborhood_count_ = params.viewpoint_neighborhood_count; - this->momentum_minimum_distance_ = params.momentum_minimum_distance; - this->momentum_time_ = params.momentum_time; - this->momentum_collision_check_step_size_ = params.momentum_collision_check_step_size; - - this->planner_norm_limit_ = params.planner_norm_limit_; - this->max_explore_dist_ = params.max_explore_dist_; - this->planner_max_iter_ = params.planner_max_iter_; - this->max_connection_iter_ = params.max_connection_iter_; - this->traj_smooth_horizon_ = params.traj_smooth_horizon_; - this->rrt_region_nodes_count_ = params.rrt_region_nodes_count_; - this->rrt_region_nodes_radius_ = params.rrt_region_nodes_radius_; - this->rrt_region_nodes_z_layers_count_up_ = params.rrt_region_nodes_z_layers_count_up_; - this->rrt_region_nodes_z_layers_step_ = params.rrt_region_nodes_z_layers_step_; - - this->priority_level_thred_ = params.priority_level_thred_; - this->dense_step_ = params.dense_step_; - - viewp_sample_ = std::make_shared(clustered_points_, - kViewingDistance_, - kBboxLength_, - kBboxBreadth_, - kBboxHeight_, - kLOcc_, - kVoxSize_, - uint16_t(kNumViewpointsPerCluster_), - kViewingDistanceInnerR_, - kViewingDistanceOuterR_, - kViewingDistanceZOffset_, - kCollCheckEndpointOffset_, - kRobotModelBBoxFraction_, - kViewpBBoxUnknownFracThresh_, - kViewpBBoxOccupiedFracThresh_); - - collision_checker_.init(kVoxSize_, - kBboxLength_, - kBboxBreadth_, - kBboxHeight_, - kLOcc_, - kViewpBBoxUnknownFracThresh_, - kViewpBBoxOccupiedFracThresh_); - - rrt_planner_.RRT_Init(planner_norm_limit_, - max_explore_dist_, - planner_max_iter_, - max_connection_iter_, - traj_smooth_horizon_, - rrt_region_nodes_count_, - rrt_region_nodes_radius_, - rrt_region_nodes_z_layers_count_up_, - rrt_region_nodes_z_layers_step_, - dense_step_); -} - -std::optional ExplorationPlanner::plan_to_given_waypoint(const ViewPoint& start_point, const ViewPoint& goal_point) -{ - Path output_path; - bool is_traj = false; - - is_traj = rrt_planner_.build_RRT(start_point, goal_point, collision_checker_, true); - - // success - if (is_traj) - { - PointSet traj = rrt_planner_.getPath(); - output_path.reserve(traj.size()); - - // convert to our path format - for (const auto &vp : traj) - { - float yaw_val; - if (vp.orientation_set) - { - // Convert quaternion to yaw - tf2::Quaternion q(vp.orientation.x, vp.orientation.y, vp.orientation.z, vp.orientation.w); - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - yaw_val = static_cast(yaw); - } - else - { - yaw_val = static_cast(vp.orientation_yaw); // fallback if yaw is already set - } - - output_path.emplace_back(static_cast(vp.x), - static_cast(vp.y), - static_cast(vp.z), - yaw_val); - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "RRT Path generated"); - - return output_path; - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), - "No RRT path created"); - - return output_path; -} - -std::optional ExplorationPlanner::select_viewpoint_and_plan(const ViewPoint& start_point, float timeout_duration) -{ - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "Start planning"); - robot_pos_ = start_point; - - this->sampled_viewpoints.clear(); - this->sampled_viewpoints.reserve(this->viewp_sample_->viewpoint_list_->size()); - - for (const auto &p : *this->viewp_sample_->viewpoint_list_) - { - this->sampled_viewpoints.push_back(pclHSVtoPoint(p)); - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "Computing Viewpoint Scores"); - - for (int viewpoint_index = 0; viewpoint_index < this->sampled_viewpoints.size(); viewpoint_index++) - { - double euclidean_distance = distancePoint2Point(start_point, this->sampled_viewpoints[viewpoint_index]); - double momentum_change_reward = 0; - double momentum_change_angle = angleBetweenHeadingAndMomentum(this->sampled_viewpoints[viewpoint_index]); - momentum_change_reward = (std::cos(momentum_change_angle) + 1.0) / 2.0; // 0 (behind) and 1 (in front) - double momentum_change_reward_before_collision_check = momentum_change_reward; - - if ((euclidean_distance >= momentum_collision_check_distance_min_ && euclidean_distance <= momentum_collision_check_distance_) && - (!collision_checker_.collisionCheckInFreeSpaceVector(start_point, this->sampled_viewpoints[viewpoint_index]))) - { - // Line of sight collision free, therefore add momentum reward - momentum_change_reward *= momentum_change_max_reward_; - } - else - { - // Line of sight blocked, therefore just add a small reward - // since path planning will likely require changing directions anyway - momentum_change_reward *= momentum_change_collision_reward_; - } - // score from viewpoint sampler - double map_processor_score = this->sampled_viewpoints[viewpoint_index].viewpoint_score; - - double score = -euclidean_distance + momentum_change_reward + map_processor_score; - - // Heavily penalize if viewpoint is too close to current location or lookahead point - if (euclidean_distance < too_close_distance_) - { - score -= too_close_penalty_ - 2.0 * euclidean_distance; - } - - // Similarly, reward if there's lots of viewpoints in cylinder between planning point and viewpoint - float cylinder_reward = 0.0; - if (euclidean_distance >= too_close_distance_) - { - cylinder_reward = getCylinderReward(viewpoint_index, start_point) * (0.25 + 0.75 * momentum_change_reward_before_collision_check); - } - score += cylinder_reward; - - // Heavily penalize if robot has already been to this viewpoint - bool already_been_to_viewpoint = doesPointMatchTrajectory(this->sampled_viewpoints[viewpoint_index]); - if (already_been_to_viewpoint) - { - // ROS_ERROR("Already been to this viewpoint!"); - score -= odometry_match_penalty_; - } - - if ((!this->sampled_viewpoints[viewpoint_index].score_initialized) || (score > this->sampled_viewpoints[viewpoint_index].score)) - { - this->sampled_viewpoints[viewpoint_index].score = score; - this->sampled_viewpoints[viewpoint_index].score_initialized = true; - - // Ignore the viewpoint if the map_processor score is not 200 (unobserved by other robots) or 150 (frontier of other robot) - // Also ignore if this robot has already been near it - if (map_processor_score < 100 || already_been_to_viewpoint == true) - // if (already_been_to_viewpoint) - { - this->sampled_viewpoints[viewpoint_index].cleared_by_other_robot = true; - } - } - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "RRT Start"); - // RRT to generate path - Path output_path; - bool is_traj = false; - - if (this->sampled_viewpoints.empty()) - { - RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), - "sampled_viewpoints empty"); - } - - for (int priority_level = 0; priority_level < priority_level_thred_ && priority_level < this->sampled_viewpoints.size(); priority_level++) - { - goalSelector(priority_level); - is_traj = rrt_planner_.build_RRT(start_point, explore_goal_, collision_checker_, true); - - // success - if (is_traj) - { - PointSet traj = rrt_planner_.getPath(); - output_path.reserve(traj.size()); - - // convert to our path format - for (const auto &vp : traj) - { - float yaw_val; - if (vp.orientation_set) - { - // Convert quaternion to yaw - tf2::Quaternion q(vp.orientation.x, vp.orientation.y, vp.orientation.z, vp.orientation.w); - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - yaw_val = static_cast(yaw); - } - else - { - yaw_val = static_cast(vp.orientation_yaw); // fallback if yaw is already set - } - - output_path.emplace_back(static_cast(vp.x), - static_cast(vp.y), - static_cast(vp.z), - yaw_val); - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "RRT Path generated"); - - return output_path; - } - } - - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), - "No RRT path created"); - return output_path; -} - -void ExplorationPlanner::goalSelector(int priority_level) -{ - // INPUT : viewpoint_cloud, Graph, robot_pos_; - // OUTPUT: explore_goal_; - - if (priority_level == 0) - { - // assignAwardToFrontiers(); - std::sort(this->sampled_viewpoints.begin(), this->sampled_viewpoints.end(), compareFrontier); - } - - if (priority_level < this->sampled_viewpoints.size()) - { - explore_goal_ = this->sampled_viewpoints[priority_level]; - explore_goal_cleared_by_other_robot_ = this->sampled_viewpoints[priority_level].cleared_by_other_robot; - } - else - { - RCLCPP_WARN(rclcpp::get_logger("exploration_planner"), "priority level > num of sampled viewpoints"); - } -} - -bool ExplorationPlanner::compareFrontier(const ViewPoint &p1, const ViewPoint &p2) -{ - // Using this comparitor will sort from largest to smallest score - if (p1.score > p2.score) - { - return true; - } - return false; -} - -bool ExplorationPlanner::doesPointMatchTrajectory(const ViewPoint &point) -{ - // Does the point match any point in executed_trajectory_ - // within given tolerances - - double distance, angle; - - // Iterate through the executed trajectory, looking for a match - for (const ViewPoint &executed_point : executed_trajectory_) - { - - // Check if it matches - distance = distancePoint2Point(executed_point, point); - if (distance <= odometry_match_distance_) - { - - angle = angleDiffPoint2Point(executed_point, point); - if (angle >= -odometry_match_angle_ && angle <= odometry_match_angle_) - { - // Match found! - return true; - } - } - } - return false; -} - -bool ExplorationPlanner::extractCylinderViewpointDistances( - const int &viewpoint_index, const ViewPoint &planning_point, std::vector &viewpoint_distances) -{ - viewpoint_distances.clear(); - - double distance_pp_to_v = distancePoint2Point(planning_point, this->sampled_viewpoints[viewpoint_index]); - - // Only reward viewpoints that are between too_close_distance_ and viewpoint_neighborhood_too_close_radius_ - // distance from planning point - if ((distance_pp_to_v > viewpoint_neighborhood_too_close_radius_) || (distance_pp_to_v < too_close_distance_)) - { - return false; - } - - // Count how many neighbors - int count = 0; - - // Iterate through all viewpoints - // INCLUDING itself - for (int i = 0; i < this->sampled_viewpoints.size(); ++i) - { - double distance_pp_to_viewpoint = distancePoint2Point(planning_point, this->sampled_viewpoints[i]); - - double distance_odom_to_viewpoint = distancePoint2Point(robot_pos_, this->sampled_viewpoints[i]); - - // If this viewpoint is closer than the subject viewpoint but is not too_close to planning point - // Then this *might* be a neighbor - if ((distance_pp_to_viewpoint <= distance_pp_to_v + 0.01) && (distance_pp_to_viewpoint > too_close_distance_) && (distance_odom_to_viewpoint > too_close_distance_)) - { - - // Extract the cylinder distances - double parallel_distance, perpendicular_distance; - bool on_line_seg = lineSegDistances(this->sampled_viewpoints[i], planning_point, this->sampled_viewpoints[viewpoint_index], - parallel_distance, perpendicular_distance); - - // Is on line segment through center of cylinder - if (on_line_seg) - { - - // Check if in 45deg cone from planning_point - if (perpendicular_distance <= parallel_distance) - { - - // Check if within cylinder - if (perpendicular_distance <= viewpoint_neighborhood_sphere_radius_) - { - // Then viewpoint is considered a "neighbor" - ++count; - viewpoint_distances.push_back(parallel_distance); - } - } - } - } - } - - // Reward this viewpoint if lots of neighboring viewpoints found - return count >= viewpoint_neighborhood_count_; -} - -float ExplorationPlanner::getCylinderReward(const int &viewpoint_index, const ViewPoint &planning_point) -{ - // Find if there's a consecutive set of viewpoints in cylinder between planning_point and viewpoint - double distance_pp_to_v = distancePoint2Point(planning_point, this->sampled_viewpoints[viewpoint_index]); - - // Extract the distances from planning_point for all viewpoints in cylinder - std::vector viewpoint_distances; - bool has_viewpoint_neighborhood = extractCylinderViewpointDistances(viewpoint_index, planning_point, viewpoint_distances); - - if (!has_viewpoint_neighborhood) - { - // no reward - return 0.0; - } - - // Process these distances - - // Sort in DESCENDING order - std::sort(viewpoint_distances.begin(), viewpoint_distances.end(), std::greater()); - - // Search through the distance in descending order - // Looking for a "gap" - float gap_start = 0; - int count_neighbors = 0; - for (int i = 1; i < viewpoint_distances.size(); i++) - { - float previous = viewpoint_distances[i - 1]; - float current = viewpoint_distances[i]; - - float gap = previous - current; // previous is greater than current - - if (gap > 3.0) - { - // Gap found - if (i < viewpoint_neighborhood_count_) - { - // No reward - return 0.0; - } - - // Stop searching for gap - gap_start = previous; - count_neighbors = i; - break; - } - } - - return (viewpoint_distances[0] - gap_start) + 0.1 * (float)count_neighbors; -} - -bool lineSegDistances(const ViewPoint &point, const ViewPoint &line_segment_start, const ViewPoint &line_segment_end, - double ¶llel_distance, double &perpendicular_distance) -{ - // code adapted from http://geomalgorithms.com/a02-_lines.html - // Return false if point is outside of line segment - // Otherwise return true, along with the parallel and perpendicular distances - - // vector end to start - double v_x = line_segment_end.x - line_segment_start.x; - double v_y = line_segment_end.y - line_segment_start.y; - double v_z = line_segment_end.z - line_segment_start.z; - - // vector start to point - double w_x = point.x - line_segment_start.x; - double w_y = point.y - line_segment_start.y; - double w_z = point.z - line_segment_start.z; - - // if outside one boundary, return false - double c1 = v_x * w_x + v_y * w_y + v_z * w_z; // dot product - if (c1 <= 0) - return false; - - // if outside other boundary, return false - double c2 = v_x * v_x + v_y * v_y + v_z * v_z; // dot product - if (c2 <= c1) - return false; - - // otherwise project point and get distance (seems inefficient?) - double b = c1 / c2; - ViewPoint point_projected; - point_projected.x = line_segment_start.x + b * v_x; - point_projected.y = line_segment_start.y + b * v_y; - point_projected.z = line_segment_start.z + b * v_z; - - perpendicular_distance = distancePoint2Point(point, point_projected); - parallel_distance = distancePoint2Point(line_segment_start, point_projected); - return true; -} - -double ExplorationPlanner::angleBetweenHeadingAndMomentum(const ViewPoint &point) -{ - // Get the momentum vector - double momentum_direction_x, momentum_direction_y, momentum_direction_z; - bool momentum_computed = computeMomentumVector(momentum_direction_x, momentum_direction_y, momentum_direction_z); - - if (!momentum_computed) - { - // No momentum -- all viewpoints will be equally weighted in terms of momentum - return 0.0; - } - - // vector to point - double diff_x = point.x - robot_pos_.x; - double diff_y = point.y - robot_pos_.y; - double diff_z = point.z - robot_pos_.z; - double diff_norm = std::sqrt(diff_x * diff_x + diff_y * diff_y + diff_z * diff_z); - - // avoid singularity - if (diff_norm < 0.01) - { - return 0.0; - } - - // normalize - diff_x /= diff_norm; - diff_y /= diff_norm; - diff_z /= diff_norm; - - // dot product - double dot_product = diff_x * momentum_direction_x + diff_y * momentum_direction_y + diff_z * momentum_direction_z; - - // cos(theta) = a.b / |a||b| - double relative_angle = std::acos(dot_product); - return relative_angle; -} - -bool ExplorationPlanner::computeMomentumVector(double &momentum_direction_x, double &momentum_direction_y, double &momentum_direction_z) -{ - // return false if no momentum computed - // momentum returned as a normalized (x,y) vector - // for now, assume z direction is 0, to encourage staying at same z level - - // Need at least two points to estimate momentum - if (this->momentum_executed_trajectory_.size() <= 1) - { - return false; - } - - // Get the start and end point of the vector - const ViewPoint ¤t_point = this->momentum_executed_trajectory_.back().point; - const ViewPoint &previous_point = this->momentum_executed_trajectory_.front().point; - - // Check not stationary first - double euclidean_distance = distancePoint2Point(current_point, previous_point); - if (euclidean_distance <= momentum_minimum_distance_) - { - return false; - } - - // Compute the momentum vector - momentum_direction_x = current_point.x - previous_point.x; - momentum_direction_y = current_point.y - previous_point.y; - momentum_direction_z = 0.0; - - // Normalize - double norm = std::sqrt(momentum_direction_x * momentum_direction_x + momentum_direction_y * momentum_direction_y + momentum_direction_z * momentum_direction_z); - - // avoid singularity (might happen since the above two distance measures are slightly different) - if (norm < 0.01) - { - return false; - } - - // normalize - momentum_direction_x /= norm; - momentum_direction_y /= norm; - momentum_direction_z /= norm; - - return true; -} - -// Junbin: Below are previous implementation to remove - -std::optional ExplorationPlanner::generate_straight_rand_path( - std::tuple start_point, float timeout_duration) -{ - // Locking mutex to prevent crashing when access the voxel map - std::optional path; - bool is_goal_point_valid = false; - // get start ti - const std::chrono::steady_clock::time_point start_time = std::chrono::steady_clock::now(); - // RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), "Starting Path Search..."); - - while (!is_goal_point_valid && std::chrono::duration_cast( - std::chrono::steady_clock::now() - start_time) - .count() < timeout_duration) - { - std::tuple goal_point = generate_goal_point(start_point); - // RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), "Point Generated..."); - if (std::get<2>(goal_point) == -1) - { - RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), - "No valid goal point found in timeout duration"); - break; - } - - float x_diff = std::get<0>(goal_point) - std::get<0>(start_point); - float y_diff = std::get<1>(goal_point) - std::get<1>(start_point); - float z_diff = std::get<2>(goal_point) - std::get<2>(start_point); - float yaw = std::atan2(y_diff, x_diff); - - path = Path(); - std::tuple first_point( - std::get<0>(start_point), std::get<1>(start_point), std::get<2>(start_point), yaw); - path.value().push_back(first_point); - - std::tuple current_point = std::make_tuple( - std::get<0>(start_point), std::get<1>(start_point), std::get<2>(start_point)); - - // start moving in the direction of the goal point - while (get_point_distance(current_point, goal_point) > 0.001) - { - if (!check_if_collided(current_point)) - { - // add small delay to stop crashing - float new_x = std::get<0>(current_point) + x_diff / float(this->checking_point_cnt); - float new_y = std::get<1>(current_point) + y_diff / float(this->checking_point_cnt); - float new_z = std::get<2>(current_point) + z_diff / float(this->checking_point_cnt); - std::tuple new_point(new_x, new_y, new_z); - current_point = new_point; - std::tuple new_point_with_yaw( - std::get<0>(new_point), std::get<1>(new_point), std::get<2>(new_point), yaw); - path.value().push_back(new_point_with_yaw); - } - else - { - RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), "Exploration: Collision detected"); - is_goal_point_valid = false; - break; - } - is_goal_point_valid = true; - } - } - if (!is_goal_point_valid) - { - RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), "No Path found in time limit"); - return std::nullopt; - } - else - { - return path; - } -} - -bool ExplorationPlanner::check_if_collided_single_voxel( - const std::tuple &point, const std::tuple &voxel_center) -{ - // Check if the point is within the voxel - float x_max = std::get<0>(voxel_center) + std::get<0>(this->voxel_size_m) + this->collision_padding_m; - float x_min = std::get<0>(voxel_center) - std::get<0>(this->voxel_size_m) - this->collision_padding_m; - float y_max = std::get<1>(voxel_center) + std::get<1>(this->voxel_size_m) + this->collision_padding_m; - float y_min = std::get<1>(voxel_center) - std::get<1>(this->voxel_size_m) - this->collision_padding_m; - float z_max = std::get<2>(voxel_center) + std::get<2>(this->voxel_size_m) + this->collision_padding_m; - float z_min = std::get<2>(voxel_center) - std::get<2>(this->voxel_size_m) - this->collision_padding_m; - bool in_x = std::get<0>(point) < x_max && std::get<0>(point) > x_min; - bool in_y = std::get<1>(point) < y_max && std::get<1>(point) > y_min; - bool in_z = std::get<2>(point) < z_max && std::get<2>(point) > z_min; - if (in_x && in_y && in_z) - { - return true; - } - else - { - return false; - } -} - -bool ExplorationPlanner::check_if_collided(const std::tuple &point) -{ - // make sure the point is within the bounds -30 to 30 - if (std::get<0>(point) > 30 || std::get<0>(point) < -30 || std::get<1>(point) > 30 || - std::get<1>(point) < -30 || std::get<2>(point) > 30 || std::get<2>(point) < -30) - { - return true; - } - - std::lock_guard lock(this->mutex); - for (const std::tuple &voxel : this->voxel_points) - { - if (voxel == std::tuple(0, 0, 0)) - { - RCLCPP_INFO(rclcpp::get_logger("exploration_planner"), "Voxel is 0,0,0"); - } - if (check_if_collided_single_voxel(point, voxel)) - { - return true; - } - } - return false; -} - -std::tuple ExplorationPlanner::generate_goal_point( - std::tuple start_point) -{ - float time_out_duration = 1.0; - const clock_t start_time = clock(); - - while ((clock() - start_time) / CLOCKS_PER_SEC < time_out_duration) - { - std::random_device rd; - std::mt19937 gen(rd()); - std::uniform_real_distribution distribution_xy(-this->max_start_to_goal_dist_m_, - this->max_start_to_goal_dist_m_); - std::uniform_real_distribution distribution_z(-this->max_z_change_m_, this->max_z_change_m_); - float delta_x = distribution_xy(gen); - float delta_y = distribution_xy(gen); - float delta_z = distribution_z(gen); - float rand_x = std::get<0>(start_point) + delta_x; - float rand_y = std::get<1>(start_point) + delta_y; - float rand_z = std::get<2>(start_point) + delta_z; - rand_z = std::max(0.5f, rand_z); // ensure don't go below the ground - std::tuple rand_point(rand_x, rand_y, rand_z); - std::tuple start_point_wo_yaw( - std::get<0>(start_point), std::get<1>(start_point), std::get<2>(start_point)); - float dist = get_point_distance(start_point_wo_yaw, rand_point); - float new_angle = std::atan2(std::get<1>(rand_point) - std::get<1>(start_point_wo_yaw), - std::get<0>(rand_point) - std::get<0>(start_point_wo_yaw)); - float angle_diff = std::abs(std::get<3>(start_point) - new_angle); - // if the z value of the point is low enough - if (angle_diff <= this->max_yaw_change_degrees * 3.14159265359 / 180) - { - // if the point is close enough to the start point - if (dist < this->max_start_to_goal_dist_m_) - { - // if the point doesnt collide with any of the voxels - if (!(check_if_collided(rand_point))) - { - return rand_point; - } - } - } - } - return std::tuple(0, 0, -1); -} - -double get_point_distance(const std::tuple &point1, - const std::tuple &point2) -{ - float x_diff = std::get<0>(point1) - std::get<0>(point2); - float y_diff = std::get<1>(point1) - std::get<1>(point2); - float z_diff = std::get<2>(point1) - std::get<2>(point2); - float dist = std::sqrt(x_diff * x_diff + y_diff * y_diff + z_diff * z_diff); - if (std::isnan(dist) || std::isinf(dist)) - { - RCLCPP_ERROR(rclcpp::get_logger("exploration_planner"), "Distance is nan or inf"); - } - return dist; -} - -double deg2rad(double deg) { return deg * M_PI / 180.0; } - -double rad2deg(double rad) { return rad * 180.0 / M_PI; } diff --git a/robot/ros_ws/src/global/planners/exploration/src/exploration_node.cpp b/robot/ros_ws/src/global/planners/exploration/src/exploration_node.cpp deleted file mode 100644 index cdbc41a72..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/exploration_node.cpp +++ /dev/null @@ -1,916 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// 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. - -#include "../include/exploration_node.hpp" -#include "../include/exploration_logic.hpp" - -#include -#include -#include - -#include "utils/utils.hpp" - -std::optional ExplorationNode::readParameters() -{ - // Read in parameters based off the default yaml file - init_params params; - this->declare_parameter("robot_frame_id"); - if (!this->get_parameter("robot_frame_id", this->robot_frame_id_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: robot_frame_id"); - return std::optional{}; - } - this->declare_parameter("lidar_frame_id"); - if (!this->get_parameter("lidar_frame_id", this->lidar_frame_id_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: lidar_frame_id"); - return std::optional{}; - } - this->declare_parameter("map_frame_id"); - if (!this->get_parameter("map_frame_id", this->map_frame_id_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: map_frame_id"); - return std::optional{}; - } - this->declare_parameter("pub_global_plan_topic"); - if (!this->get_parameter("pub_global_plan_topic", this->pub_global_plan_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_global_plan_topic"); - return std::optional{}; - } - this->declare_parameter("pub_goal_point_viz_topic"); - if (!this->get_parameter("pub_goal_point_viz_topic", this->pub_goal_point_viz_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_goal_point_viz_topic"); - return std::optional{}; - } - this->declare_parameter("pub_trajectory_viz_topic"); - if (!this->get_parameter("pub_trajectory_viz_topic", this->pub_trajectory_viz_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_trajectory_viz_topic"); - return std::optional{}; - } - this->declare_parameter("pub_grid_viz_topic"); - if (!this->get_parameter("pub_grid_viz_topic", this->pub_grid_viz_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_grid_viz_topic"); - return std::optional{}; - } - this->declare_parameter("pub_frontier_viz_topic"); - if (!this->get_parameter("pub_frontier_viz_topic", this->pub_frontier_viz_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_frontier_viz_topic"); - return std::optional{}; - } - this->declare_parameter("pub_clustered_frontier_viz_topic"); - if (!this->get_parameter("pub_clustered_frontier_viz_topic", this->pub_clustered_frontier_viz_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: pub_clustered_frontier_viz_topic"); - return std::optional{}; - } - this->declare_parameter("sub_lidar_topic"); - if (!this->get_parameter("sub_lidar_topic", this->sub_lidar_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: sub_map_topic"); - return std::optional{}; - } - this->declare_parameter("sub_map_topic"); - if (!this->get_parameter("sub_map_topic", this->sub_map_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: sub_map_topic"); - return std::optional{}; - } - this->declare_parameter("sub_vdb_topic"); - if (!this->get_parameter("sub_vdb_topic", this->sub_vdb_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: sub_vdb_topic"); - return std::optional{}; - } - this->declare_parameter("sub_robot_tf_topic"); - if (!this->get_parameter("sub_robot_tf_topic", this->sub_robot_tf_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: sub_robot_tf_topic"); - return std::optional{}; - } - this->declare_parameter("srv_exploration_toggle_topic"); - if (!this->get_parameter("srv_exploration_toggle_topic", this->srv_exploration_toggle_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: srv_exploration_toggle_topic"); - return std::optional{}; - } - this->declare_parameter("publish_visualizations"); - if (!this->get_parameter("publish_visualizations", this->publish_visualizations)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: publish_visualizations"); - return std::optional{}; - } - this->declare_parameter("num_paths_to_generate"); - if (!this->get_parameter("num_paths_to_generate", this->num_paths_to_generate_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: num_paths_to_generate"); - return std::optional{}; - } - this->declare_parameter("max_start_to_goal_dist_m"); - if (!this->get_parameter("max_start_to_goal_dist_m", params.max_start_to_goal_dist_m)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: max_start_to_goal_dist_m"); - return std::optional{}; - } - this->declare_parameter("checking_point_cnt"); - if (!this->get_parameter("checking_point_cnt", params.checking_point_cnt)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: checking_point_cnt"); - return std::optional{}; - } - this->declare_parameter("max_z_change_m"); - if (!this->get_parameter("max_z_change_m", params.max_z_change_m)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: max_z_change_m"); - return std::optional{}; - } - this->declare_parameter("collision_padding_m"); - if (!this->get_parameter("collision_padding_m", params.collision_padding_m)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: collision_padding_m"); - return std::optional{}; - } - this->declare_parameter("path_end_threshold_m"); - if (!this->get_parameter("path_end_threshold_m", params.path_end_threshold_m)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: path_end_threshold_m"); - return std::optional{}; - } - this->declare_parameter("max_yaw_change_degrees"); - if (!this->get_parameter("max_yaw_change_degrees", params.max_yaw_change_degrees)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: max_yaw_change_degrees"); - return std::optional{}; - } - this->declare_parameter("cluster_cube_dim"); - if (!this->get_parameter("cluster_cube_dim", this->cluster_cube_dim)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: cluster_cube_dim"); - return std::optional{}; - } - this->declare_parameter("voxel_cluster_count_thresh"); - if (!this->get_parameter("voxel_cluster_count_thresh", this->voxel_cluster_count_thresh)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: voxel_cluster_count_thresh"); - return std::optional{}; - } - this->declare_parameter("position_change_threshold"); - if (!this->get_parameter("position_change_threshold", this->position_change_threshold)) - { - - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: position_change_threshold"); - return std::optional{}; - } - this->declare_parameter("map_voxel_resolution"); - if (!this->get_parameter("map_voxel_resolution", this->map_voxel_resolution)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: map_voxel_resolution"); - return std::optional{}; - } - this->declare_parameter("stall_timeout_seconds"); - if (!this->get_parameter("stall_timeout_seconds", this->stall_timeout_seconds)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: stall_timeout_seconds"); - return std::optional{}; - } - - // viewpoint sampling - params.kLOcc_ = this->declare_parameter("l_occ", 1.0); - this->get_parameter("map_voxel_resolution", params.kVoxSize_); - params.kBboxLength_ = this->declare_parameter("bbox_length", 3.0); - params.kBboxBreadth_ = this->declare_parameter("bbox_breadth", 3.0); - params.kBboxHeight_ = this->declare_parameter("bbox_height", 3.0); - params.kViewingDistance_ = this->declare_parameter("viewing_distance", 3.0); - params.kNumViewpointsPerCluster_ = this->declare_parameter("num_viewpts_per_cluster", 6); - params.kViewingDistanceInnerR_ = this->declare_parameter("kViewingDistanceInnerR", 3.0); - params.kViewingDistanceOuterR_ = this->declare_parameter("kViewingDistanceOuterR", 6.0); - params.kViewingDistanceZOffset_ = this->declare_parameter("kViewingDistanceZOffset", 2.0); - params.kViewpBBoxUnknownFracThresh_ = this->declare_parameter("viewp_bbox_unknown_frac_thresh", 0.85); - params.kViewpBBoxOccupiedFracThresh_ = this->declare_parameter("viewp_bbox_known_occ_thresh", 0.0); - params.kCollCheckEndpointOffset_ = this->declare_parameter("coll_check_endpoint_offset", 0.4); - params.kRobotModelBBoxFraction_ = this->declare_parameter("robot_model_bbox_fraction", 0.1); - - // viewpoint selection - params.too_close_distance = this->declare_parameter("too_close_distance", 4.0); - params.too_close_penalty = this->declare_parameter("too_close_penalty", 5.0); - params.odometry_match_distance = this->declare_parameter("odometry_match_distance", 3.0); - params.odometry_match_angle = this->declare_parameter("odometry_match_angle", 3.14159); - params.odometry_match_penalty = this->declare_parameter("odometry_match_penalty", 1000.0); - params.momentum_collision_check_distance = this->declare_parameter("momentum_collision_check_distance", 15.0); - params.momentum_collision_check_distance_min = this->declare_parameter("momentum_collision_check_distance_min", 4.0); - params.momentum_change_max_reward = this->declare_parameter("momentum_change_max_reward", 6.0); - params.momentum_change_collision_reward = this->declare_parameter("momentum_change_collision_reward", 2.0); - params.viewpoint_neighborhood_too_close_radius = this->declare_parameter("viewpoint_neighborhood_too_close_radius", 10.0); - params.viewpoint_neighborhood_sphere_radius = this->declare_parameter("viewpoint_neighborhood_sphere_radius", 6.0); - params.viewpoint_neighborhood_count = this->declare_parameter("viewpoint_neighborhood_count", 5); - params.momentum_minimum_distance = this->declare_parameter("momentum_minimum_distance", 0.5); - params.momentum_time = this->declare_parameter("momentum_time", 2.0); - params.momentum_collision_check_step_size = this->declare_parameter("momentum_collision_check_step_size", 0.4); - - this->momentum_time_ = params.momentum_time; - // RRT - params.planner_norm_limit_ = this->declare_parameter("planner_norm_limit", 0.1); - params.max_explore_dist_ = this->declare_parameter("max_explore_dist", 100.0); - params.planner_max_iter_ = this->declare_parameter("planner_max_iter", 20000); - params.max_connection_iter_ = this->declare_parameter("max_connection_iter", 1000); - params.traj_smooth_horizon_ = this->declare_parameter("traj_smooth_horizon", 50); - params.rrt_region_nodes_count_ = this->declare_parameter("rrt_region_nodes_count", 16); - params.rrt_region_nodes_radius_ = this->declare_parameter("rrt_region_nodes_radius", 0.8); - params.rrt_region_nodes_z_layers_count_up_ = this->declare_parameter("rrt_region_nodes_z_layers_count_up", 1); - params.rrt_region_nodes_z_layers_step_ = this->declare_parameter("rrt_region_nodes_z_layers_step", 0.4); - - params.priority_level_thred_ = this->declare_parameter("priority_level_thred", 20); - params.dense_step_ = this->declare_parameter("dense_step", 0.1); - return params; -} - -ExplorationNode::ExplorationNode() : Node("exploration_node") -{ - // Initialize float 543 grid - openvdb::initialize(); - - // Initialize the exploration planner - std::optional params_opt = ExplorationNode::readParameters(); - - this->world_frame_id_ = this->map_frame_id_; - - if (params_opt.has_value()) - { - this->params = params_opt.value(); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Failed to initialize exploration planner"); - } - - this->params.voxel_size_m = std::tuple(map_voxel_resolution, map_voxel_resolution, map_voxel_resolution); - - // Initialize exploration planner - this->exploration_planner = std::make_unique(this->params); - RCLCPP_INFO(this->get_logger(), "Initialized exploration planner logic"); - - VDBUtil::setVoxelSize(*(this->exploration_planner->occ_grid), map_voxel_resolution); - VDBUtil::setVoxelSize(*(this->exploration_planner->prev_grid), map_voxel_resolution); - VDBUtil::setVoxelSize(*(this->exploration_planner->frontier_grid), map_voxel_resolution); - - RCLCPP_INFO(this->get_logger(), "Initialized exploration inner voxel grids"); -} - -void ExplorationNode::initialize() -{ - - // TF buffer and listener - tf_buffer = std::make_shared(this->get_clock()); - tf_listener = std::make_shared(*tf_buffer); - - this->sub_lidar = this->create_subscription( - sub_lidar_topic_, rclcpp::QoS(1).durability_volatile().best_effort(), std::bind(&ExplorationNode::lidarCallback, this, std::placeholders::_1)); - - // Trying message filter to avoid extrapolation when lookuptransform, not succedded yet - - // auto node_ptr = rclcpp::Node::shared_from_this(); - // RCLCPP_WARN(this->get_logger(), "Type: %s", typeid(*node_ptr).name()); - - // tf_buffer = std::make_shared(this->get_clock(), tf2::durationFromSec(10.0), node_ptr); - // tf_listener = std::make_shared(*tf_buffer); - - // lidar_filter_sub_ = std::make_shared>( - // this, sub_lidar_topic_, rmw_qos_profile_sensor_data); - - // tf_filter_ = std::make_shared>( - // *lidar_filter_sub_, *tf_buffer, map_frame_id_, 10, this->get_node_logging_interface(), this->get_node_clock_interface()); - - // tf_filter_->registerCallback(&ExplorationNode::lidarCallback, this); - - // Previous setup - - // this->sub_map = this->create_subscription( - // sub_map_topic_, 10, std::bind(&ExplorationNode::mapCallback, this, std::placeholders::_1)); - - // this->sub_vdb = this->create_subscription( - // sub_vdb_topic_, 10, std::bind(&ExplorationNode::vdbCallback, this, std::placeholders::_1)); - - this->pub_global_plan = this->create_publisher(pub_global_plan_topic_, 10); - this->pub_goal_point = - this->create_publisher(pub_goal_point_viz_topic_, 10); - this->pub_trajectory_lines = - this->create_publisher(pub_trajectory_viz_topic_, 10); - this->pub_vdb = - this->create_publisher(pub_grid_viz_topic_, 10); - this->pub_frontier_vis = - this->create_publisher(pub_frontier_viz_topic_, 10); - this->pub_clustered_frontier_vis = - this->create_publisher(pub_clustered_frontier_viz_topic_, 10); - this->planning_debug_vis = - this->create_publisher("/exploration_debug_vis", 10); - - // Set up the timer - this->timer = this->create_wall_timer(std::chrono::seconds(5), - std::bind(&ExplorationNode::timerCallback, this)); - // Set up the service - this->srv_exploration_toggle = this->create_service( - srv_exploration_toggle_topic_, std::bind(&ExplorationNode::ExplorationToggleCallback, this, - std::placeholders::_1, std::placeholders::_2)); - RCLCPP_INFO(this->get_logger(), "Exploration node initialized"); -} - -void ExplorationNode::ExplorationToggleCallback( - const std_srvs::srv::Trigger::Request::SharedPtr request, - std_srvs::srv::Trigger::Response::SharedPtr response) -{ - if (this->enable_exploration == false) - { - this->enable_exploration = true; - response->success = true; - response->message = "Exploration enabled"; - RCLCPP_INFO(this->get_logger(), "Exploration enabled"); - } - else - { - this->enable_exploration = false; - response->success = true; - response->message = "Exploration disabled"; - RCLCPP_INFO(this->get_logger(), "Exploration disabled"); - } -} - -void ExplorationNode::lidarCallback(const sensor_msgs::msg::PointCloud2::SharedPtr msg) -{ - geometry_msgs::msg::TransformStamped cloud_origin_tf; - - pcl::PointCloud::Ptr incoming_cloud(new pcl::PointCloud); - pcl::fromROSMsg(*msg, *incoming_cloud); - - try - { - cloud_origin_tf = this->tf_buffer->lookupTransform( - map_frame_id_, lidar_frame_id_, msg->header.stamp, rclcpp::Duration(0, 100000000)); - // map_frame_id_, lidar_frame_id_, msg->header.stamp); - } - catch (tf2::TransformException &ex) - { - RCLCPP_ERROR(this->get_logger(), - "Could not transform %s to %s: %s", - map_frame_id_.c_str(), - lidar_frame_id_.c_str(), - ex.what()); - return; - } - - // pre-processing: remove nan - std::vector idx; - pcl::removeNaNFromPointCloud(*incoming_cloud, *incoming_cloud, idx); - - double min_range = 0.5; - - // pre-processing: remove too close points - pcl::PointCloud::Ptr tmp(new pcl::PointCloud); - tmp->reserve(incoming_cloud->size()); - const float r2min = min_range * min_range; - for (const auto &p : incoming_cloud->points) - { - const float r2 = p.x * p.x + p.y * p.y + p.z * p.z; - if (r2 >= r2min) - tmp->points.push_back(p); - } - tmp->width = static_cast(tmp->points.size()); - tmp->height = 1; - tmp->is_dense = true; - incoming_cloud.swap(tmp); - - pcl::transformPointCloud(*incoming_cloud, *incoming_cloud, tf2::transformToEigen(cloud_origin_tf).matrix()); - tf2::Vector3 origin_vec(cloud_origin_tf.transform.translation.x, - cloud_origin_tf.transform.translation.y, - cloud_origin_tf.transform.translation.z); - - VDBUtil::updateOccMapFromNdArray(this->exploration_planner->occ_grid, incoming_cloud, origin_vec); - this->exploration_planner->collision_checker_.updateGridPtr(this->exploration_planner->occ_grid); - - // visualization_msgs::msg::Marker vdb_marker_msg; - // generateVDBMarker(this->exploration_planner->occ_grid, map_frame_id_, vdb_marker_msg); - // vdb_marker_msg.header.stamp = this->now(); - // this->pub_vdb->publish(vdb_marker_msg); - - // RCLCPP_WARN(this->get_logger(), "Lidar Msg Received"); -} - -void ExplorationNode::mapCallback(const visualization_msgs::msg::Marker::SharedPtr msg) -{ - // updating the local voxel points and generating a path only if the path is not executing - if (!this->received_first_map) - { - this->received_first_map = true; - this->world_frame_id_ = msg->header.frame_id; - RCLCPP_INFO(this->get_logger(), "Received first map"); - this->params.voxel_size_m = - std::tuple(msg->scale.x, msg->scale.y, msg->scale.z); - // this->exploration_planner = ExplorationPlanner(this->params); - this->exploration_planner = std::make_unique(this->params); - - RCLCPP_INFO(this->get_logger(), "Initialized exploration planner logic"); - } - this->exploration_planner->voxel_points.clear(); - for (int i = 0; i < msg->points.size(); i++) - { - this->exploration_planner->voxel_points.push_back( - std::tuple(msg->points[i].x, msg->points[i].y, msg->points[i].z)); - } -} - -void ExplorationNode::vdbCallback(const std_msgs::msg::ByteMultiArray::SharedPtr msg) -{ - std::string raw_data(reinterpret_cast(msg->data.data()), msg->data.size()); - std::istringstream iss(raw_data, std::ios::binary); - - openvdb::io::Stream in_stream(iss); - std::vector grids = *in_stream.getGrids(); - - if (grids.empty()) - { - RCLCPP_WARN(this->get_logger(), "Received VDB grid message, but no grids were found."); - return; - } - - using GridT = openvdb::Grid::Type>; - GridT::Ptr float_grid = openvdb::gridPtrCast(grids.front()); - - if (!float_grid) - { - RCLCPP_WARN(this->get_logger(), "First grid in received VDB message is not a FloatGrid."); - return; - } - - if (this->exploration_planner) - { - this->exploration_planner->vdb_grid = float_grid; - - if (!this->exploration_planner->first_grid_received) - { - this->exploration_planner->first_grid_received = true; - this->exploration_planner->frontier_grid->setTransform(float_grid->transform().copy()); - this->exploration_planner->prev_grid->setTransform(float_grid->transform().copy()); - RCLCPP_WARN(this->get_logger(), "Exploration Grids Initialized"); - } - // RCLCPP_WARN(this->get_logger(), "Successfully received and stored VDB FloatGrid (%zu active voxels).", - // this->exploration_planner->vdb_grid->activeVoxelCount()); - // RCLCPP_WARN(this->get_logger(), "received type is %s", grids.front()->type().c_str()); - // RCLCPP_WARN(this->get_logger(), "our type is %s", this->exploration_planner->vdb_grid->type().c_str()); - - // visualization_msgs::msg::Marker vdb_marker_msg; - // generateVDBMarker(this->exploration_planner->vdb_grid, "map", vdb_marker_msg); - // vdb_marker_msg.header.stamp = this->now(); - // this->pub_vdb->publish(vdb_marker_msg); - } - else - { - RCLCPP_WARN(this->get_logger(), "Exploration planner is not yet initialized."); - } -} - -void ExplorationNode::tfCallback(const tf2_msgs::msg::TFMessage::SharedPtr msg) -{ - // get the current location - for (int i = 0; i < msg->transforms.size(); i++) - { - if (msg->transforms[i].child_frame_id.c_str() == this->robot_frame_id_) - { - this->current_location = msg->transforms[i].transform; - if (!this->received_first_robot_tf) - { - this->received_first_robot_tf = true; - RCLCPP_WARN(this->get_logger(), "Received first robot_tf"); - } - } - } -} - -void ExplorationNode::generate_plan() -{ - RCLCPP_INFO(this->get_logger(), "Starting to generate plan..."); - - // std::tuple start_loc; - // if (this->generated_paths.size() == 0) - // { - // geometry_msgs::msg::Quaternion orientation = this->current_location.rotation; - // tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - // q.normalize(); - // double roll, pitch, yaw; - // tf2::Matrix3x3 m(q); - // m.getRPY(roll, pitch, yaw); - // start_loc = std::make_tuple(this->current_location.translation.x, - // this->current_location.translation.y, - // this->current_location.translation.z, yaw); - // } - // else - // { - // geometry_msgs::msg::Quaternion orientation = - // this->generated_paths.back().poses.back().pose.orientation; - // tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - // q.normalize(); - // double roll, pitch, yaw; - // tf2::Matrix3x3 m(q); - // m.getRPY(roll, pitch, yaw); - // start_loc = std::make_tuple(this->generated_paths.back().poses.back().pose.position.x, - // this->generated_paths.back().poses.back().pose.position.y, - // this->generated_paths.back().poses.back().pose.position.z, yaw); - // } - - ViewPoint start_point; - start_point.x = this->current_location.translation.x; - start_point.y = this->current_location.translation.y; - start_point.z = this->current_location.translation.z; - - tf2::Quaternion q; - tf2::fromMsg(this->current_location.rotation, q); - q.normalize(); - - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - - start_point.orientation = this->current_location.rotation; - start_point.orientation_set = true; - start_point.orientation_yaw = yaw; - - // Convert start_loc to Point as starting point? - - // Replace the generate_straight_rand_path with the viewpoint score and RRT? - - float timeout_duration = 5.0; - std::optional gen_path_opt = - // this->exploration_planner->generate_straight_rand_path(start_loc, timeout_duration); - this->exploration_planner->select_viewpoint_and_plan(start_point, timeout_duration); - if (gen_path_opt.has_value() && gen_path_opt.value().size() > 0) - { - RCLCPP_INFO(this->get_logger(), "Generated path with %ld points", - gen_path_opt.value().size()); - - // set the current goal location - this->current_goal_location = geometry_msgs::msg::Transform(); - this->current_goal_location.translation.x = std::get<0>(gen_path_opt.value().back()); - this->current_goal_location.translation.y = std::get<1>(gen_path_opt.value().back()); - this->current_goal_location.translation.z = std::get<2>(gen_path_opt.value().back()); - float z_rot = std::get<3>(gen_path_opt.value().back()); - tf2::Quaternion q; - q.setRPY(0, 0, z_rot); // Roll = 0, Pitch = 0, Yaw = yaw - q.normalize(); - this->current_goal_location.rotation.x = q.x(); - this->current_goal_location.rotation.y = q.y(); - this->current_goal_location.rotation.z = q.z(); - this->current_goal_location.rotation.w = q.w(); - - // publish the path - nav_msgs::msg::Path generated_single_path; - generated_single_path = nav_msgs::msg::Path(); - generated_single_path.header.stamp = this->now(); - generated_single_path.header.frame_id = world_frame_id_; - for (auto point : gen_path_opt.value()) - { - geometry_msgs::msg::PoseStamped point_msg; - point_msg.pose.position.x = std::get<0>(point); - point_msg.pose.position.y = std::get<1>(point); - point_msg.pose.position.z = std::get<2>(point); - // convert yaw rotation to quaternion - tf2::Quaternion q; - q.setRPY(0, 0, std::get<3>(point)); // Roll = 0, Pitch = 0, Yaw = yaw - q.normalize(); - point_msg.pose.orientation.x = q.x(); - point_msg.pose.orientation.y = q.y(); - point_msg.pose.orientation.z = q.z(); - point_msg.pose.orientation.w = q.w(); - point_msg.header.stamp = this->now(); - generated_single_path.poses.push_back(point_msg); - } - geometry_msgs::msg::PoseStamped last_goal_loc = generated_single_path.poses.back(); - this->current_goal_location.translation.x = last_goal_loc.pose.position.x; - this->current_goal_location.translation.y = last_goal_loc.pose.position.y; - this->current_goal_location.translation.z = last_goal_loc.pose.position.z; - this->current_goal_location.rotation.z = last_goal_loc.pose.orientation.z; - this->generated_paths.push_back(generated_single_path); - - // debug vis of RRT - PointSet coarse_path = this->exploration_planner->rrt_planner_.getCoarsePath(); - visualization_msgs::msg::MarkerArray rrt_vis_array; - - if (coarse_path.empty()) - { - planning_debug_vis->publish(rrt_vis_array); - } - else - { - const std::string frame_id = "map"; - const rclcpp::Time stamp = this->now(); - - // ----- LINE_STRIP for the polyline ----- - visualization_msgs::msg::Marker line; - line.header.frame_id = frame_id; - line.header.stamp = stamp; - line.ns = "rrt_coarse_path"; - line.id = 0; - line.type = visualization_msgs::msg::Marker::LINE_STRIP; - line.action = visualization_msgs::msg::Marker::ADD; - line.pose.orientation.w = 1.0; // identity - line.scale.x = 0.03; // line width (m) - line.color.r = 0.1f; - line.color.g = 0.5f; - line.color.b = 1.0f; - line.color.a = 0.9f; - line.lifetime = rclcpp::Duration(0, 0); // forever - - line.points.reserve(coarse_path.size()); - for (const auto &v : coarse_path) - { - geometry_msgs::msg::Point p; - p.x = v.x; - p.y = v.y; - p.z = v.z; - line.points.push_back(p); - } - rrt_vis_array.markers.push_back(line); - - // ----- SPHERE_LIST for vertices ----- - visualization_msgs::msg::Marker verts; - verts.header.frame_id = frame_id; - verts.header.stamp = stamp; - verts.ns = "rrt_coarse_path"; - verts.id = 1; - verts.type = visualization_msgs::msg::Marker::SPHERE_LIST; - verts.action = visualization_msgs::msg::Marker::ADD; - verts.pose.orientation.w = 1.0; - verts.scale.x = 0.10; - verts.scale.y = 0.10; - verts.scale.z = 0.10; // sphere diameter (m) - verts.color.r = 1.0f; - verts.color.g = 0.2f; - verts.color.b = 0.2f; - verts.color.a = 0.9f; - verts.lifetime = rclcpp::Duration(0, 0); - - verts.points.reserve(coarse_path.size()); - for (const auto &v : coarse_path) - { - geometry_msgs::msg::Point p; - p.x = v.x; - p.y = v.y; - p.z = v.z; - verts.points.push_back(p); - } - rrt_vis_array.markers.push_back(verts); - - // publish - planning_debug_vis->publish(rrt_vis_array); - } - } - else - { - RCLCPP_ERROR(this->get_logger(), "Failed to generate path, size was 0"); - } -} - -void ExplorationNode::publish_plan() -{ - nav_msgs::msg::Path full_path; - for (auto path : this->generated_paths) - { - for (auto point : path.poses) - { - full_path.poses.push_back(point); - } - } - full_path.header.stamp = this->now(); - full_path.header.frame_id = this->world_frame_id_; - this->pub_global_plan->publish(full_path); - RCLCPP_INFO(this->get_logger(), "Published full path"); -} - -void ExplorationNode::timerCallback() -{ - RCLCPP_INFO(this->get_logger(), "timer callback start"); - // get current TF to world - try - { - geometry_msgs::msg::TransformStamped transform_stamped = - this->tf_buffer->lookupTransform(this->world_frame_id_, this->robot_frame_id_, - rclcpp::Time(0)); - this->current_location = transform_stamped.transform; - if (!this->received_first_robot_tf) - { - this->received_first_robot_tf = true; - this->last_location = this->current_location; - this->last_position_change = this->now(); - RCLCPP_WARN(this->get_logger(), "Received first robot_tf"); - } - } - catch (tf2::TransformException &ex) - { - RCLCPP_ERROR(this->get_logger(), "Robot tf not received: %s", ex.what()); - } - - // Maintain the trajectory - ViewPoint vp; - vp.x = this->current_location.translation.x; - vp.y = this->current_location.translation.y; - vp.z = this->current_location.translation.z; - - vp.orientation.x = this->current_location.rotation.x; - vp.orientation.y = this->current_location.rotation.y; - vp.orientation.z = this->current_location.rotation.z; - vp.orientation.w = this->current_location.rotation.w; - - vp.orientation_set = true; - - double roll, pitch, yaw; - tf2::Quaternion tf2_q; - tf2::fromMsg(vp.orientation, tf2_q); // geometry_msgs -> tf2::Quaternion - tf2::Matrix3x3(tf2_q).getRPY(roll, pitch, yaw); - vp.orientation_yaw = yaw; - - this->exploration_planner->executed_trajectory_.push_back(vp); - this->exploration_planner->momentum_executed_trajectory_.push_back(TimedViewPoint{vp, this->now()}); - - // Prune old momentum samples (keep last momentum_time_ seconds) - const rclcpp::Time now = this->now(); - const rclcpp::Duration keep = rclcpp::Duration::from_seconds(this->momentum_time_); - - auto &traj = this->exploration_planner->momentum_executed_trajectory_; - while (!traj.empty() && (traj.front().time + keep) < now) - { - traj.erase(traj.begin()); - RCLCPP_INFO_ONCE(this->get_logger(), "Trim momentum traj"); - } - - // Update frontier map - - openvdb::Vec3d xyz; - openvdb::Coord ijk; - openvdb::FloatGrid::Accessor prev_acc = this->exploration_planner->prev_grid->getAccessor(); - openvdb::FloatGrid::Accessor lidar_acc = this->exploration_planner->occ_grid->getAccessor(); - openvdb::BoolGrid::Accessor frontier_acc = this->exploration_planner->frontier_grid->getAccessor(); - - for (auto iter = this->exploration_planner->frontier_grid->cbeginValueOn(); iter; ++iter) - { - ijk = iter.getCoord(); - // if the surrounding voxels are still frontier - bool still_front = surface_edge_frontier(lidar_acc, ijk); - if (!still_front) - { - frontier_acc.setValueOff(ijk, false); - } - } - // go through current free voxels - for (auto iter = this->exploration_planner->occ_grid->cbeginValueOn(); iter; ++iter) - { - ijk = iter.getCoord(); - openvdb::Vec3d xyz = this->exploration_planner->occ_grid->indexToWorld(ijk); - - // if (xyz.x() > explore_x_min_ && - // xyz.x() < explore_x_max_ && - // xyz.y() > explore_y_min_ && - // xyz.y() < explore_y_max_ && - // xyz.z() > explore_z_min_ && - // xyz.z() < explore_z_max_) - { - if (lidar_acc.getValue(ijk) < 0.0) - { // new free voxels - if (prev_acc.getValue(ijk) >= 0.0) - { - bool is_front = surface_edge_frontier(lidar_acc, ijk); - if (is_front) - { - frontier_acc.setValueOn(ijk, true); - } - } - } - } - } - - // update previous grid - this->exploration_planner->prev_grid = this->exploration_planner->occ_grid->deepCopy(); - - // clustering - openvdb::FloatGrid::Ptr visualized_cluster_grid_; - visualized_cluster_grid_ = openvdb::FloatGrid::create(0.0); - visualized_cluster_grid_->setTransform(this->exploration_planner->occ_grid->transform().copy()); - - std::vector> clustered_points_; - pcl::PointCloud::Ptr cluster_centroids_(new pcl::PointCloud); - - clustered_points_.clear(); - cluster_centroids_->clear(); - - point_clustering_vis(this->exploration_planner->frontier_grid, - this->cluster_cube_dim, - cluster_centroids_, - this->voxel_cluster_count_thresh, - clustered_points_, - visualized_cluster_grid_); - - // visualize - visualization_msgs::msg::Marker vdb_marker_msg; - generateVDBMarker(this->exploration_planner->occ_grid, map_frame_id_, vdb_marker_msg); - vdb_marker_msg.header.stamp = this->now(); - this->pub_vdb->publish(vdb_marker_msg); - - visualization_msgs::msg::Marker frontier_marker_msg; - generateFrontierMarker(this->exploration_planner->frontier_grid, map_frame_id_, frontier_marker_msg); - frontier_marker_msg.header.stamp = this->now(); - this->pub_frontier_vis->publish(frontier_marker_msg); - - visualization_msgs::msg::Marker clustered_marker_msg; - generateClusterMarker(visualized_cluster_grid_, map_frame_id_, clustered_marker_msg); - this->pub_clustered_frontier_vis->publish(clustered_marker_msg); - - // viewpoint sampling - this->exploration_planner->viewp_sample_->reset(clustered_points_); - this->exploration_planner->viewp_sample_->updateGridPtr(this->exploration_planner->occ_grid); - this->exploration_planner->viewp_sample_->updateCentroidListPtr(cluster_centroids_); - this->exploration_planner->viewp_sample_->sampleViewpoints(); - - RCLCPP_WARN(this->get_logger(), "Viewpoints Sampled %zu", this->exploration_planner->viewp_sample_->viewpoint_list_->size()); - - if (this->enable_exploration) - { - if (!this->is_path_executing) - { - if (this->exploration_planner->viewp_sample_->viewpoint_list_->size() > 0 && this->received_first_robot_tf) - { - - this->generate_plan(); - - this->publish_plan(); - this->is_path_executing = true; - this->last_position_change = this->now(); // Reset stall timer when starting new plan - } - else - { - RCLCPP_INFO(this->get_logger(), "viewpoint list size is %zu", this->exploration_planner->viewp_sample_->viewpoint_list_->size()); - RCLCPP_INFO(this->get_logger(), "tf received is %d", this->received_first_robot_tf); - } - } - else - { - // check if the robot has reached the goal point - std::tuple current_point = std::make_tuple( - this->current_location.translation.x, this->current_location.translation.y, - this->current_location.translation.z); - std::tuple goal_point = std::make_tuple( - this->current_goal_location.translation.x, this->current_goal_location.translation.y, - this->current_goal_location.translation.z); - if (get_point_distance(current_point, goal_point) < - this->exploration_planner->path_end_threshold_m) - { - this->is_path_executing = false; - this->generated_paths.clear(); - RCLCPP_INFO(this->get_logger(), "Reached goal point"); - } - else - { - // Check if position has changed significantly - std::tuple last_point = std::make_tuple(this->last_location.translation.x, - this->last_location.translation.y, - this->last_location.translation.z); - - if (get_point_distance(current_point, last_point) > this->position_change_threshold) - { - this->last_location = this->current_location; - this->last_position_change = this->now(); - } - else - { - // Check if we've been stationary for too long - rclcpp::Duration stall_duration = this->now() - this->last_position_change; - if (stall_duration.seconds() > this->stall_timeout_seconds) - { - RCLCPP_INFO(this->get_logger(), "Robot stationary for %f seconds, clearing plan", - stall_duration.seconds()); - this->is_path_executing = false; - this->generated_paths.clear(); - this->last_position_change = this->now(); // Reset timer to avoid spam - } - } - } - } - } - - RCLCPP_INFO(this->get_logger(), "timer callback end"); -} \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/src/exploration_node_run.cpp b/robot/ros_ws/src/global/planners/exploration/src/exploration_node_run.cpp deleted file mode 100644 index 99ac0c206..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/exploration_node_run.cpp +++ /dev/null @@ -1,11 +0,0 @@ -#include "../include/exploration_node.hpp" - -int main(int argc, char *argv[]) -{ - rclcpp::init(argc, argv); - auto node = std::make_shared(); - node->initialize(); - rclcpp::spin(node); - rclcpp::shutdown(); - return 0; -} \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/src/rrt_planner.cpp b/robot/ros_ws/src/global/planners/exploration/src/rrt_planner.cpp deleted file mode 100644 index 324742084..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/rrt_planner.cpp +++ /dev/null @@ -1,784 +0,0 @@ -/* - * Modified RRT Connection Function with Voxel Planner - * Copyright (C) 2020 Fan Yang - All rights reserved - * Original Author: fanyang2@andrew.cmu.edu - * Modified by: junbiny@andrew.cmu.edu - */ - -#include "utils/rrt_planner.h" - -// #define PI 3.14159265358979323846 // already in custom_point.h - -/* ---------------------------------------------------------------------------- */ -RRT_Planner::RRT_Planner() {} - -bool RRT_Planner::RRT_Init( // const float robot_model_cube_dim, - // const openvdb::BoolGrid::Ptr& grid_map, - // const float l_occ, - // const float voxel_dim, - const float norm_limit, - const float max_explore_dist, - const int max_iter, - const int max_connection_iter, - const int smooth_horizon, - // const bool consider_unknown_vox_occupied, - const int rrt_region_nodes_count, - const float rrt_region_nodes_radius, - const int rrt_region_nodes_z_layers_count_up, - const float rrt_region_nodes_z_layers_step, - const float dense_step) -{ - // Initialization - kSmooth_horizon_ = smooth_horizon; - // kRobot_model_cube_dim_ = robot_model_cube_dim; - kNorm_limit_ = norm_limit; - dense_step_ = dense_step; - kNorm_limit_sq_ = kNorm_limit_ * kNorm_limit_; - // kVoxel_dim_ = voxel_dim; - kMax_explore_dist_ = max_explore_dist; - kMax_iter_ = max_iter; - kMax_connection_iter_ = max_connection_iter; - // kConsider_unknown_vox_occupied_ = consider_unknown_vox_occupied; - - // Clear graph - start_tree_.clear(); - end_tree_.clear(); - - last_point_found_ = false; - - // grid_map_ = openvdb::BoolGrid::Ptr(grid_map); - // kL_occ_ = l_occ; - - kRRT_region_nodes_count_ = rrt_region_nodes_count; - kRRT_region_nodes_radius_ = rrt_region_nodes_radius; - kRRT_region_nodes_z_layers_count_up_ = rrt_region_nodes_z_layers_count_up; - kRRT_region_nodes_z_layers_step_ = rrt_region_nodes_z_layers_step; - - // Set seed - unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); - generator_ = std::default_random_engine(seed); - distribution_uniform_ = std::uniform_real_distribution(0.0, 1.0); - - // Debugging publishers - // ros::NodeHandle nh; - // viz_tree_start_pub_ = nh.advertise("viz_rrt_tree_start", 1); - // viz_tree_end_pub_ = nh.advertise("viz_rrt_tree_end", 1); - - return true; -} - -void RRT_Planner::addRegionVertices(Tree &tree) -{ - // Assumes edges has been initialized with one node - // Add new nodes, without collision checking, around this point - // These node form a cylinder - - if (kRRT_region_nodes_count_ > 0) - { - - // Get the existing point - const ViewPoint &point_center = tree.vertices[0]; - - float two_PI_over_count = 2.0 * PI / (float)kRRT_region_nodes_count_; - - // Add new points on a circle around the center - for (int i = 0; i < kRRT_region_nodes_count_; i++) - { - - float theta = (float)i * two_PI_over_count; - - float x = point_center.x + kRRT_region_nodes_radius_ * cos(theta); - float y = point_center.y + kRRT_region_nodes_radius_ * sin(theta); - - for (int j = -kRRT_region_nodes_z_layers_count_up_; j <= kRRT_region_nodes_z_layers_count_up_; j++) - { - // Create a new point for each z layer - ViewPoint point_new; - point_new.x = x; - point_new.y = y; - point_new.z = point_center.z + j * kRRT_region_nodes_z_layers_step_; - - // Ship it - tree.insertPoint(point_new, point_center); - } - } - } -} - -bool RRT_Planner::build_RRT(const ViewPoint &start_point, - const ViewPoint &end_point, collision_checker_ns::CollisionChecker &collision_checker, const bool reset_trees) -{ - if (collision_checker.collisionCheckInFreeSpace(start_point)) - { - std::cout << "Starting Point Not Free Enough \n"; - } - - if (collision_checker.collisionCheckInFreeSpace(end_point)) - { - std::cout << "End Point Not Free Enough \n"; - } - - // Initialize the trees - if (reset_trees) - { - start_tree_.clear(); - end_tree_.clear(); - } - - // Initialize flags - last_point_found_ = false; - bool is_path = false; - bool is_trace_path = true; - - // Save start and end points in tree - start_point_ = start_point; - end_point_ = end_point; - start_tree_.goal = end_point_; - end_tree_.goal = start_point_; - - if (reset_trees) - { - start_tree_.vertices.push_back(start_point_); - end_tree_.vertices.push_back(end_point_); - - // Add region vertices - addRegionVertices(start_tree_); - addRegionVertices(end_tree_); - } - - // Intermediate points - ViewPoint p_random; - ViewPoint p_nearest, p_new, p_nearest_goal, p_new_goal; - - // Create pointers to the two trees so they can be efficiently swapped - Tree *treeA = &start_tree_; - Tree *treeB = &end_tree_; - - // Iterate until goal found or max_iterations - for (int iteration = 0; iteration < kMax_iter_; ++iteration) - { - - // Pick a new random point - p_random = randomPoint(treeA->goal); - - // Get the closest point in graph to random point - p_nearest = nearestPoint(p_random, treeA->vertices); - - // Extend towards random point - extend(p_random, p_nearest, p_new); - - if (validEdge(p_new, p_nearest, collision_checker)) - { - - // Save the new vertex - treeA->insertPoint(p_new, p_nearest); - - // Extend from goal, too - p_nearest_goal = nearestPoint(p_new, treeB->vertices); - extend(p_new, p_nearest_goal, p_new_goal); // direction; start; new - ViewPoint p_new2; - if (validEdge(p_new_goal, p_nearest_goal, collision_checker)) - { - - // Save the new vertex - treeB->insertPoint(p_new_goal, p_nearest_goal); - - // Try to connect to the goal - int connection_iters = 0; - while (!nearGoal(p_new_goal, p_new) && connection_iters <= kMax_connection_iter_) - { - - // Extend towards goal - extend(p_new, p_new_goal, p_new2); - - if (validEdge(p_new2, p_new_goal, collision_checker)) - { - - // Save the new vertex - treeB->insertPoint(p_new2, p_new_goal); - p_new_goal = p_new2; - } - else - { - break; - } - - // Increment loop counter - connection_iters++; - } - if (connection_iters > kMax_connection_iter_) - { - printPoint(p_new); - printPoint(p_new_goal); - } - } - - // Check termination condition, add edge between trees (sort of) - if (nearGoal(p_new_goal, p_new)) - { - // treeB->edges[p_new] = p_new_goal; - treeB->insertPoint(p_new, p_new_goal); - last_point_ = p_new; - last_point_found_ = true; - is_path = true; - break; - } - } - - // Use the goal tree if it was better - if (treeA->vertices.size() > treeB->vertices.size()) - { - swapTreePointers(&treeA, &treeB); - } - } - - // Construct the path from the graph - if (is_path) - { - path_.clear(); - coarse_path_.clear(); - is_trace_path = extractPathFromGraph(collision_checker); - if (!is_trace_path) - { - path_.clear(); - } - } - - return (is_path && is_trace_path); -} - -// bool RRT_Planner::visualizeTrees() -// { -// // Convert both trees to point cloud msgs -// // So they can be viewed in rviz -// // (just the vertices, not the edges) - -// // First tree -// PCLCloudType pcl_cloud_start = pointSet2PCL(start_tree_.vertices); -// sensor_msgs::PointCloud2Ptr pcl_cloud_start_ros(new sensor_msgs::PointCloud2); -// pcl::toROSMsg(pcl_cloud_start, *pcl_cloud_start_ros); -// pcl_cloud_start_ros->header.frame_id = "map"; -// pcl_cloud_start_ros->header.stamp = ros::Time::now(); -// viz_tree_start_pub_.publish(pcl_cloud_start_ros); - -// // Second tree -// PCLCloudType pcl_cloud_end = pointSet2PCL(end_tree_.vertices); -// sensor_msgs::PointCloud2Ptr pcl_cloud_end_ros(new sensor_msgs::PointCloud2); -// pcl::toROSMsg(pcl_cloud_end, *pcl_cloud_end_ros); -// pcl_cloud_end_ros->header.frame_id = "map"; -// pcl_cloud_end_ros->header.stamp = ros::Time::now(); -// viz_tree_end_pub_.publish(pcl_cloud_end_ros); -// } - -/*bool RRT_Planner::collisionCheckInFreeSpace(const ViewPoint& p) { - bool is_in_collision; - // TODO - this conversion is unnecessary -> see second comment below - pcl::PointXYZI query_pt = point2PCL(p); - // LEGACY DEBT - TODO convert method to new point type - if(grid_map_ == NULL) { - // std::cout << "[RRT_Planner::collisionCheckInFreeSpace] Grid ptr within class is NULL - not initialized." << "\n"; - return false; - } - - if(!vdbmap::VDBUtil::checkPointInFreeSpace(grid_map_, query_pt, - kRobot_model_cube_dim_, kL_occ_, - kConsider_unknown_vox_occupied_)) { - is_in_collision = true; - return is_in_collision; - } else { - is_in_collision = false; - return is_in_collision; - } -}*/ - -void RRT_Planner::swapTreePointers(Tree **a, Tree **b) -{ - - // Swap the tree pointers - Tree *temp_tree = *a; - *a = *b; - *b = temp_tree; -} - -ViewPoint RRT_Planner::randomPoint(ViewPoint goal) -{ - - // With some probability, select goal instead of random point - double r = distribution_uniform_(generator_); - if (r < 0.90) - { - - // New random point - ViewPoint p; - float range, range_z; - - float dist = norm(subtract(end_point_, start_point_)); - - if (r < 0.50) - { - // Sample more at closer range - range = (dist + 3) * 1.5; - range_z = (end_point_.z - start_point_.z + 3) * 1.5; - } - else - { - // Wider sample range - range = (dist + 5) * 2.0; - range_z = (end_point_.z - start_point_.z + 3) * 2.0; - } - - p.x = (distribution_uniform_(generator_) - 0.5) * range + (start_point_.x + end_point_.x) / 2.0; - p.y = (distribution_uniform_(generator_) - 0.5) * range + (start_point_.y + end_point_.y) / 2.0; - p.z = (distribution_uniform_(generator_) - 0.5) * range_z + (start_point_.z + end_point_.z) / 2.0; - - return p; - } - else - { - - // Select the goal instead - return goal; - } -} - -void RRT_Planner::reversePath(PointSet &path) -{ - // Reverse the order of the trajectory, save in place - - std::reverse(path.begin(), path.end()); -} - -void RRT_Planner::extend(const ViewPoint &p_rand, const ViewPoint &p_near, ViewPoint &p_new) -{ - // Extend from p_near towards p_rand - // Return the extended point p_new - - // Get unit vector in direction - ViewPoint direction = subtract(p_rand, p_near); - double diff = norm(direction); - direction = divide(direction, diff); - - // Extend along unit vector, without going past p_rand - ViewPoint dist_step = multiply(direction, fmin(kNorm_limit_, diff)); - - // Get the new point - p_new = addPoint(p_near, dist_step); -} - -// bool RRT_Planner::validEdge(const ViewPoint &p_new, const ViewPoint &p_near, collision_checker_ns::CollisionChecker &collision_checker) -// { -// // Collision checking at discrete points between the two points? - -// ViewPoint check_point = p_near; -// ViewPoint direction = subtract(p_new, p_near); -// double diff = norm(direction); -// direction = divide(direction, diff); -// ViewPoint step = multiply(direction, double(kNorm_limit_)); // kVoxel_dim_ - -// int num_steps = ceil(diff / kNorm_limit_); // kVoxel_dim_ - -// // while(norm(subtract(check_point,p_near)) < diff) { -// for (int i = 0; i < num_steps; i++) -// { -// check_point = addPoint(check_point, step); -// if (collision_checker.collisionCheckInFreeSpace(check_point)) -// { -// return false; -// } -// } -// return true; -// } - -bool RRT_Planner::validEdge(const ViewPoint &p_new, const ViewPoint &p_near, collision_checker_ns::CollisionChecker &collision_checker) -{ - openvdb::Vec3d hit_point; - if (collision_checker.collisionCheckRayStrict(p_new, p_near, hit_point)) - { - return false; - } - return true; -} - -ViewPoint RRT_Planner::nearestPoint(const ViewPoint &p_rand, const PointSet &vertices) -{ - // Get the nearest vertex - // TODO KD-tree? - - bool first = true; - double min_norm_dist_sq = 0; - ViewPoint p_near; - - // Iterate through the vertices - for (const auto &vertex : vertices) - { - - // Compute the distance - double norm_dist_sq = normSq(subtract(vertex, p_rand)); - if (first || (norm_dist_sq < min_norm_dist_sq)) - { - min_norm_dist_sq = norm_dist_sq; - p_near = vertex; - first = false; - } - } - return p_near; -} - -inline bool RRT_Planner::nearGoal(const ViewPoint &p_new, const ViewPoint &goal) -{ - // Check if p_new is sufficiently close to the goal - - ViewPoint diff = subtract(goal, p_new); - double norm_dist_sq = normSq(diff); - if (norm_dist_sq < kNorm_limit_sq_) - { - return true; - } - return false; -} - -bool RRT_Planner::extractPathFromGraph(collision_checker_ns::CollisionChecker &collision_checker) -{ - // Extract path from graph - - bool is_trace_success = true; - - // Was the goal found? - if (!last_point_found_) - { - - // If not, stay stationary - path_.push_back(start_point_); - } - else - { - - // Found goal -- build path along graph - - // Initialize stuff - ViewPoint current; - PointSet path; - current = last_point_; - path.push_back(current); - std::size_t n_start_edges = start_tree_.edges.size(); - - // Construct a path - if (start_tree_.edges.count(current)) - { - - bool solution_found = false; - - // Upper bound to avoid infinite loop - for (int i = 0; i < 2 * n_start_edges; ++i) - { - - // Find parent - auto next_edge = start_tree_.edges.find(current); - if (next_edge != start_tree_.edges.end()) - { - - // Follow parent - current = next_edge->second; - path.push_back(current); - } - else - { - - // Reached root - solution_found = true; - break; - } - - // This condition appears to check for infinite loops? - // if (start_tree_.edges.find(parent) != start_tree_.edges.end() && equal(start_tree_.edges.find(parent)->second,current)) { - // is_trace_success = false; - // return false; - // }else{ - // Follow parent - // current = parent; - // } - // path.push_back(current); - } - if (!solution_found) - { - // Something has gone wrong - is_trace_success = false; - return false; - } - } - else - { - // Something has gone wrong - is_trace_success = false; - return false; - } - - // Do the same for end_edges - - // Initialize stuff - ViewPoint current1; - PointSet path1; - current1 = last_point_; - path1.push_back(current1); - std::size_t n_end_edges = end_tree_.edges.size(); - - // Construct a path - if (end_tree_.edges.count(current1)) - { - - bool solution_found = false; - - // Upper bound to avoid infinite loop - for (int i = 0; i < 2 * n_end_edges; ++i) - { - - // Find parent - auto next_edge = end_tree_.edges.find(current1); - if (next_edge != end_tree_.edges.end()) - { - - // Follow parent - current1 = next_edge->second; - path1.push_back(current1); - } - else - { - - // Reached root - solution_found = true; - break; - } - } - if (!solution_found) - { - // Something has gone wrong - is_trace_success = false; - return false; - } - } - else - { - // Something has gone wrong - is_trace_success = false; - return false; - } - - // Stitch the two paths together - if (equal(current, start_point_)) - { - reversePath(path); - path_.insert(path_.end(), path.begin(), path.end()); - path_.insert(path_.end(), path1.begin(), path1.end()); - } - else - { - // This case should no longer happen (since trees don't get directly swapped any more) - reversePath(path1); - path_.insert(path_.end(), path1.begin(), path1.end()); - path_.insert(path_.end(), path.begin(), path.end()); - } - } - - // Error checking - int path_size = path_.size(); - if (!equal(path_[0], start_point_) || !equal(path_[path_size - 1], end_point_)) - { - // Something has gone wrong - is_trace_success = false; - return false; - } - - // copy coarse path - coarse_path_ = path_; - - // Smooth the trajectory - pathSmooth(path_, collision_checker); - - // Ship it! - return is_trace_success; -} - -PointSet RRT_Planner::getPath() -{ - return path_; -} - -PointSet RRT_Planner::getCoarsePath() -{ - return coarse_path_; -} - -void RRT_Planner::interpolate(const ViewPoint &p1, const ViewPoint &p2, PointSet &path) -{ - // Interpolate points between two point -- exclusive of p1 and p2 - - ViewPoint check_point = p1; - ViewPoint direction = subtract(p2, p1); - double dist = norm(direction); - direction = divide(direction, dist); - ViewPoint step = multiply(direction, dense_step_); - while (norm(subtract(check_point, p1)) < dist - dense_step_) - { - check_point = addPoint(check_point, step); - path.push_back(check_point); - } -} - -// void RRT_Planner::pathSmooth(PointSet &path, collision_checker_ns::CollisionChecker &collision_checker) -// { -// /* Interpolate points --> smooth path */ -// // DEBUG -// // std::cout << "Smoothing Path ..."< check_idx) -// { -// forward_idx = int(fmin(forward_idx, num_path - 1)); -// p_forward = temp_path[forward_idx]; -// if (validEdge(p_forward, p_check, collision_checker)) -// { -// interpolate(p_check, p_forward, path); -// check_idx = forward_idx - 1; -// break; -// } -// forward_idx--; -// } -// last_p = temp_path[check_idx]; -// check_idx++; -// cur_p = temp_path[check_idx]; -// if (normSq(subtract(cur_p, last_p)) > kNorm_limit_sq_) -// { -// interpolate(last_p, cur_p, path); -// } -// } -// } - -void RRT_Planner::pathSmooth(PointSet &path, - collision_checker_ns::CollisionChecker &collision_checker) -{ - // straight line, no shortening - if (path.size() < 2) - return; - - // original discretized path (copy) - PointSet Pr; - Pr.push_back(path.front()); - for (size_t j = 0; j + 1 < path.size(); ++j) - { - if (equal(path[j], path[j+1])) continue; - interpolate(path[j], path[j+1], Pr); - Pr.push_back(path[j+1]); - } - - path.clear(); - - // Start with the first point - ViewPoint anchor = Pr.front(); - path.push_back(anchor); - - // Helpers to bridge ViewPoint <-> VDB world coords - auto fromVdb = [](const openvdb::Vec3d &v) -> ViewPoint - { - ViewPoint p; - p.x = v.x(); - p.y = v.y(); - p.z = v.z(); - return p; - }; - - // Tunables - const double step_delta = std::max(1e-3, static_cast(dense_step_)); // linear push step - const int max_pivot_steps = 5; // guard for very thick obstacles - const double normal_inflation = 1.0; // for estimateSurfaceNormalAtHit - - // Scan forward along Pr, only appending when blocked (or at the very end) - - for (size_t i = 1; i < Pr.size(); ++i) - { - // Try to see Pr[i] from current anchor - openvdb::Vec3d hit_world; - const bool blocked = collision_checker.collisionCheckRayStrict(anchor, Pr[i], hit_world); - - if (!blocked) - { - // Visible, connect only if it's the last point on path - if (i == Pr.size() - 1) - { - interpolate(anchor, Pr[i], path); - path.push_back(Pr[i]); - } - continue; - } - - // Blocked on Pr[i]: try to insert a first-hit pivot - bool pivot_inserted = false; - - // Estimate outward normal from occupancy around the hit - openvdb::Vec3d n_world; - bool have_normal = collision_checker.estimateSurfaceNormalAtHit(hit_world, - normal_inflation, - n_world); - if (have_normal) - { - // Linearly push along the normal until the local footprint is free - double delta = 0; - ViewPoint pivot; - - for (int step_count=0; step_count < max_pivot_steps; step_count++) - { - delta += step_delta; - pivot = fromVdb(hit_world + n_world * delta); - if (!collision_checker.collisionCheckInFreeSpace(pivot)) - { - if (validEdge(anchor, pivot, collision_checker) and - validEdge(pivot, Pr[i], collision_checker)) - { - pivot_inserted = true; - interpolate(anchor, pivot, path); - anchor = pivot; - // new anchor - path.push_back(anchor); - if (i == Pr.size() - 1) - { - interpolate(anchor, Pr[i], path); - path.push_back(Pr[i]); - } - break; - } - } - } - } - else - { - std::cout << "no normal at hit point " << hit_world.x() << ", " << hit_world.y() << ", " << hit_world.z() << '\n'; - pivot_inserted = false; - } - - // No pivot added, then fast forward to the last visible point on path - if (!pivot_inserted) - { - // Pivot failed (or became unnecessary) — append the last visible original waypoint - interpolate(anchor, Pr[i-1], path); - anchor = Pr[i-1]; - // new anchor - path.push_back(anchor); - if (i == Pr.size() - 1) - { - path.push_back(Pr[i]); - } - } - } -} - -// void RRT_Planner::updateGridPtr(const openvdb::BoolGrid::Ptr& grid) { -// grid_map_ = grid; -// } diff --git a/robot/ros_ws/src/global/planners/exploration/src/utils.cpp b/robot/ros_ws/src/global/planners/exploration/src/utils.cpp deleted file mode 100644 index 5d7768f51..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/utils.cpp +++ /dev/null @@ -1,772 +0,0 @@ -#include "utils/utils.hpp" -#include -#include -#include - -std_msgs::msg::ColorRGBA heightToColor(double norm_z) -{ - std_msgs::msg::ColorRGBA color; - color.a = 1.0; - - double h = norm_z * 0.8; - int i = static_cast(h * 6.0); - double f = (h * 6.0) - i; - double q = 1.0 - f; - - switch (i % 6) - { - case 0: - color.r = 1.0; - color.g = f; - color.b = 0.0; - break; - case 1: - color.r = q; - color.g = 1.0; - color.b = 0.0; - break; - case 2: - color.r = 0.0; - color.g = 1.0; - color.b = f; - break; - case 3: - color.r = 0.0; - color.g = q; - color.b = 1.0; - break; - case 4: - color.r = f; - color.g = 0.0; - color.b = 1.0; - break; - case 5: - color.r = 1.0; - color.g = 0.0; - color.b = q; - break; - default: - color.r = 1.0; - color.g = 0.5; - color.b = 0.5; - break; - } - return color; -} - -std_msgs::msg::ColorRGBA valueToRainbowColor(float value) -{ - std_msgs::msg::ColorRGBA color; - value = std::fmod(value, 1.0f); // Wrap values > 1.0 around - - float r = std::fabs(value * 6 - 3) - 1; - float g = 2 - std::fabs(value * 6 - 2); - float b = 2 - std::fabs(value * 6 - 4); - - color.r = std::clamp(r, 0.0f, 1.0f); - color.g = std::clamp(g, 0.0f, 1.0f); - color.b = std::clamp(b, 0.0f, 1.0f); - color.a = 1.0; - return color; -} - -// void generateVDBMarker(const GridT::Ptr& grid, -// const std::string& frame_id, -// visualization_msgs::msg::Marker& marker_msg) -// { -// marker_msg.header.frame_id = frame_id; -// marker_msg.header.stamp = rclcpp::Clock().now(); -// marker_msg.ns = "vdb_grid"; -// marker_msg.id = 0; -// marker_msg.type = visualization_msgs::msg::Marker::CUBE_LIST; -// marker_msg.action = visualization_msgs::msg::Marker::ADD; -// marker_msg.pose.orientation.w = 1.0; -// marker_msg.color.a = 1.0; -// marker_msg.frame_locked = true; - -// double voxel_size = grid->voxelSize()[0]; -// marker_msg.scale.x = voxel_size; -// marker_msg.scale.y = voxel_size; -// marker_msg.scale.z = voxel_size; - -// openvdb::CoordBBox bbox = grid->evalActiveVoxelBoundingBox(); -// double z_min = grid->indexToWorld(bbox.min()).z(); -// double z_max = grid->indexToWorld(bbox.max()).z(); -// double dz = z_max - z_min; - -// for (auto iter = grid->cbeginValueOn(); iter; ++iter) { -// openvdb::Vec3d world = grid->indexToWorld(iter.getCoord()); - -// geometry_msgs::msg::Point pt; -// pt.x = world.x(); -// pt.y = world.y(); -// pt.z = world.z(); -// marker_msg.points.push_back(pt); - -// double norm_z = (dz > 1e-3) ? (world.z() - z_min) / dz : 0.5; -// marker_msg.colors.push_back(heightToColor(norm_z)); -// } - -// if (marker_msg.points.empty()) { -// marker_msg.action = visualization_msgs::msg::Marker::DELETE; -// } -// } - -void generateVDBMarker(const openvdb::FloatGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg) -{ - marker_msg.header.frame_id = frame_id; - marker_msg.header.stamp = rclcpp::Clock().now(); - marker_msg.ns = "vdb_grid"; - marker_msg.id = 0; - marker_msg.type = visualization_msgs::msg::Marker::CUBE_LIST; - marker_msg.action = visualization_msgs::msg::Marker::ADD; - marker_msg.pose.orientation.w = 1.0; - marker_msg.color.a = 1.0; - marker_msg.frame_locked = true; - - double voxel_size = grid->voxelSize()[0]; - marker_msg.scale.x = voxel_size; - marker_msg.scale.y = voxel_size; - marker_msg.scale.z = voxel_size; - - openvdb::CoordBBox bbox = grid->evalActiveVoxelBoundingBox(); - double z_min = grid->indexToWorld(bbox.min()).z(); - double z_max = grid->indexToWorld(bbox.max()).z(); - double dz = z_max - z_min; - - for (auto iter = grid->cbeginValueOn(); iter; ++iter) - { - if (iter.getValue() <= 0.0f) - { - continue; - } - - openvdb::Vec3d world = grid->indexToWorld(iter.getCoord()); - - geometry_msgs::msg::Point pt; - pt.x = world.x(); - pt.y = world.y(); - pt.z = world.z(); - marker_msg.points.push_back(pt); - - double norm_z = (dz > 1e-3) ? (world.z() - z_min) / dz : 0.5; - marker_msg.colors.push_back(heightToColor(norm_z)); - } - - if (marker_msg.points.empty()) - { - marker_msg.action = visualization_msgs::msg::Marker::DELETE; - } -} - -void generateFrontierMarker(const openvdb::BoolGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg) -{ - marker_msg.header.frame_id = frame_id; - marker_msg.header.stamp = rclcpp::Clock().now(); - marker_msg.ns = "vdb_grid"; - marker_msg.id = 0; - marker_msg.type = visualization_msgs::msg::Marker::CUBE_LIST; - marker_msg.action = visualization_msgs::msg::Marker::ADD; - marker_msg.pose.orientation.w = 1.0; - marker_msg.color.a = 1.0; - marker_msg.frame_locked = true; - - double voxel_size = grid->voxelSize()[0]; - marker_msg.scale.x = voxel_size; - marker_msg.scale.y = voxel_size; - marker_msg.scale.z = voxel_size; - - openvdb::CoordBBox bbox = grid->evalActiveVoxelBoundingBox(); - double z_min = grid->indexToWorld(bbox.min()).z(); - double z_max = grid->indexToWorld(bbox.max()).z(); - double dz = z_max - z_min; - - for (auto iter = grid->cbeginValueOn(); iter; ++iter) - { - if (!iter.getValue()) - { - continue; - } - - openvdb::Vec3d world = grid->indexToWorld(iter.getCoord()); - - geometry_msgs::msg::Point pt; - pt.x = world.x(); - pt.y = world.y(); - pt.z = world.z(); - - std_msgs::msg::ColorRGBA color; - color.r = 1.0; - color.g = 0.0; - color.b = 0.0; - color.a = 1.0; - - marker_msg.points.push_back(pt); - marker_msg.colors.push_back(color); - } - - if (marker_msg.points.empty()) - { - marker_msg.action = visualization_msgs::msg::Marker::DELETE; - } -} - -void generateClusterMarker(const openvdb::FloatGrid::Ptr &grid, - const std::string &frame_id, - visualization_msgs::msg::Marker &marker_msg) -{ - marker_msg.header.frame_id = frame_id; - marker_msg.header.stamp = rclcpp::Clock().now(); - marker_msg.ns = "cluster_grid"; - marker_msg.id = 0; - marker_msg.type = visualization_msgs::msg::Marker::CUBE_LIST; - marker_msg.action = visualization_msgs::msg::Marker::ADD; - marker_msg.pose.orientation.w = 1.0; - marker_msg.color.a = 1.0; - marker_msg.frame_locked = true; - - double voxel_size = grid->voxelSize()[0]; - marker_msg.scale.x = voxel_size; - marker_msg.scale.y = voxel_size; - marker_msg.scale.z = voxel_size; - - for (auto iter = grid->cbeginValueOn(); iter; ++iter) - { - float cluster_val = iter.getValue(); - if (cluster_val <= 0.0f) - continue; // skip unclustered voxels - - openvdb::Vec3d world = grid->indexToWorld(iter.getCoord()); - - geometry_msgs::msg::Point pt; - pt.x = world.x(); - pt.y = world.y(); - pt.z = world.z(); - marker_msg.points.push_back(pt); - - marker_msg.colors.push_back(valueToRainbowColor(cluster_val)); - } - - if (marker_msg.points.empty()) - { - marker_msg.action = visualization_msgs::msg::Marker::DELETE; - } -} - -bool surface_edge_frontier(openvdb::FloatGrid::Accessor accessor, - openvdb::Coord ijk) -{ - openvdb::math::Coord ijk_n; - float gridvalue; - std::vector unknown_list; - std::vector occupied_list; - for (int di = -1; di <= 1; ++di) - { - ijk_n[0] = ijk[0] + di; - for (int dj = -1; dj <= 1; ++dj) - { - ijk_n[1] = ijk[1] + dj; - for (int dk = -1; dk <= 1; ++dk) - { - ijk_n[2] = ijk[2] + dk; - bool surround_state = accessor.probeValue(ijk_n, gridvalue); - // if there is unknown neighbor, surround_state will be inactive - if (!surround_state) - { - unknown_list.push_back(Eigen::Vector3d(di, dj, dk)); - // std::cout << "pushing unknown" << '\n'; - } - else if (gridvalue > 0) - { - occupied_list.push_back(Eigen::Vector3d(di, dj, dk)); - // std::cout << "pushing occ" << '\n'; - } - } - } - } - if (unknown_list.empty() || occupied_list.empty()) - { - return false; - } - for (size_t i = 0; i < unknown_list.size(); ++i) - { - for (size_t j = 0; j < occupied_list.size(); ++j) - { - Eigen::Vector3d un_occ_diff_vec = unknown_list[i] - occupied_list[j]; - // std::cout << "diff is " << un_occ_diff_vec.dot(un_occ_diff_vec) << '\n'; - if (un_occ_diff_vec.dot(un_occ_diff_vec) <= 3) - { - return true; - } - } - } - return false; -} - -void create_clustering_coordbbox(const openvdb::BoolGrid::Ptr grid, - openvdb::math::CoordBBox &coord_bbox_out, - const openvdb::Vec3d ¢er, - const float frontier_count_cube_dim) -{ - openvdb::Vec3d bbox_min, bbox_max; - openvdb::Coord ijk_bbox_min, ijk_bbox_max; - openvdb::math::Transform &grid_tf(grid->transform()); - - bbox_min.x() = center.x() - (frontier_count_cube_dim / 2); - bbox_max.x() = center.x() + (frontier_count_cube_dim / 2); - - bbox_min.y() = center.y() - (frontier_count_cube_dim / 2); - bbox_max.y() = center.y() + (frontier_count_cube_dim / 2); - - bbox_min.z() = center.z() - (frontier_count_cube_dim / 2); - bbox_max.z() = center.z() + (frontier_count_cube_dim / 2); - - ijk_bbox_min = grid_tf.worldToIndexCellCentered(bbox_min); - ijk_bbox_max = grid_tf.worldToIndexCellCentered(bbox_max); - - coord_bbox_out.reset(ijk_bbox_min, ijk_bbox_max); -} - -void point_clustering_vis(openvdb::BoolGrid::Ptr grid, - const float cluster_cube_dim, - pcl::PointCloud::Ptr cluster_cloud, - const int voxel_cluster_count_thresh, - std::vector> &clustered_points, - openvdb::FloatGrid::Ptr visualized_clusters) -{ - // for visualization only - float cluster_num = 0; - openvdb::FloatGrid::Accessor vis_acc = visualized_clusters->getAccessor(); - // - - openvdb::BoolGrid::Ptr grid_copy = grid->deepCopy(); - int voxel_count = 0; - openvdb::BoolGrid::Accessor acc = grid_copy->getAccessor(); - const openvdb::math::Transform &grid_tf(grid_copy->transform()); - pcl::PointXYZI cluster_score_point; - bool voxel_value = false; - std::vector points_buffer; - for (openvdb::BoolGrid::ValueOnCIter itr = grid_copy->cbeginValueOn(); itr; ++itr) - { - voxel_value = itr.getValue(); - // voxels that have not been "marked" before - if (voxel_value) - { - // for visualization only - std::vector vis_boxes; - // - - openvdb::Coord ijk = itr.getCoord(); - openvdb::Vec3d xyz = grid_tf.indexToWorld(ijk); - // create a bbox centered around xyz - openvdb::math::CoordBBox bbox; - create_clustering_coordbbox(grid_copy, bbox, xyz, cluster_cube_dim); - // iterate through bbox and find intersecting grid voxels - // save points to buffer, copy from buffer to output list - // if cluster is big enough - // count neighboring voxels (with voxel_value > voxel_mark_priority_medium) within bbox - voxel_count = 0; - points_buffer.clear(); - for (auto ijk = bbox.beginXYZ(); ijk; ++ijk) - { - openvdb::Coord idx = *ijk; - openvdb::Vec3d pt_xyz = grid_tf.indexToWorld(idx); - if (acc.getValue(idx)) - { - Eigen::Vector3d point{pt_xyz.x(), pt_xyz.y(), pt_xyz.z()}; - points_buffer.push_back(point); - voxel_count += 1; - - // for visualization only - openvdb::Coord vis_ijk = idx; - vis_boxes.push_back(vis_ijk); - // - } - } - // if nn vox count > thresh - // insert xyz and nn vox count as an XYZI point into cluster cloud - // mark all nn voxels and cluster centroid vox as --> voxel_mark_priority_medium - if (voxel_count >= voxel_cluster_count_thresh) - { - Eigen::Vector3d cluster_centroid = VDBUtil::computeCentroid(points_buffer); - cluster_score_point.x = cluster_centroid.x(); - cluster_score_point.y = cluster_centroid.y(); - cluster_score_point.z = cluster_centroid.z(); - cluster_score_point.intensity = voxel_count; - cluster_cloud->push_back(cluster_score_point); - mark_voxels_bool(points_buffer, grid_copy, false); - clustered_points.push_back(points_buffer); - - // for visualization only, one cluster one value, thus one color. - cluster_num += 0.1; - for (size_t pt = 0; pt < vis_boxes.size(); ++pt) - { - vis_acc.setValue(vis_boxes[pt], cluster_num); - } - // - } - } - } -} - -void mark_voxels_bool(const std::vector &point_list, - openvdb::BoolGrid::Ptr grid, - const bool voxel_value) -{ - openvdb::BoolGrid::Accessor acc = grid->getAccessor(); - const openvdb::math::Transform &grid_tf(grid->transform()); - for (auto &point : point_list) - { - openvdb::Vec3d xyz{point.x(), point.y(), point.z()}; - openvdb::Coord ijk = grid_tf.worldToIndexCellCentered(xyz); - acc.setValue(ijk, voxel_value); - } -} - -Eigen::Vector3d VDBUtil::computeCentroid(const std::vector &point_list) -{ - - Eigen::Vector3d sum = std::accumulate(point_list.begin(), - point_list.end(), - Eigen::Vector3d::Zero().eval()); - // Eigen::Vector3d centroid {sum / point_list.size()}; - - return sum / point_list.size(); -} - -void VDBUtil::updateOccMapFromNdArray(openvdb::FloatGrid::Ptr grid_logocc, - pcl::PointCloud::Ptr xyz, - const tf2::Vector3 &origin, - float l_free, - float l_occ, - float l_min, - float l_max, - int hit_thickness, - double threshold, - float visited_cleared_logocc_min_thresh) -{ - openvdb::FloatGrid::Accessor acc = grid_logocc->getAccessor(); - openvdb::Vec3d origin2(origin.x(), origin.y(), origin.z()); - openvdb::Vec3d origin_ijk = grid_logocc->worldToIndex(origin2); - - for (size_t i = 0; i < xyz->points.size(); ++i) - { - openvdb::Vec3d p_ijk = grid_logocc->worldToIndex(openvdb::Vec3d(xyz->points[i].x, - xyz->points[i].y, - xyz->points[i].z)); - - openvdb::Vec3d dir(p_ijk - origin_ijk); - double range = dir.length(); - dir.normalize(); - openvdb::math::Ray ray(origin_ijk, dir); - // ray, start_time, max_time - - if (range <= threshold) - { - openvdb::math::DDA, 0> dda(ray, 0., range); - // want to stop before dda.time == dda.maxTime - do - { - openvdb::Coord ijk(dda.voxel()); - float ll = acc.getValue(ijk); - if (ll < visited_cleared_logocc_min_thresh) - { - acc.setValue(ijk, std::max(l_min, ll + l_free)); - } - dda.step(); - } while (dda.time() < dda.maxTime()); - - // post condition, at hit voxel - for (int i = 0; i < hit_thickness; ++i) - { - openvdb::Coord ijk(dda.voxel()); - float ll = acc.getValue(ijk); - if (ll < visited_cleared_logocc_min_thresh) - { - acc.setValue(ijk, std::min(l_max, ll + l_occ)); - } - dda.step(); - } - } - else - { - openvdb::math::DDA, 0> dda(ray, 0., std::min(range, threshold)); - do - { - openvdb::Coord ijk(dda.voxel()); - float ll = acc.getValue(ijk); - if (ll < visited_cleared_logocc_min_thresh) - { - acc.setValue(ijk, std::max(l_min, ll + l_free)); - } - dda.step(); - } while (dda.time() < dda.maxTime()); - } - } -} - -void VDBUtil::setVoxelSize(openvdb::GridBase &grid, double vs) -{ - // cell-centered - const openvdb::math::Vec3d offset(vs / 2., vs / 2., vs / 2.); - openvdb::math::Transform::Ptr tf = openvdb::math::Transform::createLinearTransform(vs); - tf->postTranslate(offset); - grid.setTransform(tf); -} - -bool VDBUtil::collCheckPointInPartialFreeSpace(const openvdb::FloatGrid::Ptr grid, - const Eigen::Vector3d &point_xyz, - const float length, - const float breadth, - const float height, - const float l_occ, - const float unknown_fraction_thresh, - const float occupied_fraction_thresh) -{ - - bool point_is_in_free_space; - if (grid->empty()) - { - std::cerr << "[VDBUtil::checkPointInFreeSpace] Grid is empty. \ - Cannot perform collision check. Exiting." - << "\n"; - return false; - } - const openvdb::math::Transform &grid_tf(grid->transform()); - openvdb::Vec3d xyz(point_xyz.x(), point_xyz.y(), point_xyz.z()); - openvdb::math::CoordBBox coord_bbox; - - VDBUtil::createCoordBBox(grid, coord_bbox, xyz, - length, breadth, height); - - long int bbox_total_vox_count = 0; - long int unknown_vox_count = 0; - long int occupied_vox_count = 0; - openvdb::FloatGrid::Accessor acc = grid->getAccessor(); - for (auto ijk = coord_bbox.beginXYZ(); ijk; ++ijk) - { - ++bbox_total_vox_count; - openvdb::Coord idx = *ijk; - float vox_val = 0.0; - bool vox_state = acc.probeValue(idx, vox_val); - if (vox_state && vox_val > l_occ) - { - ++occupied_vox_count; - } - else if (!vox_state) - { - ++unknown_vox_count; - } - } - double occupied_fraction = double(occupied_vox_count) / double(bbox_total_vox_count); - double unknown_fraction = double(unknown_vox_count) / double(bbox_total_vox_count); - // if ( occupied_fraction <= occupied_fraction_thresh && unknown_fraction <= unknown_fraction_thresh) { - // point_is_in_free_space = true; - // } else { // in collision - // point_is_in_free_space = false; - // } - - if (occupied_fraction > occupied_fraction_thresh || unknown_fraction > unknown_fraction_thresh) - { - point_is_in_free_space = false; - } - else - { // in free space - point_is_in_free_space = true; - } - - return point_is_in_free_space; -} - -bool VDBUtil::checkCollisionAlongRay(const openvdb::FloatGrid::Ptr& grid, - const openvdb::Vec3d &p1_xyz, - const openvdb::Vec3d &p2_xyz, - const float bbox_length, - const float bbox_breadth, - const float bbox_height, - const float l_occ, - openvdb::Vec3d &hit_point, - float dist_offset_from_cluster, - bool consider_unknown_occupied) -{ - - bool in_collision = false; - openvdb::Coord ray_element_ijk; - openvdb::Vec3d ray_element_xyz; - - openvdb::FloatGrid::Accessor acc = grid->getAccessor(); - const openvdb::math::Transform &grid_tf(grid->transform()); - - openvdb::Vec3d p1_ijk = grid_tf.worldToIndex(p1_xyz); - openvdb::Vec3d p2_ijk = grid_tf.worldToIndex(p2_xyz); - openvdb::Vec3d dir(p2_ijk - p1_ijk); - double distance = dir.length(); - dir.normalize(); - openvdb::math::Ray ray(p1_ijk, dir); - openvdb::math::DDA, 0> dda(ray, 0., distance); - // want to stop before dda.time == dda.maxTime - // do { - while (dda.next() < dda.maxTime()) - { - - ray_element_ijk = dda.voxel(); - // check for robot model cube occupancy - ray_element_xyz = grid->indexToWorld(ray_element_ijk); - openvdb::math::CoordBBox coord_bbox; - VDBUtil::createCoordBBox(grid, coord_bbox, ray_element_xyz, - bbox_length, bbox_breadth, bbox_height); - if (VDBUtil::checkIntersectWithCoordBBox(coord_bbox, grid, - l_occ, consider_unknown_occupied)) - { - in_collision = true; - break; - } - dda.step(); - } - // } while (dda.time() < dda.maxTime()); - // } while (dda.next() < dda.maxTime()); - - if (in_collision) - { - // openvdb::Vec3d hit_ray(ray_element_ijk - p1_ijk); - // double hit_distance = hit_ray.length(); - // double min_threshold_distance = distance - dist_offset_from_cluster; - // if (hit_distance >= min_threshold_distance) - // { - // return false; - // } - // post condition, at hit voxel - hit_point = ray_element_xyz; - return true; - } - else - { - return false; - } -} - -long int VDBUtil::countVoxelsInsideBbox(const openvdb::FloatGrid::Ptr &grid, - const Eigen::Vector3d &bbox_center, - const float length, - const float breadth, - const float height) -{ - - openvdb::CoordBBox bbox; - long int vox_count = 0; - float log_occ_val = 0.0; - const openvdb::math::Transform &grid_tf(grid->transform()); - openvdb::FloatTree::Ptr grid_tree = grid->treePtr(); - createCoordBBox(grid, - bbox, - convertEigenVecToVec3D(bbox_center), - length, - breadth, - height); - - for (auto bbox_iter = bbox.beginXYZ(); bbox_iter; ++bbox_iter) - { - openvdb::Coord ijk = *bbox_iter; - if (grid_tree->probeValue(ijk, log_occ_val)) - { - vox_count += 1; - } - } - return vox_count; -} - -// void VDBUtil::createCoordBBox(const openvdb::FloatGrid::Ptr grid, -// openvdb::math::CoordBBox &coord_bbox_out, -// const openvdb::Vec3d ¢er, -// const float bbox_length, -// const float bbox_breadth, -// const float bbox_height) -// { - -// openvdb::Vec3d bbox_min, bbox_max; -// openvdb::Coord ijk_bbox_min, ijk_bbox_max; -// openvdb::math::Transform &grid_tf(grid->transform()); - -// bbox_min.x() = center.x() - (bbox_length / 2); -// bbox_max.x() = center.x() + (bbox_length / 2); - -// bbox_min.y() = center.y() - (bbox_breadth / 2); -// bbox_max.y() = center.y() + (bbox_breadth / 2); - -// bbox_min.z() = center.z() - (bbox_height / 2); -// bbox_max.z() = center.z() + (bbox_height / 2); - -// ijk_bbox_min = grid_tf.worldToIndexCellCentered(bbox_min); -// ijk_bbox_max = grid_tf.worldToIndexCellCentered(bbox_max); - -// coord_bbox_out.reset(ijk_bbox_min, ijk_bbox_max); -// } - -// The above implementation rounds to the nearest cell center. -// If bbox_max lands just past a cell center, rounding can pull it back and -// you’ll silently drop the outermost layer of cells. -// Use a continuous index bbox and floor/ceil: -void VDBUtil::createCoordBBox(const openvdb::FloatGrid::Ptr grid, - openvdb::math::CoordBBox &coord_bbox_out, - const openvdb::Vec3d ¢er, - const float bbox_length, - const float bbox_breadth, - const float bbox_height) -{ - const auto& tf = grid->transform(); - const openvdb::Vec3d half(bbox_length*0.5, bbox_breadth*0.5, bbox_height*0.5); - - // World-space box -> continuous index-space box - const openvdb::BBoxd wbox(center - half, center + half); - const openvdb::BBoxd ibox = tf.worldToIndex(wbox); - - // Integer (cell-centered) bounds: floor(min), ceil(max) - const openvdb::Coord ijk_min( - static_cast(std::floor(ibox.min().x() + 0.5)), - static_cast(std::floor(ibox.min().y() + 0.5)), - static_cast(std::floor(ibox.min().z() + 0.5))); - - const openvdb::Coord ijk_max( - static_cast(std::ceil (ibox.max().x() - 0.5)), - static_cast(std::ceil (ibox.max().y() - 0.5)), - static_cast(std::ceil (ibox.max().z() - 0.5))); - - coord_bbox_out.reset(ijk_min, ijk_max); -} - -bool VDBUtil::checkIntersectWithCoordBBox(const openvdb::math::CoordBBox &search_bbox, - const openvdb::FloatGrid::Ptr grid, - float occupancy_threshold, - bool consider_unknown_occupied) -{ - - openvdb::math::Transform &grid_tf(grid->transform()); - const size_t count = search_bbox.volume(); - openvdb::Vec3d xyz; - openvdb::FloatGrid::Accessor acc = grid->getAccessor(); - for (auto iter = search_bbox.beginXYZ(); iter; ++iter) - { - openvdb::Coord ijk = *iter; - float vox_val = 0.0; - bool vox_state = acc.probeValue(ijk, vox_val); - if (vox_state && vox_val > occupancy_threshold) - { - return true; - } - else if (consider_unknown_occupied && std::fabs(vox_val) <= FLT_EPSILON) - { - return true; - } - } - return false; -} - -openvdb::Vec3d VDBUtil::convertEigenVecToVec3D(const Eigen::Vector3d &point) -{ - - return openvdb::Vec3d(point.x(), point.y(), point.z()); -} \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/src/viewpoint_sampling.cpp b/robot/ros_ws/src/global/planners/exploration/src/viewpoint_sampling.cpp deleted file mode 100644 index 827325211..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/viewpoint_sampling.cpp +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Viewpoint Sampling Module - * Copyright (C) 2020 Rohit Garg - All rights reserved - * Original author: rg1@cmu.edu / rohitgarg617@gmail.com - * Modified by junbiny@andrew.cmu.edu, removed matplotlib part and modified viewpoint sampling method - */ - -#include "../include/viewpoint_sampling.hpp" - -namespace viewpoint_sampling_ns -{ - // constructor - ViewpointSampling::ViewpointSampling(ListOfClustersT &cluster_list, - double viewing_distance, - float bbox_length, - float bbox_breadth, - float bbox_height, - float l_occ, - float vox_size, - uint16_t num_viewp_sample_per_cluster, - double viewing_distance_inner_r, - double viewing_distance_outer_r, - double viewing_distance_z_offset, - double coll_check_endpoint_offset, - double bbox_fraction_viewp_centroid_coll_check, - double viewp_bbox_unknown_frac_thresh, - double viewp_bbox_occ_frac_thresh) - { - - viewing_distance_ = viewing_distance; - bbox_length_ = bbox_length; - bbox_breadth_ = bbox_breadth; - bbox_height_ = bbox_height; - l_occ_ = l_occ; - vox_size_ = vox_size; - num_viewp_to_sample_ = num_viewp_sample_per_cluster; - viewing_distance_inner_r_ = viewing_distance_inner_r; - viewing_distance_outer_r_ = viewing_distance_outer_r; - viewing_distance_z_offset_ = viewing_distance_z_offset; - coll_check_endpoint_offset_ = coll_check_endpoint_offset; - bbox_fraction_viewp_centroid_coll_check_ = bbox_fraction_viewp_centroid_coll_check; - cluster_list_ = std::make_shared(cluster_list); - viewpoint_list_ = pcl::PointCloud::Ptr(new pcl::PointCloud()); - viewp_bbox_unknown_frac_thresh_ = viewp_bbox_unknown_frac_thresh; - viewp_bbox_occ_frac_thresh_ = viewp_bbox_occ_frac_thresh; - } - - // reset data stored by the class - void ViewpointSampling::reset(ListOfClustersT &cluster_list) - { - - // TODO -> not sure if this make_shared is required - cluster_list_ = std::make_shared(cluster_list); - xy_points_.clear(); - viewpoint_list_->clear(); - } - - // update the global map grid pointer - void ViewpointSampling::updateGridPtr(const openvdb::FloatGrid::Ptr &grid) - { - grid_map_ = grid; - } - - // update the cluster centroid list pointer - void ViewpointSampling::updateCentroidListPtr( - const pcl::PointCloud::Ptr ¢roid_list) - { - centroid_list_ = centroid_list; - } - - // main method that samples viewpoints - void ViewpointSampling::sampleViewpoints() - { - cluster_idx_ = -1; - - for (const auto &cluster : *cluster_list_) - { - - cluster_idx_ += 1; - - // current_cluster_centroid_ = computeMean(); - pcl::PointXYZI centroid = centroid_list_->at(cluster_idx_); - current_cluster_score_ = centroid.intensity; - current_cluster_centroid_.x() = centroid.x; - current_cluster_centroid_.y() = centroid.y; - current_cluster_centroid_.z() = centroid.z; - sampleViewpointsAroundCentroid(); - } - } - - // remove z component from 3D points - void ViewpointSampling::projectToXYPlane() - { - xy_points_.clear(); - for (const auto &point : cluster_list_->at(cluster_idx_)) - { - Eigen::Vector2d point_in{point.x(), point.y()}; - xy_points_.push_back(point_in); - } - } - - // logging to file - void ViewpointSampling::logDataToFile() - { - - std::string file1 = "cluster"; - std::string file_path1 = setupFilePath(cluster_idx_, file1); - writeVecToCSVfile(file_path1, xy_points_); - - std::string file2 = "fit_line_wsolver" + std::to_string(solver_type_); - std::string file_path2 = setupFilePath(cluster_idx_, file2); - Eigen::MatrixXd output_mat = fitted_line_; - writeToCSVfile(file_path2, output_mat); - } - - // generate file path - std::string ViewpointSampling::setupFilePath(int file_index, std::string file_name) - { - std::string fname = file_name + "_"; - std::string path = "/home/rohit/rWorkspace/vdb_map_ws/src/map_processor/logs/"; - std::string idx = std::to_string(file_index); - std::string fformat = ".txt"; - return path + fname + idx + fformat; - } - - // write matrix to a CSV file - void ViewpointSampling::writeToCSVfile(std::string file_name, Eigen::MatrixXd &matrix) - { - const static Eigen::IOFormat CSVFormat(Eigen::StreamPrecision, Eigen::DontAlignCols, ", ", "\n"); - std::ofstream file(file_name.c_str()); - file << matrix.format(CSVFormat); - file.close(); - } - - // write vector to a CSV file - void ViewpointSampling::writeVecToCSVfile(std::string file_name, - std::vector &points) - { - const static Eigen::IOFormat CSVFormat(Eigen::StreamPrecision, 0, ", ", "\n"); - std::ofstream file(file_name.c_str()); - for (const auto &point : points) - { - file << point(0) << ", " << point(1) << "\n"; - } - file.close(); - } - - // splitting into two separate vectors - void ViewpointSampling::toXVecYVec() - { - for (auto point : xy_points_) - { - cluster_x_points_.push_back(point.x()); - cluster_y_points_.push_back(point.y()); - } - } - - // discretize line into a list of points - void ViewpointSampling::drawPointsAlongLine(float spacing, float length) - { - double start_x = current_cluster_centroid_.x(); - uint64_t n_points = std::floor(length / spacing); - for (int i = 0; i < n_points; ++i) - { - double line_x_pt = start_x + (i * spacing); - double line_y_pt = (fitted_line_(0) * line_x_pt) + fitted_line_(0); - line_x_points_.push_back(line_x_pt); - line_y_points_.push_back(line_y_pt); - } - } - - // Ax=b -> A matrix - void ViewpointSampling::createAMatrix() - { - A_mat_ = Eigen::MatrixXd::Ones(xy_points_.size(), 2); - uint32_t idx = 0; - for (const auto &point : xy_points_) - { - Eigen::Vector2d point_in{point.x(), 1}; - A_mat_.row(idx) = point_in; - idx += 1; - } - } - - // Ax=b -> b matrix - void ViewpointSampling::createBMatrix() - { - b_vec_ = Eigen::VectorXd::Zero(xy_points_.size()); - uint32_t idx = 0; - for (const auto &point : xy_points_) - { - b_vec_(idx) = point.y(); - idx += 1; - } - } - - // for getting the cluster centroid - Eigen::Vector3d ViewpointSampling::computeMean() - { - - double sum_x = 0, sum_y = 0, sum_z = 0; - for (const auto &point : cluster_list_->at(cluster_idx_)) - { - sum_x += point.x(); - sum_y += point.y(); - sum_z += point.z(); - } - uint32_t n_points = cluster_list_->at(cluster_idx_).size(); - Eigen::Vector3d point_out{sum_x / n_points, sum_y / n_points, sum_z / n_points}; - return point_out; - } - - // get two points at a perpendicular distance from a line and a point on the line - void ViewpointSampling::computePointsPerpToLine() - { - - opposing_viewpoints_ = Eigen::MatrixXd::Zero(2, 3); - - double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; - - double x_c = current_cluster_centroid_.x(); // cluster xy coords - double y_c = current_cluster_centroid_.y(); - - double m = fitted_line_(0); // slope of the line - double r = viewing_distance_; // distance from the line - - // ref: http://www.rasmus.is/uk/t/F/Su58k05.htm#:~:text=To%20find%20a%20direction%20vector,%2B%20by%20%2B%20c%20%3D%200. - // ref: https://math.stackexchange.com/questions/2043054/find-a-point-on-a-perpendicular-line-a-given-distance-from-another-point - opposing_viewpoints_(0, 0) = x_c + - std::sqrt(std::pow(r, 2) / (1 + (1 / std::pow(m, 2)))); - - opposing_viewpoints_(1, 0) = x_c - - std::sqrt(std::pow(r, 2) / (1 + (1 / std::pow(m, 2)))); - - opposing_viewpoints_(0, 1) = y_c - - (1 / m) * std::sqrt(std::pow(r, 2) / (1 + (1 / std::pow(m, 2)))); - - opposing_viewpoints_(1, 1) = y_c + - (1 / m) * std::sqrt(std::pow(r, 2) / (1 + (1 / std::pow(m, 2)))); - - // z -> same height as the cluster centroid since the - // view plane is perpendicular to XY plane - opposing_viewpoints_(0, 2) = current_cluster_centroid_.z(); - opposing_viewpoints_(1, 2) = current_cluster_centroid_.z(); - } - - // select one out of the two points on either side of the line - void ViewpointSampling::selectOnePoint() - { - - long int active_vox_count_side1 = 0, active_vox_count_side2 = 0; - - active_vox_count_side1 = VDBUtil::countVoxelsInsideBbox(grid_map_, - opposing_viewpoints_.row(0), - bbox_length_, - bbox_breadth_, - bbox_height_); - - active_vox_count_side2 = VDBUtil::countVoxelsInsideBbox(grid_map_, - opposing_viewpoints_.row(1), - bbox_length_, - bbox_breadth_, - bbox_height_); - - // viewpoint selection is happening using the number of active voxels - // found in a bbox centered around the opposing points - // whichever bbox has a higher count is chosen - if (active_vox_count_side1 >= active_vox_count_side2) - { - selected_viewpoint_ = opposing_viewpoints_.row(0); - } - else - { - selected_viewpoint_ = opposing_viewpoints_.row(1); - } - direction_vector_ = current_cluster_centroid_ - selected_viewpoint_; - } - - // assuming x-forward, z-up convention - float ViewpointSampling::computeYawFromDirVector() - { - return std::atan2(direction_vector_(1), direction_vector_(0)); - } - - pcl::PointXYZHSV ViewpointSampling::constructViewpoint() - { - pcl::PointXYZHSV view_point; - - view_point.x = selected_viewpoint_.x(); - view_point.y = selected_viewpoint_.y(); - view_point.z = selected_viewpoint_.z(); - view_point.h = heading_; - view_point.s = current_cluster_score_; - view_point.v = 0.0; - return view_point; - } - - // line fit using linear least squares - bool ViewpointSampling::fitLineToPoints(int solver_flag) - { - - createAMatrix(); - createBMatrix(); - // ref: https://eigen.tuxfamily.org/dox/group__LeastSquares.html - switch (solver_flag) - { - case 1: - useSVDDecomposition(); - break; - case 2: - useQRDecomposition(); - break; - case 3: - useNormalEquations(); - break; - } - return true; - } - - // SVD is the most accurate but slowest - void ViewpointSampling::useSVDDecomposition() - { - fitted_line_ = Eigen::Vector2d::Zero(); - fitted_line_ = A_mat_.bdcSvd(Eigen::ComputeThinU | Eigen::ComputeThinV).solve(b_vec_); - } - - // QR decomposition sits between SVD and normal equations - void ViewpointSampling::useQRDecomposition() - { - fitted_line_ = Eigen::Vector2d::Zero(); - fitted_line_ = A_mat_.colPivHouseholderQr().solve(b_vec_); - } - - // solving A^T A x = A^T b (linear least squares using normal equations) - // fastest but also least accurate - void ViewpointSampling::useNormalEquations() - { - // Normal Equations method - fitted_line_ = Eigen::Vector2d::Zero(); - fitted_line_ = (A_mat_.transpose() * A_mat_).ldlt().solve(A_mat_.transpose() * b_vec_); - } - - // create viewpoints on a circle centered around the centroid - void ViewpointSampling::sampleViewpointsAroundCentroid() - { - - /* viewing_distance_inner_r_ = viewing_distance_inner_r; - viewing_distance_outer_r_ = viewing_distance_outer_r; - viewing_distance_z_offset_ */ - - // successively try viewpoints on different horizontal circles until some - // are successfully found - - double angle_spacing = num_viewp_to_sample_ / (2 * M_PI); - int count_viewpoints_added = 0; - - // First, try outer circle with 0 z offset - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_outer_r_, - 0.0, - angle_spacing); - - if (count_viewpoints_added > 1) - { - // Don't add any more - return; - } - - // Next, try inner circle with 0 z offset - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_inner_r_, - 0.0, - angle_spacing); - - if (count_viewpoints_added > 0) - { - // Don't add any more - return; - } - - // Try outer circle with +/- z offset - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_outer_r_, - +viewing_distance_z_offset_, - angle_spacing); - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_outer_r_, - -viewing_distance_z_offset_, - angle_spacing); - - if (count_viewpoints_added > 0) - { - // Don't add any more - return; - } - - // Try inner circle with +/- z offset - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_inner_r_, - +viewing_distance_z_offset_, - angle_spacing); - count_viewpoints_added += sampleViewpointsAroundCentroidParamaterized( - viewing_distance_inner_r_, - -viewing_distance_z_offset_, - angle_spacing); - - if (count_viewpoints_added > 0) - { - // Don't add any more - return; - } - } - - // create viewpoints on a circle centered around the centroid - // with preset parameters - int ViewpointSampling::sampleViewpointsAroundCentroidParamaterized( - const double viewing_distance, - const double z_offset, - const double angle_spacing) - { - // return the number of viewpoints added - - int count_viewpoints_added = 0; - double psi = 0.0; - - for (int i = 0; i < num_viewp_to_sample_; ++i) - { - - psi += angle_spacing * i; - - Eigen::Vector3d viewpoint; - viewpoint.x() = current_cluster_centroid_.x() + (viewing_distance * std::cos(psi)); - viewpoint.y() = current_cluster_centroid_.y() + (viewing_distance * std::sin(psi)); - viewpoint.z() = current_cluster_centroid_.z() + z_offset; - - // bool viewpoint_is_in_free_space = VDBUtil::checkPointInFreeSpace(grid_map_, - // viewpoint, bbox_length_, - // bbox_breadth_, bbox_height_, - // l_occ_, true); - - bool viewpoint_is_in_free_space = - VDBUtil::collCheckPointInPartialFreeSpace(grid_map_, - viewpoint, bbox_length_, - bbox_breadth_, bbox_height_, - l_occ_, - viewp_bbox_unknown_frac_thresh_, - viewp_bbox_occ_frac_thresh_); - - openvdb::Vec3d hp; - openvdb::Vec3d viewpoint_vbd(viewpoint.x(), viewpoint.y(), viewpoint.z()); - openvdb::Vec3d centroid_vdb(current_cluster_centroid_.x(), - current_cluster_centroid_.y(), - current_cluster_centroid_.z()); - - bool consider_unknown_occupied = false; - - double ray_coll_check_bbox_length = - bbox_fraction_viewp_centroid_coll_check_ * bbox_length_; - double ray_coll_check_bbox_breadth = - bbox_fraction_viewp_centroid_coll_check_ * bbox_breadth_; - double ray_coll_check_bbox_height = - bbox_fraction_viewp_centroid_coll_check_ * bbox_height_; - - bool viewpoint_to_centroid_is_free_space = !VDBUtil::checkCollisionAlongRay( - grid_map_, - viewpoint_vbd, - centroid_vdb, - ray_coll_check_bbox_length, - ray_coll_check_bbox_breadth, - ray_coll_check_bbox_height, - l_occ_, - hp, - coll_check_endpoint_offset_, - consider_unknown_occupied); - - if (viewpoint_is_in_free_space && viewpoint_to_centroid_is_free_space) - { - selected_viewpoint_ = viewpoint; - direction_vector_ = current_cluster_centroid_ - selected_viewpoint_; - heading_ = computeYawFromDirVector(); - viewpoint_list_->push_back(constructViewpoint()); - count_viewpoints_added++; - } - } - return count_viewpoints_added; - } - -} // namespace viewpoint_sampling_ns \ No newline at end of file diff --git a/robot/ros_ws/src/global/planners/exploration/src/waypoint_routing_node.cpp b/robot/ros_ws/src/global/planners/exploration/src/waypoint_routing_node.cpp deleted file mode 100644 index c83c39878..000000000 --- a/robot/ros_ws/src/global/planners/exploration/src/waypoint_routing_node.cpp +++ /dev/null @@ -1,351 +0,0 @@ -// Copyright (c) 2024 Carnegie Mellon University -// -// 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. - -#include "../include/exploration_node.hpp" -#include "../include/exploration_logic.hpp" - -#include -#include -#include - -#include "utils/utils.hpp" - -class WaypointRoutingNode : public ExplorationNode -{ - using ExplorationNode::ExplorationNode; -public: - void initialize() override; - -private: - std::string sub_target_path_topic_; - std::deque target_path; - - rclcpp::Subscription::SharedPtr sub_target_path; - void targetpathCallback(const geometry_msgs::msg::PoseArray::SharedPtr msg); - - void timerCallback() override; - void generate_plan() override; -}; - -void WaypointRoutingNode::initialize() -{ - this->declare_parameter("sub_target_path_topic"); - if (!this->get_parameter("sub_target_path_topic", this->sub_target_path_topic_)) - { - RCLCPP_ERROR(this->get_logger(), "Cannot read parameter: sub_target_path_topic"); - return; - } - - // TF buffer and listener - tf_buffer = std::make_shared(this->get_clock()); - tf_listener = std::make_shared(*tf_buffer); - - this->sub_lidar = this->create_subscription( - sub_lidar_topic_, rclcpp::QoS(1).durability_volatile().best_effort(), std::bind(&WaypointRoutingNode::lidarCallback, this, std::placeholders::_1)); - this->sub_target_path = this->create_subscription( - sub_target_path_topic_, rclcpp::QoS(10), std::bind(&WaypointRoutingNode::targetpathCallback, this, std::placeholders::_1)); - - this->pub_global_plan = this->create_publisher(pub_global_plan_topic_, 10); - this->pub_goal_point = - this->create_publisher(pub_goal_point_viz_topic_, 10); - this->pub_trajectory_lines = - this->create_publisher(pub_trajectory_viz_topic_, 10); - this->pub_vdb = - this->create_publisher(pub_grid_viz_topic_, 10); - - // Set up the timer - this->timer = this->create_wall_timer(std::chrono::seconds(5), - std::bind(&WaypointRoutingNode::timerCallback, this)); - // Set up the service - this->srv_exploration_toggle = this->create_service( - srv_exploration_toggle_topic_, std::bind(&WaypointRoutingNode::ExplorationToggleCallback, this, - std::placeholders::_1, std::placeholders::_2)); - RCLCPP_INFO(this->get_logger(), "Exploration node initialized"); -} - -void WaypointRoutingNode::targetpathCallback(const geometry_msgs::msg::PoseArray::SharedPtr msg) -{ - RCLCPP_INFO(this->get_logger(), "Received target path with %zu poses", msg->poses.size()); - this->target_path.assign(msg->poses.begin(), msg->poses.end()); -} - -void WaypointRoutingNode::generate_plan() -{ - RCLCPP_INFO(this->get_logger(), "Starting to generate plan..."); - - // std::tuple start_loc; - // if (this->generated_paths.size() == 0) - // { - // geometry_msgs::msg::Quaternion orientation = this->current_location.rotation; - // tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - // q.normalize(); - // double roll, pitch, yaw; - // tf2::Matrix3x3 m(q); - // m.getRPY(roll, pitch, yaw); - // start_loc = std::make_tuple(this->current_location.translation.x, - // this->current_location.translation.y, - // this->current_location.translation.z, yaw); - // } - // else - // { - // geometry_msgs::msg::Quaternion orientation = - // this->generated_paths.back().poses.back().pose.orientation; - // tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - // q.normalize(); - // double roll, pitch, yaw; - // tf2::Matrix3x3 m(q); - // m.getRPY(roll, pitch, yaw); - // start_loc = std::make_tuple(this->generated_paths.back().poses.back().pose.position.x, - // this->generated_paths.back().poses.back().pose.position.y, - // this->generated_paths.back().poses.back().pose.position.z, yaw); - // } - - ViewPoint start_point; - - if (this->generated_paths.empty() || this->generated_paths.back().poses.empty()) - { - // Case 1: no generated path yet → use current_location - geometry_msgs::msg::Quaternion orientation = this->current_location.rotation; - - tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - q.normalize(); - - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - - start_point.x = this->current_location.translation.x; - start_point.y = this->current_location.translation.y; - start_point.z = this->current_location.translation.z; - - start_point.orientation = orientation; - start_point.orientation_yaw = yaw; - start_point.orientation_set = true; - } - else - { - // Case 2: use last pose of last generated path - const auto &last_pose = this->generated_paths.back().poses.back().pose; - geometry_msgs::msg::Quaternion orientation = last_pose.orientation; - - tf2::Quaternion q(orientation.x, orientation.y, orientation.z, orientation.w); - q.normalize(); - - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - - start_point.x = last_pose.position.x; - start_point.y = last_pose.position.y; - start_point.z = last_pose.position.z; - - start_point.orientation = orientation; - start_point.orientation_yaw = yaw; - start_point.orientation_set = true; - } - - // convert first goal waypoint to ViewPoint - ViewPoint goal_viewpoint; - - const auto &first_pose = target_path.front(); - - goal_viewpoint.x = first_pose.position.x; - goal_viewpoint.y = first_pose.position.y; - goal_viewpoint.z = first_pose.position.z; - - // Keep quaternion + yaw consistent - tf2::Quaternion q(first_pose.orientation.x, - first_pose.orientation.y, - first_pose.orientation.z, - first_pose.orientation.w); - q.normalize(); - - double roll, pitch, yaw; - tf2::Matrix3x3(q).getRPY(roll, pitch, yaw); - - goal_viewpoint.orientation = first_pose.orientation; - goal_viewpoint.orientation_yaw = yaw; - goal_viewpoint.orientation_set = true; - - float timeout_duration = 5.0; - std::optional gen_path_opt = - // this->exploration_planner->generate_straight_rand_path(start_loc, timeout_duration); - // this->exploration_planner->select_viewpoint_and_plan(start_point, timeout_duration); - this->exploration_planner->plan_to_given_waypoint(start_point, goal_viewpoint); - if (gen_path_opt.has_value() && gen_path_opt.value().size() > 0) - { - RCLCPP_INFO(this->get_logger(), "Generated path with %ld points", - gen_path_opt.value().size()); - - // set the current goal location - this->current_goal_location = geometry_msgs::msg::Transform(); - this->current_goal_location.translation.x = std::get<0>(gen_path_opt.value().back()); - this->current_goal_location.translation.y = std::get<1>(gen_path_opt.value().back()); - this->current_goal_location.translation.z = std::get<2>(gen_path_opt.value().back()); - float z_rot = std::get<3>(gen_path_opt.value().back()); - tf2::Quaternion q; - q.setRPY(0, 0, z_rot); // Roll = 0, Pitch = 0, Yaw = yaw - q.normalize(); - this->current_goal_location.rotation.x = q.x(); - this->current_goal_location.rotation.y = q.y(); - this->current_goal_location.rotation.z = q.z(); - this->current_goal_location.rotation.w = q.w(); - - // publish the path - nav_msgs::msg::Path generated_single_path; - generated_single_path = nav_msgs::msg::Path(); - generated_single_path.header.stamp = this->now(); - generated_single_path.header.frame_id = world_frame_id_; - for (auto point : gen_path_opt.value()) - { - geometry_msgs::msg::PoseStamped point_msg; - point_msg.pose.position.x = std::get<0>(point); - point_msg.pose.position.y = std::get<1>(point); - point_msg.pose.position.z = std::get<2>(point); - // convert yaw rotation to quaternion - tf2::Quaternion q; - q.setRPY(0, 0, std::get<3>(point)); // Roll = 0, Pitch = 0, Yaw = yaw - q.normalize(); - point_msg.pose.orientation.x = q.x(); - point_msg.pose.orientation.y = q.y(); - point_msg.pose.orientation.z = q.z(); - point_msg.pose.orientation.w = q.w(); - point_msg.header.stamp = this->now(); - generated_single_path.poses.push_back(point_msg); - } - geometry_msgs::msg::PoseStamped last_goal_loc = generated_single_path.poses.back(); - this->current_goal_location.translation.x = last_goal_loc.pose.position.x; - this->current_goal_location.translation.y = last_goal_loc.pose.position.y; - this->current_goal_location.translation.z = last_goal_loc.pose.position.z; - this->current_goal_location.rotation.z = last_goal_loc.pose.orientation.z; - this->generated_paths.push_back(generated_single_path); - } - else - { - RCLCPP_ERROR(this->get_logger(), "Failed to generate path, size was 0"); - } -} - -void WaypointRoutingNode::timerCallback() -{ - RCLCPP_INFO(this->get_logger(), "timer callback start"); - // get current TF to world - try - { - geometry_msgs::msg::TransformStamped transform_stamped = - this->tf_buffer->lookupTransform(this->world_frame_id_, this->robot_frame_id_, - rclcpp::Time(0)); - this->current_location = transform_stamped.transform; - if (!this->received_first_robot_tf) - { - this->received_first_robot_tf = true; - this->last_location = this->current_location; - this->last_position_change = this->now(); - RCLCPP_WARN(this->get_logger(), "Received first robot_tf"); - } - } - catch (tf2::TransformException &ex) - { - RCLCPP_ERROR(this->get_logger(), "Robot tf not received: %s", ex.what()); - } - - - // visualize - visualization_msgs::msg::Marker vdb_marker_msg; - generateVDBMarker(this->exploration_planner->occ_grid, map_frame_id_, vdb_marker_msg); - vdb_marker_msg.header.stamp = this->now(); - this->pub_vdb->publish(vdb_marker_msg); - - if (this->enable_exploration) - { - if (!this->is_path_executing) - { - if (!target_path.empty() && this->received_first_robot_tf) - { - - this->generate_plan(); - - this->publish_plan(); - this->is_path_executing = true; - this->last_position_change = this->now(); // Reset stall timer when starting new plan - } - else - { - RCLCPP_WARN(this->get_logger(), "target path size is %zu", target_path.size()); - RCLCPP_WARN(this->get_logger(), "tf received is %d", this->received_first_robot_tf); - } - } - else - { - // check if the robot has reached the goal point - std::tuple current_point = std::make_tuple( - this->current_location.translation.x, this->current_location.translation.y, - this->current_location.translation.z); - std::tuple goal_point = std::make_tuple( - this->current_goal_location.translation.x, this->current_goal_location.translation.y, - this->current_goal_location.translation.z); - if (get_point_distance(current_point, goal_point) < - this->exploration_planner->path_end_threshold_m) - { - this->is_path_executing = false; - this->generated_paths.clear(); - this->target_path.pop_front(); - RCLCPP_INFO(this->get_logger(), "Reached goal point"); - } - else - { - // Check if position has changed significantly - std::tuple last_point = std::make_tuple(this->last_location.translation.x, - this->last_location.translation.y, - this->last_location.translation.z); - - if (get_point_distance(current_point, last_point) > this->position_change_threshold) - { - this->last_location = this->current_location; - this->last_position_change = this->now(); - } - else - { - // Check if we've been stationary for too long - rclcpp::Duration stall_duration = this->now() - this->last_position_change; - if (stall_duration.seconds() > this->stall_timeout_seconds) - { - RCLCPP_INFO(this->get_logger(), "Robot stationary for %f seconds, clearing plan", - stall_duration.seconds()); - this->is_path_executing = false; - this->generated_paths.clear(); - this->target_path.pop_front(); - this->last_position_change = this->now(); // Reset timer to avoid spam - } - } - } - } - } - - RCLCPP_INFO(this->get_logger(), "timer callback end"); -} - -int main(int argc, char *argv[]) -{ - rclcpp::init(argc, argv); - auto node = std::make_shared(); - node->initialize(); - rclcpp::spin(node); - rclcpp::shutdown(); - return 0; -} \ No newline at end of file diff --git a/simulation/isaac-sim/docker/docker-compose.yaml b/simulation/isaac-sim/docker/docker-compose.yaml index 03d14ed67..b74ef802e 100644 --- a/simulation/isaac-sim/docker/docker-compose.yaml +++ b/simulation/isaac-sim/docker/docker-compose.yaml @@ -65,6 +65,11 @@ services: # offset in meters. - ISAAC_SIM_FOLLOW_CAM=${ISAAC_SIM_FOLLOW_CAM:-} - ISAAC_SIM_FOLLOW_CAM_OFFSET=${ISAAC_SIM_FOLLOW_CAM_OFFSET:-} + # Optional raw sensor_msgs/Image feed of the follow camera (topic name, + # published on the followed drone's ROS domain) and its "WxH" resolution + # — a third-person video source for headless runs. + - ISAAC_SIM_FOLLOW_CAM_TOPIC=${ISAAC_SIM_FOLLOW_CAM_TOPIC:-} + - ISAAC_SIM_FOLLOW_CAM_RES=${ISAAC_SIM_FOLLOW_CAM_RES:-} # Spawn-row center "x,y" in meters (default origin) — for scenes whose # origin is cluttered; see launch_scripts/pegasus_app.py. - ISAAC_SIM_SPAWN_XY=${ISAAC_SIM_SPAWN_XY:-} diff --git a/simulation/isaac-sim/launch_scripts/pegasus_app.py b/simulation/isaac-sim/launch_scripts/pegasus_app.py index 62373e8ce..ea56b3063 100644 --- a/simulation/isaac-sim/launch_scripts/pegasus_app.py +++ b/simulation/isaac-sim/launch_scripts/pegasus_app.py @@ -614,6 +614,29 @@ def _setup_follow_cam(self, stage): except Exception as exc: # headless variants may have no viewport carb.log_warn(f"Could not switch viewport to follow cam: {exc}") + # ISAAC_SIM_FOLLOW_CAM_TOPIC=: also publish the follow cam as a + # raw sensor_msgs/Image on the followed drone's ROS domain, so a + # headless run (CI, video capture without a display) still yields a + # third-person feed. ISAAC_SIM_FOLLOW_CAM_RES="WxH" (default 1280x720). + topic = os.environ.get("ISAAC_SIM_FOLLOW_CAM_TOPIC", "").strip() + if topic: + width, height = 1280, 720 + res_raw = os.environ.get("ISAAC_SIM_FOLLOW_CAM_RES", "").strip().lower() + if res_raw: + try: + width, height = (int(v) for v in res_raw.split("x")) + except ValueError: + carb.log_warn(f"ISAAC_SIM_FOLLOW_CAM_RES='{res_raw}' is not WxH — using 1280x720.") + try: + from scene_prep import add_camera_image_publisher + + add_camera_image_publisher( + "/World/FollowCamPublisher", FOLLOW_CAM_PATH, topic, + width=width, height=height, frame_id="follow_cam", domain_id=i, + ) + except Exception as exc: + carb.log_error(f"[follow_cam] could not wire the ROS image publisher: {exc}") + def _author_follow_cam(self): from isaacsim.core.utils.viewports import set_camera_view diff --git a/simulation/isaac-sim/utils/scene_prep.py b/simulation/isaac-sim/utils/scene_prep.py index de2ea11d5..61ac7a392 100644 --- a/simulation/isaac-sim/utils/scene_prep.py +++ b/simulation/isaac-sim/utils/scene_prep.py @@ -413,6 +413,72 @@ def add_overhead_camera_publisher(parent_graph_path: str, f"domain_id={domain_id}") +def add_camera_image_publisher(graph_path: str, + camera_prim_path: str, + topic: str, + width: int = 1280, + height: int = 720, + frame_id: str = "follow_cam", + domain_id: int = 1): + """Publish an existing USD camera as a raw ``sensor_msgs/Image`` stream. + + Builds a standalone OmniGraph at ``graph_path`` (own ROS2Context on + ``domain_id``; OnPlaybackTick → IsaacCreateRenderProduct → + ROS2CameraHelper). Used by pegasus_app's follow camera so a headless + Isaac Sim (no viewport window) still yields a third-person video feed + that ``ros2 bag`` / a recorder node can capture — CI runners and demo + captures alike. + """ + import omni.graph.core as og # lazy so non-sim contexts can import scene_prep + + controller = og.Controller() + g = graph_path + if og.get_graph_by_path(g) is None: + og.Controller.create_graph({"graph_path": g, "evaluator_name": "execution"}) + + nodes = { + "context": f"{g}/ROS2Context", + "playback": f"{g}/OnPlaybackTick", + "create_rp": f"{g}/CreateRenderProduct", + "rgb": f"{g}/RGBHelper", + "frame": f"{g}/FrameId", + "topic": f"{g}/Topic", + } + controller.edit( + graph_id=g, + edit_commands={ + og.Controller.Keys.CREATE_NODES: [ + (nodes["context"], "isaacsim.ros2.bridge.ROS2Context"), + (nodes["playback"], "omni.graph.action.OnPlaybackTick"), + (nodes["create_rp"], "isaacsim.core.nodes.IsaacCreateRenderProduct"), + (nodes["rgb"], "isaacsim.ros2.bridge.ROS2CameraHelper"), + (nodes["frame"], "omni.graph.nodes.ConstantString"), + (nodes["topic"], "omni.graph.nodes.ConstantString"), + ], + og.Controller.Keys.CONNECT: [ + (f"{nodes['playback']}.outputs:tick", f"{nodes['create_rp']}.inputs:execIn"), + (f"{nodes['create_rp']}.outputs:execOut", f"{nodes['rgb']}.inputs:execIn"), + (f"{nodes['create_rp']}.outputs:renderProductPath", + f"{nodes['rgb']}.inputs:renderProductPath"), + (f"{nodes['context']}.outputs:context", f"{nodes['rgb']}.inputs:context"), + (f"{nodes['frame']}.inputs:value", f"{nodes['rgb']}.inputs:frameId"), + (f"{nodes['topic']}.inputs:value", f"{nodes['rgb']}.inputs:topicName"), + ], + og.Controller.Keys.SET_VALUES: [ + (("inputs:domain_id", nodes["context"]), int(domain_id)), + (("inputs:cameraPrim", nodes["create_rp"]), camera_prim_path), + (("inputs:width", nodes["create_rp"]), int(width)), + (("inputs:height", nodes["create_rp"]), int(height)), + (("inputs:type", nodes["rgb"]), "rgb"), + (("inputs:value", nodes["frame"]), str(frame_id)), + (("inputs:value", nodes["topic"]), str(topic)), + ], + }, + ) + print(f"[scene_prep] Camera image publisher wired: {camera_prim_path} -> {topic} " + f"({width}x{height} raw Image, domain_id={domain_id})") + + # --------------------------------------------------------------------------- # Consolidate root prims under /World # --------------------------------------------------------------------------- diff --git a/stacks/full_default/launch/stack.launch.xml b/stacks/full_default/launch/stack.launch.xml index 104d464a9..545ef5efa 100644 --- a/stacks/full_default/launch/stack.launch.xml +++ b/stacks/full_default/launch/stack.launch.xml @@ -102,9 +102,9 @@ + Swapping the global planner = replacing this include, e.g. with the + asm_exploration_planner module's exploration_planner.launch.xml plus + the global_plan_navigate_bridge (see stacks/full_exploration). --> diff --git a/stacks/full_droan_cpu/launch/stack.launch.xml b/stacks/full_droan_cpu/launch/stack.launch.xml index c3a0469c7..9885a90c7 100644 --- a/stacks/full_droan_cpu/launch/stack.launch.xml +++ b/stacks/full_droan_cpu/launch/stack.launch.xml @@ -115,9 +115,9 @@ + Swapping the global planner = replacing this include, e.g. with the + asm_exploration_planner module's exploration_planner.launch.xml plus + the global_plan_navigate_bridge (see stacks/full_exploration). --> diff --git a/stacks/full_exploration/README.md b/stacks/full_exploration/README.md new file mode 100644 index 000000000..1b60f68f1 --- /dev/null +++ b/stacks/full_exploration/README.md @@ -0,0 +1,49 @@ +# full_exploration + +`full_default` with the global planner swapped: the random-walk planner is +replaced by the **frontier-based geometric exploration planner** from the +external **asm_exploration_planner** module — the planner the Construction +Project flew on a real construction site. It integrates the filtered Ouster +cloud into its own OpenVDB occupancy map, extracts and clusters frontiers, +samples (optionally bounded) viewpoints around them, and publishes a +shortened, collision-checked RRT path on `global_plan`. Because that planner +only *publishes* plans, the stack also runs trunk's +`global_plan_navigate_bridge`, which turns each new plan into a `NavigateTask` +goal for `droan_gl` (a task executor that plans only while a goal is active). +Everything else — local planner, trajectory controller, PID, safety monitor, +takeoff/landing, VDB mapping, GCS bridge — is unchanged from `full_default`. + +The only differences vs `full_default` are the global-planner block in +`launch/stack.launch.xml` (two includes replacing one) and the +`asm_exploration_planner` pin in `modules.repos`. + +Bring-up: + +```bash +airstack module sync # pulls asm_exploration_planner per this stack's modules.repos +airstack up --stack full_exploration --sim isaac +airstack ready +``` + +Then, after takeoff, enable exploration from the GCS Tasks panel (the +`behavior/global_plan_toggle` service). Exploration runs until toggled off. + +Notes: + +- The exploration planner needs no extra image dependencies (OpenVDB, PCL, + Eigen ship in the trunk robot image), so no `airstack module lock --build`. +- Planner tuning lives in the module: `exploration_planner/config/ + exploration_planner.yaml` (open-world defaults) and + `exploration_planner_construction_site.yaml` (bounded exploration inside + site extents, conservative unknown-as-occupied collision checks). Select + the latter by passing `exploration_config` on the module include. +- `path_end_threshold_m` (default 3.0 m) must stay above the bridge's + `goal_tolerance_m` (1.0 m): `droan_gl` stops short of the goal, and the + planner only replans once the vehicle is inside that radius. +- `vdb_mapping` still runs: the GCS/RViz layouts and the topic keepalive + consume its map, even though the planner does not. +- `wiring.md` is not committed yet (bootstrap-deferred): generate it from the + first validated run with + `airstack test -m wiring --stack full_exploration --sim isaacsim --num-robots 1` + and commit the observed graph. +- Module repo: [castacks/asm_exploration_planner](https://github.com/castacks/asm_exploration_planner). diff --git a/stacks/full_exploration/docker-compose.yaml b/stacks/full_exploration/docker-compose.yaml new file mode 100644 index 000000000..4588bf25a --- /dev/null +++ b/stacks/full_exploration/docker-compose.yaml @@ -0,0 +1,11 @@ +# Per-stack image composition: the P4 machinery +# (tools/compose_module_layers.py) composes per-module dependency layers on +# top of the trunk base image and emits a compose override for `airstack up`. +# +# full_exploration pins the asm_exploration_planner module (see +# modules.repos). The module declares NO extra apt/pip deps — OpenVDB, PCL and +# Eigen are already in the trunk robot image via its package.xml rosdep keys +# — so there is no dependency layer to compose and nothing is committed here. +# The empty services map keeps this file valid YAML for the stack-anatomy +# contract (tests/meta/test_stack_layout_contract.py). +services: {} diff --git a/stacks/full_exploration/launch/stack.launch.xml b/stacks/full_exploration/launch/stack.launch.xml new file mode 100644 index 000000000..b3a105901 --- /dev/null +++ b/stacks/full_exploration/launch/stack.launch.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_exploration/modules.repos b/stacks/full_exploration/modules.repos new file mode 100644 index 000000000..faf4c5ccf --- /dev/null +++ b/stacks/full_exploration/modules.repos @@ -0,0 +1,22 @@ +# modules.repos : module pins for the full_exploration stack. +# +# vcstool format, PINNED to tags/commits (never branches). `airstack module sync` +# reads this file into the gitignored modules/ dir; a stack with a pinned .repos +# IS a localized release set. +# +# airstack_compat is a top-level sibling of repositories: (vcstool ignores it, +# AirStack tooling reads it) declaring the trunk semver range this stack was +# tested against. sync warns on mismatch; it never gates. +# +# full_exploration pulls the asm_exploration_planner module (frontier-based +# geometric exploration global planner); everything else — including the +# global_plan_navigate_bridge that turns its plans into NavigateTask goals — +# is trunk-resident. v0.1.0 is the module's first tag; move the pin +# deliberately (airstack module add --version ). +airstack_compat: ">=0.21.0-dev.8 <0.22.0" +repositories: + asm_exploration_planner: + type: git + url: git@github.com:castacks/asm_exploration_planner.git + version: v0.1.0 +x-local-modules: [] diff --git a/stacks/full_macvo/launch/stack.launch.xml b/stacks/full_macvo/launch/stack.launch.xml index af8d5cd4b..8e313f4c8 100644 --- a/stacks/full_macvo/launch/stack.launch.xml +++ b/stacks/full_macvo/launch/stack.launch.xml @@ -121,9 +121,9 @@ + Swapping the global planner = replacing this include, e.g. with the + asm_exploration_planner module's exploration_planner.launch.xml plus + the global_plan_navigate_bridge (see stacks/full_exploration). --> diff --git a/stacks/full_mighty/launch/stack.launch.xml b/stacks/full_mighty/launch/stack.launch.xml index b032f9562..3165e511b 100644 --- a/stacks/full_mighty/launch/stack.launch.xml +++ b/stacks/full_mighty/launch/stack.launch.xml @@ -104,9 +104,9 @@ + Swapping the global planner = replacing this include, e.g. with the + asm_exploration_planner module's exploration_planner.launch.xml plus + the global_plan_navigate_bridge (see stacks/full_exploration). --> diff --git a/stacks/full_raven/README.md b/stacks/full_raven/README.md new file mode 100644 index 000000000..1dd11efb0 --- /dev/null +++ b/stacks/full_raven/README.md @@ -0,0 +1,66 @@ +# full_raven + +`full_default` with the global planner swapped: the random-walk planner +(`random_walk_planner`) is replaced by **RAVEN** — *Resilient Aerial +Navigation via Open-Set Semantic Memory and Behavior Adaptation* (Kim, +Alama, Kurdydyk, Keller, Keetha, Wang, Bisk, Scherer — CMU AirLab, ICRA +2026) — from the external **asm_raven** module. RAVEN builds a RayFronts +semantic ray-frontier map from the ZED left RGB + ground-truth depth and +picks a behavior each frame (voxel-based → ray-based → LVLM-guided → +frontier exploration) to publish a semantic `global_plan` toward an +operator's open-vocabulary prompt ("fire hydrant"). The rest of the stack +(droan_gl local planner, trajectory controller, PID, safety monitor, +takeoff/landing, GCS) is unchanged from `full_default`. + +The module has two halves: + +- a **GPU sidecar container** (`raven`, ROS 2 Humble image, ~10 GB) running + the RayFronts mapper + behavior manager — declared by the module's compose + fragment and started by `airstack up` while the module is synced; +- **`raven_bridge`** in the robot container, relaying the sidecar's plan onto + the canonical `global_plan` and holding the operator prompt. + +Because RAVEN publishes plans on a topic (it is not a NavigateTask client), +the stack also includes trunk's `global_plan_navigate_bridge`, which turns +each new plan into a `tasks/navigate` goal for droan_gl. The only +differences vs `full_default` are those two includes in +`launch/stack.launch.xml` plus the `asm_raven` pin in `modules.repos`. + +Bring-up: + +```bash +airstack module sync # pulls asm_raven (+ its RayFronts submodule) per this stack's modules.repos +# first run only: build the sidecar image (~15 min on a 32-core host, ~10 GB); also happens +# implicitly on the first `airstack up` +docker compose -f docker-compose.yaml -f .airstack/generated/docker-compose.modules.yaml build raven +airstack up --stack full_raven --sim isaac --scene retro-neighborhood +airstack ready +``` + +Then set a semantic target (empty string clears → pure frontier exploration): + +```bash +ros2 topic pub --once /robot_1/raven/set_prompt std_msgs/msg/String "{data: 'fire hydrant'}" +ros2 service call /robot_1/raven/clear_prompt std_srvs/srv/Trigger +``` + +Notes: + +- RAVEN's scenes are the AirLab Nucleus stages already in + `simulation/scenes.yaml`: `retro-neighborhood`, `construction-site`, + `abandoned-factory` (spawn pose via `ISAAC_SIM_SPAWN_XY`). +- Single robot (`robot_1`, DDS domain 1) by default: the sidecar's + `RAVEN_ROBOT_NAME` / `RAVEN_ROS_DOMAIN_ID` select another; RAVEN's + visualization topics are global (`/filtered_voxel_bbox`, `/mode_text`, ...). +- The first sidecar start downloads the RADIO and SigLIP models (torch hub / + Hugging Face) into `modules/asm_raven/cache/`, which persists across + container recreations. +- The LVLM-guided behavior needs RAVEN's optional external LVLM server + (`/lvlm_output`); without one it degrades gracefully (falls through to + frontier exploration). `RAVEN_BEHAVIORS=voxel,ray,frontier` selects the + no-LVLM ablation explicitly. +- Sidecar rviz layout: `raven_bridge/rviz/raven.rviz` in the module. +- `AIRSTACK_NO_MODULE_COMPOSE=1 airstack up ...` brings the stack up without + the sidecar (raven_bridge then idles). +- Module repo: [castacks/asm_raven](https://github.com/castacks/asm_raven) + (pin `v0.1.0` is a placeholder until the first tag). diff --git a/stacks/full_raven/docker-compose.yaml b/stacks/full_raven/docker-compose.yaml new file mode 100644 index 000000000..14a54c1ff --- /dev/null +++ b/stacks/full_raven/docker-compose.yaml @@ -0,0 +1,13 @@ +# Per-stack image composition: the P4 machinery +# (tools/compose_module_layers.py) composes per-module dependency layers on +# top of the trunk base image and emits a compose override for `airstack up`. +# +# full_raven pins the asm_raven module (see modules.repos). asm_raven adds no +# robot-image layer (raven_bridge is plain rclpy); its heavy half — the +# RayFronts GPU SIDECAR container — is declared by the module's own compose +# fragment (asm_raven/compose/raven.yaml), which `airstack module sync` +# merges into .airstack/generated/docker-compose.modules.yaml and `airstack +# up` includes automatically. Nothing is committed here. The empty services +# map keeps this file valid YAML for the stack-anatomy contract +# (tests/meta/test_stack_layout_contract.py). +services: {} diff --git a/stacks/full_raven/launch/stack.launch.xml b/stacks/full_raven/launch/stack.launch.xml new file mode 100644 index 000000000..713fd89c4 --- /dev/null +++ b/stacks/full_raven/launch/stack.launch.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/stacks/full_raven/modules.repos b/stacks/full_raven/modules.repos new file mode 100644 index 000000000..4c41c6c38 --- /dev/null +++ b/stacks/full_raven/modules.repos @@ -0,0 +1,23 @@ +# modules.repos : module pins for the full_raven stack. +# +# vcstool format, PINNED to tags/commits (never branches). `airstack module sync` +# reads this file into the gitignored modules/ dir; a stack with a pinned .repos +# IS a localized release set. +# +# airstack_compat is a top-level sibling of repositories: (vcstool ignores it, +# AirStack tooling reads it) declaring the trunk semver range this stack was +# tested against. sync warns on mismatch; it never gates. +# +# full_raven pulls the asm_raven module (RAVEN semantic global planner: the +# RayFronts sidecar container + raven_bridge relay); everything else is +# trunk-resident. asm_raven vendors the RayFronts fork as a git submodule — +# `vcs import --recursive` (what sync runs) pulls it automatically. +# PLACEHOLDER PIN: v0.1.0 is tagged at the module's first push / first green +# module CI — update the version below then. +airstack_compat: ">=0.21.0-dev.8 <0.22.0" +repositories: + asm_raven: + type: git + url: git@github.com:castacks/asm_raven.git + version: v0.1.0 +x-local-modules: [] diff --git a/tests/colcon_unit_test_packages.yaml b/tests/colcon_unit_test_packages.yaml index 78f761039..92e9a408c 100644 --- a/tests/colcon_unit_test_packages.yaml +++ b/tests/colcon_unit_test_packages.yaml @@ -10,6 +10,7 @@ robot: packages: - lidar_point_cloud_filter + - global_plan_navigate_bridge # Linter skip lives in lidar_point_cloud_filter setup.cfg + test/conftest.py. # ament pytest does not honor PYTEST_ADDOPTS -m. # launch_testing is skipped via PYTEST_DISABLE_PLUGIN_AUTOLOAD in the test. diff --git a/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml b/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml new file mode 100644 index 000000000..0d1384d4f --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml @@ -0,0 +1,22 @@ +# Registry entry for the exploration_planner module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/exploration_planner.yaml (CI-stamped only). +name: exploration_planner +repo: https://github.com/castacks/asm_exploration_planner +description: >- + Frontier-based geometric exploration global planner — OpenVDB occupancy map, + frontier clustering, bounded viewpoint sampling and an RRT path to the chosen + viewpoint, published as global_plan; developed for the CMU AirLab + construction-site project +maintainer: ajong@andrew.cmu.edu # Andrew Jong +license: BSD-3-Clause-Clear +type: ros_package +registered_ref: v0.1.0 +airstack_compat: ">=0.21.0-dev.8 <0.22.0" # DECLARED (from module.yaml) +notes: >- + Extracted from trunk (robot/ros_ws/src/global/planners/exploration, history + preserved) together with the bounded-exploration additions from the + construction-site demo branch. Publishes a path on global_plan and serves + the behavior/global_plan_toggle service; the trunk + global_plan_navigate_bridge turns each plan into a NavigateTask goal for + droan_gl. Consumed by trunk reference stack full_exploration. diff --git a/tests/meta/fixtures/modules_index/modules/raven.yaml b/tests/meta/fixtures/modules_index/modules/raven.yaml new file mode 100644 index 000000000..085fbb899 --- /dev/null +++ b/tests/meta/fixtures/modules_index/modules/raven.yaml @@ -0,0 +1,22 @@ +# Registry entry for the raven module (schema/module-entry.schema.json). +# airstack_compat is the DECLARED range copied from the module's module.yaml; +# the VERIFIED matrix lives in compat/raven.yaml (CI-stamped only). +name: raven +repo: https://github.com/castacks/asm_raven +description: >- + RAVEN semantic aerial navigation (ICRA 2026) — the RayFronts open-set + semantic ray-frontier map and behavior manager (voxel, ray, LVLM and + frontier search) in a GPU sidecar container publishing global_plan toward a + natural-language target, plus the raven_bridge plan/prompt relay on the robot +maintainer: ajong@andrew.cmu.edu # Andrew Jong +license: BSD-3-Clause-Clear +type: ros_package +registered_ref: v0.1.0 +airstack_compat: ">=0.21.0-dev.8 <0.22.0" # DECLARED (from module.yaml) +notes: >- + Wrapper (raven_bridge, sidecar Dockerfile, compose fragment, test_stack) is + BSD-3-Clause-Clear; the vendored RayFronts fork (seungchan-kim/RayFronts, + branch raven) keeps its own license — see the module's VENDORED.md. The + sidecar is declared through the manifest's compose fragment (first module to + bring its own container) and is built on first `airstack up` (~20 GB image). + Consumed by trunk reference stack full_raven. diff --git a/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml b/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml new file mode 100644 index 000000000..c50a84a64 --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml @@ -0,0 +1,15 @@ +# Registry entry for the full_exploration trunk reference stack (schema/stack-entry.schema.json). +name: full_exploration +repo: https://github.com/castacks/AirStack +path: stacks/full_exploration +description: >- + Full autonomy with the frontier-based geometric exploration planner + (exploration_planner module) in place of the random walk, driving droan_gl + through the global_plan_navigate_bridge; everything else is identical to + full_default +airstack_compat: ">=0.21.0-dev.8 <0.22.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_exploration/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + Requires the exploration_planner module (registry: + modules/exploration_planner.yaml) pinned in the stack's modules.repos. + wiring.md is pending the stack's first validated wiring-snapshot run. diff --git a/tests/meta/fixtures/modules_index/stacks/full_raven.yaml b/tests/meta/fixtures/modules_index/stacks/full_raven.yaml new file mode 100644 index 000000000..747a0ac1e --- /dev/null +++ b/tests/meta/fixtures/modules_index/stacks/full_raven.yaml @@ -0,0 +1,14 @@ +# Registry entry for the full_raven trunk reference stack (schema/stack-entry.schema.json). +name: full_raven +repo: https://github.com/castacks/AirStack +path: stacks/full_raven +description: >- + Full autonomy with RAVEN open-set semantic navigation as the global planner + (RayFronts GPU sidecar container + raven_bridge, driving droan_gl through the + global_plan_navigate_bridge); everything else is identical to full_default +airstack_compat: ">=0.21.0-dev.8 <0.22.0" # from the stack's modules.repos airstack_compat key +wiring: stacks/full_raven/wiring.md # docs site embeds the CI-generated wiring.md from trunk +notes: >- + Requires the raven module (registry: modules/raven.yaml) pinned in the + stack's modules.repos; the first `airstack up` builds the sidecar image. + wiring.md is pending the stack's first validated wiring-snapshot run. diff --git a/tests/meta/launch_lint_allowlist.txt b/tests/meta/launch_lint_allowlist.txt index 3c6548823..417415097 100644 --- a/tests/meta/launch_lint_allowlist.txt +++ b/tests/meta/launch_lint_allowlist.txt @@ -9,12 +9,10 @@ # the lint until its line is removed; adding a NEW line needs the same # scrutiny as an RFC change. # -# What remains: a standalone utility, a vendored driver tree, a module launch -# awaiting its canonical rewrite, and the interface safety boundary (wrapped -# by design until RFC #380 Part 2). +# What remains: a standalone utility, a vendored driver tree, and the +# interface safety boundary (wrapped by design until RFC #380 Part 2). # # Paths are relative to the repo root, one per line; '#' starts a comment. common/ros_packages/airstack_common/launch/playback.launch.xml robot/docker/zed/ws/src/zed_wrapper/launch/zed_camera.launch.py -robot/ros_ws/src/global/planners/exploration/launch/exploration_launch.xml robot/ros_ws/src/interface/interface_bringup/launch/interface.launch.py diff --git a/tests/meta/test_module_overlay_contract.py b/tests/meta/test_module_overlay_contract.py index 70cf0ebcd..bd274d959 100644 --- a/tests/meta/test_module_overlay_contract.py +++ b/tests/meta/test_module_overlay_contract.py @@ -217,3 +217,85 @@ def test_add_twice_keeps_single_entry(sandbox): data = read_repos(sandbox) names = [e["name"] for e in data["x-local-modules"]] assert names == ["hello_module"] + + +# ── compose fragments: relative host paths become absolute ────────────────── + +SIDECAR_REL = os.path.join("tests", "fixtures", "modules", "sidecar_module") + + +def write_sidecar_module(sb): + """A module whose compose fragment declares a whole extra service built + from a Dockerfile inside the module (the asm_raven sidecar shape), with + every kind of relative host path compose resolves against the declaring + file's directory.""" + root = sb / SIDECAR_REL + (root / "docker").mkdir(parents=True) + (root / "compose").mkdir() + (root / "env").mkdir() + (root / "module.yaml").write_text( + "name: sidecar_module\n" + "description: Compose-fragment sidecar fixture for overlay path rewriting.\n" + "maintainer: test@example.com\n" + "license: BSD-3-Clause-Clear\n" + "type: ros_package\n" + 'airstack_compat: ">=0.19.0 <0.21.0"\n' + "targets: [robot]\n" + "compose: compose/sidecar.yaml\n", + encoding="utf-8", + ) + (root / "docker" / "Dockerfile.sidecar").write_text("FROM busybox\n", encoding="utf-8") + (root / "env" / "sidecar.env").write_text("X=1\n", encoding="utf-8") + (root / "compose" / "sidecar.yaml").write_text( + "services:\n" + " sidecar:\n" + " image: sidecar:test\n" + " build:\n" + " context: .\n" + " dockerfile: docker/Dockerfile.sidecar\n" + " env_file:\n" + " - ./env/sidecar.env\n" + " - path: env/optional.env\n" + " required: false\n" + " volumes:\n" + " - ./cache:/root/.cache:rw\n" + " - named_vol:/data\n" + " networks: [airstack_network]\n", + encoding="utf-8", + ) + return root + + +def test_compose_fragment_paths_are_absolutized(sandbox): + root = write_sidecar_module(sandbox) + run_airstack(sandbox, "module", "add", SIDECAR_REL) + compose = yaml.safe_load((sandbox / GENERATED_REL).read_text(encoding="utf-8")) + real = os.path.realpath(root) + svc = compose["services"]["sidecar"] + + # build: context "." -> module dir; relative dockerfile resolved against it + assert svc["build"]["context"] == real + assert svc["build"]["dockerfile"] == os.path.join(real, "docker", "Dockerfile.sidecar") + # env_file: strings and {path, required} mappings + assert svc["env_file"][0] == os.path.join(real, "env", "sidecar.env") + assert svc["env_file"][1] == {"path": os.path.join(real, "env", "optional.env"), + "required": False} + # volumes: relative bind absolutized, named volume untouched + assert f"{real}/cache:/root/.cache:rw" in svc["volumes"] + assert "named_vol:/data" in svc["volumes"] + # verbatim passthrough of everything else + assert svc["image"] == "sidecar:test" + assert svc["networks"] == ["airstack_network"] + # nothing in the generated file is still relative to the fragment + text = (sandbox / GENERATED_REL).read_text(encoding="utf-8") + assert "context: ." not in text and "./env" not in text and "./cache" not in text + + +def test_compose_fragment_sync_is_idempotent_and_doctor_clean(sandbox): + write_sidecar_module(sandbox) + run_airstack(sandbox, "module", "add", SIDECAR_REL) + before = (sandbox / GENERATED_REL).read_text(encoding="utf-8") + run_airstack(sandbox, "module", "sync") + assert (sandbox / GENERATED_REL).read_text(encoding="utf-8") == before + code, out = run_airstack(sandbox, "module", "doctor") + assert code == 0 and "overlay OK" in out diff --git a/tools/module_overlay.py b/tools/module_overlay.py index 243147741..beb9d04c0 100755 --- a/tools/module_overlay.py +++ b/tools/module_overlay.py @@ -32,12 +32,19 @@ ``optitrack.natnet.emulator``). - a manifest ``compose:`` fragment → merged verbatim into the generated file, - with relative host paths rewritten to absolute (compose files resolve - relative bind sources against ambiguous bases when merged via ``-f``; the - generated file is machine-local and regenerated on every sync, so absolute - paths are the unambiguous choice). When a fragment is declared it is the - source of truth for that module's mounts — the ``exts/`` auto-mount is - skipped to avoid double mounts (the dfm2 pilot's friction log, item 5). + with relative host paths rewritten to absolute: bind-mount sources, + ``build.context`` (and a relative ``build.dockerfile``, resolved against + the absolute context exactly as compose would), and ``env_file`` entries. + Compose resolves all of these against the directory of the file that + declares them, which for the generated file is ``.airstack/generated/``, + not the module — and relative bind sources resolve against ambiguous bases + when files are merged via ``-f``. The generated file is machine-local and + regenerated on every sync, so absolute paths are the unambiguous choice. + Fragments may declare whole extra services this way — e.g. a GPU sidecar + container built from a Dockerfile inside the module (the asm_raven + RayFronts sidecar). When a fragment is declared it is the source of truth + for that module's mounts — the ``exts/`` auto-mount is skipped to avoid + double mounts (the dfm2 pilot's friction log, item 5). - ``data`` / other types → no overlay action yet (noted). @@ -169,17 +176,73 @@ def desired_isaac_links(root, modules): return links -def _abs_host_path(src, base_dir): - """Rewrite a compose bind source relative to ``base_dir`` into an absolute path.""" - if src.startswith(("./", "../")): +def _abs_host_path(src, base_dir, bare_is_relative=False): + """Rewrite a compose host path relative to ``base_dir`` into an absolute path. + + Bind-mount sources are only rewritten when they are explicitly relative + (``./x``, ``../x``, ``.``) — a bare name there is a named volume. For + ``build.context`` / ``build.dockerfile`` / ``env_file`` (``bare_is_relative``) + any non-absolute, non-URL path is relative to ``base_dir``, as compose + itself would resolve it against the declaring file's directory. + """ + if not isinstance(src, str) or not src: + return src + if src == "." or src.startswith(("./", "../")): + return os.path.normpath(str(base_dir / src)) + if bare_is_relative and not os.path.isabs(src) and "://" not in src \ + and not src.startswith("git@") and not src.startswith("~"): return os.path.normpath(str(base_dir / src)) return src +def _abs_build(build, module_dir): + """Absolutize a compose ``build`` entry (string context or mapping).""" + if isinstance(build, str): + return _abs_host_path(build, module_dir, bare_is_relative=True) + if not isinstance(build, dict): + return build + build = dict(build) + context = _abs_host_path(build.get("context", "."), module_dir, bare_is_relative=True) + build["context"] = context + dockerfile = build.get("dockerfile") + if isinstance(dockerfile, str) and dockerfile and not os.path.isabs(dockerfile) \ + and os.path.isabs(context): + # compose resolves a relative dockerfile against the context dir + build["dockerfile"] = os.path.normpath(os.path.join(context, dockerfile)) + return build + + +def _abs_env_file(env_file, module_dir): + """Absolutize a compose ``env_file`` entry (string, list of strings, or + list of ``{path, required}`` mappings).""" + def one(entry): + if isinstance(entry, str): + return _abs_host_path(entry, module_dir, bare_is_relative=True) + if isinstance(entry, dict) and isinstance(entry.get("path"), str): + return {**entry, "path": _abs_host_path(entry["path"], module_dir, bare_is_relative=True)} + return entry + if isinstance(env_file, list): + return [one(e) for e in env_file] + return one(env_file) + + def _merge_service(target, source, module_dir): """Merge one compose service definition into ``target`` (fragment semantics).""" for key, value in source.items(): - if key == "volumes" and isinstance(value, list): + if key == "build": + target["build"] = _abs_build(value, module_dir) + elif key == "env_file": + value = _abs_env_file(value, module_dir) + if isinstance(value, list): + merged = target.setdefault("env_file", []) + if not isinstance(merged, list): + merged = target["env_file"] = [merged] + for entry in value: + if entry not in merged: + merged.append(entry) + else: + target["env_file"] = value + elif key == "volumes" and isinstance(value, list): merged = target.setdefault("volumes", []) for entry in value: if isinstance(entry, str): From 7cf0912c4ff11e7e1c6483f2d2183568ebd27eb7 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 20:08:33 -0400 Subject: [PATCH 2/7] docs(modules): document sidecar-container modules (compose fragment with build:) in modules.md and the create-module skill Co-Authored-By: Claude Fable 5.1 --- .agents/skills/create-module/SKILL.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.agents/skills/create-module/SKILL.md b/.agents/skills/create-module/SKILL.md index ae3d4901e..72e121d50 100644 --- a/.agents/skills/create-module/SKILL.md +++ b/.agents/skills/create-module/SKILL.md @@ -101,6 +101,27 @@ is remapping other modules' topics or overriding canonical names in module launc files: in a conventional stack, including the module must require **zero** remaps, so only deviations appear in stack files.) +## Heavy dependencies: overlay layer vs sidecar container + +Two patterns exist for deps that do not belong in the trunk robot image: + +- **Overlay layer** (`dockerfile: Dockerfile.module`, tier 2) — the module's + deps are built ON TOP of the robot image and its nodes run in the robot + container (asm_macvo: TensorRT + weights). Right when the module's nodes must + share the robot's process space/ROS distro and the deps are compatible with + the robot image's Ubuntu/Python. +- **Sidecar container** (`compose: .yaml` with its own `build:`) — + a second container next to the robot on `airstack_network`, same + `ROS_DOMAIN_ID`, talking DDS (asm_raven: RayFronts on Ubuntu 22.04 / ROS 2 + Humble / torch cu130 / patched OpenVDB — incompatible with the Jazzy robot + image). The overlay absolutizes the fragment's `build.context`, + `build.dockerfile`, `env_file` and bind sources; `airstack up` builds the + image on first run. Keep a thin robot-side colcon package (`_bridge`) + that owns the canonical launch file and relays the sidecar's module-private + topics onto canonical names — the launch-arg interface must still exist on + the robot side. Cross-distro DDS works for unchanged message types; note it + in the README. + ## Steps (by hand, until `airstack module create` lands) 1. Create the repo with the anatomy above; write `module.yaml` first. From 95bd9409ada642ddaf446764f3e43df4340f095a Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 20:36:16 -0400 Subject: [PATCH 3/7] fix(isaac): single-drone launch script honors ISAAC_SIM_SPAWN_XY example_one_px4_pegasus_launch_script.py hardcoded the spawn at the world origin; only the multi-drone script read ISAAC_SIM_SPAWN_XY. Several imported stages (construction-site, retro-neighborhood) are enclosed at the origin, so --scene runs with one robot could not move the drone off it. Co-Authored-By: Claude Fable 5.1 --- .../example_one_px4_pegasus_launch_script.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py b/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py index c59fcbbe8..974860861 100755 --- a/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py +++ b/simulation/isaac-sim/launch_scripts/example_one_px4_pegasus_launch_script.py @@ -11,7 +11,7 @@ (pass ``save_scene_to=`` below) Env (see pegasus_app.py): ISAAC_SIM_LIVESTREAM, ISAAC_SIM_HEADLESS, -PLAY_SIM_ON_START, and ISAAC_SIM_SCENE / ISAAC_SIM_STAGE_SCALE (set by +PLAY_SIM_ON_START, ISAAC_SIM_SPAWN_XY, and ISAAC_SIM_SCENE / ISAAC_SIM_STAGE_SCALE (set by `airstack up --scene ` — a Pegasus catalog key or USD URL; default: Default Environment). """ @@ -26,21 +26,29 @@ simulation_app = create_simulation_app() from pegasus.simulator.params import SIMULATION_ENVIRONMENTS # noqa: E402 -from pegasus_app import PegasusApp, resolve_scene_from_env # noqa: E402 +from pegasus_app import ( # noqa: E402 + PegasusApp, + resolve_scene_from_env, + resolve_spawn_center_from_env, +) def main(): # Scene from `airstack up --scene` (or set ISAAC_SIM_SCENE to any # catalog key / USD URL); stage_scale converts cm-authored stages to m. env_url, stage_scale = resolve_scene_from_env(SIMULATION_ENVIRONMENTS) - print(f"[example_one] Scene: {env_url} (stage_scale={stage_scale})") + # ISAAC_SIM_SPAWN_XY="x,y" (meters) moves the single drone off the world + # origin — several imported stages are enclosed or unlit there. + spawn_x, spawn_y = resolve_spawn_center_from_env() + print(f"[example_one] Scene: {env_url} (stage_scale={stage_scale}) " + f"spawn=({spawn_x}, {spawn_y})") PegasusApp( env_url=env_url, stage_scale=stage_scale, drone_configs=[ { "domain_id": 1, # MAVLink port = 14540 + vehicle_id (= domain_id) - "x_m": 0.0, "y_m": 0.0, "z_m": 0.07, + "x_m": spawn_x, "y_m": spawn_y, "z_m": 0.07, # Single-drone scenes keep the historical prim/node names. "prim": "/World/base_link", "node_name": "PX4Multirotor", From 199600082764dfee7ea40cc019d112056ab7417d Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 21:07:17 -0400 Subject: [PATCH 4/7] stacks(full_exploration, full_raven): rebase on full_mighty Both new stacks now take their local layer from full_mighty (asm_mighty v0.1.1 pinned alongside the planner module): MIGHTY's bridge follows the published global_plan directly, so the Path->NavigateTask adapter is not needed there (global_plan_navigate_bridge stays in trunk for droan_gl-based stacks), and MIGHTY plans on the Ouster LiDAR, which keeps navigation working in unlit imported stages where the stereo disparity droan_gl consumes is unavailable. Docs tables, release notes, registry fixture entries and the generated catalog updated; both READMEs flag asm_mighty's known fixed-yaw-at-goal bug (fix being upstreamed separately). Co-Authored-By: Claude Fable 5.1 --- docs/development/stacks.md | 8 ++-- docs/modules/exploration_planner.md | 2 +- docs/modules/index.md | 6 +-- docs/modules/mighty.md | 2 + docs/release_notes/index.md | 11 +++-- docs/robot/autonomy_modes.md | 4 +- stacks/full_exploration/README.md | 36 +++++++++------- .../full_exploration/launch/stack.launch.xml | 34 +++++++-------- stacks/full_exploration/modules.repos | 4 ++ stacks/full_raven/README.md | 22 ++++++---- stacks/full_raven/launch/stack.launch.xml | 41 +++++++++---------- stacks/full_raven/modules.repos | 4 ++ .../modules/exploration_planner.yaml | 7 ++-- .../stacks/full_exploration.yaml | 11 ++--- .../modules_index/stacks/full_raven.yaml | 12 +++--- 15 files changed, 114 insertions(+), 90 deletions(-) diff --git a/docs/development/stacks.md b/docs/development/stacks.md index cc712580a..a17f6fdcb 100644 --- a/docs/development/stacks.md +++ b/docs/development/stacks.md @@ -31,8 +31,8 @@ Anatomy is enforced by a unit test: `tests/meta/test_stack_layout_contract.py` | [`full_droan_cpu`](https://github.com/castacks/AirStack/tree/develop/stacks/full_droan_cpu) | CPU DROAN planner + live `disparity_expansion` (for machines without the GPU planner). | | [`full_macvo`](https://github.com/castacks/AirStack/tree/develop/stacks/full_macvo) | MAC-VO as the planner's disparity source. Requires the `asm_macvo` module (`airstack module add asm_macvo`). | | [`full_mighty`](https://github.com/castacks/AirStack/tree/develop/stacks/full_mighty) | The MIGHTY map-based local planner (+ acl-mapping voxel world model) in place of `droan_gl`. Requires the `asm_mighty` module (pinned in the stack's `modules.repos`). | -| [`full_exploration`](https://github.com/castacks/AirStack/tree/develop/stacks/full_exploration) | The frontier-based geometric exploration planner in place of the random walk (plus the `global_plan_navigate_bridge`). Requires the `asm_exploration_planner` module (pinned in the stack's `modules.repos`). | -| [`full_raven`](https://github.com/castacks/AirStack/tree/develop/stacks/full_raven) | RAVEN open-set semantic navigation as the global planner: RayFronts GPU sidecar container + `raven_bridge` (plus the `global_plan_navigate_bridge`). Requires the `asm_raven` module. | +| [`full_exploration`](https://github.com/castacks/AirStack/tree/develop/stacks/full_exploration) | `full_mighty` with the frontier-based geometric exploration planner in place of the random walk. Requires the `asm_exploration_planner` and `asm_mighty` modules (pinned in the stack's `modules.repos`). | +| [`full_raven`](https://github.com/castacks/AirStack/tree/develop/stacks/full_raven) | `full_mighty` with RAVEN open-set semantic navigation as the global planner: RayFronts GPU sidecar container + `raven_bridge`. Requires the `asm_raven` and `asm_mighty` modules. | | [`lite_default`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_default) | Onboard-lite topology, unsplit: interface, sensors, perception, flat Local layer, behavior; **no global, no logging**. | | [`lite_offload_global`](https://github.com/castacks/AirStack/tree/develop/stacks/lite_offload_global) | A **split stack**: `onboard.launch.xml` (= lite topology) + `offboard.launch.xml` (global layer only) + `bridge.yaml`. | @@ -287,7 +287,7 @@ ignored. Select the topology with `--stack`: | CPU DROAN topology | `--stack full_droan_cpu` | | MAC-VO disparity topology | `--stack full_macvo` (requires the `asm_macvo` module) | | MIGHTY map-based local planner | `--stack full_mighty` (requires the `asm_mighty` module) | -| Frontier exploration global planner | `--stack full_exploration` (requires the `asm_exploration_planner` module) | -| RAVEN semantic (language-prompted) global planner | `--stack full_raven` (requires the `asm_raven` module; first run builds the sidecar image) | +| Frontier exploration global planner | `--stack full_exploration` (requires the `asm_exploration_planner` + `asm_mighty` modules) | +| RAVEN semantic (language-prompted) global planner | `--stack full_raven` (requires the `asm_raven` + `asm_mighty` modules; first run builds the sidecar image) | | Onboard-lite, unsplit | `--stack lite_default` | | Split onboard/offboard | `--stack lite_offload_global:onboard` / `:offboard` (+ generate the router config from `bridge.yaml`) | diff --git a/docs/modules/exploration_planner.md b/docs/modules/exploration_planner.md index 1e40d0fc2..755117bfe 100644 --- a/docs/modules/exploration_planner.md +++ b/docs/modules/exploration_planner.md @@ -53,4 +53,4 @@ matrix, read the declaration as intent. ## Registry notes -> Extracted from trunk (robot/ros_ws/src/global/planners/exploration, history preserved) together with the bounded-exploration additions from the construction-site demo branch. Publishes a path on global_plan and serves the behavior/global_plan_toggle service; the trunk global_plan_navigate_bridge turns each plan into a NavigateTask goal for droan_gl. Consumed by trunk reference stack full_exploration. +> Extracted from trunk (robot/ros_ws/src/global/planners/exploration, history preserved) together with the bounded-exploration additions from the construction-site demo branch. Publishes a path on global_plan and serves the behavior/global_plan_toggle service; the MIGHTY bridge (full_exploration) follows the published plan directly, while droan_gl-based stacks add the trunk global_plan_navigate_bridge. Consumed by trunk reference stack full_exploration. diff --git a/docs/modules/index.md b/docs/modules/index.md index 833fdc597..e605120c5 100644 --- a/docs/modules/index.md +++ b/docs/modules/index.md @@ -21,7 +21,7 @@ directory and is never hand-edited. | [dfm2_disturbances](dfm2_disturbances.md) | Isaac Sim disturbance library (fan/vent force fields, strobe lights, lens flare) | `isaac_extension` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_dfm2_disturbances) | | [exploration_planner](exploration_planner.md) | Frontier-based geometric exploration global planner — OpenVDB occupancy map, frontier clustering, bounded viewpoint sampling and an RRT path to the chosen viewpoint, published as global_plan; developed for the CMU AirLab construction-site project | `ros_package` | ajong@andrew.cmu.edu | `>=0.21.0-dev.8 <0.22.0` | [repo](https://github.com/castacks/asm_exploration_planner) · [full_exploration](../../stacks/full_exploration/README.md) | | [macvo](macvo.md) | MAC-VO learned stereo visual odometry (ICRA 2025 best paper) — macvo_ros2 wrapper around the MAC-VO network, publishing odometry, a covariance-aware point cloud, and the disparity image the local planner can consume | `ros_package` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_macvo) · [full_macvo](../../stacks/full_macvo/README.md) | -| [mighty](mighty.md) | MIGHTY Hermite-spline local planner (MIT ACL, RA-L 2026) with its acl-mapping voxel world model and a bridge to AirStack's NavigateTask / trajectory_controller seam — a map-based replacement for the DROAN local planner | `ros_package` | ajong@andrew.cmu.edu | `>=0.20.0-alpha.16 <0.21.0` | [repo](https://github.com/castacks/asm_mighty) · [full_mighty](../../stacks/full_mighty/README.md) | +| [mighty](mighty.md) | MIGHTY Hermite-spline local planner (MIT ACL, RA-L 2026) with its acl-mapping voxel world model and a bridge to AirStack's NavigateTask / trajectory_controller seam — a map-based replacement for the DROAN local planner | `ros_package` | ajong@andrew.cmu.edu | `>=0.20.0-alpha.16 <0.21.0` | [repo](https://github.com/castacks/asm_mighty) · [full_exploration](../../stacks/full_exploration/README.md) · [full_mighty](../../stacks/full_mighty/README.md) · [full_raven](../../stacks/full_raven/README.md) | | [optitrack](optitrack.md) | OptiTrack NatNet mocap integration — natnet_ros2 client + PX4 external-vision fusion bridges on the robot, and the Motive-compatible NatNet server emulator for Isaac Sim | `ros_package` | ajong@andrew.cmu.edu | `>=0.19.0-alpha.18 <0.20.0` | [repo](https://github.com/castacks/asm_optitrack) | | [raven](raven.md) | RAVEN semantic aerial navigation (ICRA 2026) — the RayFronts open-set semantic ray-frontier map and behavior manager (voxel, ray, LVLM and frontier search) in a GPU sidecar container publishing global_plan toward a natural-language target, plus the raven_bridge plan/prompt relay on the robot | `ros_package` | ajong@andrew.cmu.edu | `>=0.21.0-dev.8 <0.22.0` | [repo](https://github.com/castacks/asm_raven) · [full_raven](../../stacks/full_raven/README.md) | @@ -37,10 +37,10 @@ The stacks below are the ones REGISTERED in the index; the site nav's |-------|-------------|-----------------|--------|----------------| | [full_default](../../stacks/full_default/README.md) | The current full-autonomy topology as a self-contained stack folder — the baseline most users start from and the stack other stacks are copied from | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_default/wiring.md) | [full_default.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_default.yaml) | | [full_droan_cpu](../../stacks/full_droan_cpu/README.md) | Full autonomy with the CPU DROAN local planner (droan_local_planner + live disparity_expansion world model) instead of the GPU droan_gl node | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_droan_cpu/wiring.md) | [full_droan_cpu.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_droan_cpu.yaml) | -| [full_exploration](../../stacks/full_exploration/README.md) | Full autonomy with the frontier-based geometric exploration planner (exploration_planner module) in place of the random walk, driving droan_gl through the global_plan_navigate_bridge; everything else is identical to full_default | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_exploration.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_exploration.yaml) | +| [full_exploration](../../stacks/full_exploration/README.md) | Full autonomy (full_mighty topology: MIGHTY LiDAR-based local planner) with the frontier-based geometric exploration planner (exploration_planner module) in place of the random walk; everything else is identical to full_mighty | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_exploration.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_exploration.yaml) | | [full_macvo](../../stacks/full_macvo/README.md) | Full autonomy with MAC-VO learned stereo visual odometry as the disparity source for the local planner (droan_gl consumes /$ROBOT_NAME/perception/macvo/disparity) | `>=0.19.0-alpha.18 <0.21.0` | [wiring.md](../../stacks/full_macvo/wiring.md) | [full_macvo.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_macvo.yaml) | | [full_mighty](../../stacks/full_mighty/README.md) | Full autonomy with the local planning layer swapped to the MIGHTY module (mighty planner + acl-mapping voxel world model fed by the filtered Ouster cloud + NavigateTask bridge) in place of droan_gl; everything else is identical to full_default | `>=0.20.0-alpha.16 <0.21.0` | *not committed yet* | [full_mighty.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_mighty.yaml) | -| [full_raven](../../stacks/full_raven/README.md) | Full autonomy with RAVEN open-set semantic navigation as the global planner (RayFronts GPU sidecar container + raven_bridge, driving droan_gl through the global_plan_navigate_bridge); everything else is identical to full_default | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_raven.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_raven.yaml) | +| [full_raven](../../stacks/full_raven/README.md) | Full autonomy (full_mighty topology: MIGHTY LiDAR-based local planner) with RAVEN open-set semantic navigation as the global planner (RayFronts GPU sidecar container + raven_bridge); everything else is identical to full_mighty | `>=0.21.0-dev.8 <0.22.0` | *not committed yet* | [full_raven.yaml](https://github.com/castacks/airstack-modules-index/blob/main/stacks/full_raven.yaml) | ## See also diff --git a/docs/modules/mighty.md b/docs/modules/mighty.md index 935467e9e..308fc773f 100644 --- a/docs/modules/mighty.md +++ b/docs/modules/mighty.md @@ -49,7 +49,9 @@ matrix, read the declaration as intent. ## Registered stacks using this module +- [full_exploration](../../stacks/full_exploration/README.md) - [full_mighty](../../stacks/full_mighty/README.md) +- [full_raven](../../stacks/full_raven/README.md) ## Registry notes diff --git a/docs/release_notes/index.md b/docs/release_notes/index.md index 79dd02eb5..98d74794f 100644 --- a/docs/release_notes/index.md +++ b/docs/release_notes/index.md @@ -28,13 +28,16 @@ its own notes. --> [asm_exploration_planner](https://github.com/castacks/asm_exploration_planner) (the frontier-based geometric exploration planner from the construction-site project, extracted from trunk's `robot/ros_ws/src/global/planners/exploration` - with its bounded viewpoint sampling; stack `full_exploration`). Both plug in - as one `` swap against `full_default`. + with its bounded viewpoint sampling; stack `full_exploration`). Both stacks + are one `` swap against `full_mighty`: the MIGHTY bridge follows + the published `global_plan` directly, and its LiDAR-based world model keeps + working in unlit scenes where stereo disparity does not. - **`global_plan_navigate_bridge`.** New trunk package that turns a `global_plan` topic into `NavigateTask` goals (cancel-then-resend on each new plan), so global planners that only publish a path can drive the - task-executor local planner `droan_gl`. Included by `full_exploration` and - `full_raven`; `full_default`'s random walk remains its own action client. + task-executor local planner `droan_gl` (for droan-based stacks; the MIGHTY + bridge follows the topic itself). `full_default`'s random walk remains its + own action client. - **Compose fragments may declare sidecar services with a `build:` section.** `airstack module sync` now rewrites a module compose fragment's relative `build.context` / `build.dockerfile` / `env_file` paths to absolute, like it diff --git a/docs/robot/autonomy_modes.md b/docs/robot/autonomy_modes.md index 5396ff8c6..aa7eec9f4 100644 --- a/docs/robot/autonomy_modes.md +++ b/docs/robot/autonomy_modes.md @@ -15,8 +15,8 @@ environment variable is a preflight error.) | **`full_droan_cpu`** | `full_default` with the CPU DROAN local planner (`droan_local_planner` + `disparity_expansion`) instead of the GPU `droan_gl` node | | **`full_macvo`** | `full_default` with MAC-VO as the disparity source — requires `airstack module add asm_macvo` first | | **`full_mighty`** | `full_default` with the MIGHTY map-based local planner (`asm_mighty` module: planner + acl-mapping voxel world model + NavigateTask bridge) in place of `droan_gl` | -| **`full_exploration`** | `full_default` with the frontier-based geometric exploration planner (`asm_exploration_planner` module) in place of the random walk, driving `droan_gl` through the `global_plan_navigate_bridge` | -| **`full_raven`** | `full_default` with RAVEN semantic navigation (`asm_raven` module: RayFronts GPU sidecar + `raven_bridge`) as the global planner — fly toward a natural-language target | +| **`full_exploration`** | `full_mighty` with the frontier-based geometric exploration planner (`asm_exploration_planner` module) in place of the random walk; MIGHTY's bridge follows the published `global_plan` | +| **`full_raven`** | `full_mighty` with RAVEN semantic navigation (`asm_raven` module: RayFronts GPU sidecar + `raven_bridge`) as the global planner — fly toward a natural-language target | Instead of picking a stack per container, `airstack up --fleet ` launches a whole **fleet**: `config/fleets/.yaml` declares who exists, which diff --git a/stacks/full_exploration/README.md b/stacks/full_exploration/README.md index 1b60f68f1..5fba7b867 100644 --- a/stacks/full_exploration/README.md +++ b/stacks/full_exploration/README.md @@ -1,21 +1,22 @@ # full_exploration -`full_default` with the global planner swapped: the random-walk planner is +`full_mighty` with the global planner swapped: the random-walk planner is replaced by the **frontier-based geometric exploration planner** from the external **asm_exploration_planner** module — the planner the Construction Project flew on a real construction site. It integrates the filtered Ouster cloud into its own OpenVDB occupancy map, extracts and clusters frontiers, samples (optionally bounded) viewpoints around them, and publishes a -shortened, collision-checked RRT path on `global_plan`. Because that planner -only *publishes* plans, the stack also runs trunk's -`global_plan_navigate_bridge`, which turns each new plan into a `NavigateTask` -goal for `droan_gl` (a task executor that plans only while a goal is active). -Everything else — local planner, trajectory controller, PID, safety monitor, -takeoff/landing, VDB mapping, GCS bridge — is unchanged from `full_default`. +shortened, collision-checked RRT path on `global_plan`. That planner only +*publishes* plans; the MIGHTY bridge (`asm_mighty`) follows the `global_plan` +topic directly, so no Path→NavigateTask adapter is needed (a droan_gl-based +variant would add trunk's `global_plan_navigate_bridge`). Everything else — +MIGHTY local planner + acl-mapping world model (LiDAR-based, so it works in +unlit scenes), trajectory controller, PID, safety monitor, takeoff/landing, +VDB mapping, GCS bridge — is unchanged from `full_mighty`. -The only differences vs `full_default` are the global-planner block in -`launch/stack.launch.xml` (two includes replacing one) and the -`asm_exploration_planner` pin in `modules.repos`. +The only differences vs `full_mighty` are the global-planner include in +`launch/stack.launch.xml` and the `asm_exploration_planner` pin in +`modules.repos`. Bring-up: @@ -30,16 +31,23 @@ Then, after takeoff, enable exploration from the GCS Tasks panel (the Notes: +- `asm_mighty` v0.1.1 has a known fixed-yaw-at-goal bug (the vehicle does not + turn toward its goal); a fix is being upstreamed — re-pin `modules.repos` + when it lands. + - The exploration planner needs no extra image dependencies (OpenVDB, PCL, - Eigen ship in the trunk robot image), so no `airstack module lock --build`. + Eigen ship in the trunk robot image); MIGHTY needs its `nlohmann-json3-dev` + layer, so run `airstack module lock --build` before `airstack up` (as for + `full_mighty`). - Planner tuning lives in the module: `exploration_planner/config/ exploration_planner.yaml` (open-world defaults) and `exploration_planner_construction_site.yaml` (bounded exploration inside site extents, conservative unknown-as-occupied collision checks). Select the latter by passing `exploration_config` on the module include. -- `path_end_threshold_m` (default 3.0 m) must stay above the bridge's - `goal_tolerance_m` (1.0 m): `droan_gl` stops short of the goal, and the - planner only replans once the vehicle is inside that radius. +- `path_end_threshold_m` (default 3.0 m) is the radius around the plan's + final pose inside which the planner considers the plan complete and picks + the next frontier; keep it above the local planner's arrival tolerance + (MIGHTY's follower stops ~1–2 m short of the route end). - `vdb_mapping` still runs: the GCS/RViz layouts and the topic keepalive consume its map, even though the planner does not. - `wiring.md` is not committed yet (bootstrap-deferred): generate it from the diff --git a/stacks/full_exploration/launch/stack.launch.xml b/stacks/full_exploration/launch/stack.launch.xml index b3a105901..9bfb5afee 100644 --- a/stacks/full_exploration/launch/stack.launch.xml +++ b/stacks/full_exploration/launch/stack.launch.xml @@ -1,5 +1,5 @@ - - + + - - diff --git a/stacks/full_exploration/modules.repos b/stacks/full_exploration/modules.repos index faf4c5ccf..3f098e4ba 100644 --- a/stacks/full_exploration/modules.repos +++ b/stacks/full_exploration/modules.repos @@ -19,4 +19,8 @@ repositories: type: git url: git@github.com:castacks/asm_exploration_planner.git version: v0.1.0 + asm_mighty: + type: git + url: git@github.com:castacks/asm_mighty.git + version: v0.1.1 x-local-modules: [] diff --git a/stacks/full_raven/README.md b/stacks/full_raven/README.md index 1dd11efb0..317069153 100644 --- a/stacks/full_raven/README.md +++ b/stacks/full_raven/README.md @@ -1,6 +1,6 @@ # full_raven -`full_default` with the global planner swapped: the random-walk planner +`full_mighty` with the global planner swapped: the random-walk planner (`random_walk_planner`) is replaced by **RAVEN** — *Resilient Aerial Navigation via Open-Set Semantic Memory and Behavior Adaptation* (Kim, Alama, Kurdydyk, Keller, Keetha, Wang, Bisk, Scherer — CMU AirLab, ICRA @@ -9,8 +9,10 @@ semantic ray-frontier map from the ZED left RGB + ground-truth depth and picks a behavior each frame (voxel-based → ray-based → LVLM-guided → frontier exploration) to publish a semantic `global_plan` toward an operator's open-vocabulary prompt ("fire hydrant"). The rest of the stack -(droan_gl local planner, trajectory controller, PID, safety monitor, -takeoff/landing, GCS) is unchanged from `full_default`. +(MIGHTY local planner from the `asm_mighty` module, trajectory controller, +PID, safety monitor, takeoff/landing, GCS) is unchanged from `full_mighty`. +MIGHTY plans on the Ouster LiDAR, so navigation keeps working in dark or +untextured scenes where stereo disparity (the DROAN planners' input) does not. The module has two halves: @@ -20,11 +22,11 @@ The module has two halves: - **`raven_bridge`** in the robot container, relaying the sidecar's plan onto the canonical `global_plan` and holding the operator prompt. -Because RAVEN publishes plans on a topic (it is not a NavigateTask client), -the stack also includes trunk's `global_plan_navigate_bridge`, which turns -each new plan into a `tasks/navigate` goal for droan_gl. The only -differences vs `full_default` are those two includes in -`launch/stack.launch.xml` plus the `asm_raven` pin in `modules.repos`. +RAVEN publishes plans on a topic (it is not a NavigateTask client); the +MIGHTY bridge follows the `global_plan` topic directly, so no adapter is +needed. The only difference vs `full_mighty` is the global-planner include in +`launch/stack.launch.xml` plus the `asm_raven` pin in `modules.repos` (a +droan_gl-based variant would add trunk's `global_plan_navigate_bridge`). Bring-up: @@ -46,6 +48,10 @@ ros2 service call /robot_1/raven/clear_prompt std_srvs/srv/Trigger Notes: +- `asm_mighty` v0.1.1 has a known fixed-yaw-at-goal bug (the vehicle does not + turn toward its goal); a fix is being upstreamed — re-pin `modules.repos` + when it lands. + - RAVEN's scenes are the AirLab Nucleus stages already in `simulation/scenes.yaml`: `retro-neighborhood`, `construction-site`, `abandoned-factory` (spawn pose via `ISAAC_SIM_SPAWN_XY`). diff --git a/stacks/full_raven/launch/stack.launch.xml b/stacks/full_raven/launch/stack.launch.xml index 713fd89c4..11778c238 100644 --- a/stacks/full_raven/launch/stack.launch.xml +++ b/stacks/full_raven/launch/stack.launch.xml @@ -1,5 +1,5 @@ @@ -78,11 +80,15 @@ trajectory controller; action at tasks/fixed_trajectory --> - - + + + this include is the only difference vs full_mighty). --> - - diff --git a/stacks/full_raven/modules.repos b/stacks/full_raven/modules.repos index 4c41c6c38..c3ba8c3e7 100644 --- a/stacks/full_raven/modules.repos +++ b/stacks/full_raven/modules.repos @@ -20,4 +20,8 @@ repositories: type: git url: git@github.com:castacks/asm_raven.git version: v0.1.0 + asm_mighty: + type: git + url: git@github.com:castacks/asm_mighty.git + version: v0.1.1 x-local-modules: [] diff --git a/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml b/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml index 0d1384d4f..31ea40f54 100644 --- a/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml +++ b/tests/meta/fixtures/modules_index/modules/exploration_planner.yaml @@ -17,6 +17,7 @@ notes: >- Extracted from trunk (robot/ros_ws/src/global/planners/exploration, history preserved) together with the bounded-exploration additions from the construction-site demo branch. Publishes a path on global_plan and serves - the behavior/global_plan_toggle service; the trunk - global_plan_navigate_bridge turns each plan into a NavigateTask goal for - droan_gl. Consumed by trunk reference stack full_exploration. + the behavior/global_plan_toggle service; the MIGHTY bridge (full_exploration) + follows the published plan directly, while droan_gl-based stacks add the + trunk global_plan_navigate_bridge. Consumed by trunk reference stack + full_exploration. diff --git a/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml b/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml index c50a84a64..740c224ed 100644 --- a/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml +++ b/tests/meta/fixtures/modules_index/stacks/full_exploration.yaml @@ -3,13 +3,14 @@ name: full_exploration repo: https://github.com/castacks/AirStack path: stacks/full_exploration description: >- - Full autonomy with the frontier-based geometric exploration planner - (exploration_planner module) in place of the random walk, driving droan_gl - through the global_plan_navigate_bridge; everything else is identical to - full_default + Full autonomy (full_mighty topology: MIGHTY LiDAR-based local planner) with + the frontier-based geometric exploration planner (exploration_planner + module) in place of the random walk; everything else is identical to + full_mighty airstack_compat: ">=0.21.0-dev.8 <0.22.0" # from the stack's modules.repos airstack_compat key wiring: stacks/full_exploration/wiring.md # docs site embeds the CI-generated wiring.md from trunk notes: >- Requires the exploration_planner module (registry: - modules/exploration_planner.yaml) pinned in the stack's modules.repos. + modules/exploration_planner.yaml) and the mighty module, both pinned in the + stack's modules.repos. wiring.md is pending the stack's first validated wiring-snapshot run. diff --git a/tests/meta/fixtures/modules_index/stacks/full_raven.yaml b/tests/meta/fixtures/modules_index/stacks/full_raven.yaml index 747a0ac1e..783f46e12 100644 --- a/tests/meta/fixtures/modules_index/stacks/full_raven.yaml +++ b/tests/meta/fixtures/modules_index/stacks/full_raven.yaml @@ -3,12 +3,14 @@ name: full_raven repo: https://github.com/castacks/AirStack path: stacks/full_raven description: >- - Full autonomy with RAVEN open-set semantic navigation as the global planner - (RayFronts GPU sidecar container + raven_bridge, driving droan_gl through the - global_plan_navigate_bridge); everything else is identical to full_default + Full autonomy (full_mighty topology: MIGHTY LiDAR-based local planner) with + RAVEN open-set semantic navigation as the global planner (RayFronts GPU + sidecar container + raven_bridge); everything else is identical to + full_mighty airstack_compat: ">=0.21.0-dev.8 <0.22.0" # from the stack's modules.repos airstack_compat key wiring: stacks/full_raven/wiring.md # docs site embeds the CI-generated wiring.md from trunk notes: >- - Requires the raven module (registry: modules/raven.yaml) pinned in the - stack's modules.repos; the first `airstack up` builds the sidecar image. + Requires the raven module (registry: modules/raven.yaml) and the mighty + module, both pinned in the stack's modules.repos; the first `airstack up` + builds the sidecar image. wiring.md is pending the stack's first validated wiring-snapshot run. From d0a4c4a0bd197d8c22ff9bfb9cab57480d6cd07e Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 21:18:32 -0400 Subject: [PATCH 5/7] stacks(full_exploration, full_raven): pin asm_mighty v0.1.2 (arrival-yaw fix upstreamed) Co-Authored-By: Claude Fable 5.1 --- stacks/full_exploration/README.md | 3 --- stacks/full_exploration/modules.repos | 2 +- stacks/full_raven/README.md | 3 --- stacks/full_raven/modules.repos | 2 +- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/stacks/full_exploration/README.md b/stacks/full_exploration/README.md index 5fba7b867..2660309cd 100644 --- a/stacks/full_exploration/README.md +++ b/stacks/full_exploration/README.md @@ -31,9 +31,6 @@ Then, after takeoff, enable exploration from the GCS Tasks panel (the Notes: -- `asm_mighty` v0.1.1 has a known fixed-yaw-at-goal bug (the vehicle does not - turn toward its goal); a fix is being upstreamed — re-pin `modules.repos` - when it lands. - The exploration planner needs no extra image dependencies (OpenVDB, PCL, Eigen ship in the trunk robot image); MIGHTY needs its `nlohmann-json3-dev` diff --git a/stacks/full_exploration/modules.repos b/stacks/full_exploration/modules.repos index 3f098e4ba..bae47de16 100644 --- a/stacks/full_exploration/modules.repos +++ b/stacks/full_exploration/modules.repos @@ -22,5 +22,5 @@ repositories: asm_mighty: type: git url: git@github.com:castacks/asm_mighty.git - version: v0.1.1 + version: v0.1.2 x-local-modules: [] diff --git a/stacks/full_raven/README.md b/stacks/full_raven/README.md index 317069153..8b2edc947 100644 --- a/stacks/full_raven/README.md +++ b/stacks/full_raven/README.md @@ -48,9 +48,6 @@ ros2 service call /robot_1/raven/clear_prompt std_srvs/srv/Trigger Notes: -- `asm_mighty` v0.1.1 has a known fixed-yaw-at-goal bug (the vehicle does not - turn toward its goal); a fix is being upstreamed — re-pin `modules.repos` - when it lands. - RAVEN's scenes are the AirLab Nucleus stages already in `simulation/scenes.yaml`: `retro-neighborhood`, `construction-site`, diff --git a/stacks/full_raven/modules.repos b/stacks/full_raven/modules.repos index c3ba8c3e7..3c8c28bc8 100644 --- a/stacks/full_raven/modules.repos +++ b/stacks/full_raven/modules.repos @@ -23,5 +23,5 @@ repositories: asm_mighty: type: git url: git@github.com:castacks/asm_mighty.git - version: v0.1.1 + version: v0.1.2 x-local-modules: [] From 8eeb50500aa3b8d73371e85e44bab752fafb6183 Mon Sep 17 00:00:00 2001 From: Andrew Jong Date: Thu, 10 Sep 2026 21:40:17 -0400 Subject: [PATCH 6/7] Merge develop (asm_mighty v0.1.3); VERSION 0.21.0-dev.10; pin v0.1.3 in full_exploration/full_raven Co-Authored-By: Claude Fable 5.1 --- .env | 2 +- stacks/full_exploration/launch/stack.launch.xml | 5 +++-- stacks/full_exploration/modules.repos | 2 +- stacks/full_raven/launch/stack.launch.xml | 5 +++-- stacks/full_raven/modules.repos | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.env b/.env index d29e814e8..1d2dcf30e 100644 --- a/.env +++ b/.env @@ -12,7 +12,7 @@ PROJECT_NAME="airstack" # If you've run ./airstack.sh setup, then this will auto-generate from the git commit hash every time a change is made # to a Dockerfile or docker-compose.yaml file. Otherwise this can also be set explicitly to make a release version. # auto-generated from git commit hash -VERSION="0.21.0-dev.9" +VERSION="0.21.0-dev.10" # Image-tag discriminator ONLY (appears in the image tag suffix, e.g. ..._robot-x86-64_dev). # No Dockerfile consumes it: "prebuilt" does NOT bake the built ros_ws into the image today — # a real prebuilt (workspace-baked) stage is future work. Keep "dev" (mounted code, built live). diff --git a/stacks/full_exploration/launch/stack.launch.xml b/stacks/full_exploration/launch/stack.launch.xml index 9bfb5afee..9542df02a 100644 --- a/stacks/full_exploration/launch/stack.launch.xml +++ b/stacks/full_exploration/launch/stack.launch.xml @@ -81,8 +81,9 @@ - + + + + + + - + + + + + +