Skip to content

Commit c97ac4a

Browse files
committed
Prepare fine coastal routes with a bounded watchdog and 400 m preference
1 parent b21e642 commit c97ac4a

6 files changed

Lines changed: 128 additions & 27 deletions

File tree

‎README.ko.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ path = planner.route(start, goal)
8080
|---|---|
8181
| 수상 차량 로컬 수심 여유 | 1.5 m |
8282
| 수상 차량 반경 + 추적 여유 | 3.5 m + 4 m |
83-
| 해안 선호 거리 | 350 m |
83+
| 해안 선호 거리 | 400 m |
8484
| 해안 추종 중 육지와의 최대 거리 | 1.5 km |
8585
| 해안 선호에 따른 추가 거리 한도 | 100% (전체 거리 2.0배) |
8686
| 해안 지역 여유 폭 / 수심 | 40 m / 2 m |
@@ -126,11 +126,16 @@ path = planner.route(start, goal)
126126
`state['regional_waiting']`이 있으면 검증된 다음 경로가 준비될 때까지 추진을
127127
멈춥니다. 물리 계산과 센서 수신은 계속합니다. 선택적인 해안 경로 준비에 실패하거나
128128
결과가 비어 있어도 검증된 해안 경로를 외해 목표로 바꾸지 않습니다.
129+
계획 감시는 제한 시간 안의 작업을 유지하고 완료된 결과부터 반영합니다.
130+
경로 준비 지연만으로 차량의 물리적 후퇴를 요청하지 않습니다.
129131

130132
기본 지형 자료 서버에 연결할 수 없으면 더 작은 구역으로 경로를 계산합니다.
131133
검증된 자료를 사용하며 수심과 육지 확인 기준은 같습니다.
132134

133-
- 일반 구역: 20 km × 20 km
135+
- 해안에서 1.5 km 이내인 구역: 6 km × 6 km부터 검사
136+
- 정밀 구역에서 안전한 연결 경로를 찾지 못하면: 20 km × 20 km로 확장
137+
- 일반 지역 검사 범위: 20 km × 20 km
138+
- 전체 백그라운드 경로 준비 제한 시간: 660초
134139
- 자료 서버 장애 시 재시도 구역: 6 km × 6 km
135140
- 두 구역 모두 자료 부족: 추진을 멈추고 재시도
136141

‎README.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Optional renewals keep the current checked coastal runway until a coastal contin
8080
|---|---|
8181
| Surface local depth clearance | 1.5 m |
8282
| Surface hull radius + tracking margin | 3.5 m + 4 m |
83-
| Coastal preferred distance | 350 m |
83+
| Coastal preferred distance | 400 m |
8484
| Maximum coastal-following distance from land | 1.5 km |
8585
| Maximum extra distance for coastal preference | 100% (2.0× total length) |
8686
| Coastal regional buffer / depth | 40 m / 2 m |
@@ -126,12 +126,17 @@ Pass the final port as `progress_target` when the local target is a course waypo
126126
When `state['regional_waiting']` is present, the caller holds thrust until a
127127
checked replacement is ready. Physics and sensor sampling continue during the
128128
wait. Failed or empty optional renewal cannot replace a checked coastal route
129-
with an offshore goal.
129+
with an offshore goal. A planning watchdog preserves pending work within its
130+
budget and consumes completed results before retrying. Planning delays do not
131+
request a physical retreat.
130132

131133
When the baseline terrain provider is unavailable, regional planning tries a
132134
smaller region. It uses verified source data and the same depth and land checks.
133135

134-
- Normal region: 20 km × 20 km
136+
- Near-shore region, within 1.5 km of shore: 6 km × 6 km first
137+
- Wider region when the fine grid has no checked connection: 20 km × 20 km
138+
- Ordinary regional coverage: 20 km × 20 km
139+
- Total background route preparation budget: 660 seconds
135140
- Source outage retry: 6 km × 6 km
136141
- Missing data in both regions: hold thrust and retry
137142

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "posim-routing"
7-
version = "0.1.4"
7+
version = "0.1.5"
88
description = "Global maritime and terrain checked coastal route planning"
99
requires-python = ">=3.10"
1010
license = {text = "Apache-2.0"}

‎src/posim_routing/coastal.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
GEO = Geod(ellps="WGS84")
2222
COAST_DISCOVERY_M = 80000.
23-
STANDOFF_M = 350.
23+
STANDOFF_M = 400.
2424
MAXIMUM_STANDOFF_M = 600.
2525
MINIMUM_DEPTH_M = 2.
2626
REGIONAL_BUFFER_M = 40.

‎src/posim_routing/regional.py‎

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import asyncio,math,time
77
from pyproj import Geod
88
GEO=Geod(ellps='WGS84')
9-
REGIONAL_POLICY='buffered-water-v18-coastal-350m'
9+
REGIONAL_POLICY='buffered-water-v19-coastal-400m'
10+
COMPATIBLE_POLICIES={REGIONAL_POLICY,'buffered-water-v18-coastal-350m'}
11+
PLANNING_BUDGET_SECONDS=660.
1012
REGIONAL_BUFFER_M=40.
1113
REGIONAL_DEPTH_M=2.
1214

@@ -61,9 +63,23 @@ def __init__(self,rpc,coastal=False,before_prepare=None):
6163
self.rpc=rpc;self.task=None;self.key=None;self.retry=0
6264
self.coastal=coastal;self.coast_index=None;self.index_task=None;self.probed=None
6365
self.progress=None;self.coastal_target=None
64-
self.recovery_extent=None
65-
def recover_wait(self,state):
66-
"""Retry stalled planning at another resolution without issuing movement."""
66+
self.recovery_extent=None;self.task_started=None
67+
def start_task(self,work):
68+
# The controller watchdog must not cancel terrain preparation before
69+
# its own bounded provider deadline. One budget includes all retries.
70+
self.task_started=time.monotonic()
71+
work_task=asyncio.create_task(work)
72+
self.task=asyncio.create_task(asyncio.wait_for(work_task,PLANNING_BUDGET_SECONDS))
73+
# Cancellation before the wrapper starts must still close its work.
74+
self.task.add_done_callback(lambda _: work_task.cancel() if not work_task.done() else None)
75+
def recover_wait(self,state,preserve_pending=False):
76+
"""Retry expired work; keep bounded pending or completed work consumable."""
77+
if preserve_pending and self.task is not None:
78+
elapsed=time.monotonic()-self.task_started if self.task_started is not None else PLANNING_BUDGET_SECONDS
79+
if self.task.done() or elapsed<PLANNING_BUDGET_SECONDS:
80+
state.setdefault('regional_planning',{}).update(elapsed_seconds=max(0.,elapsed),
81+
budget_seconds=PLANNING_BUDGET_SECONDS)
82+
return dict(pending=True)
6783
if self.task:self.task.cancel()
6884
self.task=None;self.retry=0.;self.probed=None;self.progress=None
6985
previous=state.get('navigation_failsafe') or {}
@@ -88,7 +104,7 @@ def active_point(self,state,key):
88104
if plan.get('mode')=='port_approach' and plan.get('arrival_radius_m')!=ARRIVAL_RADIUS_M:
89105
self.discard(state,'port_arrival_region_changed')
90106
return None
91-
if plan.get('policy')!=REGIONAL_POLICY:
107+
if plan.get('policy') not in COMPATIBLE_POLICIES:
92108
self.discard(state,'regional_clearance_policy_changed')
93109
return None
94110
from .course import passed_vertex
@@ -168,7 +184,7 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
168184
and plan.get('mode') in ('coastal','coastal_transfer','coastal_approach'))
169185
if ((port_handoff or coast_handoff) and old and len(old)==3
170186
and old[:2]==key[:2] and old[2]<key[2]
171-
and plan.get('policy')==REGIONAL_POLICY):
187+
and plan.get('policy') in COMPATIBLE_POLICIES):
172188
plan['route_key']=list(key);plan.pop('continuation',None)
173189
state['regional_handoff']=dict(previous_key=old,route_key=list(key),
174190
position=list(state['position']),reason='same_port_course_advance' if port_handoff else 'measured_coastal_shape_advance')
@@ -219,7 +235,7 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
219235
from .coastal import WAYPOINT_FORMAT
220236
renew_coast=renew_coast or (point is not None and nearest is not None
221237
and plan.get('mode')=='coastal'
222-
and (not plan.get('onward_verified') or plan.get('waypoint_format')!=WAYPOINT_FORMAT))
238+
and (plan.get('policy')!=REGIONAL_POLICY or not plan.get('onward_verified') or plan.get('waypoint_format')!=WAYPOINT_FORMAT))
223239
coast_needed=nearest is not None and (self.probed is None or distance(self.probed,state['position'])>=3000)
224240
prefer_coast=(point is not None and coast_needed and not needed and
225241
(state.get('regional_navigation') or {}).get('mode') not in ('coastal','coastal_transfer','coastal_approach'))
@@ -236,7 +252,7 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
236252
# A failed optional preference must preserve a previously
237253
# checked detour while waiting for a better coastal region.
238254
coast_plan=(plan.get('route_key')==key and plan.get('mode') in ('coastal','coastal_transfer','coastal_approach')
239-
and plan.get('policy')==REGIONAL_POLICY)
255+
and plan.get('policy') in COMPATIBLE_POLICIES)
240256
preserve_empty=(not value['points'] and (point is not None or
241257
(coast_plan and nearest is not None and not terminal and not needed)))
242258
# An optional scenic renewal cannot replace a checked coastal
@@ -264,11 +280,11 @@ def update(self,state,target,needed=False,terminal=False,retain_coast=False,prog
264280
coast_needed=nearest is not None and (self.probed is None or distance(self.probed,state['position'])>=3000)
265281
if (prefer_port or needed or coast_needed or renew_coast) and not self.task and time.monotonic()>=self.retry:
266282
state['regional_planning']=dict(status='background',mode='port_approach' if terminal else 'coastal' if coast_needed else 'detour')
267-
self.task=asyncio.create_task(self.prepare_port(state,target,key) if terminal else self.prepare(state,target,key,nearest,needed,extent=self.recovery_extent,progress_target=progress_target,coastal_target=coastal_target))
283+
self.start_task(self.prepare_port(state,target,key) if terminal else self.prepare(state,target,key,nearest,needed,extent=self.recovery_extent,progress_target=progress_target,coastal_target=coastal_target))
268284
self.recovery_extent=None
269285
if point is not None:return point
270286
plan=state.get('regional_navigation') or {}
271-
if (plan.get('route_key')==key and plan.get('policy')==REGIONAL_POLICY
287+
if (plan.get('route_key')==key and plan.get('policy') in COMPATIBLE_POLICIES
272288
and (plan.get('mode') in ('coastal','coastal_transfer','coastal_approach') or plan.get('mapped_guidance')) and plan.get('points')
273289
and plan['index']>=len(plan['points'])
274290
and distance(state['position'],plan['points'][-1])<1500
@@ -305,7 +321,7 @@ def departure_update(self,state,target,key):
305321
self.task=None
306322
if point is not None:return point
307323
if self.task is None and time.monotonic()>=self.retry:
308-
self.task=asyncio.create_task(self.prepare_departure(state,target,key))
324+
self.start_task(self.prepare_departure(state,target,key))
309325
state['regional_planning']=dict(status='background',mode='port_departure')
310326
state['regional_waiting']=dict(reason='resident_departure_corridor_pending')
311327
return state['position']
@@ -412,7 +428,7 @@ async def terrain_region(self, centre, extent):
412428
except Exception:break
413429
if (sources.get('baseline_status') or {}).get('status')!='unavailable':break
414430
raise RuntimeError('regional_terrain_unavailable')
415-
async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,progress_target=None,coastal_target=None):
431+
async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,progress_target=None,coastal_target=None,fine_checked=False):
416432
from posim_terrain.canal_override import in_region
417433
canal=in_region(state['position'])
418434
if self.coastal and not canal and nearest is None and self.index_task is not None:
@@ -433,7 +449,11 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
433449
# distant coast. The 1:10M discovery line never supplies depth.
434450
heading,_,_=GEO.inv(*pos,*nearest[1])
435451
centre=list(GEO.fwd(*pos,heading,min(6000.,nearest[0]-6000.))[:2])
436-
extent=extent or (4000 if canal else 20000)
452+
initial_extent=extent is None
453+
# Fine coastal triangles first: a 20 km grid can close navigable water
454+
# beside a headland and waste an entire provider/planner cycle.
455+
extent=extent or (4000 if canal else 6000 if nearest and nearest[0]<1500. else 20000)
456+
fine_checked=fine_checked or extent==6000
437457
if self.before_prepare is not None:await self.before_prepare()
438458
job=await self.terrain_region(centre,extent)
439459
grid=await self.rpc('/terrain/jobs/'+job['id']+'/files/terrain.json',timeout=20)
@@ -450,7 +470,7 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
450470
try:value=await asyncio.to_thread(coastal_corridor,grid,job['spec'],current,coastal_goal,guard=guard,progress_target=progress_target)
451471
except RuntimeError as error:coast_error=str(error)[:160]
452472
water_fallback=value if value is not None and value.get('mode')=='detour' else None
453-
if nearest and (value is None or water_fallback is not None) and extent>6000 and nearest[0]<1500.:
473+
if nearest and not fine_checked and (value is None or water_fallback is not None) and extent>6000 and nearest[0]<1500.:
454474
# A coarse buffered pixel can disconnect otherwise navigable coastal
455475
# water. Resolve the current shoreline before accepting a water exit.
456476
return await self.prepare(state,course_target,key,nearest,needed,extent=6000,
@@ -506,8 +526,11 @@ async def prepare(self,state,target,key,nearest=None,needed=True,extent=None,pro
506526
# Coarse regional pixels can close a harbour entrance.
507527
# Resolve it on a smaller real terrain grid before retrying;
508528
# never substitute an arbitrary offshore frontier.
509-
if not canal and extent>6000 and str(error).startswith('regional_corridor_unavailable'):
529+
if not canal and not fine_checked and extent>6000 and str(error).startswith('regional_corridor_unavailable'):
510530
return await self.prepare(state,course_target,key,nearest,needed,extent=6000,progress_target=progress_target,coastal_target=coastal_target)
531+
if not canal and initial_extent and extent==6000:
532+
return await self.prepare(state,course_target,key,nearest,needed,extent=20000,
533+
progress_target=progress_target,coastal_target=coastal_target,fine_checked=True)
511534
raise
512535
if value['points']:value['mode']='detour'
513536
if canal and value['points']:value.update(mode='canal',assumed_depth_m=10,measured_bathymetry=False)

0 commit comments

Comments
 (0)