From fd1246e8916f26dda638d01677df0239e06bda6a Mon Sep 17 00:00:00 2001 From: Boris Sorochkin Date: Sun, 21 Jun 2026 21:14:23 +0300 Subject: [PATCH] feat(runner): START_LAT/START_LON open-water cold-start seed Random-passage mode autoroutes from the boat's current position; if the persisted/default start is inshore (e.g. the Venice lagoon route origin), a coarse-grid A* can't route out and the boat sits drawing unreachable destinations. START_LAT/START_LON override the SignalK-resume / route-origin cold-start position so the demo can seed open water. A live hot-restart position (controller-supplied) still wins, so the boat does not teleport back to the seed mid-run. Documented in deploy/DEPLOYMENT.md; unit-tested in tests/test_start_pos.py. Co-Authored-By: Claude Opus 4.8 (1M context) --- deploy/DEPLOYMENT.md | 3 +++ src/yey/boats/simulator/engine/runner.py | 28 +++++++++++++++++++++ tests/test_start_pos.py | 31 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/test_start_pos.py diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md index 181a01b..46678e3 100644 --- a/deploy/DEPLOYMENT.md +++ b/deploy/DEPLOYMENT.md @@ -52,6 +52,7 @@ Add these to the existing `boat-sim` environment to enable the demo: |---|---|---| | `GEOGRID_API_URL` | `http://bathy:8089/v1/gebco2020` | point depth/routing at the `bathy` service instead of the public OpenTopoData (avoids the 429 quota). Use the address by which the sim reaches `bathy` (service name on a Compose/ECS network, or `localhost` with host networking). | | `RANDOM_PASSAGES` | `1` | enable random-passage mode | +| `START_LAT` / `START_LON` | `44.7` / `13.1` | **open-water cold-start seed** (decimal degrees). Overrides the SignalK-resume / route-origin start so the boat begins in open water. **Required for random-passage mode** if the persisted/default start is inshore (e.g. the Venice lagoon): a coarse-grid A\* can't route *out* of a lagoon, so the boat would sit there drawing destinations it can never reach. A live hot-restart position still wins (no mid-run teleport). Pick a point inside the `bathy` bbox with open sea room — e.g. `44.7,13.1` (N Adriatic, ~−39 m, W of Istria). | | `PASSAGE_MIN_NM` | `8` | min passage length (nM). **8–15 is the demo default** (a fresh routed passage roughly every 1.5–2.5 h at ~6 kn); use `40`/`60` for realistic-length passages (~7–10 h each). | | `PASSAGE_MAX_NM` | `15` | max passage length (nM) | | `PASSAGE_ARRIVAL_NM` | `1.0` | lay the next passage when within this distance of the destination | @@ -104,6 +105,8 @@ services: SIM_WEB_PORT: "8088" GEOGRID_API_URL: http://bathy:8089/v1/gebco2020 RANDOM_PASSAGES: "1" + START_LAT: "44.7" # open-water seed (W of Istria); required so the + START_LON: "13.1" # boat doesn't start stuck inshore (e.g. lagoon) PASSAGE_MIN_NM: "8" # 8-15 nM: a new routed passage every ~1.5-2.5 h PASSAGE_MAX_NM: "15" # use 40/60 for realistic ~7-10 h passages AUTOROUTE_MAX_CELLS: "60000" diff --git a/src/yey/boats/simulator/engine/runner.py b/src/yey/boats/simulator/engine/runner.py index ddc7827..db8c091 100644 --- a/src/yey/boats/simulator/engine/runner.py +++ b/src/yey/boats/simulator/engine/runner.py @@ -34,6 +34,27 @@ "bilge_pump", "water_pump"] +def _env_start_pos() -> tuple[float, float] | None: + """Optional fixed cold-start position from START_LAT / START_LON (decimal deg). + + When both are set and parseable this overrides the SignalK-resume / route-origin + cold-start position. It exists to seed the boat in open water for the + random-passage demo: a persisted inshore position (e.g. a marina in a lagoon) + can't be autorouted out of at a coarse bathymetry grid, so the boat would sit + there drawing destinations it can never reach. A live caller-supplied start_pos + (hot restart with the last reported position) still wins, so the boat does not + teleport back to the seed mid-run. + """ + lat_s, lon_s = os.environ.get("START_LAT"), os.environ.get("START_LON") + if not lat_s or not lon_s: + return None + try: + return (float(lat_s), float(lon_s)) + except ValueError: + print(f"[sim] ignoring invalid START_LAT/START_LON: {lat_s!r}, {lon_s!r}", flush=True) + return None + + def build_data_source(settings: Settings): if settings.weather_source == "signalk": return SignalKDataSource(settings.signalk_host, settings.signalk_port) @@ -131,6 +152,13 @@ async def pipeline(settings: Settings, route, start_pos, report_status) -> None: sk_sink = chain.active if isinstance(chain.active, SignalKSink) else None writer = sk_sink.writer if sk_sink else None + if start_pos is None: + env_pos = _env_start_pos() + if env_pos is not None: + start_pos = env_pos + print(f"[sim] start position from START_LAT/START_LON: " + f"({env_pos[0]:.4f}, {env_pos[1]:.4f})", flush=True) + if start_pos is not None: start_lat, start_lon = start_pos idx, _ = route.resync_from_position(start_lat, start_lon) diff --git a/tests/test_start_pos.py b/tests/test_start_pos.py new file mode 100644 index 0000000..50a3d62 --- /dev/null +++ b/tests/test_start_pos.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 +"""Tests for the START_LAT/START_LON cold-start override (engine/runner.py).""" +from __future__ import annotations + +import pytest # type: ignore[import] + +from yey.boats.simulator.engine.runner import _env_start_pos # type: ignore[import] + + +def test_no_env_returns_none(monkeypatch): + monkeypatch.delenv("START_LAT", raising=False) + monkeypatch.delenv("START_LON", raising=False) + assert _env_start_pos() is None # noqa: S101 + + +def test_both_set_returns_tuple(monkeypatch): + monkeypatch.setenv("START_LAT", "44.7") + monkeypatch.setenv("START_LON", "13.1") + assert _env_start_pos() == pytest.approx((44.7, 13.1)) # noqa: S101 + + +def test_partial_env_returns_none(monkeypatch): + monkeypatch.setenv("START_LAT", "44.7") + monkeypatch.delenv("START_LON", raising=False) + assert _env_start_pos() is None # noqa: S101 + + +def test_invalid_env_returns_none(monkeypatch): + monkeypatch.setenv("START_LAT", "not-a-number") + monkeypatch.setenv("START_LON", "13.1") + assert _env_start_pos() is None # noqa: S101