mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-08-26 17:31:37 +00:00
navi-offroute: tighten hybrid gates + fix hybrid render (#34)
PROBLEM 1 (latency): short in-town Auto routes (Twin Falls->Filer ~7 mi) took 60+ seconds because urban OSM-parking density exploded hybrid candidate evaluation. Fixes in router.py: - MIN_HYBRID_DISTANCE_KM 8.0 -> 24.0 (~15 mi): in-town trips never enter hybrid eval at all -- this alone eliminates the reported latency on Matt's routes. - HYBRID_MAX_TRAILHEADS 20 -> 8: fewer candidates even on long trips. - HYBRID_OVERALL_TIMEOUT_S = 6.0: a wall-clock check inside the candidate loop bails hybrid eval past 6 s (logger.warning) and keeps the single-mode / best-so-far winner. - HYBRID_EARLY_ABORT_MIN = 30.0: once a candidate beats the single-mode winner by 30+ min, stop probing the rest and ship it. - Per-stage timing logs (logger.info) in _route_auto / _try_hybrid_auto: "single-mode probing took Xs", "hybrid candidate gathering: N candidates in Xs", "hybrid probing took Xs across N tested candidates". PROBLEM 2 (hybrid render): investigated the missing network polyline. The stated hypothesis (a hybrid drive leg using a non-"network" segment_type) is DISPROVEN -- _build_hybrid_response emits segment_type=="network" for BOTH the drive and offroad legs (verified against the live response), the OFFROUTE_NETWORK_LAYER filter matches it, MODE_COLORS is fully defined, and the store passes data.route correctly. The one real fragility is the MapLibre color match: if network_mode is ever null the whole layer can fail to paint (wilderness still draws via its static color -- matching the exact symptom). Hardened it with ["to-string", ["get","network_mode"]] so a missing/unknown mode falls through to the blue fallback and the layer always paints. I could not reproduce the exact missing-leg render headlessly (all backend shapes + frontend filters are correct), so a Chrome MCP repro is recommended to confirm #2 is resolved; if a render issue remains it should be diagnosed in-browser. Tests: hybrid synthetic-trip distances bumped 20 -> 30 km (past the new 24 km gate); new test_hybrid_early_abort_stops_probing; surface-change integration savings lowered into the 15-30 min band so both candidate sources are still probed (not early-aborted). Full offroute suite: 84 passed. npm run build: clean. PROBLEM 3 (off-road wilderness timeout) is out of scope -- separate follow-up; the wilderness pathfinder is untouched here. Co-authored-by: Matt <mj@k7zvx.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c00b69a55
commit
913cc46f2a
5 changed files with 74 additions and 11 deletions
|
|
@ -104,10 +104,12 @@ AUTO_MODE_PRIORITY = ["vehicle", "4w", "2w", "foot"]
|
|||
# switch vehicles, continue offroad" can beat the single-mode winner; Auto picks that
|
||||
# hybrid plan when it does. Leg times are summed with NO transition penalty, and the
|
||||
# minimums below keep the suggestions sensible (no short trips / trivial detours).
|
||||
MIN_HYBRID_DISTANCE_KM = 8.0 # ~5 mi: shorter single-mode wins stay as-is
|
||||
MIN_HYBRID_DISTANCE_KM = 24.0 # ~15 mi: in-town trips never enter hybrid eval
|
||||
HYBRID_MIN_TIME_SAVINGS_MIN = 15.0 # a hybrid must beat the winner by at least this
|
||||
HYBRID_MIN_OFFROAD_KM = 0.8 # ~0.5 mi: reject trivial offroad detours
|
||||
HYBRID_MAX_TRAILHEADS = 20 # cap candidates (closest to the route first)
|
||||
HYBRID_MAX_TRAILHEADS = 8 # cap candidates (closest to the route first)
|
||||
HYBRID_OVERALL_TIMEOUT_S = 6.0 # bail hybrid eval past this, keep single-mode/best-so-far
|
||||
HYBRID_EARLY_ABORT_MIN = 30.0 # a candidate beating the winner by this much ends probing
|
||||
HYBRID_TRAILHEAD_BUFFER_M = 2000 # candidate trailheads within 2 km of the route
|
||||
# (drive_mode, offroad_mode) transition pairs, tried at each candidate trailhead.
|
||||
HYBRID_PAIRS = [("vehicle", "4w"), ("vehicle", "2w"), ("vehicle", "foot"), ("4w", "foot")]
|
||||
|
|
@ -863,6 +865,7 @@ class OffrouteRouter:
|
|||
best_result = None
|
||||
best_minutes = None
|
||||
last_error = None
|
||||
_probe_t0 = time.perf_counter()
|
||||
for candidate in priority:
|
||||
result = self.route(
|
||||
start_lat, start_lon, end_lat, end_lon,
|
||||
|
|
@ -877,6 +880,8 @@ class OffrouteRouter:
|
|||
best_result["selected_mode"] = candidate
|
||||
else:
|
||||
last_error = result
|
||||
logger.info("single-mode probing took %.2fs (%d candidate modes)",
|
||||
time.perf_counter() - _probe_t0, len(priority))
|
||||
|
||||
if best_result is not None:
|
||||
# MVUM Layer 3a: a "drive to a trailhead, switch, continue offroad" plan may
|
||||
|
|
@ -931,6 +936,8 @@ class OffrouteRouter:
|
|||
if len(coords) < 2:
|
||||
return None
|
||||
|
||||
_hybrid_t0 = time.perf_counter()
|
||||
_gather_t0 = _hybrid_t0
|
||||
# Gather transition candidates from every available source; each yields the
|
||||
# same {lat, lon, name, road_class, ...} record shape, so they mix freely and
|
||||
# share the closest-first sort + cap below.
|
||||
|
|
@ -954,6 +961,8 @@ class OffrouteRouter:
|
|||
line = LineString([(lon, lat) for (lat, lon) in coords])
|
||||
candidates.sort(key=lambda th: line.distance(Point(th["lon"], th["lat"])))
|
||||
candidates = candidates[:HYBRID_MAX_TRAILHEADS]
|
||||
logger.info("hybrid candidate gathering: %d candidates in %.2fs",
|
||||
len(candidates), time.perf_counter() - _gather_t0)
|
||||
|
||||
# Per trailhead, leg1 depends only on drive_mode and leg2 only on offroad_mode,
|
||||
# so route each distinct mode once and recombine across pairs.
|
||||
|
|
@ -961,9 +970,19 @@ class OffrouteRouter:
|
|||
offroad_modes = sorted({o for _, o in HYBRID_PAIRS})
|
||||
|
||||
threshold = best_minutes - HYBRID_MIN_TIME_SAVINGS_MIN
|
||||
early_abort_at = best_minutes - HYBRID_EARLY_ABORT_MIN
|
||||
winner = None
|
||||
winner_minutes = None
|
||||
_probe_t0 = time.perf_counter()
|
||||
tested = 0
|
||||
for th in candidates:
|
||||
if time.perf_counter() - _hybrid_t0 > HYBRID_OVERALL_TIMEOUT_S:
|
||||
logger.warning(
|
||||
"hybrid eval exceeded %.1fs after %d/%d candidates; using %s",
|
||||
HYBRID_OVERALL_TIMEOUT_S, tested, len(candidates),
|
||||
"best hybrid so far" if winner is not None else "single-mode winner")
|
||||
break
|
||||
tested += 1
|
||||
leg1_by_mode = {}
|
||||
for dm in drive_modes:
|
||||
r = self.route(start_lat, start_lon, th["lat"], th["lon"],
|
||||
|
|
@ -990,6 +1009,14 @@ class OffrouteRouter:
|
|||
if total < threshold and (winner_minutes is None or total < winner_minutes):
|
||||
winner_minutes = total
|
||||
winner = (leg1, leg2, dm, om, th)
|
||||
# Early abort: a candidate that beats the single-mode winner by a wide
|
||||
# margin is good enough -- stop probing the rest and ship it.
|
||||
if winner_minutes is not None and winner_minutes <= early_abort_at:
|
||||
logger.info("hybrid early-abort: candidate beats single-mode by >=%.0f min "
|
||||
"after %d candidates", HYBRID_EARLY_ABORT_MIN, tested)
|
||||
break
|
||||
logger.info("hybrid probing took %.2fs across %d tested candidates",
|
||||
time.perf_counter() - _probe_t0, tested)
|
||||
|
||||
if winner is None:
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -155,9 +155,11 @@ def test_integration_with_hybrid(monkeypatch):
|
|||
r.spatial_index = None
|
||||
r.trailhead_index = _FakeTrailheads([trailhead])
|
||||
|
||||
best = _winning_single_mode(distance_km=20.0, minutes=120.0)
|
||||
# 70-min single-mode vs ~50-min hybrid = ~20 min savings: qualifies (>15) but is
|
||||
# below the early-abort margin (30), so BOTH candidate sources are probed.
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=70.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
best, 70.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is not None
|
||||
assert out["selected_mode"] == "hybrid"
|
||||
# BOTH candidate types were probed as leg-1 destinations (drive-to-transition).
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ def test_hybrid_wins_with_big_savings(monkeypatch):
|
|||
return _ok_leg(4.0, 30.0) # 4w/2w/foot offroad legs (>= 0.8 km)
|
||||
monkeypatch.setattr(OffrouteRouter, "route", fake_route)
|
||||
|
||||
best = _winning_single_mode(distance_km=20.0, minutes=120.0)
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=120.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is not None
|
||||
|
|
@ -181,7 +181,7 @@ def test_hybrid_skips_trivial_offroad_detour(monkeypatch):
|
|||
return _ok_leg(0.3, 5.0) # < 0.8 km offroad
|
||||
monkeypatch.setattr(OffrouteRouter, "route", fake_route)
|
||||
|
||||
best = _winning_single_mode(distance_km=20.0, minutes=120.0)
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=120.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is None
|
||||
|
|
@ -191,7 +191,7 @@ def test_no_trailheads_falls_back_to_single_mode(monkeypatch):
|
|||
r = _bare_router(_FakeTrailheads([])) # no candidates near the line
|
||||
monkeypatch.setattr(OffrouteRouter, "route",
|
||||
lambda self, *a, **k: _ok_leg(5.0, 10.0))
|
||||
best = _winning_single_mode(distance_km=20.0, minutes=120.0)
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=120.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is None
|
||||
|
|
@ -210,7 +210,38 @@ def test_hybrid_not_taken_when_savings_below_threshold(monkeypatch):
|
|||
return _ok_leg(4.0, 20.0)
|
||||
monkeypatch.setattr(OffrouteRouter, "route", fake_route)
|
||||
|
||||
best = _winning_single_mode(distance_km=20.0, minutes=50.0)
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=50.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 50.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is None
|
||||
|
||||
|
||||
def test_hybrid_early_abort_stops_probing(monkeypatch):
|
||||
# Three trailheads, each yielding a hybrid that beats the single-mode winner by
|
||||
# ~70 min (>= HYBRID_EARLY_ABORT_MIN). The first qualifying candidate must end
|
||||
# probing, so not all three are routed as leg-1 destinations.
|
||||
ths = [
|
||||
{"lat": 44.0, "lon": -114.20, "name": "TH1", "road_class": "track"},
|
||||
{"lat": 44.0, "lon": -114.22, "name": "TH2", "road_class": "track"},
|
||||
{"lat": 44.0, "lon": -114.24, "name": "TH3", "road_class": "track"},
|
||||
]
|
||||
r = _bare_router(_FakeTrailheads(ths))
|
||||
seen = []
|
||||
|
||||
def fake_route(self, s_lat, s_lon, e_lat, e_lon, mode="foot",
|
||||
boundary_mode="pragmatic", annotate_mvum=True, **k):
|
||||
seen.append((round(e_lat, 4), round(e_lon, 4)))
|
||||
if mode == "vehicle":
|
||||
return _ok_leg(12.0, 20.0)
|
||||
return _ok_leg(4.0, 30.0)
|
||||
monkeypatch.setattr(OffrouteRouter, "route", fake_route)
|
||||
|
||||
best = _winning_single_mode(distance_km=30.0, minutes=120.0) # hybrids save ~70 min
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is not None and out["selected_mode"] == "hybrid"
|
||||
# early-abort: probing stopped before all three trailheads were evaluated
|
||||
probed_ths = {(d for d in seen)} # noqa: F841 (readability)
|
||||
th_dests = {(round(t["lat"], 4), round(t["lon"], 4)) for t in ths}
|
||||
seen_th_dests = th_dests & set(seen)
|
||||
assert len(seen_th_dests) < 3 # did not probe every candidate
|
||||
|
|
|
|||
|
|
@ -949,7 +949,7 @@ def test_hybrid_consumes_parking_candidates(monkeypatch):
|
|||
r.trailhead_index = None # no trailheads -> parking must still be gathered
|
||||
r.parking_index = _FakeParking([parking])
|
||||
|
||||
best = _hybrid_winning_single_mode(distance_km=20.0, minutes=120.0)
|
||||
best = _hybrid_winning_single_mode(distance_km=30.0, minutes=120.0)
|
||||
out = r._try_hybrid_auto(44.0, -114.0, 44.0, -114.5, "pragmatic",
|
||||
best, 120.0, frozenset({"vehicle", "4w", "2w", "foot"}))
|
||||
assert out is not None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue