Skip to content

Add waypoint_flight system test judged by a standalone track checker - #378

Merged
andrewjong merged 4 commits into
developfrom
waypoint-flight-test
Aug 4, 2026
Merged

Add waypoint_flight system test judged by a standalone track checker#378
andrewjong merged 4 commits into
developfrom
waypoint-flight-test

Conversation

@andrewjong

@andrewjong andrewjong commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Adds an end-to-end ordered-waypoint navigation test to the system test harness — the standard acceptance check for "I integrated/swapped a planner, does the drone actually fly the route?"

  • tests/system/test_waypoint_flight.py (mark: waypoint_flight): 4-phase chain per (sim, num_robots, iteration) — PX4 ready → takeoff → send route to the local planner's NavigateTask action (/robot_N/tasks/navigate) as a nav_msgs/Path → land. Reuses the flight-cycle workers from test_fixed_trajectory.py (chain guard, takeoff/land, odom CSV capture); landing always runs after a successful takeoff.
  • tests/waypoint_checker.py: standalone, stdlib-only judge. Pass iff the odometry track passes within --waypoint-tolerance (default 1.5 m) of every waypoint in order, each within --waypoint-timeout (default 120 s, odometry clock) of the previous arrival. Prints a JSON verdict (per-waypoint reached / closest approach / elapsed), exit 0/1.

Design notes

  • Judgment is on the odometry track, not the action result — the success criterion is independent of which planner produced the motion, so the same test judges any planner module dropped into the stack. The action result is recorded as a metric (navigate_action_success) but not asserted.
  • The checker is deliberately harness-independent (runs on any ros2 topic echo --csv odometry dump), so it can judge waypoint flight on non-AirStack ROS 2 systems as well — this also makes it reusable as the automated judge for the ICRA'27 paper's agent-evaluation study (Sec. VI-C), which is the immediate motivation for landing it in develop.
  • Waypoints are specified relative to the robot pose at dispatch (x forward along initial heading, z up), so routes are spawn-point and simulator agnostic. Default route: 10 m square at takeoff altitude.

Recorded metrics (per robot)

waypoint_success, waypoints_reached, navigate_action_success, route_time_sim_s, worst_closest_approach_m, ready_duration_sys_s

New pytest options

Option Default
--waypoints 10,0,0; 10,10,0; 0,10,0; 0,0,0
--waypoint-tolerance 1.5
--waypoint-timeout 120

Docs added to tests/README.md and the AGENTS.md test table; mark registered in pytest.ini; VERSION bumped to 0.19.0-alpha.9 with a CHANGELOG entry.

Test plan

  • Checker logic unit-sanity (in-order enforcement, unreachable waypoint, budget) — passes locally
  • pytest --collect-only collects 16 tests (4 phases × 4 default env params) with all cross-module imports resolving
  • Full flight run against Isaac Sim + stock stack on a local RTX 4090 (4/4 phases pass in 3m20s; see validation comment)
  • ms-airsim (Blocks) full flight run, local RTX 4090: 4/4 phases pass (see second validation comment)
  • Isaac Sim re-confirmed on the final default route: 4/4 phases pass

🤖 Generated with Claude Code

New end-to-end acceptance test for planner integration/swaps:
takeoff -> ordered waypoint route -> land, per (sim, num_robots, iter).

- tests/system/test_waypoint_flight.py (mark: waypoint_flight): after
  takeoff, sends the route to the local planner's NavigateTask action
  as a nav_msgs/Path and captures odometry throughout; reuses the
  flight-cycle workers from test_fixed_trajectory.py (chain guard,
  takeoff/land, odom CSV capture).
- tests/waypoint_checker.py: standalone stdlib-only judge — the
  odometry track must pass within --waypoint-tolerance of every
  waypoint IN ORDER, each within --waypoint-timeout of the previous
  arrival. Success is defined purely on the odometry track (not the
  action result), so swapping the global or local planner leaves the
  judgment unchanged; the checker also runs outside the harness on
  any ros2 `topic echo --csv` odometry dump.
- Waypoints are relative to the robot pose at dispatch (x forward
  along heading, z up), so routes are spawn/sim agnostic. Default:
  10 m square at takeoff altitude.
- New pytest options: --waypoints, --waypoint-tolerance,
  --waypoint-timeout; mark registered in pytest.ini; docs in
  tests/README.md and AGENTS.md; VERSION 0.19.0-alpha.9 + CHANGELOG.

Metrics recorded per robot: waypoint_success, waypoints_reached,
navigate_action_success, route_time_sim_s, worst_closest_approach_m.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andrewjong
andrewjong marked this pull request as draft August 4, 2026 19:09
Validated end-to-end against Isaac Sim + the stock stack (4/4 phases
pass in 3m20s; corners cut 3.75/5.13 m, final goal error 0.63 m).
Fixes found by flying:

- Path header frame: an empty frame_id crashed droan_gl (uncaught
  tf2::InvalidArgumentException in its plan TF transform); the goal now
  carries the frame from the odometry snapshot (fallback "map").
- Dense plan dispatch: sparse poses get corner-skipped by the local
  planner's distance-walking look-ahead; the route is now interpolated
  at 1 m from the current pose (mirrors real global-planner output).
- Route/tolerance semantics: the stack's contract is "reach the goal
  precisely, follow the corridor loosely" (droan_gl cost =
  deviation - path_distance cuts corners ~4-7 m). Split tolerances:
  intermediate corridor 15 m, final goal 2.5 m (new --goal-tolerance;
  NavigateTask's 1.5 m + tracking lag). Default route is now an open
  30 m square — NavigateTask succeeds on distance to the FINAL pose,
  so closed loops succeed instantly without flying (documented).
- Settle capture: the action succeeds on the tracking point, which
  leads the drone by up to the look-ahead distance (~10 m); capture
  now continues until the drone is stationary (max 30 s) so the goal
  approach is recorded. New metric: final_goal_error_m.
- waypoint_checker: closest_approach now reports the true minimum over
  the remaining track instead of the tolerance-boundary crossing
  (arrival stays first-crossing, ordering semantics unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andrewjong

Copy link
Copy Markdown
Member Author

Validation: full flight in Isaac Sim (stock stack) ✅

Ran airstack test -m waypoint_flight --sim isaacsim --num-robots 1 --stress-iterations 1 --gui -v locally (RTX 4090). Final result: 4/4 phases pass in 3m20se41e30dc.

Metric Value
ready_duration_sys_s 75.5
takeoff altitude_error_m −0.167
corner closest approaches 3.75 m, 5.13 m (tolerance 15 m)
final_goal_error_m 0.63 m (tolerance 2.5 m)
route_time_sim_s 101.7
landing final_altitude_m −0.004

Getting to a pass took 5 instrumented flights; each failure was informative and drove a calibration fix (all in e41e30dc):

  1. Empty frame_id crashed the local plannerdroan_gl_node dies with an uncaught tf2::InvalidArgumentException when the NavigateTask path header has an empty frame (gl_interface.cpp:85). Test now stamps the frame from the odometry snapshot. ⚠️ Separate repo issue worth filing: a malformed goal shouldn't be able to kill the planner node.
  2. Closed-loop routes succeed instantly — NavigateTask's success criterion is distance to the final pose, so a square that returns to start "succeeds" without flying. The checker (judging the odometry track, not the action result) correctly refused this — default route is now an open square, constraint documented.
  3. Sparse waypoint poses get corner-skipped — the plan-follower walks the path by distance with a look-ahead; the test now dispatches a dense 1 m-interpolated plan like a real global planner.
  4. Stock droan_gl cuts corners by design (cost = deviation - path_distance, 1:1) — ~4–7 m observed. Tolerances recalibrated to the stack's actual contract: loose 15 m corridor for intermediate waypoints, tight 2.5 m for the final goal (new --goal-tolerance).
  5. The action succeeds on the tracking point, which leads the drone by up to the look-ahead (~10 m) — capture now continues until the drone is stationary, and the checker reports true closest approach rather than the tolerance-boundary crossing.

🤖 Generated with Claude Code

Validated on both sim backends with the identical default config
(open 30 m square climbing to ~20 m AGL):
- Isaac Sim: corners 5.67/5.72 m, final goal 0.28 m, 4/4 phases
- ms-airsim (Blocks): corners 5.93/5.67 m, final goal 0.89 m, 4/4

At the old takeoff-altitude route the drone collided with a Blocks
obstacle (disparity was streaming, so DROAN had perception — the
corner-cut diagonals leave the forward stereo's coverage). This test
judges route-following, not obstacle avoidance, so the default route
flies above the clutter; documented in the option help and README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andrewjong

Copy link
Copy Markdown
Member Author

Validation part 2: ms-airsim (Blocks) + Isaac re-confirmation ✅

Both backends now pass 4/4 phases on the identical default configuration (50be34dd): open 30 m square climbing to ~20 m AGL, 15 m corridor / 2.5 m goal tolerance.

Metric Isaac Sim ms-airsim (Blocks)
corner closest approaches 5.67 m, 5.72 m 5.93 m, 5.67 m
final_goal_error_m 0.28 m 0.89 m
route_time_sim_s ~90 88.9
landing final_altitude_m −0.149 m

The cross-backend agreement of the corner-cut depth (~5.7 m both sims) is a nice incidental datapoint for the identical-autonomy-code claim — same planner behavior, different physics/render backends.

Findings from ms-airsim validation

  1. Obstacle collision at takeoff altitude (fixed in defaults): the original route at ~10 m AGL flew the drone into a Blocks obstacle. Disparity was confirmed streaming (~4.7 Hz) — DROAN had perception, but its corner-cut diagonals leave the forward stereo's coverage. Default route now climbs +10 m: this test judges route-following, not obstacle avoidance. A dedicated obstacle-avoidance benchmark would be a separate test.
  2. Flaky bring-up (1/4 runs): PX4 SITL never linked with AirSim after "sensors settle" — px4_ready timed out at 300 s and the chain correctly skipped/failed. PX4 runs in a tmux pane, so the failure was invisible post-teardown; this is exactly the case for the release-gate item "dead tmux pane dumps its last 50 lines."
  3. Transient false ON_GROUND (1/4 runs, did not reproduce): LandTask succeeded 11 s into a 20 m descent with the drone at 14.86 m — MAVROS landed_state reported ON_GROUND mid-air. Rerun landed cleanly (−0.149 m). Worth watching for in CI; if it recurs, the land detector config in the msairsim+PX4 path deserves a look.

🤖 Generated with Claude Code

@andrewjong
andrewjong marked this pull request as ready for review August 4, 2026 21:26
Captured mid-route during the validated flights: Isaac Sim viewport
with the drone on the square route, ms-airsim Blocks with the drone
clearing the obstacle field (collision count 0), and the Foxglove GCS
dashboard showing the planned path, expanded obstacle voxels, robot
task panel, and live stereo feed. Embedded in the waypoint section of
tests/README.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@andrewjong

Copy link
Copy Markdown
Member Author

Screenshots from the validation flights 📸

Captured mid-route during the passing validation runs (now also embedded in tests/README.md, commit 53e0a115):

Isaac Sim — drone on the waypoint route:

Isaac Sim waypoint flight

ms-airsim (Blocks) — clearing the obstacle field at ~20 m AGL (collision count 0, top-left):

ms-airsim Blocks waypoint flight

Foxglove GCS dashboard during the route — planned path + expanded obstacle voxels in the 3D panel, Robot Tasks panel, live stereo feed:

Foxglove during waypoint flight

🤖 Generated with Claude Code

@andrewjong
andrewjong merged commit 55d9b88 into develop Aug 4, 2026
2 checks passed
@andrewjong
andrewjong deleted the waypoint-flight-test branch August 4, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant