MIGHTY Hermite-spline local planner (Kondo, Wu, Kumar, How — MIT ACL / UPenn, RA-L 2026, arXiv:2511.10822) packaged as an AirStack module, together with its acl-mapping voxel world model and a mighty_bridge adapter onto AirStack's local-planner seam.
AirStack's default local planner (from 0.21): it replaced the DROAN local
planner behind the exact same interfaces — the tasks/navigate NavigateTask
action in, receding-horizon trajectory_controller/trajectory_override
trajectories out — so the trajectory controller, PID, safety monitor, and
takeoff/landing pipeline are untouched. DROAN lives on as the
asm_droan module (full_droan /
full_droan_cpu stacks) for depth-camera-only vehicles.
The swap was motivated by a judged obstacle-route evaluation in Isaac Sim (the AirStack agent study's obstacle-avoidance rung): a 40×40 m pillar field, fresh judge-issued multi-checkpoint routes per flight, judged on simulator ground truth against a 1.0 m clearance gate, a 2.5 m final goal tolerance, and a 240 s/checkpoint budget. Same vehicle, same trajectory controller, same safety monitor, same judge, same field — the only variable is the local planner.
Quantitatively:
DROAN (droan_gl) frozen config |
DROAN + yaw-sweep unstick (its best config) | MIGHTY (this module, v0.1.0) | |
|---|---|---|---|
| Judged route passes | 0/5 | 0/10 | 5/5 official + 1 validation |
| Collisions | 0 | 0 | 0 |
| Min pillar clearance | — (routes not completed) | 0.01–0.76 m (gate: 1.0 m) | 1.59–1.65 m |
| Final goal error | — | — | 0.01–0.15 m (gate: 2.5 m) |
| Dominant failure mode | absorbing hover (blocked collision votes) | close-quarters shaves past the gate | — |
DROAN's 2018-era reactive design has no persistent map: accumulated collision votes cannot be erased by looking again, so cluttered pockets become absorbing hover states, and its ~86° forward stereo FOV plus voxel quantization produce close-quarters near-contacts. The yaw-sweep heuristic eliminated the hovers (routes complete, zero crashes) but could not clear the 1.0 m gate. Both failure modes are architectural — which is what a map-based planner with explicit corridor margins fixes: MIGHTY holds a persistent sliding voxel map with explicit unknown-space handling, plans through convex safe corridors with a tunable clearance margin, and takes the full 360° lidar.
Qualitatively, the flown tracks tell the same story — DROAN's tracks knot into loops and mid-field wandering and terminate away from their route ends; MIGHTY's are taut leg-followers with singular avoidance bulges, every one terminating at the final checkpoint:
The clearance profiles show the margin mechanism directly: every MIGHTY
run's troughs cluster at 1.6–2.0 m — the planner's ~1.5 m nominal margin
(planner_Co 1.2 + half bounding box) plus tracking wobble — and never
approach the 1.0 m gate, versus DROAN's 0.01–0.76 m near-contact
distribution on the same field:
The judged-eval iteration that produced v0.1.0 also hardened the
integration itself (ten distinct defects found and fixed, from QoS
mismatches to replan-anchor timeline drift behind the trajectory
controller — see the git history dev1→dev10). Headline integration
lesson: a timeline-open-loop planner running behind a tracking controller
needs vehicle-anchored replanning and a receding-horizon
trajectory_override handoff, both now built into mighty_bridge.
flowchart LR
OUSTER[filtered lidar cloud] --> GM[global_mapper_ros<br/>occupied + unknown voxel grids]
GM --> MIGHTY[mighty_node<br/>A* + safe corridor + Hermite-spline NLP]
ODOM[odometry] --> BR[mighty_bridge]
BR -- state --> MIGHTY
BR -- term_goal (route checkpoints) --> MIGHTY
NAV[NavigateTask<br/>tasks/navigate] --> BR
MIGHTY -- committed Trajectory --> BR
BR -- TrajectoryXYZVYaw segments --> TC[trajectory_controller]
- global_mapper_ros (acl-mapping): sliding-window voxel map that follows
the drone (occupied + unknown voxel-center clouds), registered via TF
(
map-> lidar frame). - mighty_node: A* front end over the voxel map, convex safe-flight corridors (DecompUtil), quintic-Hermite-spline soft-constrained L-BFGS back end (GCOPTER-derived — no solver licenses). CPU-only.
- mighty_bridge: serves NavigateTask (walks the goal path's poses as
successive
term_goalcheckpoints; a newer goal preempts the active one;navigate_timeout_sbounds an unreachable goal), follows aglobal_planroute once airborne, converts odometry ->dynus_interfaces/State(twist rotated to world frame), and converts each committeddynus_interfaces/Trajectoryinto a decimatedTrajectoryXYZVYawpublished as a receding-horizontrajectory_override. The bridge puts the trajectory controller in TRACK mode before it forwards overrides (AirStack 0.20.x leaves it in ROBOT_POSE after takeoff, where overrides are merged but never flown) and, on completion of a leg, turns the vehicle to the leg's requested yaw (arrival_yaw) — MIGHTY itself drops the goal orientation.
| Package | Origin | Role |
|---|---|---|
mighty |
vendored (mit-acl/mighty) | planner core (+ fake_sim, gtests) |
dynus_interfaces |
vendored | State/Goal/Trajectory/DynTraj msgs |
decomp_util, decomp_ros_msgs |
vendored (DecompROS2) | convex decomposition + msgs |
decomp_ros_utils |
new shim | header-only decomp<->ROS conversions (no rviz deps) |
fla_interfaces, fla_utils, global_mapper, global_mapper_ros |
vendored (acl-mapping) | voxel world model |
mighty_bridge |
new | AirStack seam adapter + canonical module launch |
Pins, licenses, and the Jazzy-port patch list: VENDORED.md. Everything is BSD-3/Apache-2.0-class permissive; no Gurobi.
On AirStack 0.21.0-dev.10 or newer nothing is needed: the default
full_default stack (and lite_default / lite_offload_global) pins this
module in its modules.repos, and airstack up adds the pin and syncs it
when it is missing. The module declares no image-level deps (the trunk robot
image ships the nlohmann-json header), so no module lock --build step.
airstack up --sim isaac # full_default = MIGHTYExplicitly, or on a custom stack:
airstack module add https://github.com/castacks/asm_mighty --version v0.1.4On a 0.20.x trunk (DROAN still the in-tree default) pin v0.1.3 and run
airstack module lock --build — that release carries the nlohmann-json3-dev
dep layer itself — then airstack up --stack full_mighty --sim isaac.
See mighty_bridge/launch/mighty_module.launch.xml — every cross-module
endpoint is a declared arg with a canonical default:
| Arg | Default | Direction |
|---|---|---|
mighty_lidar_topic |
/$ROBOT_NAME/sensors/ouster/point_cloud |
in |
mighty_lidar_frame |
ouster |
(TF) |
mighty_odometry_topic |
/$ROBOT_NAME/odometry_conversion/odometry |
in |
mighty_global_plan_topic |
/$ROBOT_NAME/global_plan |
in |
mighty_is_airborne_topic |
/$ROBOT_NAME/takeoff_landing_planner/is_airborne |
in (arms the follower after takeoff, clears route memory on landing) |
mighty_trajectory_override_topic |
/$ROBOT_NAME/trajectory_controller/trajectory_override |
out |
mighty_set_trajectory_mode_service |
/$ROBOT_NAME/trajectory_controller/set_trajectory_mode |
out (srv) |
mighty_navigate_task_action |
/$ROBOT_NAME/tasks/navigate |
serves |
mighty_mapper_occupancy_grid_topic / mighty_mapper_unknown_grid_topic |
occupancy_grid / unknown_grid (inside mighty/) |
mapper out |
mighty_occupancy_grid_topic / mighty_unknown_grid_topic |
occupancy_grid / unknown_grid |
planner in |
mighty_z_min |
0.5 |
planner altitude floor [m] |
mighty_mapper_z_ground / mighty_mapper_z_min_unknown |
0.25 / 0.1 |
mapper ground cut / unknown-volume floor [m] |
The mapper -> planner grid seam is an arg pair so a stack can interpose its
own node (e.g. lidar occupancy ∪ no-fly volumes) without copying this launch
file: point the mapper at occupancy_grid_lidar, run the node from there to
occupancy_grid, leave the planner side at its default. The altitude-band
args exist because the YAML defaults assume ground at map z 0; a mission into
an excavation below the takeoff point sets all three negative.
-
mighty_bridge/config/mighty_airstack.yaml— planner params (v/a/j limits, z band, clearance marginplanner_Co, bbox). Derived from upstreammighty.yaml; AirStack-changed values documented in the header. -
mighty_bridge/config/global_mapper_airstack.yaml— voxel map (window size follows the drone in all axes, resolution, hit/miss). -
Bridge params — set on the
mighty_bridgenode. They are read once at startup: set them from the launch (<set_parameter>scoped around the module include, or a<param>on the node), not withros2 param set.Param Default Meaning waypoint_tolerance_m,segment_stride,term_goal_republish_s,override_period_scheckpoint walk, decimation, term_goalrepublish, override ratenavigate_timeout_s240 abort a NavigateTask that has not reached its goal (0 = never) follow_global_plan,follow_settle_s,follow_lookahead_mtrue, 3, 8 global_planfollower: enable, settle time (follow_use_is_airbornetrue arm the follower off takeoff_landing_planner/is_airborne(after settling) and clear its route memory when it goes false (landing)follow_min_climb_m1.5 fallback arming gate when no is_airborneis published: climb above the first odometry sample (AirStack's TakeoffTask climbs ~3 m)follow_plan_stale_s15 drop a route whose global_planwent silent this long (0 = never)follow_airborne_above_m0 (off) count the vehicle as airborne above this map altitude, so a bridge (re)started mid-flight still engages catchup_release_s6 max time the follower withholds carrots while MIGHTY flies to its committed end arrival_yawgoalon completion of a leg: goal= the final pose's yaw (hold heading when its quaternion is identity),hold= keep heading,off= MIGHTY's behaviour (lands facing +x)arrival_yaw_velocity0.3 velocity of the two-waypoint turn-in-place override
- v0.1.5 — two
mighty_bridgefollower fixes from the first MIGHTY-default exploration runs. (FIX 8) The follower arms off the stack'stakeoff_landing_planner/is_airborneonce the vehicle has settled (newmighty_is_airborne_topiclaunch arg,follow_use_is_airborneparam) and the relative-climb fallback defaults to 1.5 m instead of 8 m — a study-route value that AirStack's ~3 m TakeoffTask never reached, so plans were adopted but never engaged. (FIX 9)is_airbornegoing false (landing) clears the follower state — airborne flag, adopted route, completed-route memory — so a second flight in one session flies again, and the "route already completed" check is keyed on the plan's pose list rather than a 2 m radius around its end (a global planner re-targeting the same spot was dropped forever until the nodes restarted). No wiring change beyond the new input. - v0.1.4 — AirStack 0.21 default-planner release, code-identical to
v0.1.3:
airstack_compat→>=0.21.0-dev.10 <0.22.0; thenlohmann-json3-devapt dep is dropped frommodule.yamlbecause the trunk robot image installs it (the default stack needs no composed layer). Consumed by trunk'sfull_default,lite_defaultandlite_offload_global;full_mightywas folded intofull_default. - v0.1.3 — launch args for the mapper -> planner grid seam
(
mighty_mapper_*_topic,mighty_*_grid_topic) and the altitude band (mighty_z_min,mighty_mapper_z_ground,mighty_mapper_z_min_unknown);.gitignore, tracked__pycache__removed. - v0.1.2 — bridge seam fixes from flying the module on AirStack 0.20.x
(RayFronts notebook/067): TRACK mode before overrides (the "planner never
replans" hang), NavigateTask timeout + preemption, stale
global_plandrop, mid-flight restart gate, catch-up gate release, "route completed" memory only while still at the end, and arrival yaw. Numbered FIX 1-7 inmighty_bridge/bridge_node.py's docstring and log lines. - v0.1.1 — receding-horizon
trajectory_overridereplaces ADD_SEGMENT merging; vehicle-anchored replanning; follower completion contract. - v0.1.0 — first release (judged-eval hardening, DROAN comparison).
colcon test --packages-select mighty— 6 upstream gtest suites (44 tests) including the L-BFGS gradient check.tools/smoke_sim.py— standalone synthetic-input smoke harness (no Isaac, no controller): publishes odometry + a synthetic pillar cloud + TFs at canonical names; then exercise the NavigateTask action and watchtrajectory_segment_to_add.

