Skip to content

Ticket 108: Terrain-, Obstacle-, and Geofence-Aware Contingency Paths

Status

Planned (deferred). The gap is real and reproducible today; closing it changes the meaning of rth_is_feasible and every divert verdict, so it is scheduled rather than patched.

Goal

Make the return-to-home and divert paths the estimator computes obey the same terrain, obstacle, and geofence evidence the planned route already obeys. Today they are energy-only: a contingency path is "feasible" when the battery covers it, regardless of what is in the way.

Why This Matters

The README asks the question the tool exists to answer — "can it still fly home from every waypoint?" — and the checklist prints ✓ RTH reserve PASS reserve intact for RTH from all N leg(s). Both read as a statement about the route home. Both are statements about watt-hours.

For a BVLOS operator this is the wrong failure mode in the wrong direction: the mission that most needs a contingency check is the one flying a valley or a ridge line, and that is exactly the mission where a straight-line energy budget is most optimistic. The same gap covers landing-zone divert reachability, which is the other half of the contingency story.

Current gap

estimate_emergency_path (bvlos_sim/estimator/execution/energy.py:1094) builds the RTH path as wind-triangle segments plus a terminal vertical phase. compute_divert_estimate (bvlos_sim/estimator/execution/divert.py:45) does the same for landing-zone diverts using a geodesic Dubins path. Neither module contains a single reference to terrain, obstacles, or geofences:

$ grep -c "terrain" bvlos_sim/estimator/execution/energy.py
0
$ grep -c "terrain" bvlos_sim/estimator/execution/divert.py
0
$ grep -c "geofence\|obstacle" bvlos_sim/estimator/execution/divert.py
0

The terrain provider is loaded, active, and consulted for the planned route — it is simply never queried along the path home.

Reproduction

A terrain grid holding a 1500 m ridge in a box on the direct line between the last waypoint and home, and an L-shaped route at 300 m AMSL that dodges the box on the way out:

Route item is_feasible rth_distance_m
takeoff True 0
wp1 True 3003
wp2 True 5397
landing True 5230

rth_is_feasible: True. Every timeline point is feasible. The direct path from wp2 to home crosses a ridge 1200 m above the cruise altitude.

The route is not merely un-checked — it is checked and passed. With constraints.min_terrain_clearance_m: 30.0 and the same terrain asset, the mission reports obstacle feasible, because the planned route genuinely clears the box. The evidence covers the ridge; only the contingency path ignores it.

Scope

  • Sample the RTH path from estimate_emergency_path against the terrain provider at the same sub-segment resolution the planned route uses, and apply constraints.min_terrain_clearance_m when it is set.
  • Apply constraints.min_obstacle_clearance_m and the obstacle provider to the same samples.
  • Evaluate geofence conflicts along the RTH and divert geometry, honouring floor_m/ceiling_m altitude bands and active_from/active_until windows as the planned-route check does.
  • Do the same for compute_divert_estimate, so landing-zone reachability means reachable, not merely within energy range.
  • Report the outcome structurally: a per-timeline-point reason (for example rth_terrain_clearance, rth_geofence_conflict) rather than a bare False, so an operator can tell an energy shortfall from an obstruction.
  • Decide and document the altitude policy: an RTH path that must climb over terrain costs more energy than the straight line currently billed, so the clearance check and the energy model have to agree on one profile.

Integration Requirements

  • Contingency checks must consume the same providers already on EstimationContext; no new asset types and no live lookups.
  • When no terrain, obstacle, or geofence asset is configured, behaviour is unchanged — this ticket must not turn a missing asset into a failure, which is the readiness gate's job, not the energy model's.
  • estimator-envelope field names stay stable; the RTH timeline point gains fields rather than changing the meaning of existing ones. A changed is_feasible on a mission with terrain configured is a deliberate, version-bumped contract change and needs a golden-fixture update in the same commit.
  • Determinism is preserved: same inputs, same path samples, same verdict.

Acceptance Criteria

  1. The reproduction above returns rth_is_feasible: False with a structured terrain reason on the points whose path home crosses the ridge, while the planned route continues to pass its own clearance check.
  2. A divert to a landing zone on the far side of a geofenced or obstructed volume is reported unreachable, not merely energy-feasible.
  3. A mission with no terrain, obstacle, or geofence asset produces byte-identical output to today.
  4. The RTH energy figure and the RTH clearance check are computed from the same altitude profile; a path that climbs to clear terrain is billed for the climb.
  5. docs/missions.md and the README stop implying that "can it fly home" is answered when only the battery was checked.

Out of Scope

  • Replanning: computing an alternative obstacle-free path home. This ticket detects that the modelled path is obstructed; choosing a different one is Ticket 036 / 044 territory.
  • 3D Dubins or terrain-following contingency profiles.
  • Dynamic obstacles or traffic.